From 398a7cbf704bd210a4027252111e178cad5efd9a Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Wed, 7 Oct 2026 03:40:22 -0700 Subject: [PATCH] D2 step 3: route input, host vocabulary and DispatchScope leave plane_host; TLS is the connector's - build_input.rs becomes crate::route_input, beside route_tables: ConfigTables::of names no plane_host type. - The host capability vocabulary (EngineHost and its slices, PlaneSlots, ContainerGateSink, the gauntlet and its runner registries) becomes crate::plane::host; slots_through moves beside it. plane_host keeps a transitional glob re-export for its HOT files and the legacy plane crates. - DispatchScope, SettleAdmission and EgressFaultDetail become crate::plane::dispatch_scope (spec Part 3: "DispatchScope stands"); SessionScope and DurableScope stay for the HOT deletion. - session_meter becomes crate::governance::session_meter (the ledger crate cannot name EngineHost). - EgressTrustHost, its install/get and PassThroughEgressTrust move into crate::secure beside SecureLayer; GuardedEgressTrust moves to busbar_core_connector::tls::trust and the root installs it. The four registry methods and peer_leaf_pin leave the trait (no caller). - The SPKI DER walk moves to busbar_core_connector::tls::spki with its walk tests (from a2a's key_info_tests). The connector owns pinning (spec Part 3): SecuredIo::peer_leaf becomes peer_spki(), computed by the connector's wrap at handshake time; the kernel's observe path reads it and never walks DER. The kernel's TLS double reports a fixed per-leaf test pin. - Construction known_sites follow runtime_slot_key and reclaim_all; door-only's EGRESS_TRUST row follows the static to secure.rs. Row 82: 19 production files (plane_host 15, proto 3, store/planes 1), from 24. --- Cargo.lock | 1 + crates/busbar-a2a/src/a2a/card.rs | 2 +- crates/busbar-a2a/src/a2a/key_info.rs | 39 - crates/busbar-a2a/src/a2a/mod.rs | 1 - .../src/a2a/tests/egress_seam_tests.rs | 2 +- crates/busbar-a2a/src/a2a/transport.rs | 8 +- crates/busbar-core-admin/src/v1/service.rs | 4 +- crates/busbar-core-connector/Cargo.toml | 3 + .../busbar-core-connector/src/tls/engine.rs | 31 +- .../src/tls/engine_tests.rs | 18 +- crates/busbar-core-connector/src/tls/mod.rs | 2 + .../src/tls}/spki.rs | 28 +- .../src/tls/spki_tests.rs} | 48 +- crates/busbar-core-connector/src/tls/trust.rs | 33 + .../src/tls/trust_tests.rs | 21 + crates/busbar-kernel/src/admin_verbs.rs | 4 +- crates/busbar-kernel/src/appbuild.rs | 12 +- crates/busbar-kernel/src/auth/audience.rs | 4 +- crates/busbar-kernel/src/cost.rs | 6 +- crates/busbar-kernel/src/door.rs | 2 +- .../src/egress/engine/observe.rs | 8 +- .../src/egress/engine/resolve.rs | 4 +- .../src/egress/engine/tests/observe_tests.rs | 2 +- .../src/egress/engine/tests/tls_tests.rs | 6 +- crates/busbar-kernel/src/egress/fixtures.rs | 28 +- crates/busbar-kernel/src/egress/seam.rs | 4 +- .../src/egress/tests/duplex_ws_tests.rs | 16 +- crates/busbar-kernel/src/governance/mod.rs | 3 + .../session_meter.rs | 4 +- .../tests/session_meter_tests.rs | 2 +- crates/busbar-kernel/src/hooks/taps.rs | 4 +- crates/busbar-kernel/src/host_services.rs | 2 +- crates/busbar-kernel/src/ingress/duplex_ws.rs | 4 +- crates/busbar-kernel/src/lib.rs | 1 + .../busbar-kernel/src/plane/dispatch_scope.rs | 309 ++++ crates/busbar-kernel/src/plane/door.rs | 19 +- crates/busbar-kernel/src/plane/host.rs | 1527 ++++++++++++++++ crates/busbar-kernel/src/plane/mod.rs | 5 + crates/busbar-kernel/src/plane/registry.rs | 30 +- .../{plane_host => plane}/slots_through.rs | 2 +- .../src/plane/tests/dispatch_scope_tests.rs | 112 ++ .../tests/host_gauntlet_session_tests.rs} | 0 .../tests/host_plane_answer_tests.rs} | 0 .../busbar-kernel/src/plane_driver/gated.rs | 2 +- .../busbar-kernel/src/plane_driver/hooks.rs | 2 +- crates/busbar-kernel/src/plane_host/egress.rs | 8 +- .../src/plane_host/egress_trust.rs | 161 -- .../busbar-kernel/src/plane_host/identity.rs | 2 +- crates/busbar-kernel/src/plane_host/mod.rs | 1550 +---------------- crates/busbar-kernel/src/plane_host/scope.rs | 310 +--- .../plane_host/tests/egress_trust_tests.rs | 133 -- .../src/plane_host/tests/scope_tests.rs | 98 +- crates/busbar-kernel/src/plane_routes.rs | 2 +- crates/busbar-kernel/src/probe.rs | 2 +- .../build_input.rs => route_input.rs} | 36 +- crates/busbar-kernel/src/route_tables.rs | 6 +- crates/busbar-kernel/src/secure.rs | 94 +- crates/busbar-kernel/src/state.rs | 18 +- crates/busbar-kernel/src/store/vocab.rs | 16 +- .../src/test_support/engine_kit/core_impl.rs | 2 +- .../src/test_support/engine_kit/mod.rs | 2 +- .../src/test_support/fixtures.rs | 4 +- crates/busbar-kernel/src/test_support/mod.rs | 46 +- crates/busbar-kernel/src/test_support/seam.rs | 2 +- crates/busbar-kernel/src/tests/admin_verbs.rs | 2 +- crates/busbar-kernel/src/tests/door_tests.rs | 2 +- crates/busbar-kernel/src/tests/probe_tests.rs | 2 +- .../route_input_tests.rs} | 2 +- .../src/tests/route_tables_tests.rs | 2 +- .../busbar-kernel/src/tests/secure_tests.rs | 34 + .../tests/spentledger_cross_plane.rs | 2 +- crates/busbar-plane-llm/src/codec/mod.rs | 2 +- .../busbar-plane-streaming/src/codec/mod.rs | 2 +- crates/busbar-voice/src/runtime/metering.rs | 2 +- crates/busbar/src/root/connector.rs | 3 +- crates/busbar/src/root/gauntlet_install.rs | 4 +- crates/busbar/src/root/gauntlet_kernel.rs | 8 +- crates/busbar/src/root/linked.rs | 4 +- crates/busbar/src/root/model_egress.rs | 6 +- .../busbar/src/root/tests/gauntlet_kernel.rs | 10 +- crates/busbar/src/root/tests/linked.rs | 2 +- .../root/tests/metric_family_conformance.rs | 2 +- .../src/root/tests/percall_meter_shadow.rs | 4 +- crates/busbar/src/root/tests/plane_node.rs | 2 +- .../tests/plane_host_universal_purity.rs | 4 +- .../tests/plane_meter_seam_reachability.rs | 2 +- docs/security/1.6.0-security-posture.md | 4 +- qa/construction.toml | 15 +- qa/door-only.toml | 2 +- 89 files changed, 2432 insertions(+), 2549 deletions(-) delete mode 100644 crates/busbar-a2a/src/a2a/key_info.rs rename crates/{busbar-kernel/src/plane_host => busbar-core-connector/src/tls}/spki.rs (89%) rename crates/{busbar-a2a/src/a2a/tests/key_info_tests.rs => busbar-core-connector/src/tls/spki_tests.rs} (79%) create mode 100644 crates/busbar-core-connector/src/tls/trust.rs create mode 100644 crates/busbar-core-connector/src/tls/trust_tests.rs rename crates/busbar-kernel/src/{plane_host => governance}/session_meter.rs (98%) rename crates/busbar-kernel/src/{plane_host => governance}/tests/session_meter_tests.rs (99%) create mode 100644 crates/busbar-kernel/src/plane/dispatch_scope.rs create mode 100644 crates/busbar-kernel/src/plane/host.rs rename crates/busbar-kernel/src/{plane_host => plane}/slots_through.rs (97%) create mode 100644 crates/busbar-kernel/src/plane/tests/dispatch_scope_tests.rs rename crates/busbar-kernel/src/{plane_host/tests/gauntlet_session_tests.rs => plane/tests/host_gauntlet_session_tests.rs} (100%) rename crates/busbar-kernel/src/{plane_host/tests/plane_answer_tests.rs => plane/tests/host_plane_answer_tests.rs} (100%) delete mode 100644 crates/busbar-kernel/src/plane_host/egress_trust.rs delete mode 100644 crates/busbar-kernel/src/plane_host/tests/egress_trust_tests.rs rename crates/busbar-kernel/src/{plane_host/build_input.rs => route_input.rs} (89%) rename crates/busbar-kernel/src/{plane_host/tests/build_input_tests.rs => tests/route_input_tests.rs} (95%) create mode 100644 crates/busbar-kernel/src/tests/secure_tests.rs diff --git a/Cargo.lock b/Cargo.lock index dc23d1bc5e..8eaed1fb03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,6 +575,7 @@ name = "busbar-core-connector" version = "1.6.0" dependencies = [ "axum", + "base64 0.22.1", "busbar-contract", "busbar-kernel", "busbar-plugin-loader", diff --git a/crates/busbar-a2a/src/a2a/card.rs b/crates/busbar-a2a/src/a2a/card.rs index bbb4eed2cc..3347d2580f 100644 --- a/crates/busbar-a2a/src/a2a/card.rs +++ b/crates/busbar-a2a/src/a2a/card.rs @@ -251,7 +251,7 @@ pub(crate) fn parse(card: &Value) -> Result { /// THE ONE DIGEST RENDERING THIS PLANE USES: `sha256/`. /// /// Over BYTES rather than over `&str`, because the transport-layer pin -/// ([`super::key_info::pin_hash`]) hashes a DER structure and a second rendering written for it would +/// (the connector's `tls::spki::pin`) hashes a DER structure and a second rendering written for it would /// be a second spelling of one operator-facing value. An operator comparing a configured pin /// against an audit row, and comparing either against what `openssl dgst -sha256 -binary | base64` /// printed, has to be comparing one spelling. diff --git a/crates/busbar-a2a/src/a2a/key_info.rs b/crates/busbar-a2a/src/a2a/key_info.rs deleted file mode 100644 index 011a66cd28..0000000000 --- a/crates/busbar-a2a/src/a2a/key_info.rs +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE TRANSPORT-LAYER IDENTITY: a peer certificate's SubjectPublicKeyInfo, hashed. -//! -//! An A2A card's signature is OPTIONAL. Where a vendor does not sign, the only authenticity root left -//! is the one the network already established: the certificate the card endpoint proved it held the -//! private key for. Pinning the SPKI rather than the whole certificate is the standard choice and it is -//! the right one here — a certificate is re-issued on every renewal and its bytes change each time, -//! while the key underneath it survives a renewal. -//! -//! ## ONE WALK, LIFTED TO THE NEUTRAL HOST -//! -//! The DER walk this file used to own now lives in [`busbar_kernel::plane_host::spki`]: when the HOST owns an -//! outbound hop (the egress seam) it must hand the plane the SAME pin string the plane would have -//! computed here, byte for byte, and a second copy of the walk would be a second answer to "what is -//! this key's pin". So the walk lives in ONE neutral place and this module re-exports it — the a2a -//! spelling ([`pin_hash`]) and the host spelling ([`busbar_kernel::plane_host::spki::pin`]) are the SAME -//! function, which is what makes a host-computed pin and a plane-computed pin identical. -//! -//! The DER handed to [`pin_hash`] is the leaf certificate of a handshake that ALREADY COMPLETED under -//! the ordinary chain-and-name check ([`super::transport`]). This is therefore an OBSERVATION of a -//! connection somebody else already verified, never a substitute for verifying one. - -// The walk, the error taxonomy and the pin rendering all live in the neutral host module now; a2a -// re-exports them under their historical names so this plane's tests keep one spelling. Since the -// transport's hop moved onto the egress seam, the HOST computes the observed pin (from the same -// [`busbar_kernel::plane_host::spki::pin`] this name aliases), so no non-test code reaches `pin_hash` any -// more — the walk, its error taxonomy and the pin rendering are all exercised only by the tests. -#[cfg(all(test, feature = "test-support"))] -pub(crate) use busbar_kernel::plane_host::spki::pin as pin_hash; -#[cfg(all(test, feature = "test-support"))] -pub(crate) use busbar_kernel::plane_host::spki::{ - subject_public_key_info, SpkiError as KeyInfoError, -}; - -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/key_info_tests.rs"] -mod key_info_tests; diff --git a/crates/busbar-a2a/src/a2a/mod.rs b/crates/busbar-a2a/src/a2a/mod.rs index abf94c6b08..207b4e1fd9 100644 --- a/crates/busbar-a2a/src/a2a/mod.rs +++ b/crates/busbar-a2a/src/a2a/mod.rs @@ -623,7 +623,6 @@ pub(crate) mod words; // refresh timer drives the same `due` this one does. See the standing rule: unify the duplicate // before a second copy can drift from the first. pub(crate) use busbar_kernel::trust::reverify; -pub(crate) mod key_info; pub mod serve; pub(crate) mod sign; pub use busbar_plane_a2a::a2a::task; diff --git a/crates/busbar-a2a/src/a2a/tests/egress_seam_tests.rs b/crates/busbar-a2a/src/a2a/tests/egress_seam_tests.rs index 56fbc15c8e..4fd03bc94f 100644 --- a/crates/busbar-a2a/src/a2a/tests/egress_seam_tests.rs +++ b/crates/busbar-a2a/src/a2a/tests/egress_seam_tests.rs @@ -13,7 +13,7 @@ //! so a self-signed test CA cannot be reached through the seam — the same reason `egress_tests` uses //! plaintext). The peer-SPKI dimension is therefore `None == None` here; its byte-identity over TLS is //! by CONSTRUCTION — the host and the plane decode the pin through the one shared -//! `busbar_kernel::plane_host::spki::pin` (`a2a::key_info::pin_hash` re-exports it), so there is no second spelling +//! connector `tls::spki::pin`, so there is no second spelling //! to diverge. `client_identity_offered` is asserted directly (both compute `is_some()`). use std::io::{Read, Write}; diff --git a/crates/busbar-a2a/src/a2a/transport.rs b/crates/busbar-a2a/src/a2a/transport.rs index 319a73b5ff..f6ef787729 100644 --- a/crates/busbar-a2a/src/a2a/transport.rs +++ b/crates/busbar-a2a/src/a2a/transport.rs @@ -36,7 +36,7 @@ //! //! An unsigned agent card has no JWS root, and the only authenticity root left for it is the //! certificate the endpoint proved it held the key for. So this transport records the leaf -//! certificate's SubjectPublicKeyInfo pin ([`super::key_info::pin_hash`]) on every TLS hop. +//! certificate's SubjectPublicKeyInfo pin (the connector's `tls::spki::pin`) on every TLS hop. //! //! **The certificate is read AFTER the ordinary verification, off a handshake that already //! succeeded.** `reqwest`'s `tls_info` hands back the leaf of a connection the chain-and-name check @@ -158,8 +158,8 @@ impl Resolver for TokioResolver { // // The hop no longer runs behind a `reqwest::Response` this plane holds — the host owns the socket and // the verified handshake, and hands back the `sha256/…` pin on the seam's `seam::Buffered`, decoded -// from the SAME bytes `busbar_kernel::plane_host::spki::pin` produces (which `super::key_info::pin_hash` -// re-exports), so the pin string is byte-identical to the one this file used to compute. `None` on a +// from the SAME bytes the connector's `tls::spki::pin` produces, so the pin string is +// byte-identical to the one this file used to compute. `None` on a // plaintext hop and `None` where the certificate cannot be read, unchanged: `super::verify` refuses // a transport-pinned registration whose fetch produced no observed pin, because "we could not look" // and "it matched" are the two answers a pin exists to keep apart. @@ -394,7 +394,7 @@ impl ReqwestTransport { // THE HOP, over the HOSTLESS egress seam. The host owns the socket, the pinned client (its own // refusing resolver), the verified handshake and the capped read; it hands back the status, the - // `Location`, the peer SPKI pin (decoded from the same bytes `super::key_info::pin_hash` produces) + // `Location`, the peer SPKI pin (decoded from the same bytes the connector's `tls::spki::pin` produces) // and whether busbar's own client identity was carried into the handshake. The deadline rides // the desc (`timeout_ms`), the identity/trust anchors ride their opaque refs. let buffered = diff --git a/crates/busbar-core-admin/src/v1/service.rs b/crates/busbar-core-admin/src/v1/service.rs index 3202ece841..a95825518e 100644 --- a/crates/busbar-core-admin/src/v1/service.rs +++ b/crates/busbar-core-admin/src/v1/service.rs @@ -1341,7 +1341,7 @@ fn plane_named_def_list( busbar_kernel::plane::registry::plane_decl_for_config_section(section.key()) .and_then(|d| d.named_def_list) .map_or_else(Vec::new, |f| { - f(app as &dyn busbar_kernel::plane_host::PlaneSlots) + f(app as &dyn busbar_kernel::plane::host::PlaneSlots) }) } @@ -1354,7 +1354,7 @@ fn plane_named_def_get( ) -> Option { busbar_kernel::plane::registry::plane_decl_for_config_section(section.key()) .and_then(|d| d.named_def_get) - .and_then(|f| f(app as &dyn busbar_kernel::plane_host::PlaneSlots, name)) + .and_then(|f| f(app as &dyn busbar_kernel::plane::host::PlaneSlots, name)) } #[cfg(test)] diff --git a/crates/busbar-core-connector/Cargo.toml b/crates/busbar-core-connector/Cargo.toml index 89905cdafd..9ee7ca14f1 100644 --- a/crates/busbar-core-connector/Cargo.toml +++ b/crates/busbar-core-connector/Cargo.toml @@ -57,6 +57,9 @@ webpki = { workspace = true } zeroize = { workspace = true } # STUN FINGERPRINT (CRC-32) on the dtls engine's consent probes. crc32fast = { workspace = true } +# THE PEER'S SPKI PIN (`tls::spki`): the connector owns pinning (spec Part 3), and the pin's one +# spelling is `sha256/` of the leaf's SubjectPublicKeyInfo (the digest is ring's). +base64 = { workspace = true } # THE HOST'S SOCKETS AND THEIR READINESS (`io`, `socket`, `compose`, `wire`): the connector owns every # OS socket (BUSBAR-1.6.0.md THE DESIGN, §5). `tokio` is asked for its reactor registration diff --git a/crates/busbar-core-connector/src/tls/engine.rs b/crates/busbar-core-connector/src/tls/engine.rs index 7ea05336f7..d5fd15f391 100644 --- a/crates/busbar-core-connector/src/tls/engine.rs +++ b/crates/busbar-core-connector/src/tls/engine.rs @@ -231,14 +231,35 @@ impl Pending { /// /// The handshake failed; the error is the TLS stack's, unwrapped. pub async fn run(self, tcp: TcpStream) -> io::Result { - self.connector.connect(self.name, tcp).await.map(Secured) + self.connector + .connect(self.name, tcp) + .await + .map(Secured::over) } } -/// A secured client stream. -pub struct Secured(tokio_rustls::client::TlsStream); +/// A secured client stream, and the SPKI pin of the leaf its verified handshake produced. +pub struct Secured(tokio_rustls::client::TlsStream, Option); impl Secured { + /// The secured stream over `tls`, its peer pin computed ONCE, now: a leaf the DER walk refuses + /// (or no leaf) leaves the pin absent, never a pass. + fn over(tls: tokio_rustls::client::TlsStream) -> Self { + let pin = tls + .get_ref() + .1 + .peer_certificates() + .and_then(|certs| certs.first()) + .and_then(|leaf| super::spki::pin(leaf.as_ref()).ok()); + Self(tls, pin) + } + + /// The `sha256/` SPKI pin of the peer's leaf ([`super::spki::pin`]), or `None`. + #[must_use] + pub fn peer_spki(&self) -> Option<&str> { + self.1.as_deref() + } + /// The protocol ALPN agreed. #[must_use] pub fn alpn(&self) -> Option<&[u8]> { @@ -368,8 +389,8 @@ impl seam::SecuredIo for Secured { fn alpn(&self) -> Option<&[u8]> { Secured::alpn(self) } - fn peer_leaf(&self) -> Option<&[u8]> { - Secured::peer_leaf(self) + fn peer_spki(&self) -> Option<&str> { + Secured::peer_spki(self) } fn tcp(&self) -> &TcpStream { Secured::tcp(self) diff --git a/crates/busbar-core-connector/src/tls/engine_tests.rs b/crates/busbar-core-connector/src/tls/engine_tests.rs index a9e8fbcedb..1cbf0962e7 100644 --- a/crates/busbar-core-connector/src/tls/engine_tests.rs +++ b/crates/busbar-core-connector/src/tls/engine_tests.rs @@ -217,7 +217,7 @@ async fn the_mtls_fixture_accepts_the_engine_identity_and_records_its_leaf() { assert_eq!( peer_key_pin(&resp), Some( - busbar_kernel::plane_host::spki::pin(&server.leaf_der) + crate::tls::spki::pin(&server.leaf_der) .expect("server leaf") .as_str() ), @@ -584,7 +584,7 @@ async fn stack_b( .extensions() .get::() .and_then(|t| t.peer_certificate()) - .map(|der| busbar_kernel::plane_host::spki::pin(der).expect("walkable leaf")); + .map(|der| crate::tls::spki::pin(der).expect("walkable leaf")); let body = resp.bytes().await.expect("body"); ( Outcome::Answered { @@ -636,8 +636,7 @@ async fn known_leaf_tls_pin_and_sni_are_observed_and_webpki_refuses_the_private_ body: "over tls".to_string() } ); - let expected_pin = - busbar_kernel::plane_host::spki::pin(&material.leaf_der).expect("fixture leaf"); + let expected_pin = crate::tls::spki::pin(&material.leaf_der).expect("fixture leaf"); assert_eq!( leaf_pin.as_deref(), Some(expected_pin.as_str()), @@ -869,14 +868,21 @@ async fn the_pin_off_a_real_handshake_is_the_serving_leafs_key_and_not_a_look_al .await .expect("the rooted hop answers"); assert_eq!(resp.status(), 200); - let expected = busbar_kernel::plane_host::spki::pin(&serving.leaf_der).expect("serving leaf"); - let other = busbar_kernel::plane_host::spki::pin(&look_alike.leaf_der).expect("look-alike"); + let expected = crate::tls::spki::pin(&serving.leaf_der).expect("serving leaf"); + let other = crate::tls::spki::pin(&look_alike.leaf_der).expect("look-alike"); assert_ne!(expected, other, "two keys, two pins"); assert_eq!( peer_key_pin(&resp), Some(expected.as_str()), "the pin is read off the handshake that served, so it is the serving leaf's key" ); + // RED ARM: a look-alike's pin (same name, another key) is not what this hop observed, so a + // registration pinned to it is refused. + assert_ne!( + peer_key_pin(&resp), + Some(other.as_str()), + "a look-alike key's pin never matches the observed one" + ); } /// What a mutual-TLS far end decided about each connection: `Ok(n)` when the handshake completed diff --git a/crates/busbar-core-connector/src/tls/mod.rs b/crates/busbar-core-connector/src/tls/mod.rs index b65486fc1f..77686ce539 100644 --- a/crates/busbar-core-connector/src/tls/mod.rs +++ b/crates/busbar-core-connector/src/tls/mod.rs @@ -50,6 +50,8 @@ use std::sync::Arc; pub mod client; pub mod engine; +pub mod spki; +pub mod trust; use busbar_contract::transport::wire::{ConnectionSecurity, RawIo, SecuredIoFut}; use busbar_kernel::config::secret::SecretResolver; diff --git a/crates/busbar-kernel/src/plane_host/spki.rs b/crates/busbar-core-connector/src/tls/spki.rs similarity index 89% rename from crates/busbar-kernel/src/plane_host/spki.rs rename to crates/busbar-core-connector/src/tls/spki.rs index 8e12040657..d23b8bb881 100644 --- a/crates/busbar-kernel/src/plane_host/spki.rs +++ b/crates/busbar-core-connector/src/tls/spki.rs @@ -1,20 +1,19 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! THE TRANSPORT-LAYER IDENTITY, computed HOST-SIDE: a peer certificate's SubjectPublicKeyInfo, -//! hashed and rendered in the ONE canonical spelling the whole tree shares. +//! THE TRANSPORT-LAYER IDENTITY: a peer certificate's SubjectPublicKeyInfo, hashed and rendered in +//! the ONE canonical spelling the whole tree shares. //! -//! This is the NEUTRAL host home of the DER walk that used to live only inside the a2a plane. It was -//! lifted here — not duplicated — for the egress seam: when the host owns the outbound hop it also -//! owns the certificate the handshake produced, and the host must be able to hand the plane the SAME -//! pin string the plane would have computed itself, byte for byte. A second copy of the walk would be -//! a second answer to "what is this key's pin", which is exactly the divergence a pin exists to -//! prevent. So there is ONE walk, here, and the a2a plane's `spki` module re-exports it. +//! The connector owns pinning, SPKI and mTLS (spec Part 3), so the one DER walk lives here, beside +//! the TLS wrap ([`super::engine::Layer`]) that computes each secured stream's pin at handshake time +//! and hands it to the kernel computed (`busbar_kernel::secure::SecuredIo::peer_spki`). A second copy +//! of the walk would be a second answer to "what is this key's pin", which is exactly the divergence +//! a pin exists to prevent. //! //! ## WHERE THE CERTIFICATE COMES FROM, and why this does not weaken anything //! //! The DER handed to [`pin`] is the leaf certificate of a handshake that ALREADY COMPLETED under the -//! ordinary chain-and-name check (the host egress chokepoint's pinned client, or the a2a transport). This +//! ordinary chain-and-name check (the wrap's own verified handshake). This //! module is therefore an OBSERVATION of a connection somebody else already verified, never a //! substitute for verifying one: a handshake that was not verified produces no response to read a //! certificate off. @@ -57,7 +56,12 @@ pub enum SpkiError { /// The bytes ran out mid-element. Truncated, /// A tag that is not what RFC 5280 puts at this position. - UnexpectedTag { want: u8, got: u8 }, + UnexpectedTag { + /// The tag RFC 5280 puts here. + want: u8, + /// The tag the bytes carry. + got: u8, + }, /// An indefinite length, a non-minimal length, or a length wider than any certificate needs. All /// three are legal BER and none of them is DER, and each gives one certificate a second encoding — /// which is a second pin for one key. @@ -190,3 +194,7 @@ pub fn pin(cert_der: &[u8]) -> Result { let digest = ring::digest::digest(&ring::digest::SHA256, key_info); Ok(format!("sha256/{}", B64.encode(digest.as_ref()))) } + +#[cfg(test)] +#[path = "spki_tests.rs"] +mod tests; diff --git a/crates/busbar-a2a/src/a2a/tests/key_info_tests.rs b/crates/busbar-core-connector/src/tls/spki_tests.rs similarity index 79% rename from crates/busbar-a2a/src/a2a/tests/key_info_tests.rs rename to crates/busbar-core-connector/src/tls/spki_tests.rs index 321b900eec..2dc8b2e310 100644 --- a/crates/busbar-a2a/src/a2a/tests/key_info_tests.rs +++ b/crates/busbar-core-connector/src/tls/spki_tests.rs @@ -11,7 +11,6 @@ //! when those two agree. use super::*; -use base64::Engine as _; use rcgen::{CertificateParams, KeyPair, PublicKeyData}; use sha2::{Digest, Sha256}; @@ -37,9 +36,9 @@ fn the_walk_lands_on_the_subject_public_key_info_and_not_on_the_member_beside_it } #[test] -fn the_pin_is_the_sha256_of_those_bytes_in_the_planes_one_digest_spelling() { +fn the_pin_is_the_sha256_of_those_bytes_in_the_one_digest_spelling() { let (cert_der, expected_key_pin) = cert_and_its_key_pin(); - let pin = pin_hash(&cert_der).expect("a pin"); + let pin = pin(&cert_der).expect("a pin"); assert_eq!( pin, format!( @@ -49,12 +48,6 @@ fn the_pin_is_the_sha256_of_those_bytes_in_the_planes_one_digest_spelling() { "the pin is the operator-facing value; it must be exactly what `openssl … | openssl dgst \ -sha256 -binary | base64` prints, or nobody can obtain it out of band" ); - assert_eq!( - pin, - crate::a2a::card::sha256_tagged(&expected_key_pin), - "ONE digest rendering on this plane. A second spelling here is a second value an operator \ - has to know is the same one." - ); } #[test] @@ -80,15 +73,15 @@ fn two_certificates_over_the_same_key_pin_identically_and_a_new_key_does_not() { "the two certificates must genuinely differ, or this test proves nothing" ); assert_eq!( - pin_hash(first.der()).expect("pin"), - pin_hash(second.der()).expect("pin"), + pin(first.der()).expect("pin"), + pin(second.der()).expect("pin"), "a renewal keeps the key, so it must keep the pin" ); let (other_cert, _) = cert_and_its_key_pin(); assert_ne!( - pin_hash(first.der()).expect("pin"), - pin_hash(&other_cert).expect("pin"), + pin(first.der()).expect("pin"), + pin(&other_cert).expect("pin"), "a different key must be a different pin, or the pin distinguishes nothing" ); } @@ -100,8 +93,8 @@ fn a_truncated_certificate_refuses_rather_than_hashing_whatever_it_reached() { let (cert_der, _) = cert_and_its_key_pin(); for cut in [1usize, 2, 8, cert_der.len() / 2, cert_der.len() - 1] { assert_eq!( - pin_hash(&cert_der[..cut]), - Err(KeyInfoError::Truncated), + pin(&cert_der[..cut]), + Err(SpkiError::Truncated), "{cut} bytes of a certificate is not a certificate" ); } @@ -112,8 +105,8 @@ fn a_document_that_is_not_a_certificate_refuses_by_tag() { // An OCTET STRING where a SEQUENCE belongs. Refused by NAME rather than by falling off the end, // so an operator reading the log learns the peer sent something that is not a certificate. assert_eq!( - pin_hash(&[0x04, 0x02, 0xAA, 0xBB]), - Err(KeyInfoError::UnexpectedTag { + pin(&[0x04, 0x02, 0xAA, 0xBB]), + Err(SpkiError::UnexpectedTag { want: 0x30, got: 0x04 }) @@ -142,8 +135,8 @@ fn every_ber_length_form_that_gives_one_certificate_a_second_encoding_is_refused ), ] { assert_eq!( - pin_hash(&bytes), - Err(KeyInfoError::NotDer(why)), + pin(&bytes), + Err(SpkiError::NotDer(why)), "{why} must be refused, not tolerated" ); } @@ -182,20 +175,3 @@ fn a_version_one_certificate_with_no_version_member_is_walked_correctly() { not there" ); } - -/// FAITHFULNESS: the HOST spelling of the pin (the neutral [`busbar_kernel::plane_host::spki::pin`] the egress -/// seam hands back in the observed head) is the SAME string as the a2a plane's own [`pin_hash`], byte -/// for byte, over the same certificate DER. This is the whole reason the walk was lifted to one place: -/// a governed hop must be able to return the pin the plane would have computed itself, so the plane's -/// SPKI classification is unchanged whether the hop went through the seam or the plane's own transport. -#[test] -fn the_host_pin_equals_the_plane_pin_byte_for_byte() { - let (cert_der, _) = cert_and_its_key_pin(); - let plane = pin_hash(&cert_der).expect("the plane pin"); - let host = busbar_kernel::plane_host::spki::pin(&cert_der).expect("the host pin"); - assert_eq!( - host.as_bytes(), - plane.as_bytes(), - "the host-computed pin must equal the plane-computed pin byte for byte" - ); -} diff --git a/crates/busbar-core-connector/src/tls/trust.rs b/crates/busbar-core-connector/src/tls/trust.rs new file mode 100644 index 0000000000..6eddcb9f01 --- /dev/null +++ b/crates/busbar-core-connector/src/tls/trust.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE EGRESS-TRUST CAPABILITY THE COMPOSITION ROOT INSTALLS (`busbar_kernel::secure::EgressTrustHost`): +//! the deployment's one destination guard and this crate's TLS wrap, both the connector's. The kernel +//! reaches them only through the capability; this decides nothing itself. + +use std::net::IpAddr; +use std::sync::Arc; + +use busbar_kernel::config::Destinations; +use busbar_kernel::host_services::{DestJudge, DestRefusal}; +use busbar_kernel::secure::{EgressTrustHost, SecureLayer}; + +/// The capability the composition root installs: every answer judged by the deployment's one +/// destination guard, and every outbound connection secured by the one TLS wrap. +pub struct GuardedEgressTrust(pub Arc, pub Arc); + +impl EgressTrustHost for GuardedEgressTrust { + fn secure_layer(&self) -> Option> { + Some(Arc::clone(&self.1)) + } + fn judge_answer(&self, host: &str, addrs: &[IpAddr], class: u32) -> Result<(), DestRefusal> { + self.0.judge_answer(host, addrs, class) + } + fn destinations_applied(&self, d: &Destinations) { + self.0.destinations_applied(d); + } +} + +#[cfg(test)] +#[path = "trust_tests.rs"] +mod tests; diff --git a/crates/busbar-core-connector/src/tls/trust_tests.rs b/crates/busbar-core-connector/src/tls/trust_tests.rs new file mode 100644 index 0000000000..9324bc9bbf --- /dev/null +++ b/crates/busbar-core-connector/src/tls/trust_tests.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-core-connector/src/tls/trust.rs`. + +use super::*; + +/// The root's capability hands every answer to the guard behind it, deciding nothing itself, and +/// carries the wrap it was built with. +#[test] +fn the_guarded_seam_judges_by_the_guard_behind_it() { + let judged = GuardedEgressTrust( + busbar_kernel::egress::fixtures::private_refusing(&[]), + Arc::new(crate::tls::engine::Layer), + ); + let private: IpAddr = "10.0.0.5".parse().unwrap(); + let public: IpAddr = "93.184.216.34".parse().unwrap(); + assert!(judged.judge_answer("db.test", &[private], 0).is_err()); + assert_eq!(judged.judge_answer("api.test", &[public], 0), Ok(())); + assert!(judged.secure_layer().is_some()); +} diff --git a/crates/busbar-kernel/src/admin_verbs.rs b/crates/busbar-kernel/src/admin_verbs.rs index 8ecb1f52ae..cac569ca34 100644 --- a/crates/busbar-kernel/src/admin_verbs.rs +++ b/crates/busbar-kernel/src/admin_verbs.rs @@ -10,7 +10,7 @@ //! //! - [`PlaneTrust`] + [`PlaneVerbError`] + [`registered`] are the RESOLVE/LOOK half (ADMIN-2): a plane //! states only WHERE a registration is resolved from and what LOOKING at one means; the neutral host -//! ([`crate::plane_host::EngineHost`]) is the one snapshot handle both steps read through. A plane's +//! ([`crate::plane::host::EngineHost`]) is the one snapshot handle both steps read through. A plane's //! resolve/look never produce a `Forbidden`/`Scope` answer — authorization is enforced by the admin //! auth middleware BEFORE the handler runs — so the three shapes here (`NotFound`, `Validation`, //! `Internal`) are the whole of what a verb can refuse with. Core maps them back onto its own frozen @@ -34,7 +34,7 @@ use axum::http::HeaderMap; use axum::response::Response; use busbar_contract::abi::mechanism::route::RouteMethod; -use crate::plane_host::EngineHost; +use crate::plane::host::EngineHost; /// THE NEUTRAL PLANE-VERB ERROR — the three, and only three, shapes a plane's `resolve`/`look` ever /// produce (a validation refusal with or without its named condition). A plane never produces a diff --git a/crates/busbar-kernel/src/appbuild.rs b/crates/busbar-kernel/src/appbuild.rs index 4f9f5c8bb5..fbe2b3cb52 100644 --- a/crates/busbar-kernel/src/appbuild.rs +++ b/crates/busbar-kernel/src/appbuild.rs @@ -29,7 +29,7 @@ use crate::{ ingress, ir, json, limits, metrics, net_guard, oauth_as, observability, operation, plane, plugin_routes, profile, proto, ratelimit, state, store, telemetry, tls, transport, trust, }; -use busbar_kernel::plane_host::{ +use busbar_kernel::route_input::{ AffinityInput, AuthStyleInput, ClientSettingsInput, FailoverInput, HealthInput, HealthModeInput, LaneInput, OnExhaustedInput, PlaneBuildInput, PoolInput, PoolMemberInput, }; @@ -533,7 +533,7 @@ impl InstalledLimits { destinations, } = self; guard.commit(); - crate::plane_host::egress_trust::destinations_applied(&destinations); + crate::secure::destinations_applied(&destinations); staged.armed = false; crate::rate_apply::rates_applied(&rates.raw()); } @@ -1583,7 +1583,7 @@ pub fn build_app_from_config( // THE PRIOR GENERATION'S SLOTS, so a plane's `build` can CARRY accumulated // coordination off its own prior runtime object across this apply — the same // neutral `&dyn PlaneSlots` a container plane's `build_runtime` receives below. - prior: prior.map(|p| p as &dyn busbar_kernel::plane_host::PlaneSlots), + prior: prior.map(|p| p as &dyn busbar_kernel::plane::host::PlaneSlots), }; (decl.build)(&ctx).map(|obj| (decl.key, obj)) }) @@ -1615,7 +1615,7 @@ pub fn build_app_from_config( slot_key, f( cfg.tool_defs.as_any(), - prior.map(|p| p as &dyn busbar_kernel::plane_host::PlaneSlots), + prior.map(|p| p as &dyn busbar_kernel::plane::host::PlaneSlots), ), ) }) { @@ -1683,7 +1683,7 @@ pub fn build_app_from_config( // The FIXED global-default failover (production has no operator knob for it) — carried so the // plane's `build_runtime` sets its own runtime object's failover config identically to the // pre-pivot inline lowering, and so the test fixture can override the whole-App deadline. - default_failover: Some(busbar_kernel::plane_host::FailoverInput { + default_failover: Some(busbar_kernel::route_input::FailoverInput { timeout_secs: crate::config::DEFAULT_FAILOVER_DEADLINE_SECS, exclusions: None, max_hops: crate::config::DEFAULT_FAILOVER_CAP, @@ -1707,7 +1707,7 @@ pub fn build_app_from_config( { let slot = f( &fallback_build_input as &dyn std::any::Any, - prior.map(|p| p as &dyn busbar_kernel::plane_host::PlaneSlots), + prior.map(|p| p as &dyn busbar_kernel::plane::host::PlaneSlots), ); plane_slots.insert(fallback_runtime_key, slot); } diff --git a/crates/busbar-kernel/src/auth/audience.rs b/crates/busbar-kernel/src/auth/audience.rs index daf255c762..642fa09c7c 100644 --- a/crates/busbar-kernel/src/auth/audience.rs +++ b/crates/busbar-kernel/src/auth/audience.rs @@ -51,12 +51,12 @@ use base64::Engine as _; /// What could be established about a presented bearer's audience binding. /// -/// The enum itself relocated to the neutral seam (`busbar_kernel::plane_host::AudienceBinding`) so +/// The enum itself relocated to the neutral seam (`busbar_kernel::plane::host::AudienceBinding`) so /// a plane reads the [`inspect_bearer`] verdict — routed host-side through /// `EngineHost::identity_audience_binding` — without naming this core auth module. Re-exported at its /// original path so core's own callers (the auth-chain pre-filter, `oauth_as`) and every variant name /// are unchanged; the JUDGEMENT below stays here, since it reaches core's governance token prefix. -pub use busbar_kernel::plane_host::AudienceBinding as Binding; +pub use busbar_kernel::plane::host::AudienceBinding as Binding; /// Establish what can be established about `token`'s binding to `expected_aud`. /// diff --git a/crates/busbar-kernel/src/cost.rs b/crates/busbar-kernel/src/cost.rs index b94721fdcb..e14e7bb66c 100644 --- a/crates/busbar-kernel/src/cost.rs +++ b/crates/busbar-kernel/src/cost.rs @@ -518,7 +518,7 @@ impl CostModel { /// /// `pub` (was crate-private): the first of the two questions the pre-admission pricing guard /// asks, answered for a plane through the - /// [`BudgetHost::cost_pricing_enabled`](busbar_kernel::plane_host::BudgetHost::cost_pricing_enabled) + /// [`BudgetHost::cost_pricing_enabled`](busbar_kernel::plane::host::BudgetHost::cost_pricing_enabled) /// seam, which downcasts the opaque cost handle and drives this same read. pub fn pricing_enabled(&self) -> bool { self.card.pricing_enabled() @@ -582,7 +582,7 @@ impl CostModel { } /// PRICE a neutral [`busbar_contract::billing::Usage`] for `model` into nano-units — the - /// host-side entry point the [`MeteringHost::price_usage`](busbar_kernel::plane_host::MeteringHost::price_usage) + /// host-side entry point the [`MeteringHost::price_usage`](busbar_kernel::plane::host::MeteringHost::price_usage) /// seam a live carrier drives folds through. /// /// **THE ONE FUNCTION** over one row at this card: `None` is every refusal it can give — a @@ -634,7 +634,7 @@ impl CostModel { /// /// `pub` (was crate-private): the second of the pricing guard's two questions, answered for a /// plane through the - /// [`BudgetHost::cost_model_unpriced`](busbar_kernel::plane_host::BudgetHost::cost_model_unpriced) + /// [`BudgetHost::cost_model_unpriced`](busbar_kernel::plane::host::BudgetHost::cost_model_unpriced) /// seam over the same opaque handle. #[inline] pub fn model_unpriced(&self, model: &str) -> bool { diff --git a/crates/busbar-kernel/src/door.rs b/crates/busbar-kernel/src/door.rs index b6282a7a34..bbbc0289b2 100644 --- a/crates/busbar-kernel/src/door.rs +++ b/crates/busbar-kernel/src/door.rs @@ -105,7 +105,7 @@ pub struct AdmitVerdict { /// `Retry-After`, in whole seconds, as the door rendered it. A refusal here is always a budget in /// the chain without headroom: the door refuses for no other reason. pub fn admit_verdict( - outcome: Result<(Option<&crate::plane_host::AdmitHandle>, Option), Option>, + outcome: Result<(Option<&crate::plane::host::AdmitHandle>, Option), Option>, ) -> AdmitVerdict { match outcome { Ok((grant, effective_pool)) => AdmitVerdict { diff --git a/crates/busbar-kernel/src/egress/engine/observe.rs b/crates/busbar-kernel/src/egress/engine/observe.rs index 8ac47fe07b..91f025e7c3 100644 --- a/crates/busbar-kernel/src/egress/engine/observe.rs +++ b/crates/busbar-kernel/src/egress/engine/observe.rs @@ -16,7 +16,8 @@ //! The pooled-reuse propagation is pinned by this module's spike test (two sequential requests on //! ONE connection both carry [`PeerKeyPin`]) — the keystone the whole design stands on. //! -//! The pin is computed ONCE at connect time (`spki::pin` over the leaf), never per request. An +//! The pin is computed ONCE at connect time, by the connector's wrap over the leaf its handshake +//! verified (`SecuredIo::peer_spki`; the connector owns pinning), never per request. An //! unwalkable certificate yields absence, never a pass — "we could not look" and "it matched" are //! the two answers a pin exists to keep apart, and absence is the refusing arm application-side. @@ -33,7 +34,7 @@ type BoxError = Box; /// The observed transport-layer identity of the peer: the `sha256/` pin of the leaf /// certificate's SubjectPublicKeyInfo, in the one canonical spelling -/// (`crate::plane_host::spki::pin`). Cloneable and cheap: the pool clones it into every +/// (`busbar_core_connector::tls::spki::pin`). Cloneable and cheap: the pool clones it into every /// response's extensions. #[derive(Clone, Debug)] pub struct PeerKeyPin(pub Arc); @@ -171,8 +172,7 @@ where // observes NOTHING — honestly absent, matching the empty TlsInfo of old. let key_pin = if observe { io.secured() - .and_then(|tls| tls.peer_leaf()) - .and_then(|leaf| crate::plane_host::spki::pin(leaf).ok()) + .and_then(|tls| tls.peer_spki()) .map(|pin| PeerKeyPin(pin.into())) } else { None diff --git a/crates/busbar-kernel/src/egress/engine/resolve.rs b/crates/busbar-kernel/src/egress/engine/resolve.rs index e64dee78d5..a6cadfa34e 100644 --- a/crates/busbar-kernel/src/egress/engine/resolve.rs +++ b/crates/busbar-kernel/src/egress/engine/resolve.rs @@ -19,7 +19,7 @@ //! The pooled posture's arm is [`EgressResolver::Judged`]: the name is resolved once per new //! connection and EVERY address it answered with is judged by the deployment's one destination //! guard (OWNER ruling DESTINATION GUARD: the connector's, installed by the root and asked through -//! `plane_host::egress_trust::egress_trust_host`, the root-installed egress-trust seam) before `HttpConnector` sees any of them. A refused answer is a resolver error, so the dial fails as a connect failure and nothing +//! `secure::egress_trust_host`, the root-installed egress-trust seam) before `HttpConnector` sees any of them. A refused answer is a resolver error, so the dial fails as a connect failure and nothing //! is connected; an admitted answer is handed on whole, and the `Uri` keeps the name for SNI, the //! certificate check and `Host`. A pooled connection was therefore dialled to an address this arm //! judged, which is what makes reusing it safe. @@ -35,7 +35,7 @@ use hyper_util::client::legacy::connect::dns::{GaiAddrs, GaiFuture, GaiResolver, use busbar_contract::abi::host::conn::connector::EGRESS_PROVIDER; use crate::host_services::DestJudge; -use crate::plane_host::egress_trust::{egress_trust_host, EgressTrustHost, PassThroughEgressTrust}; +use crate::secure::{egress_trust_host, EgressTrustHost, PassThroughEgressTrust}; type BoxError = Box; diff --git a/crates/busbar-kernel/src/egress/engine/tests/observe_tests.rs b/crates/busbar-kernel/src/egress/engine/tests/observe_tests.rs index dadf1ad132..0b09bc2246 100644 --- a/crates/busbar-kernel/src/egress/engine/tests/observe_tests.rs +++ b/crates/busbar-kernel/src/egress/engine/tests/observe_tests.rs @@ -107,7 +107,7 @@ async fn extras_propagate_through_pooled_reuse_both_responses_carry_the_peer_pin Duration::from_secs(10), true, )); - let expected = crate::plane_host::spki::pin(&material.leaf_der).expect("fixture leaf"); + let expected = crate::egress::fixtures::double_pin(&material.leaf_der); for round in 1..=2 { let resp = client diff --git a/crates/busbar-kernel/src/egress/engine/tests/tls_tests.rs b/crates/busbar-kernel/src/egress/engine/tests/tls_tests.rs index 8ff15ec92c..11e0a1ce64 100644 --- a/crates/busbar-kernel/src/egress/engine/tests/tls_tests.rs +++ b/crates/busbar-kernel/src/egress/engine/tests/tls_tests.rs @@ -184,11 +184,7 @@ async fn the_pinned_posture_presents_its_identity_and_a_demanding_peer_refuses_w assert_eq!(resp.status(), 200); assert_eq!( peer_key_pin(&resp), - Some( - crate::plane_host::spki::pin(&server.leaf_der) - .expect("server leaf") - .as_str() - ), + Some(crate::egress::fixtures::double_pin(&server.leaf_der).as_str()), "the pinned posture observes the peer by construction" ); let body = resp.into_body().collect().await.expect("body").to_bytes(); diff --git a/crates/busbar-kernel/src/egress/fixtures.rs b/crates/busbar-kernel/src/egress/fixtures.rs index 4a3d8a97e9..f1694c068a 100644 --- a/crates/busbar-kernel/src/egress/fixtures.rs +++ b/crates/busbar-kernel/src/egress/fixtures.rs @@ -649,8 +649,9 @@ impl crate::secure::ClientTls for DoubleClient { Some("OK") => { let alpn = from_hex_or_dash(words.next().unwrap_or("-")); let leaf = from_hex_or_dash(words.next().unwrap_or("-")); + let pin = leaf.as_deref().map(double_pin); let secured: Box = - Box::new(DoubleSecured { tcp, alpn, leaf }); + Box::new(DoubleSecured { tcp, alpn, pin }); Ok(secured) } Some("REFUSE") => Err(std::io::Error::new( @@ -667,18 +668,31 @@ impl crate::secure::ClientTls for DoubleClient { } } +/// THE PIN THE [`TlsDouble`] REPORTS for a peer that presented `leaf`: a FIXED test spelling, one +/// per leaf, not an SPKI pin. The connector owns pinning (spec Part 3), so this crate never walks a +/// certificate; what a kernel test proves with it is that the engine carries the pin its secured +/// stream reported onto every response served over that connection. The real pin of a real +/// handshake is proven where TLS lives (the connector's `tls/engine_tests.rs`). +#[must_use] +pub fn double_pin(leaf: &[u8]) -> String { + use std::hash::{Hash as _, Hasher as _}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + leaf.hash(&mut h); + format!("double/{:016x}", h.finish()) +} + struct DoubleSecured { tcp: tokio::net::TcpStream, alpn: Option>, - leaf: Option>, + pin: Option, } impl crate::secure::SecuredIo for DoubleSecured { fn alpn(&self) -> Option<&[u8]> { self.alpn.as_deref() } - fn peer_leaf(&self) -> Option<&[u8]> { - self.leaf.as_deref() + fn peer_spki(&self) -> Option<&str> { + self.pin.as_deref() } fn tcp(&self) -> &tokio::net::TcpStream { &self.tcp @@ -909,10 +923,10 @@ pub fn spawn_double( // ── the TLS wrap of a test binary no composition root boots ──────────────────────────────────── /// The egress-trust capability a test binary with no composition root installs to carry a TLS wrap: -/// the pass-through primitives (so every other answer is the one no capability gives), and `layer`. +/// `layer`, and every other answer the one no capability gives. pub struct TlsEgressTrust(pub Arc); -impl crate::plane_host::egress_trust::EgressTrustHost for TlsEgressTrust { +impl crate::secure::EgressTrustHost for TlsEgressTrust { fn secure_layer(&self) -> Option> { Some(Arc::clone(&self.0)) } @@ -922,5 +936,5 @@ impl crate::plane_host::egress_trust::EgressTrustHost for TlsEgressTrust { /// egress-trust capability the composition root installs at boot in a shipped process (first /// install wins). pub fn install_test_tls(layer: Arc) { - crate::plane_host::egress_trust::install_egress_trust_host(Arc::new(TlsEgressTrust(layer))); + crate::secure::install_egress_trust_host(Arc::new(TlsEgressTrust(layer))); } diff --git a/crates/busbar-kernel/src/egress/seam.rs b/crates/busbar-kernel/src/egress/seam.rs index 501746a184..42769d2d8c 100644 --- a/crates/busbar-kernel/src/egress/seam.rs +++ b/crates/busbar-kernel/src/egress/seam.rs @@ -19,7 +19,7 @@ //! The wire bytes are the host's `build_pinned_client` reqwest codec — the SAME one a plane's own //! transport used — so status/body/headers/ALPN are unchanged by moving the hop host-side. The peer //! SPKI pin is decoded from the same observed-identity bytes the neutral -//! `busbar_kernel::plane_host::spki` pin walk produces, which a plane's own transport calls to +//! `busbar_core_connector::tls::spki` pin walk produces, which a plane's own transport calls to //! compute its pin, so the pin string is byte-identical. The body cap / `ReadEnd` classification is //! re-expressed over the poll seam to match //! [`crate::egress::upstream::read_capped`] exactly (Ok-0 = Complete, Fault = TransportError, an over-cap probe = @@ -37,10 +37,10 @@ use busbar_contract::abi::hot::{EgressDesc, EgressKind, StatusClass, POD_VERSION}; use crate::egress::upstream::ReadEnd; +use crate::plane::dispatch_scope::DispatchScope; use crate::plane_host::egress::{ borrowed_range, drive_close, drive_open, drive_poll, scope_bits, OpenOutcome, OpenedHead, }; -use crate::plane_host::scope::DispatchScope; // The neutral buffered/fault RETURN shapes and the [`HostlessEgress`] driver trait relocated to // `busbar_kernel::egress::seam` (field-neutral `std` data + the plugin `EgressFailClass`), so a 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 fe1b3aeb3f..aaba62d291 100644 --- a/crates/busbar-kernel/src/egress/tests/duplex_ws_tests.rs +++ b/crates/busbar-kernel/src/egress/tests/duplex_ws_tests.rs @@ -61,26 +61,26 @@ struct GatePlane { } #[async_trait::async_trait] -impl crate::plane_host::GauntletPlane for GatePlane { +impl crate::plane::host::GauntletPlane for GatePlane { fn verify_destination( &self, - _req: &crate::plane_host::GauntletRequest<'_>, - ) -> crate::plane_host::VerifyOutcome { + _req: &crate::plane::host::GauntletRequest<'_>, + ) -> crate::plane::host::VerifyOutcome { if self.refuse { - crate::plane_host::VerifyOutcome::Refuse( + crate::plane::host::VerifyOutcome::Refuse( axum::response::Response::builder() .status(axum::http::StatusCode::FORBIDDEN) .body(axum::body::Body::from("destination refused")) .expect("refusal response builds"), ) } else { - crate::plane_host::VerifyOutcome::Proceed + crate::plane::host::VerifyOutcome::Proceed } } async fn drive( self: Box, - _req: crate::plane_host::GauntletRequest<'_>, + _req: crate::plane::host::GauntletRequest<'_>, ) -> axum::response::Response { axum::response::Response::builder() .status(500) @@ -98,7 +98,7 @@ async fn spawn_gauntlet_ws_server(refuse: bool) -> SocketAddr { upgrade: axum::extract::ws::WebSocketUpgrade, ) -> axum::response::Response { let gov = busbar_contract::records::PlaneRequestCtx::default(); - let req = crate::plane_host::GauntletRequest { + let req = crate::plane::host::GauntletRequest { gov: &gov, destination: "model-x", correlation_id: 1, @@ -596,7 +596,7 @@ async fn spawn_accept_gauntlet_ws_server(refuse: bool) -> SocketAddr { upgrade: axum::extract::ws::WebSocketUpgrade, ) -> axum::response::Response { let gov = busbar_contract::records::PlaneRequestCtx::default(); - let req = crate::plane_host::GauntletRequest { + let req = crate::plane::host::GauntletRequest { gov: &gov, destination: "model-x", correlation_id: 1, diff --git a/crates/busbar-kernel/src/governance/mod.rs b/crates/busbar-kernel/src/governance/mod.rs index 5d391244fd..054182f6e7 100644 --- a/crates/busbar-kernel/src/governance/mod.rs +++ b/crates/busbar-kernel/src/governance/mod.rs @@ -774,6 +774,9 @@ pub struct RotatedCredential { // public spelling is the substrate's own, and nothing outside busbar-core reaches this path. pub mod revocation; +// THE KERNEL-OWNED SESSION METER: a live carrier's turns ledgered as raw counts and judged by the +// key's budget view (D2 step 3, from plane_host). +pub mod session_meter; // The RESOLVED runtime mint policy (`auth.policy:`) — de-aliased out of `admin` (1.6.0, stage 2a): // core governance/config infrastructure read on the hot mint path, not admin-API surface. pub mod mint_policy; diff --git a/crates/busbar-kernel/src/plane_host/session_meter.rs b/crates/busbar-kernel/src/governance/session_meter.rs similarity index 98% rename from crates/busbar-kernel/src/plane_host/session_meter.rs rename to crates/busbar-kernel/src/governance/session_meter.rs index a9fdf86ab2..0bab514da0 100644 --- a/crates/busbar-kernel/src/plane_host/session_meter.rs +++ b/crates/busbar-kernel/src/governance/session_meter.rs @@ -8,7 +8,7 @@ //! //! A plane opens a [`SessionAccount`] for the presenting key and reports each turn's raw counts per //! class. The kernel appends them through the ONE metering path every plane ledgers through -//! ([`BudgetHost::meter_ledger`](super::BudgetHost::meter_ledger)), unconditionally, and answers +//! ([`BudgetHost::meter_ledger`](crate::plane::host::BudgetHost::meter_ledger)), unconditionally, and answers //! [`TurnVerdict::Live`] or [`TurnVerdict::MustClose`] — never a figure. No price is computed or //! stored here: the money is the read-time view over those counts and the plane's own card. //! @@ -23,8 +23,8 @@ //! model closed at once). This check reads the live view instead: nothing is priced at write, nothing //! is stored but counts, and spend the chain takes from other traffic counts too. -use super::{EngineHost, MeterPin}; use crate::billing::Usage; +use crate::plane::host::{EngineHost, MeterPin}; use busbar_contract::records::VirtualKey; use busbar_kernel_ledger::cost::{plane_fee_lane, split_plane_lane, PER_SESSION}; use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/crates/busbar-kernel/src/plane_host/tests/session_meter_tests.rs b/crates/busbar-kernel/src/governance/tests/session_meter_tests.rs similarity index 99% rename from crates/busbar-kernel/src/plane_host/tests/session_meter_tests.rs rename to crates/busbar-kernel/src/governance/tests/session_meter_tests.rs index ff920db6c6..37d5ff45e6 100644 --- a/crates/busbar-kernel/src/plane_host/tests/session_meter_tests.rs +++ b/crates/busbar-kernel/src/governance/tests/session_meter_tests.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! Tests for `crates/busbar-kernel/src/plane_host/session_meter.rs`. +//! Tests for `crates/busbar-kernel/src/governance/session_meter.rs`. use super::SessionAccount; use crate::plane_host::{EngineHost, EngineHostImpl}; diff --git a/crates/busbar-kernel/src/hooks/taps.rs b/crates/busbar-kernel/src/hooks/taps.rs index 2803e846dd..bd0b1955d2 100644 --- a/crates/busbar-kernel/src/hooks/taps.rs +++ b/crates/busbar-kernel/src/hooks/taps.rs @@ -85,7 +85,7 @@ impl<'a> StageShape<'a> { /// WEDGE 2e: the neutral, host-taking twin of core's `proxy_vocab::fire_stage_taps`. The caller passes /// the stage's tap slice (in wedge 3, one of `host.tap_hooks_response()`/`_routing()`/`_candidate()`) /// and the `host`; each tap's `groups:` scope is honored via -/// [`host.caller_in_hook_groups`](crate::plane_host::EngineHost::caller_in_hook_groups) — the neutral +/// [`host.caller_in_hook_groups`](crate::plane::host::EngineHost::caller_in_hook_groups) — the neutral /// fold of the `&App::groups_registry` self+ancestors walk — so this is byte-behavior-identical to /// core's raw-tree-walk version. pub fn fire_stage_taps( @@ -99,7 +99,7 @@ pub fn fire_stage_taps( // (empty = every caller). Resolved against this deployment's group registry through the host seam // (self + ancestors), never a raw `&App::groups_registry` tree. caller_group: Option<&str>, - host: &dyn crate::plane_host::EngineHost, + host: &dyn crate::plane::host::EngineHost, ) { fire_stage_taps_where(taps, shape, stage, signals, &|groups: &[String]| { host.caller_in_hook_groups(caller_group, groups) diff --git a/crates/busbar-kernel/src/host_services.rs b/crates/busbar-kernel/src/host_services.rs index b92c39a789..2156202d74 100644 --- a/crates/busbar-kernel/src/host_services.rs +++ b/crates/busbar-kernel/src/host_services.rs @@ -130,7 +130,7 @@ pub trait DestJudge: Send + Sync { /// A config commit: the deployment's destinations are now `d`. A judge that re-reads its /// metadata lists at every commit (as 1.5.5 did) takes them from here; the default keeps what /// it was built with. Raised through the egress-trust capability the root installs the - /// deployment's one guard behind (`plane_host::egress_trust::destinations_applied`), no static + /// deployment's one guard behind (`secure::destinations_applied`), no static /// of its own (door-only:static-seam). fn destinations_applied(&self, d: &crate::config::Destinations) { let _ = d; diff --git a/crates/busbar-kernel/src/ingress/duplex_ws.rs b/crates/busbar-kernel/src/ingress/duplex_ws.rs index 5f218e01a6..52a1d035f4 100644 --- a/crates/busbar-kernel/src/ingress/duplex_ws.rs +++ b/crates/busbar-kernel/src/ingress/duplex_ws.rs @@ -27,7 +27,7 @@ use futures::channel::mpsc::{channel as bounded, unbounded, Receiver, UnboundedS use futures::{SinkExt, StreamExt}; use crate::ingress::byte_duplex::{serve_messages, DuplexPlane}; -use crate::plane_host::{run_gauntlet_session, GauntletPlane, GauntletRequest}; +use crate::plane::host::{run_gauntlet_session, GauntletPlane, GauntletRequest}; /// How many inbound frames one accepted socket may hold for a reader that has not taken them yet. The /// queue is what stands between a client's write rate and this node's memory: unbounded, every frame a @@ -293,7 +293,7 @@ pub struct WsArrival { /// 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, + 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, diff --git a/crates/busbar-kernel/src/lib.rs b/crates/busbar-kernel/src/lib.rs index 43fcda211d..3ca1368955 100644 --- a/crates/busbar-kernel/src/lib.rs +++ b/crates/busbar-kernel/src/lib.rs @@ -300,6 +300,7 @@ pub mod residual_log; // breaker, one refusal (spec Part 3, the outbound table, step 1: "KERNEL | route (pool walk, member, // breaker)"). pub mod route; +pub mod route_input; pub mod route_tables; /// THE CONNECTOR'S TLS WRAP, as the kernel reaches it: this crate names no TLS library (THE DESIGN: /// TLS stays in the connector); the egress engine's https arm, the duplex `wss` dial and the diff --git a/crates/busbar-kernel/src/plane/dispatch_scope.rs b/crates/busbar-kernel/src/plane/dispatch_scope.rs new file mode 100644 index 0000000000..e7eef9ee31 --- /dev/null +++ b/crates/busbar-kernel/src/plane/dispatch_scope.rs @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE PER-DISPATCH ARENA a plane's host handles live in ([`DispatchScope`]) and its supporting +//! vocabulary (`SettleAdmission`, `EgressFaultDetail`). It stands beside the host capability +//! vocabulary ([`super::host`]), whose breaker, metering and govern slices take it (spec Part 3, +//! the HostSlots paragraph: "DispatchScope stands"). It names only [`busbar_contract::abi::hot`] and +//! `std`. + +use busbar_contract::abi::hot::{ + AdmissionId, EgressFailClass, EgressId, PipeId, Signal, StatusClass, VerifyLease, +}; +use std::sync::Mutex; + +/// The neutral FAILURE detail the host stashes when an `egress_open` fails, so the plane can read it +/// back through `egress_fault` and compose its OWN operator string. Held in the [`DispatchScope`] (not +/// a process global) so the bytes live exactly for the dispatch that produced them and are reclaimed +/// with it; the CAUSE (the flattened transport-error chain) and the URL are kept SEPARATE so each +/// plane chooses to include or strip the url. +#[derive(Clone, Debug)] +pub struct EgressFaultDetail { + /// The neutral failure class the plane maps to its own failover/refusal taxonomy. + pub class: EgressFailClass, + /// The observed status (0 when the failure was before a response head). + pub status: u16, + /// The flattened cause-message bytes (the transport-error chain), url-free. + pub cause: String, + /// The target url bytes, kept separate from the cause. + pub url: String, +} + +/// A reclaim action for a handle whose release is an explicit host call (close an egress, kill a +/// subprocess, drop a leadership lease). Phase 2 fills these with the real host-side calls; each runs +/// exactly once, when the [`DispatchScope`] drops. +type Reclaim = Box; + +/// A settle-capable breaker admission held in the arena — the leak-safety-critical resource of the +/// BREAKER family. Its `Drop` (run by [`DispatchScope::reclaim_all`]) releases the real single-flight +/// half-open probe, so a dropped/cancelled dispatch that never settled cannot wedge the cell in +/// `HalfOpen`; [`settle`](Self::settle) instead records the observed outcome against the breaker, +/// after which the guard's release `Drop` is a no-op. +/// +/// The concrete implementor (`plane_host::breaker::BreakerAdmission`) owns the real +/// `store::planes::Admission` RAII token; this trait lets the arena hold it behind a boxed object and +/// still drive its one settle, WITHOUT `scope` depending on the private breaker types. +pub trait SettleAdmission: Send { + /// Record the observed `signal` against the breaker exactly once and return the resulting ABI + /// [`StatusClass`]. Invoked at most once via [`DispatchScope::settle_admission`]; after it, the + /// guard's probe-release `Drop` becomes a no-op (the recorded outcome already consumed HalfOpen). + fn settle(&mut self, signal: &Signal) -> StatusClass; +} + +/// Which kind of host handle an [`Entry`] carries. Kept alongside the raw id so the Phase-2 fan-out +/// can resolve a plane-held handle-id back to its registered resource (e.g. `egress_write(id)`), and +/// so the same raw `u64` under two kinds never collides. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum HandleKind { + /// A breaker/failover admission grant. + Admission, + /// A one-shot / duplex governed egress. + Egress, + /// A subprocess (or raw-connection) duplex byte pipe. + Pipe, + /// A single-flight counterparty-verification leadership lease. + VerifyLease, +} + +/// One registered, reclaimable resource. Either an RAII guard whose `Drop` reclaims it (the shape the +/// real breaker `store::planes::Admission` takes in Phase 2 — boxed as `dyn Send` so this scaffold does +/// not reach a private type), or an explicit [`Reclaim`] closure the arena runs once on drop. +enum Resource { + /// An RAII guard: dropping the box runs the guard's `Drop`. Used for the real admission guard in + /// Phase 2 and by the arena's own unit test. + Guard(Box), + /// An explicit reclaim call, taken and run once on scope drop. + Closer(Option), + /// A breaker admission: dropping it releases the single-flight half-open probe (the leak-safety + /// reclaim), and it can be SETTLED once — recording the outcome — before the scope ends. Boxed + /// behind [`SettleAdmission`] so the arena never names the private breaker types. + Admission(Box), +} + +/// A registered handle: its kind, its raw id (what the plane holds), and the resource to reclaim. +struct Entry { + // `kind` + `raw` are the Phase-2 lookup key: the fan-out resolves a plane-held handle-id back to + // its registered resource (e.g. `egress_write(EgressId)` → this entry). The scaffold only RECLAIMS + // (which needs `res` alone), so they are write-only until that fan-out lands. + #[allow(dead_code)] + kind: HandleKind, + #[allow(dead_code)] + raw: u64, + res: Resource, +} + +/// The mutable interior of a [`DispatchScope`]. Guarded by a `Mutex` because every vtable fn holds +/// only a shared `&DispatchScope` (via the recovered `HostState`) yet must register/reclaim. +#[derive(Default)] +struct Registry { + entries: Vec, + /// Monotonic id source; `0` is the reserved `NONE` sentinel of every handle newtype, so ids start + /// at `1`. + next: u64, +} + +/// The per-dispatch-invocation arena of acquired host handles — the leak-safety keystone. +/// +/// Core opens ONE of these per dispatch, hands the plane a `HostCtx` that recovers a +/// `HostState` referencing it, and the plane's host calls register every handle they acquire here. On +/// `Drop` — whenever the dispatch future ends OR is dropped — [`reclaim_all`](Self::reclaim_all) +/// reclaims every registered handle, so a cancelled/dropped dispatch can never leak a bare host handle. +pub struct DispatchScope { + reg: Mutex, + /// The last failed `egress_open`'s neutral fault detail, stashed here so the plane reads it back + /// through `egress_fault` after a non-`Ok` open. Lives with the dispatch (reclaimed on drop); holds + /// only the LAST fault (the "read it immediately after the failing open" contract, like the govern + /// refusal reason). + egress_fault: Mutex>, +} + +impl Default for DispatchScope { + fn default() -> Self { + Self::new() + } +} + +impl DispatchScope { + /// Open an empty dispatch arena. + #[must_use] + pub fn new() -> Self { + DispatchScope { + reg: Mutex::new(Registry::default()), + egress_fault: Mutex::new(None), + } + } + + /// STASH the neutral fault detail of a just-failed `egress_open`, so a following `egress_fault` + /// hands it to the plane. Overwrites any prior unread fault (the last-fault contract). + pub fn stash_egress_fault(&self, detail: EgressFaultDetail) { + *self.egress_fault.lock().unwrap_or_else(|e| e.into_inner()) = Some(detail); + } + + /// TAKE (and clear) the stashed egress fault, or `None` when none is pending. Consuming so a stale + /// fault cannot be re-read against a later, unrelated open. + pub fn take_egress_fault(&self) -> Option { + self.egress_fault + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take() + } + + /// Poison-recovering lock: a panic mid-register must not wedge the arena for the reclaim path, so + /// recover the guard rather than cascade the poison (same discipline as `store::*_recover`). + fn lock(&self) -> std::sync::MutexGuard<'_, Registry> { + self.reg.lock().unwrap_or_else(|e| e.into_inner()) + } + + /// Allocate the next non-zero raw handle id. + fn next_raw(reg: &mut Registry) -> u64 { + reg.next += 1; + reg.next + } + + /// Register a breaker admission as an RAII `guard` (the real `store::planes::Admission` in Phase 2; + /// a test guard here). Its `Drop` runs the actual release when the scope drops. + pub fn register_admission(&self, guard: Box) -> AdmissionId { + let mut reg = self.lock(); + let raw = Self::next_raw(&mut reg); + reg.entries.push(Entry { + kind: HandleKind::Admission, + raw, + res: Resource::Guard(guard), + }); + AdmissionId(raw) + } + + /// Register a settle-capable breaker admission (the real `store::planes::Admission` RAII token, + /// wrapped so it can also be settled) — the BREAKER family's leak-safety keystone. Returns the + /// arena's [`AdmissionId`]; the plane never holds the bare probe. On scope drop the guard's `Drop` + /// releases the probe; a prior [`settle_admission`](Self::settle_admission) makes that a no-op. + pub fn register_settling_admission(&self, guard: Box) -> AdmissionId { + let mut reg = self.lock(); + let raw = Self::next_raw(&mut reg); + reg.entries.push(Entry { + kind: HandleKind::Admission, + raw, + res: Resource::Admission(guard), + }); + AdmissionId(raw) + } + + /// Settle the breaker admission `id`: record the observed `signal` against the breaker and return + /// the resulting ABI [`StatusClass`], REMOVING the entry (so the guard's probe-release `Drop` runs + /// now, a no-op after the record). Returns `None` when no live admission carries `id` — a stale or + /// already-settled handle the caller maps to `Gone`. Recording runs with the lock released, matching + /// [`reclaim_all`](Self::reclaim_all)'s discipline. + pub fn settle_admission(&self, id: AdmissionId, signal: &Signal) -> Option { + if id.is_none() { + return None; + } + let entry = { + let mut reg = self.lock(); + let pos = reg + .entries + .iter() + .position(|e| e.raw == id.0 && matches!(e.res, Resource::Admission(_)))?; + reg.entries.remove(pos) + }; + match entry.res { + Resource::Admission(mut guard) => { + let class = guard.settle(signal); + drop(guard); // release the probe (a no-op now the outcome is recorded) + Some(class) + } + // Unreachable: the `matches!` above selected an `Admission` entry. + _ => None, + } + } + + /// Register an open governed egress with the `reclaim` that closes it (Phase 2: `egress_close`). + pub fn register_egress(&self, reclaim: Reclaim) -> EgressId { + let mut reg = self.lock(); + let raw = Self::next_raw(&mut reg); + reg.entries.push(Entry { + kind: HandleKind::Egress, + raw, + res: Resource::Closer(Some(reclaim)), + }); + EgressId(raw) + } + + /// Register a subprocess/raw-connection pipe with the `reclaim` that kills/closes it (Phase 2). + pub fn register_pipe(&self, reclaim: Reclaim) -> PipeId { + let mut reg = self.lock(); + let raw = Self::next_raw(&mut reg); + reg.entries.push(Entry { + kind: HandleKind::Pipe, + raw, + res: Resource::Closer(Some(reclaim)), + }); + PipeId(raw) + } + + /// Register a verification leadership lease with the `reclaim` that releases it (Phase 2). + pub fn register_lease(&self, reclaim: Reclaim) -> VerifyLease { + let mut reg = self.lock(); + let raw = Self::next_raw(&mut reg); + reg.entries.push(Entry { + kind: HandleKind::VerifyLease, + raw, + res: Resource::Closer(Some(reclaim)), + }); + VerifyLease(raw) + } + + /// How many handles are currently registered (test/observability hook). + #[must_use] + pub fn registered(&self) -> usize { + self.lock().entries.len() + } + + /// Reclaim EVERY registered handle NOW, in reverse (LIFO) acquisition order: drop each guard + /// (running its real `Drop`) and run each closer exactly once. Idempotent — a second call finds an + /// empty registry. Called by `Drop`; exposed so a test can assert synchronous reclaim on abort. + /// + /// EVERY ENTRY GETS ITS OWN BOUNDARY. Reclaiming runs code this loop does not own — a closer that + /// kills a subprocess, a guard's real `Drop` — and one of those panicking must not cost the + /// others. Unbounded, a single panic here skips every remaining entry, leaking exactly what this + /// arena exists to reclaim; and because the loop is reached from `Drop`, that panic unwinding out + /// of a drop already running during an unwind aborts the process. The boundary contains it to the + /// entry that raised it, and the loop carries on. + pub fn reclaim_all(&self) { + // Take the entries OUT under the lock, then reclaim with the lock released so a reclaim that + // re-enters the arena cannot deadlock. + let drained: Vec = { + let mut reg = self.lock(); + std::mem::take(&mut reg.entries) + }; + for entry in drained.into_iter().rev() { + let kind = entry.kind; + // `AssertUnwindSafe`: nothing observable survives a panicking reclaim. The entry is + // already out of the registry and is consumed here whichever way it ends, so there is no + // half-updated state left for a later reader to see. + let reclaimed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + match entry.res { + Resource::Guard(g) => drop(g), + Resource::Admission(g) => drop(g), // Drop releases the single-flight probe. + Resource::Closer(Some(reclaim)) => reclaim(), + Resource::Closer(None) => {} + } + })); + if reclaimed.is_err() { + tracing::warn!( + handle_kind = ?kind, + "a host-handle reclaim panicked; the remaining handles were reclaimed anyway" + ); + } + } + } +} + +impl Drop for DispatchScope { + fn drop(&mut self) { + self.reclaim_all(); + } +} + +#[cfg(test)] +#[path = "tests/dispatch_scope_tests.rs"] +mod tests; diff --git a/crates/busbar-kernel/src/plane/door.rs b/crates/busbar-kernel/src/plane/door.rs index c90c6c86fb..6c48a81bfa 100644 --- a/crates/busbar-kernel/src/plane/door.rs +++ b/crates/busbar-kernel/src/plane/door.rs @@ -393,13 +393,13 @@ fn admission(slot: &dyn std::any::Any) -> Option( - slots: &dyn crate::plane_host::PlaneSlots, + slots: &dyn crate::plane::host::PlaneSlots, ) -> Option> { slots.plane_slot(door(I)?.reg.key).cloned() } fn named_def_list( - slots: &dyn crate::plane_host::PlaneSlots, + slots: &dyn crate::plane::host::PlaneSlots, ) -> Vec { let (Some(d), Some(slot)) = (door(I), slot_of::(slots)) else { return Vec::new(); @@ -416,7 +416,7 @@ fn named_def_list( } fn named_def_get( - slots: &dyn crate::plane_host::PlaneSlots, + slots: &dyn crate::plane::host::PlaneSlots, name: &str, ) -> Option { let d = door(I)?; @@ -431,7 +431,7 @@ fn named_def_get( } fn registry_contains( - slots: &dyn crate::plane_host::PlaneSlots, + slots: &dyn crate::plane::host::PlaneSlots, name: &str, ) -> bool { let Some(slot) = slot_of::(slots) else { @@ -441,7 +441,7 @@ fn registry_contains( .is_some_and(|s| s.section.contains_def(name)) } -fn reresolve_gates(next: &mut dyn crate::plane_host::ContainerGateSink) { +fn reresolve_gates(next: &mut dyn crate::plane::host::ContainerGateSink) { let Some(d) = door(I) else { return; }; @@ -711,10 +711,11 @@ struct HookRow { admission: fn(&dyn std::any::Any) -> Option, build: fn(&BuildCtx) -> Option>, config_validate: fn(&str, &serde_json::Value) -> Result<(), String>, - named_def_list: fn(&dyn crate::plane_host::PlaneSlots) -> Vec, - named_def_get: fn(&dyn crate::plane_host::PlaneSlots, &str) -> Option, - registry_contains: fn(&dyn crate::plane_host::PlaneSlots, &str) -> bool, - reresolve_gates: fn(&mut dyn crate::plane_host::ContainerGateSink), + named_def_list: fn(&dyn crate::plane::host::PlaneSlots) -> Vec, + named_def_get: + fn(&dyn crate::plane::host::PlaneSlots, &str) -> Option, + registry_contains: fn(&dyn crate::plane::host::PlaneSlots, &str) -> bool, + reresolve_gates: fn(&mut dyn crate::plane::host::ContainerGateSink), parse_section: fn(&serde_yaml::Value) -> Result, String>, default_section: fn() -> Box, admin_routes: fn(&dyn std::any::Any) -> Vec, diff --git a/crates/busbar-kernel/src/plane/host.rs b/crates/busbar-kernel/src/plane/host.rs new file mode 100644 index 0000000000..0b06b0f271 --- /dev/null +++ b/crates/busbar-kernel/src/plane/host.rs @@ -0,0 +1,1527 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE HOST CAPABILITY VOCABULARY a plane is served through: [`EngineHost`] as the sum of its +//! capability slices (breaker, lane pool, clock, telemetry, journal, mount, registry, hook config, +//! budget, identity, admission, completion), [`PlaneSlots`] (a plane's per-generation runtime +//! object, read off the engine snapshot), the gauntlet sequence ([`run_gauntlet`]) and the runner +//! registries the kernel loop installs into. + +use crate::breaker::CanonicalSignal; +use crate::plane::approvals::Sealer; +use crate::plane::calllog::CallInput; +use crate::plane::dispatch_scope::DispatchScope; +use crate::store::Unavailable; +use crate::trust::validate::{Lapsed, Standing}; +use crate::trust::TrustState; +use busbar_contract::abi::hot::{AdmissionId, Signal, StatusClass}; +use busbar_contract::auth::{AuthPrincipal, IdentityRefusal}; +use busbar_contract::records::{PlaneRequestCtx, VirtualKey}; +use std::sync::Arc; + +/// The outcome of a refusal-fidelity admit driven over the host `govern_admit_reason` seam. +#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] +pub enum GovAdmit { + /// Admitted — the RAII grant is registered in the arena the caller passed. + Admitted, + /// A limit blocked — the RENDERED reason (byte-identical to the plane's own + /// `format!("{blocked:?}")`) and the block's recovery floor in whole seconds. + Blocked { + /// The rendered reason bytes the host copied out (the exact `{blocked:?}` the plane surfaces). + reason: String, + /// The recovery floor in whole seconds (`0` when the block does not self-recover / never rolls). + retry_after_secs: u64, + }, +} + +/// The verdict of a request-admission gate fired over the host `gate_decide` seam. +#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] +pub enum GateOutcome { + /// No gate objected (or none is attached) — the request proceeds. + Proceed, + /// A gate refused the request. Reconstructed from the `GateVerdictOut` header + the copied-out + /// buffers, byte-identical to the in-process `GateVerdict::Reject`. + Reject { + /// The hook's refusal status, already clamped to the 4xx band by the gate. + status: u16, + /// The hook's own refusal message (empty on a fail-closed refusal). + message: String, + /// The transport/policy name, for the audit row and the log line (empty on a fail-closed refusal). + hook: String, + }, +} + +/// The verdict of a request-admission TRANSFORM (`prompt: rw` rewrite) chain fired over the host +/// `transform_over` seam — the TAP/observe-transform half of the hook surface, the twin of +/// [`GateOutcome`] for the rewrite pass. When no rewrite hook is attached the plane never calls the +/// seam (it guards on [`AdmissionHost::tap_attached`]), so the request/response path is BYTE-IDENTICAL +/// to a deployment with the seam absent: the tap is a no-op absent hooks. +#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] +pub enum TransformVerdict { + /// The transform chain ran. `args_json` is the (possibly rewritten) payload the plane should send + /// upstream; `applied` is `true` IFF a hook actually committed a rewrite (so the plane can keep + /// its original bytes untouched — and therefore byte-identical — when no rewrite landed, even + /// though a chain of purely-abstaining hooks fired). Reconstructed byte-for-byte from the same + /// serde_json round-trip the gate seam uses (`preserve_order` OFF ⇒ a `Value` object is a + /// sorted-stable `BTreeMap`). + Proceed { + /// Whether ANY hook in the chain committed a rewrite to the payload. + applied: bool, + /// The payload bytes to send upstream — the rewritten `arguments`/`params` when `applied`, or + /// a faithful re-serialization of the original otherwise (the plane uses its own bytes then). + args_json: Vec, + }, + /// A `prompt: rw` gate REJECTED the request on the transform path (reject > rewrite > abstain). + /// Same clamped/sanitized semantics as a decide-path reject, reconstructed identically to + /// [`GateOutcome::Reject`]. + Reject { + /// The hook's refusal status, already clamped to the 4xx band. + status: u16, + /// The hook's own refusal message (empty on a fail-closed refusal). + message: String, + /// The transport/policy name, for the audit row and the log line (empty on a fail-closed refusal). + hook: String, + }, +} + +/// What could be established about a presented bearer's RFC 8707 audience binding — the outcome of +/// the host `identity_audience_binding` pre-filter, for credentials busbar did not mint. +/// +/// Relocated here from `busbar_kernel::auth::audience` so a plane reads the pre-filter verdict without +/// naming the core auth module; the binding JUDGEMENT (which reaches core's governance token prefix) +/// stays core behind [`EngineHost::identity_audience_binding`]. Core re-exports this at +/// `crate::auth::audience::Binding`, so its own callers and the enum's variants are unchanged. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AudienceBinding { + /// A busbar-signed token. The real audience check happens in the verifier, which has the + /// signature and the claims; the pre-filter must not pre-judge it. + Deferred, + /// A JWT whose `aud` includes the expected value. Not an admission — the chain still verifies it. + Bound, + /// A JWT whose `aud` does not include the expected value, or which carries no `aud` at all. Both + /// are refused for the same reason: minted for someone else, or for nobody in particular. + Mismatch, + /// Not a JWT and not a busbar token: nothing to read. Refused. + Opaque, +} + +/// The raw wire outcome of a host-driven completion: the pipeline's HTTP status and body bytes, +/// for the plane to shape into its protocol's own result. Neutral — no axum `Response`, no `App`. +#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] +pub struct HostCompletion { + /// The pipeline's HTTP status. + pub status: u16, + /// The pipeline's response body bytes (bounded by the `max_body_bytes` the caller passed). + pub body: bytes::Bytes, +} + +/// Why the host produced no completion. NEUTRAL: the kernel names the fact, and the plane that asked +/// words the refusal in its own vocabulary (lean-core: no plane word is a kernel literal). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CompletionRefusal { + /// No plane installed the resolved-completion ingress, so there is no dialect to drive. + NotInstalled, + /// The pipeline answered, but its body could not be read within `max_body_bytes`; carries the + /// read error's text. + BodyUnread(String), +} + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// THE GAUNTLET SEAM — one shared request sequence every protocol plane rides. +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// The neutral per-request facts of ONE gauntlet traversal that the SHARED sequence +/// ([`run_gauntlet`]) reads and threads — the resolved identity, the destination the pre-admission +/// verify judges, and the correlation/timing a plane's stage-6 record joins on. Everything protocol- +/// or dialect-specific (the parsed body, the dialect handler, the wire framing, the plane's engine) +/// lives in the plane's own [`GauntletPlane`] value, NEVER here — so this names no plane type. +pub struct GauntletRequest<'a> { + /// Stage 1 — the resolved caller identity/scope, threaded from the auth layer that ran upstream. + pub gov: &'a busbar_contract::records::PlaneRequestCtx, + /// The destination key the pre-admission `verify_destination` judges — a model for the LLM plane, + /// a tool/server for the MCP plane. Opaque to the shared sequence; each plane spells its meaning. + pub destination: &'a str, + /// The per-request correlation id the plane's stage-6 audit record joins on (single-accounting). + pub correlation_id: u64, + /// The header-arrival epoch (whole seconds) the request was admitted at — the metering window base. + pub charged_at: u64, + /// The monotonic start instant for the request-duration metric. + pub started: std::time::Instant, +} + +/// The outcome of the pre-admission destination verification (stage 2). +pub enum VerifyOutcome { + /// The destination is permitted; the sequence proceeds to the plane's `drive`. + Proceed, + /// The destination is refused. The plane returns its OWN already-finished, protocol-native + /// response (metrics/webhook already emitted plane-side); [`run_gauntlet`] returns it verbatim, + /// so refusal shaping stays byte-identical to the plane's in-place rejection. + Refuse(axum::response::Response), +} + +/// THE LOOP'S ANSWER SHAPE at the unified boundary (DECISIONS #28), plane-neutral. +/// +/// When a rider runs on the kernel Teller loop (`busbar_kernel::teller::run_unit`), the loop returns +/// only the money/lifecycle `Ended`; the request's ANSWER bytes are read back out of the plane's own +/// `ctx.key`-keyed binding table (the admin template — `AdminUnitTable`). `PlaneAnswer` is what a +/// rider stashes into that table and the outer async handler serves. Two shapes, per #28: +/// +/// - [`PlaneAnswer::Unary`] — buffered status + headers + body, crosses the sync channel and settles +/// at Encode on the final byte count (the admin cleanliness caller and every unary verb, including +/// an SSE body materialised after dispatch). +/// - [`PlaneAnswer::Live`] — a live body the outer handler serves directly, governed at admit +/// (auth/verify/approve/admit/audit ran unary), bypassing the Encode-emits-bytes path (the llm +/// RouteLeg emits this). +/// +/// NEUTRAL BY CONSTRUCTION: nothing here names a plane, a dialect or a verb, and it lives in the +/// neutral substrate tier — NOT in `busbar-kernel`, which keeps naming zero planes and zero answer +/// shapes. It coexists with the shipped [`crate::plane_host`] `PlaneDispatch`-role carriers (#28): a +/// plane's arena (`DispatchScope`) rides its own loop value, so nothing in this shape carries it. +pub enum PlaneAnswer { + /// Buffered: status, headers, body bytes. Settles at Encode on the final byte count. + Unary( + axum::http::StatusCode, + axum::http::HeaderMap, + axum::body::Bytes, + ), + /// A live body the outer async handler serves directly, governed at admit. + Live(axum::response::Response), +} + +impl PlaneAnswer { + /// Materialise either shape into the one response the outer handler serves. `Unary` is rebuilt + /// status-for-header-for-byte so a buffered answer serves byte-identically to the response the + /// plane shaped; `Live` is handed through untouched. + pub fn into_response(self) -> axum::response::Response { + match self { + PlaneAnswer::Unary(status, headers, body) => { + let mut resp = axum::response::Response::new(axum::body::Body::from(body)); + *resp.status_mut() = status; + *resp.headers_mut() = headers; + resp + } + PlaneAnswer::Live(resp) => resp, + } + } +} + +/// THE PLANE-NEUTRAL IN-FLIGHT ANSWER TABLE (DECISIONS #28), keyed by the unit's `ctx.key`. +/// +/// The kernel Teller loop (`busbar_kernel::teller::run_unit`) returns only the money/lifecycle +/// `Ended`; a rider stashes its [`PlaneAnswer`] here at the step that produced it (Route, or a +/// pre-charge refusal) and the outer async handler reads it back by key AFTER the loop returns — +/// mirroring `AdminUnitTable` (the admin template, `admin_mount.rs`). NEUTRAL BY CONSTRUCTION: the +/// key is a bare `u64` (the raw `ctx.key`), so this names no plane, no dialect and no verb, and it +/// lives in the neutral substrate tier — every rider (mcp first, then a2a/voice/llm) reuses this ONE +/// table with zero new seam code. +#[derive(Default)] +pub struct PlaneInFlight { + slots: std::sync::Mutex>>, +} + +impl PlaneInFlight { + /// An empty table. + #[must_use] + pub fn new() -> Self { + PlaneInFlight::default() + } + + /// Open a slot for a unit about to run. Idempotent; a re-open clears any prior answer. + pub fn open(&self, key: u64) { + self.slots + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(key, None); + } + + /// Record the unit's answer, at the step that produced it. + pub fn store(&self, key: u64, answer: PlaneAnswer) { + self.slots + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(key, Some(answer)); + } + + /// Take the unit's answer out and forget the slot, after the loop returned. `None` if the unit + /// produced no answer (a path that never reached the step that stores one). + #[must_use] + pub fn take(&self, key: u64) -> Option { + self.slots + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&key) + .flatten() + } +} + +/// A protocol plane's contribution to the shared gauntlet: the pre-admission destination check +/// (stage 2, sync) and the byte-identical engine that admits, routes, meters and finishes the +/// request (stages 4+5, async). The SHARED sequence ([`run_gauntlet`]) owns only stage 1 (identity, +/// already resolved and threaded via `req.gov`) and the stage-2→drive ORDER — verify strictly before +/// any charge, so nothing can reject an already-charged request. The sequence pulls NOTHING out of +/// `drive`: the plane's admission/route/metering/finish stay inside it, byte-identical. Names are +/// neutral; a plane implements this in its own crate (`busbar-mcp`/`busbar-a2a`) or in core (the LLM +/// native plane) identically — they are siblings on this one seam. +#[async_trait::async_trait] +pub trait GauntletPlane: Send + Sync { + /// STAGE 2 — pre-admission destination verification. Sync; runs BEFORE `drive`. `Proceed` clears + /// the request to admission; `Refuse` carries the plane's OWN finished, protocol-native rejection. + fn verify_destination(&self, req: &GauntletRequest<'_>) -> VerifyOutcome; + + /// STAGES 4+5 — the plane's OWN engine: budget-admission, route/failover, egress, and the plane's + /// own metering, returning the (possibly streaming) response. Byte-identical to the plane's + /// in-place dispatch. Takes `self: Box` so the plane moves its owned per-request payload + /// (body/parsed form/grant) into the engine; object-safe, so `run_gauntlet` drives it as `dyn`. + async fn drive(self: Box, req: GauntletRequest<'_>) -> axum::response::Response; + + /// THIS PLANE'S CAPABILITY KEY, for the composition-tier host-selection seam + /// ([`register_gauntlet_runner`]). `None` (the DEFAULT) means "run me on the substrate loop, + /// exactly as today" — so with nothing overridden and nothing registered, every path is + /// byte-identical to the shipped release. A plane opts onto the unified kernel loop by returning + /// its key AND the composition root registering a runner under it (an oracle-gated flip, #29). + /// NEUTRAL: the trait names no plane; each plane spells its own key, and the seam only ever + /// compares strings. + fn capability_key(&self) -> Option<&str> { + None + } +} + +/// The successful OPEN-PASS ADMISSION result of the session gate in [`run_gauntlet_session`] — the request cleared the verify-before- +/// charge gate. Carries the per-request `correlation_id` so a SESSION opener ([`run_gauntlet_session`]) +/// can join its own later durable/audit rows on it. A one-shot [`run_gauntlet`] discards it and proceeds +/// straight to `drive`; a session opener returns it to the plane, which then reserves/binds/opens its +/// live carrier AFTER (nothing charged before the gate cleared). +#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Admitted { + /// The per-request correlation id the caller threads into its stage-6 / durable-session record. + pub correlation_id: u64, +} + +// THE SUBSTRATE TELLER LOOP IS GONE (W2.b, DECISIONS #28). The gauntlet's old seat on that loop — the +// `GauntletAdapter` (a pass-through `TellerPlane`), `gauntlet_unit`, and the `admit_open` open-pass gate — +// was deleted with `busbar-substrate/src/teller/`. Every shipped plane now rides the UNIFIED +// `busbar_kernel::teller` loop via a registered runner on the host-selection seam below; the fallback for +// an unregistered/test plane is the inline verify-then-drive in `run_gauntlet[_session]`, byte-identical +// to what that adapter loop produced (it wrapped nothing). + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// THE COMPOSITION-TIER HOST-SELECTION SEAM (loop unification, DECISIONS #28). +// +// A per-capability-keyed registry of KERNEL-LOOP runners the composition root installs at boot +// (mirrors the admin-mount-seam, busbar-core/src/admin/seam.rs). The kernel loop lives in +// `busbar-kernel`, which this tier does not depend on, so the runners are FN-POINTERS root injects. +// The gauntlet free fns below consult the registry by the request's plane capability key: if a +// runner is registered, route to it; UNSET (the default) runs today's substrate loop, byte-identical. +// NEUTRAL: the seam names no plane — it is a `&str`-keyed table, exactly as `AdminUnitTable` is +// `ctx.key`-keyed. Per-plane flip = registering that one plane's runner, oracle-gated (#29). +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// A kernel-loop one-shot runner, as the composition root injects it: the kernel-loop twin of +/// [`run_gauntlet`], erased to a fn-pointer so this tier need not name `busbar-kernel`. +pub type GauntletRunner = for<'a> fn( + GauntletRequest<'a>, + Box, +) -> std::pin::Pin< + Box + Send + 'a>, +>; + +/// A kernel-loop session runner, the kernel-loop twin of [`run_gauntlet_session`]. +pub type SessionRunner = for<'a> fn( + GauntletRequest<'a>, + Box, +) -> Result; + +static ONE_SHOT_RUNNERS: std::sync::RwLock< + std::collections::BTreeMap<&'static str, GauntletRunner>, +> = std::sync::RwLock::new(std::collections::BTreeMap::new()); + +static SESSION_RUNNERS: std::sync::RwLock> = + std::sync::RwLock::new(std::collections::BTreeMap::new()); + +/// Register a kernel-loop one-shot runner for a plane capability key. Composition root only, at boot. +/// Last-write-wins and idempotent. Registering a key is the per-plane FLIP onto the unified loop. +pub fn register_gauntlet_runner(key: &'static str, runner: GauntletRunner) { + ONE_SHOT_RUNNERS + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(key, runner); +} + +/// Register a kernel-loop session runner for a plane capability key. Composition root only, at boot. +pub fn register_session_runner(key: &'static str, runner: SessionRunner) { + SESSION_RUNNERS + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(key, runner); +} + +/// Whether a one-shot kernel-loop runner is registered for this capability key — i.e. whether that +/// plane has been FLIPPED onto the unified kernel loop. The read-side twin of +/// [`register_gauntlet_runner`], for a composition-root boot assertion or a per-plane cutover +/// regression test. NEUTRAL: a bare `&str` lookup that names no plane. +#[must_use] +pub fn gauntlet_runner_registered(key: &str) -> bool { + one_shot_runner(key).is_some() +} + +/// Whether a SESSION kernel-loop runner is registered for this capability key — i.e. whether that +/// session plane (voice/streaming) has been FLIPPED onto the unified kernel loop's session admit. The +/// read-side twin of [`register_session_runner`], for a composition-root boot assertion or a per-plane +/// cutover regression test. NEUTRAL: a bare `&str` lookup that names no plane. +#[must_use] +pub fn session_runner_registered(key: &str) -> bool { + session_runner(key).is_some() +} + +fn one_shot_runner(key: &str) -> Option { + ONE_SHOT_RUNNERS + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .get(key) + .copied() +} + +fn session_runner(key: &str) -> Option { + SESSION_RUNNERS + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .get(key) + .copied() +} + +/// THE SHARED GAUNTLET SEQUENCE — the ONE request path every protocol plane rides. It consults the +/// host-selection seam first: a plane whose capability key has a kernel-loop runner registered rides +/// the UNIFIED `busbar_kernel::teller` loop; every other plane (a not-yet-flipped or test plane) runs +/// the inline verify-then-`drive` fallback below, byte-identical to the deleted substrate Teller loop +/// this seam used to ride (that loop wrapped nothing — see the fallback comment). +/// Stage 1 identity is already resolved (threaded via `req.gov`); the path runs the plane's +/// `verify_destination` at Verify, in the correct PRE-ADMISSION position, and only if it proceeds the +/// plane's `drive` at Route (its own byte-identical engine + metering). Returns the plane's (possibly +/// streaming) response verbatim. The plane owns admission/route/metering/finish; the loop owns solely +/// the order (nothing may reject after a charge) — so all planes enforce that invariant in ONE place. +pub async fn run_gauntlet( + req: GauntletRequest<'_>, + plane: Box, +) -> axum::response::Response { + let selected = plane.capability_key().and_then(one_shot_runner); + if let Some(runner) = selected { + return runner(req, plane).await; + } + // NO kernel-loop runner registered for this key. With W2.b every shipped plane is flipped onto the + // unified kernel loop, so this arm is the DEFAULT only for a plane not (yet) flipped or a test + // plane. It runs the plane's OWN two-stage sequence inline — verify STRICTLY before drive — which + // is BYTE-IDENTICAL to the (now-deleted) substrate Teller loop the gauntlet used to ride: that loop + // proceeded every step, opened an empty hold, settled nothing, and returned the plane's `drive` + // response (or its own pre-charge refusal) verbatim. The loop wrapped nothing, so inlining it + // removes only the redundant loop, not any behaviour (the kernel-vs-substrate shadow-compare proved + // the two produced identical bytes before the substrate loop was removed). + match plane.verify_destination(&req) { + VerifyOutcome::Refuse(resp) => resp, + VerifyOutcome::Proceed => plane.drive(req).await, + } +} + +/// THE SESSION SIBLING of [`run_gauntlet`] — the OPEN-PASS admission for a live, session-oriented plane +/// (voice/duplex) that has no one-shot `drive`-shaped Response to return. It runs the SAME +/// verify-STRICTLY-before-charge gate (a registered kernel-loop session runner, else the inline +/// fallback) and returns the [`Admitted`] result instead of +/// driving a request: the plane's own reserve/bind/open + socket bind proceed only on `Ok`, so a `Refuse` +/// costs ZERO bytes and ZERO charge (nothing opened before the gate cleared). Distinct from +/// [`run_gauntlet`] (one Response) but a TRUE sibling — they share the one verify-before-charge gate, so +/// a refactor can neither inline nor foreclose this opener, and both enforce that one order. +/// +/// Synchronous: the admission gate is `verify_destination` (sync), so a session opener (a sync +/// `begin_session`) calls this directly — there is no async `drive` leg on the session path. +/// +/// (`result_large_err`: the `Err` is the plane's OWN finished refusal `Response`, carried BY VALUE so +/// refusal shaping stays byte-identical to [`run_gauntlet`]'s verbatim return — boxing it would diverge +/// the two siblings on the type they carry a refusal in.) +#[allow(clippy::result_large_err)] +pub fn run_gauntlet_session( + req: GauntletRequest<'_>, + plane: Box, +) -> Result { + let selected = plane.capability_key().and_then(session_runner); + if let Some(runner) = selected { + return runner(req, plane); + } + // NO kernel-loop session runner registered for this key — the inline open-pass admit, BYTE-IDENTICAL + // to the (now-deleted) substrate `admit_open`: it ran the SAME verify-STRICTLY-before-charge gate + // through the substrate Teller's `open_unit` over a pass-through adapter that opened an empty hold + // and settled nothing, returning `Admitted` (the request's own correlation) on a pass or the plane's + // OWN finished refusal verbatim. Inlining removes only the redundant loop: on `Refuse` NOTHING is + // opened (zero bytes, zero charge); on `Proceed` the caller opens its own carrier next. + match plane.verify_destination(&req) { + VerifyOutcome::Refuse(resp) => Err(resp), + VerifyOutcome::Proceed => Ok(Admitted { + correlation_id: req.correlation_id, + }), + } +} + +/// The `plane_slots` companion key under which a plane's ALWAYS-PRESENT per-generation runtime object +/// is carried — DERIVED from the plane's own decl `key` by the neutral `":runtime"` convention, +/// so core spells no plane token. It is deliberately distinct from the plane's config-conditional +/// dispatch slot (carried under the bare decl key): the runtime bundle exists on every generation +/// whereas the dispatch slot is absent when the plane's config block is unspecified, so folding them +/// onto one key would change the bare key's presence semantics (and the dispatch table `build_dispatch` +/// derives from it). Named by both core's `appbuild` (which composes the slot) and the owning plane +/// (which reads it back through [`EngineHost::plane_slot`]) — each passes its decl key and gets the +/// SAME interned `&'static str`, so it lives in the neutral substrate rather than either crate. +/// +/// Interned process-lifetime (leaked once per distinct key, bounded by the plane count) so the +/// companion key is a stable `&'static str` fit for the `plane_slots` map's key type without either +/// caller holding a hard-coded literal. +pub fn runtime_slot_key(plane_key: &str) -> &'static str { + static INTERNED: std::sync::Mutex> = + std::sync::Mutex::new(std::collections::BTreeMap::new()); + let composed = format!("{plane_key}:runtime"); + let mut interned = INTERNED.lock().unwrap_or_else(|e| e.into_inner()); + if let Some(k) = interned.get(&composed) { + return k; + } + let leaked: &'static str = Box::leak(composed.clone().into_boxed_str()); + interned.insert(composed, leaked); + leaked +} + +/// A source of freshly live-bound hosts: each call returns a host reading the current snapshot, +/// so a config swap between calls is seen. Handed to transports that re-mint per frame. +pub type LiveHostFactory = std::sync::Arc std::sync::Arc + Send + Sync>; + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// OPAQUE GOVERNANCE HANDLES (App-retype WEDGE 2) — the neutral carrier tokens a plane HOLDS on its +// per-request sink so the sink's field types stop naming `busbar_kernel::governance::GovState` / +// `busbar_kernel::cost::CostModel` / `busbar_kernel::governance::AdmitGrant`. Each wraps an +// `Arc` the host minted over the concrete engine value; the plane never +// introspects it — it hands [`GovHandle`]/[`CostHandle`] BACK to the metering seams +// ([`EngineHost::meter_ledger`]/[`EngineHost::meter_series`]), which downcast host-side. This keeps +// the byte-identical `sink.gov`/`sink.cost` the plane minted (a custom test cost, the live app cost — +// whichever the sink was built from), NOT a re-read of the host snapshot. +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// An OPAQUE, cheaply-cloned (one `Arc` bump) handle to the deployment's governance state — minted +/// host-side ([`EngineHost::governance`]) and consumed host-side ([`EngineHost::meter_ledger`] / +/// [`EngineHost::meter_series`]). The plane holds it on its per-request sink WITHOUT naming +/// `busbar_kernel::governance::GovState`. +#[derive(Clone)] +pub struct GovHandle(pub Arc); + +/// The cost-model twin of [`GovHandle`] — an opaque handle to a resolved `CostModel`. KERNEL-SIDE +/// ONLY: it rides inside a [`MeterPin`] and no plane-facing signature names it (#43). +#[derive(Clone)] +pub struct CostHandle(pub Arc); + +/// THE METER PIN — what a plane holds on its per-request sink so its counts land against the +/// governance state AND the rate card that were in force when the request was admitted. OPAQUE: the +/// fields are private and only the kernel's host pins a card ([`BudgetHost::meter_pin`]), so a plane +/// carries its request's pricing context without naming a cost or price type (#43: a plane is +/// PRICING-BLIND; #71: its money obligation is one raw count per class). The plane hands the pin BACK +/// to the metering seams ([`BudgetHost::meter_ledger`], [`BudgetHost::rate_headroom`], +/// [`BudgetHost::budget_state`], [`BudgetHost::meter_series`]), which read it kernel-side. A plane +/// calls [`BudgetHost::meter_series`] UNCONDITIONALLY with the counts it observed (#43: no branch, no +/// knowledge of billing state) — #42's billing-off posture is the VIEW's job (the served figure reads +/// zero), not the write's; nothing kernel-side gates the row on the pinned card any more (that gate, +/// `meter_series_billed`, was dead code with no production caller after d09c2e0b0 and is gone). +/// +/// A plane-side host (a test double) pins NO card: its pin answers the no-card posture everywhere. +#[derive(Clone)] +pub struct MeterPin { + pub(crate) gov: GovHandle, + pub(crate) cost: CostHandle, +} + +impl MeterPin { + /// Pin `gov` and the card `cost` names. Kernel-side; a TEST that hand-builds a card pins it here. + #[cfg(any(test, feature = "test-support"))] + pub fn new(gov: GovHandle, cost: CostHandle) -> Self { + MeterPin { gov, cost } + } + + /// The governance handle the pin carries — what [`BudgetHost::meter_series`] takes. + pub fn gov(&self) -> &GovHandle { + &self.gov + } +} + +/// An opaque handle to a governance ADMISSION grant (`AdmitGrant`), held DROP-ONLY on a plane's +/// per-request sink so the admission's in-flight concurrency holds release when the last sink clone +/// drops. The plane never introspects it; it exists purely so its `Drop` (on the last clone) releases +/// the gauges — byte-identical to the sink's current `Option>`. +#[derive(Clone)] +pub struct AdmitHandle(pub Arc); + +/// BRAKE (audit D): the BREAKER-family slice of the host seam, split off `EngineHost` as a supertrait +/// so the circuit-breaker admission/settle/record cluster stays a cohesive, bounded ABI rather than +/// dissolving into the ~30-method god-trait. Groups the five `(pool, lane)` breaker seams a plane's +/// dispatch/failover legs drive: win a probe ([`breaker_admit`](Self::breaker_admit)), fold a leg's +/// outcome ([`breaker_settle`](Self::breaker_settle)), the in-place record fallbacks +/// ([`breaker_record_success`](Self::breaker_record_success) / +/// [`breaker_record_signal`](Self::breaker_record_signal)), and the cooldown read +/// ([`breaker_retry_after_secs`](Self::breaker_retry_after_secs)). PURE STRUCTURAL: every method keeps +/// its exact signature and same-dispatch body; a plane that names `EngineHost` still calls these +/// through the inherited supertrait bound. +pub trait BreakerHost: Send + Sync { + /// WIN ONE `(pool, lane)` breaker probe through the host `breaker_admit` seam, leaving the + /// settle-capable admission REGISTERED in `scope`'s arena and returning the POD [`AdmissionId`] — + /// or the store's own [`Unavailable`] refusal. Identical to + /// `busbar_kernel::plane_host::breaker::breaker_admit_over`. + fn breaker_admit( + &self, + scope: &DispatchScope, + pool: &[u8], + lane: u32, + ) -> Result; + + /// Fold a leg's classified outcome through the host `breaker_settle` seam over `admission` (looked + /// up in `scope`'s arena). `Ok` means the live admission was found and settled; a `Gone` means it + /// was already settled — the caller falls back to an in-place record. Byte-identical disposition + /// to the plane's own `record_signal`/`record_success`. + fn breaker_settle( + &self, + scope: &DispatchScope, + admission: AdmissionId, + signal: &Signal, + ) -> StatusClass; + + /// Record a SUCCESS against the `(pool, lane)` breaker cell in place — the fallback a settle leg + /// takes when no arena owns the probe (or a multi-round leg whose probe was already settled). + /// Identical to the plane's own `PlaneBreakers::record_success`. + fn breaker_record_success(&self, pool: &str, lane: usize); + + /// Record a canonical failure signal against the `(pool, lane)` breaker cell in place — the + /// fallback twin of [`breaker_record_success`](Self::breaker_record_success). Identical to the + /// plane's own `PlaneBreakers::record_signal`. + fn breaker_record_signal(&self, pool: &str, lane: usize, sig: &CanonicalSignal); + + /// The seconds until the `(pool, lane)` breaker cell's cooldown expires — the honest `Retry-After` + /// for a refused pooled dispatch, read PER MEMBER so a pool whose members trip independently + /// answers with the soonest. Identical to the plane's own `PlaneBreakers::retry_after_secs`; a + /// pure read, so it needs no `HostCtx`. + fn breaker_retry_after_secs(&self, pool: &str, lane: usize) -> u64; +} + +/// BRAKE (audit D): the LANE/POOL-family slice of the host seam, split off `EngineHost` as a supertrait +/// so the lane-runtime + pool-membership + probe-default cluster stays a cohesive, bounded ABI. Groups +/// the five seams a plane's routing/health machinery reads: the neutral breaker/lane store view +/// ([`lane_store`](Self::lane_store)), the two process-wide active-probe fallbacks +/// ([`default_probe_interval_secs`](Self::default_probe_interval_secs) / +/// [`default_probe_timeout_secs`](Self::default_probe_timeout_secs)), and the failover-pool membership +/// resolvers ([`pool_members_repeatable`](Self::pool_members_repeatable) / +/// [`plane_pool_members`](Self::plane_pool_members)). PURE STRUCTURAL: signatures and bodies are +/// unchanged; a plane that names `EngineHost` reaches these through the inherited supertrait bound. +pub trait LanePoolHost: Send + Sync { + /// The breaker/lane store this deployment routes through, as the NEUTRAL + /// [`busbar_kernel::store::LaneRuntime`](crate::store::LaneRuntime) view — the seam the engine's + /// `app.store` reads (select/health/pipeline lane admit/settle/snapshot) resolve to WITHOUT naming + /// core's `state::App`. A pure borrow of the bound snapshot's store, no `HostCtx`; the returned + /// `&dyn LaneRuntime` shares the live in-memory breaker engine, identical to `&*App::store`. + /// + /// WEDGE 2 (App-retype): additive — the transitional seam the wedge-3 `app.store → host.lane_store()` + /// flip targets. The `LaneRuntime` trait already lives in substrate (wedge 1), so this names no core type. + fn lane_store(&self) -> &dyn crate::store::LaneRuntime; + + /// The process-wide active-probe INTERVAL fallback (whole seconds) a lane with no + /// `health.interval_secs` inherits — the host-read form of `busbar_kernel::limits::default_probe_interval_secs`. + /// A pure read of the live limits registry, no `HostCtx`; byte-identical to that free fn. + /// + /// WEDGE 2 (App-retype): additive — the seam the wedge-3 `health.rs` probe-spawn flip targets. The + /// limits fns read core's runtime `LIMITS` global, so they CANNOT relocate to substrate by-identity; + /// this host seam is the neutral home instead. + fn default_probe_interval_secs(&self) -> u64; + + /// The process-wide active-probe TIMEOUT fallback (whole seconds) a lane with no `health.timeout_secs` + /// inherits — the host-read twin of [`default_probe_interval_secs`](Self::default_probe_interval_secs). + /// Byte-identical to `busbar_kernel::limits::default_probe_timeout_secs`. + fn default_probe_timeout_secs(&self) -> u64; + + /// The `(pool_name, members, repeatable)` of the failover pool `member` belongs to, off the BOUND + /// snapshot; `None` when `member` is un-pooled. `repeatable` is the pool's `repeatable:` operation + /// list (what `CandidatePoolCfg::repeatability` consults) — the extra tuple element that + /// distinguishes this seam from the 2-tuple [`plane_pool_members`](Self::plane_pool_members). + /// Identical to scanning the deployment's repeatable-carrying pool map. + fn pool_members_repeatable(&self, member: &str) -> Option<(String, Vec, Vec)>; + + /// The `(pool_name, members)` of the failover pool `member` belongs to on the plane identified by + /// the opaque registry `plane_key`, off the BOUND snapshot; `None` when `member` is un-pooled. The + /// member-selection walk derives each candidate's lane from the member's position in the returned + /// list, so name + members is the whole seam. Identical to scanning the plane's pool map. + fn plane_pool_members(&self, plane_key: &str, member: &str) -> Option<(String, Vec)>; +} + +/// THE KERNEL'S PRICING SIDE of a live carrier — the rate card that prices each increment. KERNEL-INTERNAL (#43: "a plane plugin NEVER sees its rate card or +/// fees"): it is NOT a supertrait of [`EngineHost`], so no plane-side host implements it and no plane +/// names it. A plane meters a live carrier through the kernel's count-only +/// [`SessionAccount`](crate::governance::session_meter::SessionAccount), which prices nothing (Q21b). (The hot-ABI +/// reserve-then-settle lease slots that once drove this trait were retired by KERNEL<>PLUGINS step 16: +/// a plane never hands the kernel a price, #43/#71.) Money-denominated end to end in `u128` +/// nanodollars; a plain Rust trait, not the frozen hot FFI. +pub trait MeteringHost: Send + Sync { + /// PRICE a turn's neutral [`billing::Usage`](crate::billing::Usage) counts for `model` into + /// nanodollars via the deployment's rate card — the SAME `CostModel` arithmetic the LLM + /// enforcement/derive path uses (a new ENTRY POINT over the same function). + /// + /// Semantics mirror the host's per-model rate lookup exactly: + /// - no rate card configured ⇒ `Some(0)` (pricing off — every model prices at 0, as core does); + /// - card present, `model` priced ⇒ `Some(nanos)`; + /// - card present, `model` UNKNOWN ⇒ `None` — the caller FAILS CLOSED (an unpriced passthrough model + /// must not meter as free). + fn price_usage(&self, model: &str, usage: &crate::billing::Usage) -> Option; +} + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// CAPABILITY SLICES (M4 god-trait split) — the residual flat host seam, cut into cohesive capability +// supertraits so a plane depends ONLY on the slices it uses. `EngineHost` is the SUM (a supertrait of +// every slice), so an existing `Arc` caller is unaffected — it still reaches every +// method through the inherited bound — while a plane that needs a narrower capability (a voice/bytes +// port that must NOT name LLM-only `synthesize_completion`) can take `&dyn SliceX` instead. PURELY +// STRUCTURAL: every method keeps its exact signature, doc and same-dispatch body; nothing moves in the +// wire/ABI/money plane. The single `EngineHostImpl` (busbar-core) implements each slice. +// ───────────────────────────────────────────────────────────────────────────────────────────── + +/// The CLOCK slice: the host wall-clock reads a plane's timing/window arithmetic needs, in whole +/// seconds and milliseconds. Split off `EngineHost` as a supertrait; a plane that only needs the +/// clock can take `&dyn ClockHost`. +pub trait ClockHost: Send + Sync { + /// Read the host wall clock in whole SECONDS through the `clock_now` seam — the host-driven form + /// of a plane's in-place seconds clock. Identical to `busbar_kernel::plane_host::clock_now_secs_over`. + fn clock_now_secs(&self) -> u64; + + /// Read the host wall clock in MILLISECONDS through the `clock_now` seam — the host-driven form of + /// a plane's in-place millis clock. Identical to `busbar_kernel::plane_host::clock_now_ms_over`. + fn clock_now_ms(&self) -> u64; +} + +/// The TELEMETRY slice: the plane-labelled metric emits a plane fires to close a request out and to +/// count its dispatch/failover/translation events, plus the `pool_label` cardinality bound every emit +/// path runs first. Split off `EngineHost` as a supertrait; each emit is snapshot-scoped, no `HostCtx`. +pub trait TelemetryHost: Send + Sync { + /// Stamp the plane-labelled request-completion metric family for a MOUNTED plane through the host. + /// A neutral trait seam: the plane hands its own `(plane, ingress_protocol, pool, outcome, seconds)` + /// and the host records the completion, so the plane never names core's engine snapshot to close a + /// request out. Identical to `busbar_kernel::telemetry::request_finished` over the bound snapshot. + fn request_finished( + &self, + plane: &str, + ingress_protocol: &str, + pool: &str, + outcome: &'static str, + seconds: f64, + ); + + /// Count ONE dispatch ATTEMPT on `(pool_label, lane)` — the `busbar_upstream_attempts_total` + /// upstream-attempt metric — through the host, so the engine emits it without naming core's + /// telemetry module. `pool_label` is the bounded metric label (a named pool, or the routed model + /// name for the default `""` cell); `lane` is the lane index the host resolves the `lane` label + /// from off the bound snapshot. A pure snapshot-scoped metric emit, no `HostCtx`; identical to + /// `busbar_kernel::telemetry::upstream_attempt` over the bound snapshot. + fn telemetry_upstream_attempt(&self, pool_label: &str, lane: usize); + + /// Count ONE classified upstream FAILURE on `(pool_label, lane)` by `disposition` — the + /// `busbar_upstream_failures_total` metric — through the host. The telemetry twin of + /// [`telemetry_upstream_attempt`](Self::telemetry_upstream_attempt); identical to + /// `busbar_kernel::telemetry::upstream_failure` over the bound snapshot. + fn telemetry_upstream_failure(&self, pool_label: &str, lane: usize, disposition: &'static str); + + /// Count ONE logical Closed→Open breaker TRIP on `(pool_label, lane)` — the + /// `busbar_breaker_trips_total` metric — through the host. Identical to + /// `busbar_kernel::telemetry::breaker_trip` over the bound snapshot. + fn telemetry_breaker_trip(&self, pool_label: &str, lane: usize); + + /// Count ONE FAILOVER event on `pool_label` by `reason` — the `busbar_failovers_total` metric — + /// through the host. Identical to `busbar_kernel::telemetry::failover` over the bound snapshot. + fn telemetry_failover(&self, pool_label: &str, reason: &'static str); + + /// Count ONE cross-protocol TRANSLATION hop `from → to` — the `busbar_translations_total` + /// metric — through the host. Both names come from the fixed protocol vocabulary, so the emit is + /// snapshot-independent; identical to `busbar_kernel::telemetry::translation`. + fn telemetry_translation(&self, from: &str, to: &str); + + /// Map a client-supplied model/name string to the BOUNDED `pool` metric label through the host: + /// the string verbatim when it names a configured pool or by-model lane, else the fixed + /// `"unresolved"` sentinel. Bounds the Prometheus label cardinality on every finish/webhook path. + /// Identical to `busbar_kernel::ingress::pool_label` over the bound snapshot; the returned slice + /// borrows `model` (or a `'static` sentinel), independent of the host. + fn pool_label<'a>(&self, model: &'a str) -> &'a str; +} + +/// The audit action of a residual row ([`JournalHost::settle_residual`]): counts reported, never billed. +pub const USAGE_RESIDUAL_ACTION: &str = "usage.residual"; + +/// The JOURNAL slice: the durable admin-audit / call-log emits a plane writes as a side effect of the +/// mutation it records. All fire-and-forget (a store miss never fails the recorded action). Split off +/// `EngineHost` as a supertrait. +pub trait JournalHost: Send + Sync { + /// Emit ONE hostless admin-audit record `(action, resource, outcome, principal)` to the shared + /// admin audit log. Fire-and-forget, loudly: a store write failure NEVER fails the mutation it + /// records. Identical to `busbar_kernel::audit::auditlog::emit_admin_hostless_now` — this seam needs + /// no `HostCtx`, so it is a plain forward to that engine (which stays unchanged in core). + fn audit_emit(&self, action: &str, resource: &str, outcome: &str, principal: &str); + + /// Record ONE admin-audit event `(action, resource, outcome, principal)` through the IN-PROCESS + /// ADMIN RING (`busbar_kernel::admin::audit::AUDIT::record_by`), which seals it into the retained ring + /// AND cascades the SAME record onto the durable hostless journal seam. Distinct from + /// [`audit_emit`](Self::audit_emit) (durable-only): this is the seam the DATA-plane egress + /// audit-and-allow path (a dropped cross-dialect control) writes through, so the record lands in the + /// in-process ring the egress audit-trail assertions read. `outcome` is a fixed vocabulary literal. + /// + /// WEDGE 3 (App-retype): the neutral home of the engine's `AUDIT.record_by(...)` reach. + fn audit_record(&self, action: &str, resource: &str, outcome: &'static str, principal: &str); + + /// THE SETTLE STEP'S RESIDUAL ROW (MONEY LAW, owner 2026-10-02; ARCHITECT ruling 2026-10-02): a + /// usage count no billing class records is WARNed where the reader found it, never billed, and + /// recorded here as ONE `usage.residual` row per non-empty `residual` map on the DURABLE + /// PER-PRINCIPAL journal stream ([`crate::residual_log`]): store-backed, hash-chained per + /// `principal`, replayed at boot, never evicted. NOT the admin audit ring + /// ([`audit_record`](Self::audit_record)), which is operator-rate and bounded in RAM. The row names + /// every unit and its count, the plane, the dialect that reported them, the serving lane and the + /// request id, outcome `unbilled`; `principal` is the caller the units settled against. An empty + /// map writes nothing. The row is the kernel's, so a plane hands the counts over and never spells + /// the row itself. + fn settle_residual( + &self, + residual: &std::collections::BTreeMap, + plane: &str, + dialect: &str, + lane: &str, + request_id: u64, + principal: &str, + ) { + crate::residual_log::emit(residual, plane, dialect, lane, request_id, principal); + } + + /// Emit ONE per-call record through the durable MCP call-log engine. The transient `HostCtx` the + /// chain seam needs is minted INTERNALLY (a fresh per-call arena over the live engine — the append + /// registers no host handle, so the arena choice is immaterial). Identical to + /// `busbar_kernel::calllog::emit`. + fn call_log_emit(&self, principal: &str, input: CallInput); + + /// The DEFERRED-SITE twin of [`call_log_emit`](Self::call_log_emit): emit through the + /// HOSTLESS call-log path, for a client-leg site that has no `HostCtx` to open. Identical to + /// `busbar_kernel::calllog::emit_hostless`. + fn call_log_emit_hostless(&self, principal: &str, input: CallInput); +} + +/// The MOUNT slice: the pure mount-table reads that shape an arrival's dialect and its pre-collapse +/// fallback error. Split off `EngineHost` as a supertrait; both are pure snapshot reads, no `HostCtx`. +pub trait MountHost: Send + Sync { + /// The mount-aware dialect an answer to `path` is SHAPED in — the host-driven form of + /// `busbar_kernel::ingress::native::envelope_dialect(App::planes.ingress_of(path))`. A pure snapshot + /// mount-table read, no `HostCtx`. + /// + /// WEDGE 3 (App-retype — THE FLIP): the seam core's `ArrivalHost` impl reads instead of the dropped + /// `ArrivalPayload::app`; the neutral `ArrivalPayload` now carries only the host, so this mount read + /// crosses the host seam like every other. + fn arrival_envelope_dialect(&self, path: &str) -> &'static str; + + /// The pre-collapse fallback error SHAPE by `path` — the host-driven form of + /// `busbar_kernel::fallback_error_response(&App::planes, path, status, kind, message)`. Renders the + /// unmatched-path/404 envelope in the dialect the deployment mounted `path` under; a pure snapshot + /// mount-table read, no `HostCtx`. The twin of [`arrival_envelope_dialect`](Self::arrival_envelope_dialect). + fn arrival_fallback_error( + &self, + path: &str, + status: axum::http::StatusCode, + kind: &str, + message: &str, + ) -> axum::response::Response; +} + +/// The REGISTRY slice: the per-generation registry/snapshot reads a plane pulls off the bound (or +/// live) snapshot — its type-erased runtime slot, its type-erased defs, the request-id counter, the +/// neutral secret resolver, and the host-held card-signing key. Split off `EngineHost` as a supertrait. +pub trait RegistryHost: Send + Sync { + /// Stamp the NEXT per-request correlation id — one relaxed `fetch_add` on the host-owned counter. + /// Identical to `busbar_kernel::state::App::next_request_id` (the counter is boot-seeded and carried + /// across config swaps, so the value is engine-snapshot independent). + fn next_request_id(&self) -> u64; + + /// The plane's type-erased runtime object off the BOUND snapshot (the one this host was minted + /// over), owned (an `Arc` clone) so it outlives the call. `None` when the plane contributed no + /// slot under `key` this generation. Identical to `busbar_kernel::state::App::plane_slot(key)` + /// cloned — a pure `plane_slots` map read, no `HostCtx` (mirrors [`next_request_id`]). + /// + /// [`next_request_id`]: RegistryHost::next_request_id + fn plane_slot(&self, key: &str) -> Option>; + + /// The plane's slot off the CURRENT snapshot — re-reads the LIVE handle so a config swap AFTER + /// this host was minted is seen (the dispatch-time re-validation / per-round revocation / watch + /// loops depend on this). Falls back to the bound snapshot for a snapshot-only mint (one built + /// without a live handle). A pure map read, no `HostCtx`. + fn plane_slot_live(&self, key: &str) -> Option>; + + /// The deployment's NEUTRAL secret resolver, behind the `busbar_contract::secret::SecretResolve` seam, so a + /// plane mints a delegation credential (and loads its outbound TLS PEM) WITHOUT naming the + /// engine's concrete `SecretResolver`. A pure snapshot read of `App::secret_resolver`, no + /// `HostCtx`; the returned `Arc` shares the live resolver (built-ins plus any + /// wired `kind: secret` plugin), fail-closed exactly as core resolution. + fn secret_resolver(&self) -> Arc; + + /// Sign a plane-framed agent-card signing input, returning the 64-byte Ed25519 signature (None + /// when this deployment holds no card-signing key). The card subkey is derived and held HOST-side; + /// only the bytes to sign cross in and only the signature crosses out — no key material reaches the + /// plane. Mints its transient HostCtx internally over a fresh per-call DispatchScope, drives the + /// slot synchronously, returns owned bytes — no HostCtx crosses an `.await`. + fn subkey_sign(&self, signing_input: &[u8]) -> Option<[u8; 64]>; + + /// The deployment's type-erased plane definitions (`Arc` holding the + /// per-plane config object the owning plane downcasts), off the BOUND snapshot — the + /// `App::agent_defs` field that is NOT a `plane_slots` entry. Owned (an `Arc` clone) so it + /// outlives the call; a pure snapshot read, no `HostCtx` (mirrors [`secret_resolver`](Self::secret_resolver)). + fn plane_defs(&self) -> Arc; +} + +/// The HOOK/CONFIG-FACADE slice (App-retype WEDGE 2d): the residual `busbar-llm` request-path reads off +/// the resolved hook/config facades — the rewrite/tap/gate chains, the routing policy, the requested- +/// signal bitmask and the group-membership walk. Each is a pure borrow of the bound snapshot tied to +/// `&self`, no `HostCtx`. Split off `EngineHost` as a supertrait; every borrow is byte-identical to the +/// `App::X` field read the wedge-3 flip targets. +pub trait HookConfigHost: Send + Sync { + /// Whether `caller_group` sits within (any ancestor of) one of `hook_groups` — the group-membership + /// walk a hook's `groups:` filter consults, resolved against this deployment's group registry. + /// Identical to `busbar_kernel::config::caller_in_hook_groups(caller_group, hook_groups, &App::groups_registry)`; + /// a pure tree walk over the bound snapshot's registry, no `HostCtx`. + /// + /// WEDGE 2 (App-retype): additive — the seam the wedge-3 `pipeline.rs` flip targets. Folding the + /// `&App::groups_registry` argument HOST-side means the engine reads group membership without naming + /// `busbar_kernel::config` or `App::groups_registry`. + fn caller_in_hook_groups(&self, caller_group: Option<&str>, hook_groups: &[String]) -> bool; + + /// This pool's resolved REWRITE chain `(timeout, policy)` — the phase-1 transform hooks fired for + /// requests routed to `pool`, empty (the default) ⇒ no pool rewrites. Byte-identical to + /// `busbar_kernel::state::App::pool_rewrites(pool)`; a pure keyed map read, no `HostCtx`. The tuple is + /// purely neutral (`Duration`, the [`RoutingPolicy`](busbar_contract::hooks::RoutingPolicy) trait object — api). + fn pool_rewrites( + &self, + pool: &str, + ) -> &[( + std::time::Duration, + std::sync::Arc, + )]; + + /// The GLOBAL (all-pools) rewrite chain `(timeout, policy)` fired in the phase-1 transform pass + /// BEFORE any pool rewrites — the borrow of `App::rewrite_hooks`. Empty (the default) ⇒ no globals. + /// Same neutral tuple shape as [`pool_rewrites`](Self::pool_rewrites). + fn rewrite_hooks( + &self, + ) -> &[( + std::time::Duration, + std::sync::Arc, + )]; + + /// Whether ANY registered hook holds a prompt-CONTENT grant (`prompt: ro`/`rw`) this generation — + /// the deployment gate that decides whether the request IR is built for the hook seam. A single + /// bool load of `App::any_content_hook`, byte-identical; `false` (the default) is the whole + /// zero-cost-when-off property. + fn any_content_hook(&self) -> bool; + + /// A hook was handed a unit's content (and the caller's identity, when `identity`): leave the + /// one access amendment the kernel's own gate and rewrite seams leave, on the node journal. + /// `principal` is whose content it was; `op` the ingress label — data, never branched on. + /// PROVIDED, so a plane's own hook call sites record through the same seam as the kernel's. + fn hook_read(&self, name: &str, principal: Option<&str>, op: &str, identity: bool) { + crate::audit::amend::hook_read(name, principal, op, identity); + } + + /// The DECISION GATES the plane `plane_key` attached to its entry `container` this generation + /// (the plane's per-entry `hooks:`), as resolved; empty = none. A gate-first plane's units + /// are screened by them (`abi::plane::TAIL_HOOKS_GATED`). PROVIDED: a host with no per-plane + /// gate map attaches none. + fn plane_gates_of( + &self, + plane_key: &str, + container: &str, + ) -> Vec<(u16, crate::hooks::ResolvedPolicy)> { + let _ = (plane_key, container); + Vec::new() + } + + /// The REWRITE chain the plane `plane_key` attached to its entry `container` this generation; + /// empty = none. PROVIDED: a host with no per-plane rewrite map attaches none. + fn plane_rewrites_of( + &self, + plane_key: &str, + container: &str, + ) -> Vec<( + std::time::Duration, + std::sync::Arc, + )> { + let _ = (plane_key, container); + Vec::new() + } + + /// The gate's INCREMENTAL-SCAN substrate under the operator's opt-in: the node's session store + /// and the hook-config generation a clearance is bound to; `None` = every request is screened + /// whole. PROVIDED: `None`. + fn gate_scan(&self) -> Option<(std::sync::Arc, u64)> { + None + } + + /// The GLOBAL request-stage `kind: tap` observers — the borrow of `App::tap_hooks`. Each + /// [`TapEntry`](crate::hooks::TapEntry) is a neutral `(deadline, prompt-grant, transport, groups)` + /// tuple. Held BY REF across the forward await-loop (the tap-fire pass reads it after each hop), so + /// the borrow is deliberately tied to the stable host `&self`, not a per-call temporary. + fn tap_hooks(&self) -> &[crate::hooks::TapEntry]; + + /// The RESPONSE-stage tap observers — the borrow of `App::tap_hooks_response`. Same + /// [`TapEntry`](crate::hooks::TapEntry) shape as [`tap_hooks`](Self::tap_hooks); fired once the + /// upstream response outcome is known. + fn tap_hooks_response(&self) -> &[crate::hooks::TapEntry]; + + /// The ROUTING-stage tap observers — the borrow of `App::tap_hooks_routing`. Fired per failover hop + /// with the routing/attempt projection; same [`TapEntry`](crate::hooks::TapEntry) shape. + fn tap_hooks_routing(&self) -> &[crate::hooks::TapEntry]; + + /// The CANDIDATE-stage tap observers — the borrow of `App::tap_hooks_candidate`. Fired once the + /// decision reconcile has produced the final candidate set; same [`TapEntry`](crate::hooks::TapEntry) + /// shape. + fn tap_hooks_candidate(&self) -> &[crate::hooks::TapEntry]; + + /// This pool's resolved DECISION GATES `(priority, policy)` in config order — the borrow of + /// `App::pool_gates(pool)`. Empty ⇒ no pool gates. The [`ResolvedPolicy`](crate::hooks::ResolvedPolicy) + /// carrier is already neutral substrate; a pure keyed map read, no `HostCtx`. + fn pool_gates(&self, pool: &str) -> &[(u16, crate::hooks::ResolvedPolicy)]; + + /// The GLOBAL decision gates `(priority, policy)` fired alongside the pool gates in the phase-2 + /// reconcile — the borrow of `App::global_gates`. Empty (the default) ⇒ the phase-2 pass is skipped. + /// Same neutral `(u16, ResolvedPolicy)` shape as [`pool_gates`](Self::pool_gates). + fn global_gates(&self) -> &[(u16, crate::hooks::ResolvedPolicy)]; + + /// This pool's resolved routing POLICY, or `None` for the zero-cost weighted/SWRR default — the + /// borrow of `App::pool_policy(pool)`. The [`ResolvedPolicy`](crate::hooks::ResolvedPolicy) is + /// neutral substrate; a pure keyed map read, no `HostCtx`. + fn pool_policy(&self, pool: &str) -> Option<&crate::hooks::ResolvedPolicy>; + + /// The config generation's declared-signal bitmask — the borrow of `App::requested_signals`. A + /// single load of the neutral [`RequestedSignals`](crate::hooks::RequestedSignals) newtype the + /// candidate-signal loop gates on (`requested.is_empty()` / `requested.wants(sig)`); the all-zero + /// default short-circuits the whole loop. Byte-identical to the field read. + fn requested_signals(&self) -> &crate::hooks::RequestedSignals; +} + +/// The BUDGET/METERING slice: the money-path seams a plane drives — COUNTS in, VERDICTS out. The +/// governance handle and [`MeterPin`] mints, the record-usage / record-metering accruals, the +/// reserve-then-charge meter, and the pure headroom/budget projections (a live carrier's +/// [`SessionAccount`](crate::governance::session_meter::SessionAccount) is built over them). No method names a cost or price type: pricing is the kernel's +/// ([`MeteringHost`], #43), so a plane-side host implements this slice without naming one. Split off +/// `EngineHost` as a supertrait; the accrual seams read the pin kernel-side, no `HostCtx`. +pub trait BudgetHost: Send + Sync { + /// Whether governance is configured for this deployment. Identical to + /// `busbar_kernel::state::App::governance.is_some()`. + fn governance_enabled(&self) -> bool; + + /// Record ONE metered, attributed event through the host `meter_charge` seam over `scope`'s arena, + /// billed to `caller` — the request's middleware-resolved context, carried INTO the mint (DEC-SERVE + /// G1b); `usage` supplies the counts and the `(model, provider)` words, never the key. The + /// transient `HostCtx` is minted over `scope` and consumed SYNCHRONOUSLY inside the call. + /// Fire-and-forget: a store miss is not surfaced, exactly as the in-place `record_metering` was. + fn meter_charge( + &self, + scope: &DispatchScope, + caller: &PlaneRequestCtx, + usage: &busbar_contract::abi::hot::Usage, + ); + + /// The per-caller RATE HEADROOM (min fraction of remaining request/token budget across the key's + /// chain, `None` when unconstrained) — the host-driven form of + /// `gov.rate_headroom(&app.cost, key, pool, now)` over the governance state and card `pin` carries + /// (a pure observation, no cell mutation). No `HostCtx`. `key` is the already-neutral + /// [`VirtualKey`](busbar_contract::records::VirtualKey) (api). + fn rate_headroom( + &self, + pin: &MeterPin, + key: &busbar_contract::records::VirtualKey, + pool: Option<&str>, + now: u64, + ) -> Option; + + /// The HOOK-seam budget projection for `key`: `{bucket_id, spend_at_current_rate, remaining, window}` + /// per chain bucket, derived fresh from the token ledger × the card `pin` carries — the host-driven + /// form of `gov.budget_state(&app.cost, key, now)`. Returns the neutral + /// [`BudgetBucketState`](busbar_contract::hooks::BudgetBucketState) vec (empty when the key has no chain). No + /// `HostCtx`. + fn budget_state( + &self, + pin: &MeterPin, + key: &busbar_contract::records::VirtualKey, + now: u64, + ) -> Vec; + + /// Mint the OPAQUE [`GovHandle`] for this deployment's governance state — `Some` iff governance is + /// configured. One `Arc` bump, no `HostCtx`. Byte-identical to cloning `App::governance`. + fn governance(&self) -> Option; + + /// PIN this request's metering context — `Some` iff governance is configured. The plane holds the + /// pin on its per-request sink and hands it back to the metering seams. The kernel's host pins its + /// bound snapshot's card; a plane-side host keeps this default, which pins NO card (every card read + /// through the pin answers the no-card posture). + fn meter_pin(&self) -> Option { + let cost = CostHandle(Arc::new(())); + self.governance().map(|gov| MeterPin { gov, cost }) + } + + /// The pre-admission pricing guard's VERDICT: does a PRESENT rate card leave `model` unpriced? + /// `false` for every name when no card is configured (there is no card to miss). A verdict, not a + /// rate: the plane fails the request closed on `true` and never learns a figure. + fn cost_model_unpriced(&self, model: &str) -> bool; + + /// LEDGER one delivered response's tier-split counts against the key's budget chain — the + /// host-driven form of `sink.gov.record_usage(&sink.cost, key, pool, model, tokens, now)` over the + /// governance state and card `pin` carries. A no-op on an all-zero tier, exactly as + /// `record_usage`. No `HostCtx`. + fn meter_ledger( + &self, + pin: &MeterPin, + key: &busbar_contract::records::VirtualKey, + pool: &str, + model: &str, + usage: &crate::billing::Usage, + now: u64, + ); + + /// GIVE BACK ONE FEE UNIT `fee_unit` a plane's admission counted on its own fee lane, across the + /// buckets of the key's chain `pool` reaches, in the window `now` (the count's own clock reading) + /// reached: the host-driven form of `GovState::refund_fee_unit` over the state `pin` carries. The + /// budget book only; the metering row stays. Floored at 0. + fn meter_refund_fee( + &self, + pin: &MeterPin, + key: &busbar_contract::records::VirtualKey, + pool: &str, + plane: &str, + fee_unit: &str, + now: u64, + ); + + /// Record one delivered response's RAW consumption into the per-(key, bucket, model, provider) + /// metering series — the host-driven form of + /// `sink.gov.record_metering(key_id, model, provider, usage, now)`. `gov` is the opaque handle the + /// sink minted; the host downcasts it and drives the SAME write-behind accrual (a zero-token + /// response still counts its request). No `HostCtx`. + fn meter_series( + &self, + gov: &GovHandle, + key_id: &str, + model: &str, + provider: &str, + usage: Option<&crate::billing::TokenUsage>, + now: u64, + ); +} + +/// The IDENTITY/TRUST slice: inbound identity resolution + the trust/approval seams around it — the +/// audience pre-filter, the auth-chain admit, the standing re-ask, the ask-state sealer derivation, the +/// drift quarantine settle, the one-time approval redeem, and the test-only token verifier. Split off +/// `EngineHost` as a supertrait. `#[async_trait]` because [`identity_admit`](Self::identity_admit) is +/// the one async method (it awaits a `spawn_blocking` join over the host auth chain). +#[async_trait::async_trait] +pub trait IdentityHost: Send + Sync { + /// Settle a drift disposition for `subject` through the host `drift_quarantine` seam, pulling the + /// demotion store host-side. Returns whether the slot answered `Ok`; the settle is + /// fire-and-forget, so a non-`Ok` is a durability miss, not a refusal. Identical to + /// `busbar_kernel::plane_host::trust::quarantine_settle_over`. + fn quarantine_settle(&self, subject: &str, state: TrustState) -> bool; + + /// Redeem a one-time approval against the shared spent-approval ledger the host pulls, spending + /// against the seal's own `expires_at` and the caller's `now`. `true` iff this is the FIRST + /// redemption; `false` when already spent OR the durable ledger could not answer (fail-closed). + /// Identical to `busbar_kernel::plane_host::trust::approval_redeem_q`. + fn approval_redeem(&self, nonce: &str, expires_at: u64, now: u64) -> bool; + + /// TEST-ONLY raw-token → resolved `VirtualKey` resolution over this deployment's governance state + /// (the data-plane boundary: no audience). The host-driven form of + /// `App::governance.and_then(|g| g.verify_token(token, now, None))`, for the test paths that + /// exercise the routing-policy seam WITHOUT building a full `PlaneRequestCtx` (production always + /// threads a resolved key, so this never runs there). Gated to test / `test-support` builds so no + /// production binary carries a raw-token verifier on the neutral seam. + /// + /// DEFAULT `None` (resolves nothing): the method exists only when THIS crate's `test-support` is + /// on, and a downstream implementor cannot see that feature — cargo may unify it on through some + /// other crate's dev-deps. A default lets an implementor compile either way; a host that actually + /// verifies tokens overrides it (item 115). + #[cfg(any(test, feature = "test-support"))] + fn verify_token_test(&self, token: &str) -> Option> { + let _ = token; + None + } + + /// Establish what can be established about a presented bearer's RFC 8707 audience binding against + /// `expected_aud` — the fail-closed pre-filter a plane runs BEFORE the auth chain, for credentials + /// busbar did not mint. A pure judgement (it reaches only core's governance token prefix, no live + /// engine state), so it needs no `HostCtx`. Identical to `busbar_kernel::auth::audience::inspect_bearer`. + fn identity_audience_binding(&self, token: &str, expected_aud: &str) -> AudienceBinding; + + /// Resolve INBOUND data-plane identity: run the configured auth chain + the ONE verdict resolution + /// over the caller's OWN wire credential and the live governance state, returning the resolved + /// `(AuthPrincipal, PlaneRequestCtx)` or the specific [`IdentityRefusal`]. Identical to + /// `busbar_kernel::plane_host::identity_admit_over`. + /// + /// The ONE async method: the core impl awaits a `spawn_blocking` that mints AND consumes the + /// `HostCtx` INSIDE the blocking closure, so the `!Send` pointer never crosses this `.await` and + /// the future stays `Send`. Fail-closed: a join panic maps to [`IdentityRefusal::Denied`]. + async fn identity_admit( + &self, + token: Option, + audience: String, + resource: String, + ) -> Result<(AuthPrincipal, PlaneRequestCtx), IdentityRefusal>; + + /// RE-ASK a [`Standing`] permission against the LIVE governance registry: hand back the principal + /// AS IT IS NOW, or the [`Lapsed`] reason it no longer stands. Injects the host's `GovState` + /// (through the `GovResolve` seam) INTERNALLY, so the plane holds only the `Standing`. Identical to + /// `Standing::still_permitted(app.governance, live, now)`. + fn principal_standing( + &self, + standing: &Standing, + live_gen: u64, + now: u64, + ) -> Result>, Lapsed>; + + /// Derive this deployment's ask-state [`Sealer`] from governance's fleet-shared signing secret, + /// WITHOUT the raw secret crossing to the plane. `None` when governance is disabled. Identical to + /// `busbar_kernel::plane::approvals::ask_state_sealer(app.governance)` — the derivation stays core + /// behind this seam. + fn ask_state_sealer(&self) -> Option; +} + +/// The ADMISSION slice: the request-admission gauntlet seams — the gate decision + presence pre-filter, +/// the governance admit-reason, the destination guard, the budget-admission door, the audience-bound +/// mount read, and the post-admission/not-charged finishes. Split off `EngineHost` as a supertrait. +pub trait AdmissionHost: Send + Sync { + /// Fire the operator's REQUEST-ADMISSION hook gates over the host `gate_decide` seam and + /// reconstruct the [`GateOutcome`]. Identical to `busbar_kernel::plane_host::gate_decide_over`: + /// same reconstructed facts, same key identity, same gate decision. Drives the ASYNC gate on a + /// fresh runtime, so it MUST be called from a BLOCKING thread (`spawn_blocking`). + /// + /// `plane_key` is the opaque registry key (the plane's stable decl key) the host resolves the + /// gate set and the `ingress_protocol` label from. `key` is the caller's resolved `(id, name)`; + /// `session_id` is the caller's session, `Some` only when non-empty. + #[allow(clippy::too_many_arguments)] + fn gate_decide( + &self, + plane_key: &str, + container: &str, + request_id: u64, + tool: &str, + args_json: &[u8], + key: Option<(&str, &str)>, + session_id: Option<&str>, + ) -> GateOutcome; + + /// Cheap presence pre-filter: is any request-admission hook gate attached to `container` on the + /// plane identified by the opaque registry `plane_key` (the plane's stable decl key)? Lets a plane + /// skip the blocking `gate_decide` hop when nothing is attached. Identical to + /// `App::plane_gates(plane_key).contains_key(container)`. + fn gate_attached(&self, plane_key: &str, container: &str) -> bool; + + /// Cheap presence pre-filter for the TAP/TRANSFORM half: is any `prompt: rw` rewrite hook attached + /// to `container` on the plane identified by the opaque registry `plane_key`? Lets a plane skip the + /// blocking `transform_over` hop — and stay BYTE-IDENTICAL to a build without the seam — when + /// nothing is attached. Identical to `App::plane_rewrites(plane_key).get(container).is_some()`. + /// + /// The presence check is the whole zero-cost guarantee: absent a rewrite hook a plane never + /// serializes the payload, never spawns the blocking hop, and never touches its own bytes. + fn tap_attached(&self, plane_key: &str, container: &str) -> bool; + + /// Fire the operator's REQUEST-ADMISSION TRANSFORM (`
.hooks:` `prompt: rw`) chain over the + /// host `transform_over` seam and reconstruct the [`TransformVerdict`] — the TAP/observe-transform + /// twin of [`gate_decide`](Self::gate_decide). The host re-selects the rewrite chain by + /// `(plane_key, container)` (the Seam-B inversion: the plane body names no core hook symbol), builds + /// the SAME `InvokeReq` projection the gate builds from `(tool, args_json)`, runs each hook's + /// `transform` in priority order (each seeing the prior's output — a true transform chain), and + /// returns the rewritten payload or a reject. Drives the ASYNC hooks on a fresh runtime, so it MUST + /// be called from a BLOCKING thread (`spawn_blocking`), exactly like `gate_decide`. + /// + /// `plane_key`/`container`/`request_id`/`tool`/`args_json`/`key`/`session_id` carry the identical + /// meaning they do on [`gate_decide`](Self::gate_decide). + #[allow(clippy::too_many_arguments)] + fn transform_over( + &self, + plane_key: &str, + container: &str, + request_id: u64, + tool: &str, + args_json: &[u8], + key: Option<(&str, &str)>, + session_id: Option<&str>, + ) -> TransformVerdict; + + /// Admit one unit of work for `caller` (the request's middleware-resolved context, DEC-SERVE G1b) + /// over the host `govern_admit_reason` seam, REGISTERING the RAII grant in `scope`'s arena on + /// success and returning the RENDERED refusal reason on a blocked limit. + /// Identical to `busbar_kernel::plane_host::govern_admit_reason_over`. + fn govern_admit_reason( + &self, + scope: &DispatchScope, + caller: &PlaneRequestCtx, + pool: &[u8], + ) -> GovAdmit; + + /// STAGE 2 pre-admission DESTINATION guard through the host: the pool ACL, the fallback-pool ACL, + /// and the all-or-nothing unpriced-model gate. `Ok(())` admits; `Err` is the already-finished, + /// protocol-native rejection response (finished via the not-charged terminal). Identical to + /// `busbar_kernel::ingress::destination_guard` over the bound snapshot + `gov` scope. + fn destination_guard( + &self, + gov: &PlaneRequestCtx, + proto: &'static str, + pool: &str, + started: std::time::Instant, + charged_at: u64, + ) -> Result<(), Box>; + + /// STAGE 3–4 budget-admission door: charge the chain buckets for one request under `gov` on + /// `pool`, returning the ADMISSION grant (as the opaque [`AdmitHandle`] the sink holds Drop-only) + /// and any budget DOWNGRADE re-pool, or the already-finished not-charged rejection. Identical to + /// `busbar_kernel::ingress::admission_door` over the bound snapshot; the returned `AdmitHandle` + /// wraps the SAME `AdmitGrant` the in-place door produced, so `.is_some()` (charged?) and the + /// gauge-releasing `Drop` are byte-identical. No `HostCtx` on this path. + /// + /// WEDGE 2 (App-retype): additive — the seam the wedge-3 `native_ingress::drive` flip targets + /// (a host is already minted there). The `AdmitGrant`→[`AdmitHandle`] carrier-field retype + /// (`response_body.rs` / `native_ingress.rs`) rides the same wedge-3 governance flip. + fn admission_door( + &self, + gov: &PlaneRequestCtx, + proto: &'static str, + pool: &str, + started: std::time::Instant, + charged_at: u64, + ) -> Result<(Option, Option), Box>; + + /// THE SAME DOOR, WITHOUT THE TERMINAL — the check-and-charge on its own. + /// + /// [`admission_door`](Self::admission_door) is exactly this call followed by + /// `finish_rejected` on the refusing arm, which is right for a caller whose refusal ends there + /// and wrong for one whose terminal is a step of its own: posting the refusal at the door and + /// then posting it again at the terminal would put two links on one unit's chain. So a plane + /// that owns an Audit step takes this one and hands the refusal — bytes, not a posted record — + /// to that step, and a unit posts exactly one link whichever way it ended. + /// + /// Identical to `busbar_kernel::ingress::admit_check` over the bound snapshot: the same buckets, + /// the same charge, the same downgrade re-pool, and on the refusing arm the same + /// protocol-native bytes the door would have finished. The label the terminal records those + /// bytes under is [`pool_label`](EngineHost::pool_label) of the same `pool`, which is what the + /// door computes for itself before it finishes. + fn admission_check( + &self, + gov: &PlaneRequestCtx, + proto: &'static str, + pool: &str, + charged_at: u64, + ) -> Result<(Option, Option), Box>; + + /// POST-ADMISSION finish through the host: emit the per-request metric family + request-log + /// webhook and, on a NON-2xx outcome, REFUND the flat per-request fee IFF it actually landed at + /// admission (`charged`: the admission's own handle, `None` when nothing was charged) — from + /// exactly the cells that admission's charge reached. Identical to + /// `busbar_kernel::ingress::finish_admitted` over the bound snapshot + `gov` scope. + #[allow(clippy::too_many_arguments)] + 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; + + /// NOT-CHARGED (pre-charge turn-away) finish through the host: emit metrics + the webhook with NO + /// refund, for a request rejected BEFORE the admission charge ever ran (governance guard denial or + /// a pre-routing failure). Identical to `busbar_kernel::ingress::finish_rejected` over the bound + /// snapshot + `gov` scope. + #[allow(clippy::too_many_arguments)] + 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; + + /// Whether the plane identified by the opaque registry `plane_key` is mounted under an + /// AUDIENCE-BOUND door — the deployment gate a request path reads before it trusts an inbound + /// audience claim. Identical to + /// `App::planes.mount_of(plane_key).and_then(|m| App::planes.admission_for(m)).is_some()`. A pure + /// snapshot read, no `HostCtx`. + fn plane_audience_bound(&self, plane_key: &str) -> bool; +} + +/// The COMPLETION slice — LLM-ONLY. The single seam that drives a non-streaming chat completion through +/// the resolved ingress pipeline. Split off `EngineHost` as a supertrait SPECIFICALLY so a non-LLM plane +/// (a voice/bytes port) is NOT forced to name it: such a plane takes the slices it uses and never sees +/// this one. `#[async_trait]` because [`synthesize_completion`](Self::synthesize_completion) is async. +#[async_trait::async_trait] +pub trait CompletionHost: Send + Sync { + /// Synthesize ONE non-streaming chat completion by driving `body` through the ENTIRE resolved + /// ingress pipeline (governance → pools → breaker/failover → metering → request log) under `gov`, + /// on the operator's declared `model`, and return the raw wire outcome. The dialect the request is + /// driven as is NEUTRAL to this seam: the host resolves it from the registry's residual-default + /// chat protocol (`None` — no chat dialect installed — surfaces as + /// [`CompletionRefusal::NotInstalled`], not a hard-coded identity), so MCP's `sampling/complete` + /// bridge names no LLM dialect to reach a completion, and words the refusal itself. + /// + /// The ONE async method beside [`IdentityHost::identity_admit`] — but simpler: + /// the host drives a NATIVE core async fn (no C-ABI slot, no `spawn_blocking`), so this only + /// `.await`s it. No `HostCtx` crosses the `.await`; the future is `Send`. `max_body_bytes` + /// bounds the response body read. + async fn synthesize_completion( + &self, + gov: &busbar_contract::records::PlaneRequestCtx, + model: &str, + body: bytes::Bytes, + max_body_bytes: usize, + ) -> Result; +} + +/// The neutral HOST seam a plane calls to reach the engine's host-owned capabilities. +/// +/// A plane holds an `Arc` (minted core-side over the live engine) and calls these +/// typed methods rather than naming `busbar_kernel::plane_host::*_over(&App, …)`. Each method reaches +/// the SAME host vtable slot the in-core veneer drives, so the value is identical — this is a +/// same-dispatch relocation of the reach, not a new behaviour. +/// +/// `Send + Sync` because a plane carries the handle across `.await` and between threads (e.g. into a +/// `spawn_blocking` breaker leg). That is sound precisely because no method exposes the `!Send` +/// `HostCtx`: each mints it internally, uses it synchronously, and drops it before returning. +/// +/// ## M4 — `EngineHost` is the SUM of the capability slices +/// +/// The residual flat method set has been cut into cohesive capability SUPERTRAITS +/// ([`ClockHost`], [`TelemetryHost`], [`JournalHost`], [`MountHost`], [`RegistryHost`], +/// [`HookConfigHost`], [`BudgetHost`], [`IdentityHost`], [`AdmissionHost`], [`CompletionHost`]), +/// alongside the earlier braking slices ([`BreakerHost`], [`LanePoolHost`]). Every slice is +/// PLANE-FACING and pricing-blind; the kernel's pricing ([`MeteringHost`]) is deliberately NOT one of +/// them, so a plane-side host implements the whole sum without naming a cost or price type (#43). +/// `EngineHost` now declares NO methods of its own beyond the provided [`run_gauntlet`](Self::run_gauntlet) +/// ergonomic entry — it is the SUM (a supertrait bound of every slice). An existing +/// `Arc` caller is unaffected (it still reaches every method through the inherited +/// bound); a plane that needs a narrower capability takes `&dyn SliceX` and depends only on what it +/// uses — a voice/bytes port never names LLM-only [`CompletionHost::synthesize_completion`]. +/// +/// `#[async_trait]` is inherited via the async slices; `EngineHost` itself carries only the provided +/// `run_gauntlet` future, so the attribute leaves it a thin sum. +#[async_trait::async_trait] +pub trait EngineHost: + BreakerHost + + LanePoolHost + + ClockHost + + TelemetryHost + + JournalHost + + MountHost + + RegistryHost + + HookConfigHost + + BudgetHost + + IdentityHost + + AdmissionHost + + CompletionHost + + Send + + Sync +{ + // The cross-protocol translation seam's global-fallback max-output-tokens and effort→budget-table + // reads are GONE from this neutral host trait: they are LLM-plane vocabulary, so the engine now + // reads them off the LLM plane's own per-generation runtime (`NativeRuntime`) rather than through a + // neutral `PlaneHost` method over `App`. See busbar-llm `engine/wire.rs`. + + /// Run one request through THE shared gauntlet sequence ([`run_gauntlet`]) — the ergonomic entry + /// for a plane that already holds an `Arc`. A PROVIDED method: it delegates to the + /// free [`run_gauntlet`] (which needs no host — the sequence is verify→drive over the plane), so + /// every host impl shares one body and core's own callers can use the free fn directly. + async fn run_gauntlet<'a>( + &self, + req: GauntletRequest<'a>, + plane: Box, + ) -> axum::response::Response { + run_gauntlet(req, plane).await + } +} + +/// Compile-time witness for the M4 god-trait split: any `EngineHost` implementor IS every capability +/// slice. If a future edit dropped any supertrait bound (or an impl stopped satisfying it), this stops +/// compiling — so `EngineHost` cannot silently cease to equal the SUM of its slices (the families +/// cannot re-dissolve back into the flat god-trait). +const _: () = { + fn _assert_engine_host_is_sum_of_slices() { + fn _needs_breaker() {} + fn _needs_lane_pool() {} + fn _needs_clock() {} + fn _needs_telemetry() {} + fn _needs_journal() {} + fn _needs_mount() {} + fn _needs_registry() {} + fn _needs_hook_config() {} + fn _needs_budget() {} + fn _needs_identity() {} + fn _needs_admission() {} + fn _needs_completion() {} + _needs_breaker::(); + _needs_lane_pool::(); + _needs_clock::(); + _needs_telemetry::(); + _needs_journal::(); + _needs_mount::(); + _needs_registry::(); + _needs_hook_config::(); + _needs_budget::(); + _needs_identity::(); + _needs_admission::(); + _needs_completion::(); + } +}; + +/// THE NEUTRAL TYPE-ERASED SLOT-READ SEAM the core-owned `PlaneDecl` callbacks that today force a +/// `&busbar_kernel::state::App` are neutralised over — so a plane's `on_swap` / `registry_contains` / +/// `retain_verify_gates` hook reads its own per-generation runtime object off the snapshot WITHOUT +/// the callback fn-pointer signature naming a core type. Core `impl`s it for `App` as a thin delegate +/// to the inherent `App::plane_slot`; an EXTRACTED plane reaches only [`Self::plane_slot`] and +/// stays neutral. [`Self::as_any`] is the recovery hatch an in-core plane twin uses to downcast back +/// to its concrete snapshot for a field that does not live in `plane_slots` (`agent_defs`); an +/// extracted plane never calls it. +pub trait PlaneSlots { + /// The plane's type-erased runtime object for THIS generation, keyed by the plane's decl key — + /// a pure `plane_slots` map read, borrowed (mirrors the inherent `App::plane_slot`). + fn plane_slot(&self, key: &str) -> Option<&Arc>; + + /// Recover the concrete engine snapshot as `&dyn Any` — the hatch an in-core plane twin downcasts + /// through to reach the snapshot fields that are not `plane_slots` entries. An extracted plane + /// never names a concrete type through this, so it never calls it. + fn as_any(&self) -> &dyn std::any::Any; +} + +/// THE NEUTRAL `&mut` GATE-REBUILD SINK the core-owned `PlaneDecl::reresolve_gates` callback is +/// neutralised over — the config-swap re-resolution of a plane's per-registration hook gates, moved +/// behind a trait so the fn-pointer signature names no `&mut busbar_kernel::state::App`. A `PlaneSlots` +/// (its supertrait) so the plane can read its own registry object off the same `&mut` receiver before +/// it writes the resolved gates back. The resolve-and-store is ONE method (rather than the spec's +/// `resolve` + `set` pair) because the resolved gate map value type is core-owned +/// (`Vec<(u16, busbar_kernel::hooks::ResolvedPolicy)>`) and cannot be named in this crate — so the map +/// never crosses the seam; it is built and stored entirely core-side, keyed by `plane_key`. +pub trait ContainerGateSink: PlaneSlots { + /// Resolve `containers` (each `(name, its-own-hooks)`) unioned with `section_hooks` against this + /// snapshot's hook registry, and store the resolved per-container gates under the opaque registry + /// `plane_key` (the plane's stable decl key) in the generic `App::plane_gates` map. Byte-identical + /// to the old inline `next.plane_gates.insert(plane_key, next.resolve_container_gates(...))`. + fn reresolve_container_gates( + &mut self, + plane_key: &str, + containers: &[(&str, &[String])], + section_hooks: &[String], + ); +} + +// ───────────────────────────────────────────────────────────────────────────────────────────── +// 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.) +// ───────────────────────────────────────────────────────────────────────────────────────────── +#[cfg(test)] +#[path = "tests/host_gauntlet_session_tests.rs"] +mod gauntlet_session_tests; + +#[cfg(test)] +#[path = "tests/host_plane_answer_tests.rs"] +mod plane_answer_tests; diff --git a/crates/busbar-kernel/src/plane/mod.rs b/crates/busbar-kernel/src/plane/mod.rs index ed3d538780..ae917deee3 100644 --- a/crates/busbar-kernel/src/plane/mod.rs +++ b/crates/busbar-kernel/src/plane/mod.rs @@ -92,12 +92,17 @@ pub mod approvals; pub mod config; +pub mod dispatch_scope; pub mod door; +pub mod host; pub mod observe; pub(crate) mod quarantine; /// The durable demotion record, which the root attaches to the kernel's host services. pub use quarantine::DemotionRecord; pub mod registry; +// `PlaneSlots` through any pointer to a slot holder (`Arc`, load guard, borrow), so a plane's slot +// readers can take `&impl PlaneSlots` instead of a concrete snapshot type without touching callers. +pub mod slots_through; // `store` is a core-internal plane primitive. Its module is widened to `pub` ONLY under the // test-support surface so an extracted plane's own test binary can name the durable-body // helpers it exercises (`StoreNamedTestExt`, `KIND_TASK_EVENT`, `task_event_row_from_body`); the diff --git a/crates/busbar-kernel/src/plane/registry.rs b/crates/busbar-kernel/src/plane/registry.rs index 01a17181bd..b3bb320ca4 100644 --- a/crates/busbar-kernel/src/plane/registry.rs +++ b/crates/busbar-kernel/src/plane/registry.rs @@ -201,7 +201,7 @@ impl PlaneBootCtx for BootCtx { /// HERE so a plane's own hydrate hook mints its host without naming `crate::plane_host::engine_host` /// or an `App`: the returned `Arc` is the neutral substrate seam and the app it /// wraps is the core-owned hydrate-phase `App`. - fn engine_host(&self) -> std::sync::Arc { + fn engine_host(&self) -> std::sync::Arc { // PHASE-AWARE: the hydrate phase supplies the freshly-built `app` and mints a SNAPSHOT-ONLY host // over it (no live handle yet, which is correct — hydration reads exactly the generation it // restores into); the start phase supplies the live `handle` and mints a LIVE host from it @@ -698,14 +698,14 @@ pub struct BuildCtx<'a> { pub tool_defs: &'a dyn std::any::Any, pub public_url: Option<&'a str>, /// THE PRIOR GENERATION'S SLOT MAP, or `None` on a fresh boot — the same neutral - /// [`crate::plane_host::PlaneSlots`] seam `build_runtime` receives, so a plane's `build` can CARRY + /// [`crate::plane::host::PlaneSlots`] seam `build_runtime` receives, so a plane's `build` can CARRY /// accumulated coordination state (verify-on-call coalescing epochs, a boot-resolved transport /// `OnceLock`) off its own prior runtime object across a config apply without the composition root - /// naming the plane's runtime type. Reached by key through [`crate::plane_host::PlaneSlots::plane_slot`] + /// naming the plane's runtime type. Reached by key through [`crate::plane::host::PlaneSlots::plane_slot`] /// and downcast inside the plane's own `build`, exactly as the runtime accessors downcast the live /// slot. The A2A plane reads it to carry its `VerifyGate` and card-fetch `OnceLock`; a plane with no /// carry-over ignores it. - pub prior: Option<&'a dyn crate::plane_host::PlaneSlots>, + pub prior: Option<&'a dyn crate::plane::host::PlaneSlots>, } /// A PLANE BOOT HOOK — [`PlaneDecl::hydrate`] or [`PlaneDecl::start`]. Handed the [`PlaneBootCtx`] for @@ -803,7 +803,7 @@ pub trait PlaneBootCtx { /// snapshot-only mint a hydrate hook drives its durable boot-replay off (no live handle yet at /// hydration). The returned `Arc` is the neutral substrate seam and the app it /// wraps is the core-owned hydrate-phase `App`. - fn engine_host(&self) -> std::sync::Arc; + fn engine_host(&self) -> std::sync::Arc; /// Recover the concrete core `BootCtx` as `&dyn Any` — the hatch an in-core plane twin (A2A) /// downcasts through to reach the phase fields (`app`, `handle`, `card_issuer`) that name core-live @@ -999,7 +999,7 @@ plane_behaviour! { /// plane with no live registry this generation. `None` for a plane whose section is not a /// named-definition map (the LLM plane; `proto`). /// - /// Handed the neutral [`crate::plane_host::PlaneSlots`] seam (NOT `&App`), so a plane crate reads + /// Handed the neutral [`crate::plane::host::PlaneSlots`] seam (NOT `&App`), so a plane crate reads /// its own per-generation runtime object off the snapshot without the callback naming a core type; /// an in-core plane (A2A) recovers its snapshot through the seam's `as_any` hatch. // Read only through the admin named-def surface, which the two plane sections drive; with neither @@ -1007,31 +1007,31 @@ plane_behaviour! { #[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] #[allow(clippy::type_complexity)] named_def_list: - Option Vec>, + Option Vec>, /// PROJECT ONE NAMED-DEFINITION REGISTRATION by name onto the shared read view — the single-entry /// twin of [`Self::named_def_list`], the plane half of `GET /api/v1/admin/
/{name}`. /// `None` (the fn returns `None`) when the plane has no entry by that name; the FIELD is `None` for - /// a plane with no named-definition map. Handed the same neutral [`crate::plane_host::PlaneSlots`] + /// a plane with no named-definition map. Handed the same neutral [`crate::plane::host::PlaneSlots`] /// seam as [`Self::named_def_list`]. #[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] #[allow(clippy::type_complexity)] named_def_get: - Option Option>, + Option Option>, /// IS `name` A LIVE REGISTRATION on this plane's effective snapshot — the read-side membership /// check the admin write path consults so it names no plane registry type. `None` for a plane with /// no named-definition map. #[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] #[allow(clippy::type_complexity)] - registry_contains: Option bool>, + registry_contains: Option bool>, /// RE-RESOLVE THIS PLANE'S PER-REGISTRATION HOOK GATES against the next snapshot — the plane half /// of the config-swap gate rebuild. Reads the plane's own registry off the `&mut App` and writes /// its own gate field back, so `admin::v1::service::reresolve_plane_gates` names no plane registry /// type. `None` for a plane with no per-registration hook gates (the LLM plane). #[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] - reresolve_gates: Option, + reresolve_gates: Option, /// ATTACH THIS PLANE'S ADMIN TRUST-VERB SCHEMAS to the OpenAPI document — the plane half of the /// schema pass in `busbar_kernel::admin::v1::json::handlers::openapi_doc`. Handed the SHARED response @@ -1058,7 +1058,7 @@ plane_behaviour! { /// `GovCtx`, or an `audit::Chain`. A plane whose swap-time work needs one of those is not cleanly /// separable through this seam. on_swap: Option< - fn(prior: &dyn crate::plane_host::PlaneSlots, next: &dyn crate::plane_host::PlaneSlots), + fn(prior: &dyn crate::plane::host::PlaneSlots, next: &dyn crate::plane::host::PlaneSlots), >, /// PARSE THIS PLANE'S TOP-LEVEL REGISTRY SECTION from a positionless `serde_yaml::Value` into its @@ -1096,14 +1096,14 @@ plane_behaviour! { /// seam `appbuild` composes the MCP runtime slot (`plane_slots[runtime_slot_key()]`) through, /// so core names no plane runtime type. The first argument is the plane's own section, erased as /// `&dyn Any` (its `PlaneCfg::as_any`); `prior` is the previous generation's snapshot for - /// carry-over, read through the neutral [`crate::plane_host::PlaneSlots`] seam (NOT `&App`). + /// carry-over, read through the neutral [`crate::plane::host::PlaneSlots`] seam (NOT `&App`). /// `None` for a plane whose runtime is not carried through this seam (A2A's lives in `plane_slots` /// under its decl key; the LLM plane's is the many `App` fields it already reads). #[allow(clippy::type_complexity)] build_runtime: Option< fn( &dyn std::any::Any, - prior: Option<&dyn crate::plane_host::PlaneSlots>, + prior: Option<&dyn crate::plane::host::PlaneSlots>, ) -> std::sync::Arc, >, @@ -1120,7 +1120,7 @@ plane_behaviour! { /// still fronts — the seam `appbuild` runs after building the `App`, so the carried per-subject /// flights/latches do not leak one dead entry per removed registration. `None` for a plane with no /// verify-on-call gate (the LLM / `proto` planes). - retain_verify_gates: Option, + retain_verify_gates: Option, /// THIS PLANE'S EMPTY REGISTRY SECTION, boxed as the neutral [`crate::plane::config::PlaneCfg`] — /// the value `DeployCfg`'s `#[serde(default)]` `tools:`/`agents:` field takes when the section is diff --git a/crates/busbar-kernel/src/plane_host/slots_through.rs b/crates/busbar-kernel/src/plane/slots_through.rs similarity index 97% rename from crates/busbar-kernel/src/plane_host/slots_through.rs rename to crates/busbar-kernel/src/plane/slots_through.rs index 9eab287947..4506ed1bfe 100644 --- a/crates/busbar-kernel/src/plane_host/slots_through.rs +++ b/crates/busbar-kernel/src/plane/slots_through.rs @@ -12,7 +12,7 @@ //! compiling unchanged: a `Deref` to a slot holder is itself a slot holder, forwarding both reads to //! the target. It adds no behaviour; the borrow it hands back is the target's own. -use super::PlaneSlots; +use super::host::PlaneSlots; use std::ops::Deref; use std::sync::Arc; diff --git a/crates/busbar-kernel/src/plane/tests/dispatch_scope_tests.rs b/crates/busbar-kernel/src/plane/tests/dispatch_scope_tests.rs new file mode 100644 index 0000000000..e0eaf06e07 --- /dev/null +++ b/crates/busbar-kernel/src/plane/tests/dispatch_scope_tests.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-kernel/src/plane/dispatch_scope.rs`. + +use super::*; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +/// A test RAII guard whose `Drop` bumps a shared counter — stands in for the real admission guard. +struct DropCounter(Arc); +impl Drop for DropCounter { + fn drop(&mut self) { + self.0.fetch_add(1, Ordering::SeqCst); + } +} + +#[test] +fn arena_reclaims_a_registered_guard_on_drop() { + let reclaimed = Arc::new(AtomicUsize::new(0)); + { + let scope = DispatchScope::new(); + let id = scope.register_admission(Box::new(DropCounter(reclaimed.clone()))); + assert_eq!(id, AdmissionId(1)); + assert_eq!(scope.registered(), 1); + // Not yet reclaimed while the scope is live. + assert_eq!(reclaimed.load(Ordering::SeqCst), 0); + } + // Scope dropped: the real guard `Drop` ran. + assert_eq!(reclaimed.load(Ordering::SeqCst), 1); +} + +#[test] +fn arena_reclaims_every_kind_and_runs_closers() { + let count = Arc::new(AtomicUsize::new(0)); + let scope = DispatchScope::new(); + let c = count.clone(); + scope.register_admission(Box::new(DropCounter(count.clone()))); + let cc = c.clone(); + scope.register_egress(Box::new(move || { + cc.fetch_add(1, Ordering::SeqCst); + })); + let ccc = c.clone(); + scope.register_pipe(Box::new(move || { + ccc.fetch_add(1, Ordering::SeqCst); + })); + let cccc = c.clone(); + scope.register_lease(Box::new(move || { + cccc.fetch_add(1, Ordering::SeqCst); + })); + assert_eq!(scope.registered(), 4); + // Explicit reclaim (the abort-path hardening assertion): synchronous, reclaims all four. + scope.reclaim_all(); + assert_eq!(count.load(Ordering::SeqCst), 4); + assert_eq!(scope.registered(), 0); + // Idempotent: a second reclaim (e.g. the Drop after an explicit reclaim) is a no-op. + scope.reclaim_all(); + assert_eq!(count.load(Ordering::SeqCst), 4); +} + +/// One closer that panics must not take the rest of the arena with it: the entries around it still +/// reclaim, and the panic does not escape a call that `Drop` also makes (escaping there, mid-unwind, +/// aborts the process rather than leaking). +#[test] +fn a_panicking_closer_does_not_strand_the_other_handles() { + let count = Arc::new(AtomicUsize::new(0)); + let reclaimed = Arc::new(AtomicUsize::new(0)); + let scope = DispatchScope::new(); + + // Registered first, reclaimed LAST (LIFO) — it is the one a panic ahead of it would strand. + scope.register_admission(Box::new(DropCounter(reclaimed.clone()))); + let c = count.clone(); + scope.register_egress(Box::new(move || { + c.fetch_add(1, Ordering::SeqCst); + })); + scope.register_pipe(Box::new(|| { + panic!("a closer that kills a subprocess went wrong") + })); + let c = count.clone(); + scope.register_lease(Box::new(move || { + c.fetch_add(1, Ordering::SeqCst); + })); + assert_eq!(scope.registered(), 4); + + // The panic is contained: `reclaim_all` returns rather than unwinding into its caller. + let quiet = std::panic::take_hook(); + std::panic::set_hook(Box::new(|_| {})); + scope.reclaim_all(); + std::panic::set_hook(quiet); + + assert_eq!( + count.load(Ordering::SeqCst), + 2, + "the closers on both sides of the panicking one still ran" + ); + assert_eq!( + reclaimed.load(Ordering::SeqCst), + 1, + "the guard behind the panicking closer was not stranded" + ); + assert_eq!(scope.registered(), 0, "the arena is empty either way"); +} + +#[test] +fn handle_ids_are_nonzero_and_monotonic() { + let scope = DispatchScope::new(); + let a = scope.register_egress(Box::new(|| {})); + let b = scope.register_egress(Box::new(|| {})); + assert!(!a.is_none()); + assert_eq!(a, EgressId(1)); + assert_eq!(b, EgressId(2)); +} diff --git a/crates/busbar-kernel/src/plane_host/tests/gauntlet_session_tests.rs b/crates/busbar-kernel/src/plane/tests/host_gauntlet_session_tests.rs similarity index 100% rename from crates/busbar-kernel/src/plane_host/tests/gauntlet_session_tests.rs rename to crates/busbar-kernel/src/plane/tests/host_gauntlet_session_tests.rs diff --git a/crates/busbar-kernel/src/plane_host/tests/plane_answer_tests.rs b/crates/busbar-kernel/src/plane/tests/host_plane_answer_tests.rs similarity index 100% rename from crates/busbar-kernel/src/plane_host/tests/plane_answer_tests.rs rename to crates/busbar-kernel/src/plane/tests/host_plane_answer_tests.rs diff --git a/crates/busbar-kernel/src/plane_driver/gated.rs b/crates/busbar-kernel/src/plane_driver/gated.rs index cf2c1e4fb5..32f694b9c0 100644 --- a/crates/busbar-kernel/src/plane_driver/gated.rs +++ b/crates/busbar-kernel/src/plane_driver/gated.rs @@ -68,7 +68,7 @@ pub struct GatedHooks { } /// The CURRENT generation's engine host, read per unit (a config apply replaces it). -pub type GenerationHost = Arc Arc + Send + Sync>; +pub type GenerationHost = Arc Arc + Send + Sync>; /// A governance key lookup by the principal the kernel verified. pub type PrincipalKeys = diff --git a/crates/busbar-kernel/src/plane_driver/hooks.rs b/crates/busbar-kernel/src/plane_driver/hooks.rs index 35f0cc6434..3f86b1fb18 100644 --- a/crates/busbar-kernel/src/plane_driver/hooks.rs +++ b/crates/busbar-kernel/src/plane_driver/hooks.rs @@ -281,7 +281,7 @@ pub struct EngineCaller { /// The CURRENT generation's engine host, read where it is needed: a config apply replaces the /// generation (its hook registry, its group tree, its card), and a unit bound after the apply binds /// the new one, as 1.5.5's request read the snapshot it arrived on. -pub type HostSource = Arc Arc + Send + Sync>; +pub type HostSource = Arc Arc + Send + Sync>; /// A governance key lookup by principal id. pub type KeyLookup = diff --git a/crates/busbar-kernel/src/plane_host/egress.rs b/crates/busbar-kernel/src/plane_host/egress.rs index 0e77274515..3ed0c3813b 100644 --- a/crates/busbar-kernel/src/plane_host/egress.rs +++ b/crates/busbar-kernel/src/plane_host/egress.rs @@ -256,16 +256,16 @@ fn close_and_remove(id: u64) -> bool { /// The observed peer identity: the SubjectPublicKeyInfo PIN of the leaf certificate the /// (already-verified) TLS handshake produced, in the ONE canonical spelling -/// (`sha256/` — [`super::spki::pin`]). Empty on a plaintext hop (nothing was proved) and +/// (`sha256/`, the connector's `tls::spki::pin`). Empty on a plaintext hop (nothing was proved) and /// empty where the certificate cannot be walked — reported honestly as absent rather than softened /// into a pass, exactly as `a2a::transport::peer_spki_of` reports `None`. /// /// This is the SAME pin the plane would compute itself: the host and the plane share one DER walk -/// (lifted to [`super::spki`]), so a governed hop hands back the string the plane's own spelling +/// (the connector's `tls::spki`), so a governed hop hands back the string the plane's own spelling /// yields, byte for byte — and it survives a certificate renewal because it pins the KEY, not the /// leaf bytes. fn observed_identity(resp: &http::Response) -> Vec { - // The ENGINE computed the pin once at connect time (`KeyPinObserve`, the same `spki::pin` walk) + // The ENGINE computed the pin once at connect time (`KeyPinObserve`, the connector's `spki::pin` walk) // and the pool replayed it onto this response's extensions — per-connection-correctly, so a // pooled response is attributed to ITS connection's certificate. Absent on a plaintext hop // and on an unwalkable certificate: honestly absent, never a pass. @@ -1504,7 +1504,7 @@ pub(crate) struct OpenedHead { /// The observed status/response code. pub status: u16, /// The observed peer SPKI pin (`sha256/…`), or `None` on a plaintext hop / an unwalkable cert. - /// Decoded from the SAME bytes `crate::plane_host::spki::pin` produced, so it is byte-identical to + /// Decoded from the SAME bytes `busbar_core_connector::tls::spki::pin` produced, so it is byte-identical to /// the pin a plane's own `peer_spki_of` would compute (they share that one function). pub peer_spki: Option, /// The response `Location`, verbatim, when the head surfaced one (a redirect). diff --git a/crates/busbar-kernel/src/plane_host/egress_trust.rs b/crates/busbar-kernel/src/plane_host/egress_trust.rs deleted file mode 100644 index d299a6033e..0000000000 --- a/crates/busbar-kernel/src/plane_host/egress_trust.rs +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE COMPOSITION-ROOT-OWNED EGRESS-TRUST SEAM (HOST-CAPS S3, DECISIONS #26). -//! -//! The outbound trust decision — which extra trust anchors a governed hop adds, which mTLS client -//! identity it presents, and the SubjectPublicKeyInfo pin of the peer certificate a completed -//! handshake produced — lives today as three host-side primitives: [`identity`](super::identity), -//! [`trust_anchor`](super::trust_anchor) and [`spki`](super::spki). Each is already host-owned (a -//! process registry, or a pure DER walk), but nothing NAMES the three together as ONE host capability -//! a plane reaches through a trait object. -//! -//! This seam is that name. [`EgressTrustHost`] groups the three as one capability the composition root -//! installs once at boot ([`install_egress_trust_host`]); a plane reads it back through -//! [`egress_trust_host`] and calls typed methods instead of the free functions directly, so the trust -//! machinery is swappable behind a stable boundary without any call site changing. It mirrors the -//! egress fetch seam's `install_hostless_egress` / `hostless` (see `crate::egress::seam`) and the -//! `plane_host` capability-slice pattern ([`EngineHost`](super::EngineHost) and its supertraits). -//! -//! ## Additive and DORMANT -//! -//! The production capability [`PassThroughEgressTrust`] delegates to the exact free functions the -//! shipped path calls today — the pins, refs and identities it produces are BYTE-FOR-BYTE the ones -//! `identity::register`/`resolve`, `trust_anchor::register`/`resolve` and `spki::pin` produce now. And -//! NOTHING on the shipped path consults the seam yet: the egress chokepoint still calls the free -//! functions directly, so the outbound path is unchanged until a call site opts in. Reading -//! [`egress_trust_host`] in a build that installed no capability returns `None`. - -// The registries' `register` entry points are boot-only and reached by tests until the egress call -// sites flip onto the seam — the same not-yet-mounted posture `identity`/`trust_anchor` record. -#![cfg_attr(not(test), allow(dead_code))] - -use std::net::IpAddr; -use std::sync::Arc; - -use crate::config::Destinations; -use crate::egress::engine::ClientIdentity; -use crate::host_services::{DestJudge, DestRefusal}; -use crate::plane_host::spki::SpkiError; -use crate::secure::SecureLayer; - -/// THE OUTBOUND EGRESS-TRUST HOST CAPABILITY, as a neutral trait a plane reaches through instead of -/// calling [`identity`](super::identity) / [`trust_anchor`](super::trust_anchor) / [`spki`](super::spki) -/// directly. Groups the three outbound trust primitives — client-identity registration/resolution, -/// extra-trust-anchor registration/resolution, and the peer-certificate SPKI pin — as one capability. -/// `Send + Sync` so the installed capability is a process-wide `&'static dyn`. -pub trait EgressTrustHost: Send + Sync { - /// Register a parsed mTLS client `identity` at boot, returning the opaque `client_identity_ref` a - /// hop carries. Pass-through to [`identity::register`](super::identity::register). - fn register_client_identity(&self, identity: ClientIdentity) -> u64 { - super::identity::register(identity) - } - - /// Resolve a `client_identity_ref` to its parsed identity (`None` for the reserved `0` ref or an - /// unknown one). Pass-through to [`identity::resolve`](super::identity::resolve). - fn resolve_client_identity(&self, client_identity_ref: u64) -> Option { - super::identity::resolve(client_identity_ref) - } - - /// Register a set of parsed extra-root `roots` at boot, returning the opaque `trust_anchor_ref` a - /// hop carries. Pass-through to [`trust_anchor::register`](super::trust_anchor::register). - fn register_trust_anchor(&self, roots: Vec>) -> u64 { - super::trust_anchor::register(roots) - } - - /// Resolve a `trust_anchor_ref` to its extra roots (an EMPTY vec for the reserved `0` ref or an - /// unknown one — fail-closed). Pass-through to - /// [`trust_anchor::resolve`](super::trust_anchor::resolve). - fn resolve_trust_anchor(&self, trust_anchor_ref: u64) -> Vec> { - super::trust_anchor::resolve(trust_anchor_ref) - } - - /// The `sha256/` SPKI pin of a peer certificate a completed handshake produced. Pure DER - /// walk; pass-through to [`spki::pin`](super::spki::pin). - fn peer_leaf_pin(&self, cert_der: &[u8]) -> Result { - super::spki::pin(cert_der) - } - - /// An answer the kernel's own pooled client resolved for `host`, judged whole under `class` by - /// the deployment's destination guard (OWNER DESTINATION GUARD; the connector decides). INTERIM - /// for the eight pooled-client builders; struck when D1-D6 move onto `conns` (Phase B). - /// - /// # Errors - /// - /// The refusal of the first refused address; with no guard behind the capability (the - /// pass-through) every answer: FAIL CLOSED, never allowed. - fn judge_answer(&self, host: &str, _: &[IpAddr], _: u32) -> Result<(), DestRefusal> { - Err(DestRefusal { - verdict: busbar_contract::abi::host::service::DEST_NO_HOST, - reason: format!("host `{host}` was not dialled: no destination guard is installed"), - }) - } - /// A config commit: the deployment's destinations are now `d`, raised to the guard behind the - /// capability ([`DestJudge::destinations_applied`]); the pass-through has none and keeps nothing. - fn destinations_applied(&self, _: &Destinations) {} - - /// THE TLS AN OUTBOUND CONNECTION IS SECURED WITH: the connector's wrap (`crate::secure`), the - /// one TLS path (THE DESIGN: TLS stays in the connector; this crate names no TLS library). The - /// trust anchors, the pin and the client identity above are what it applies. `None` on the - /// pass-through, which has no TLS behind it: an https dial then fails, naming the missing wrap. - fn secure_layer(&self) -> Option> { - None - } -} - -/// The production egress-trust capability: a BYTE-FOR-BYTE pass-through to the host-side primitives. -/// The composition root installs this today, so a call site that opts onto the seam gets exactly -/// the refs, identities and pins the free functions produce now. -pub struct PassThroughEgressTrust; - -impl EgressTrustHost for PassThroughEgressTrust {} - -/// The capability the composition root installs: the pass-through primitives, every answer judged by -/// the deployment's one destination guard, and every outbound connection secured by the one TLS -/// wrap (both the connector's; this decides nothing). -pub struct GuardedEgressTrust(pub Arc, pub Arc); - -impl EgressTrustHost for GuardedEgressTrust { - fn secure_layer(&self) -> Option> { - Some(Arc::clone(&self.1)) - } - fn judge_answer(&self, host: &str, addrs: &[IpAddr], class: u32) -> Result<(), DestRefusal> { - self.0.judge_answer(host, addrs, class) - } - fn destinations_applied(&self, d: &Destinations) { - self.0.destinations_applied(d); - } -} - -/// THE PROCESS-WIDE egress-trust capability, installed once by the composition root -/// ([`install_egress_trust_host`]). A plane reads it back through [`egress_trust_host`] and gets `None` -/// in a build that installed none — the dormant default, under which the egress chokepoint calls the -/// free primitives directly and the outbound path is unchanged. -static EGRESS_TRUST: std::sync::OnceLock> = std::sync::OnceLock::new(); - -/// Install the process egress-trust capability — the composition root's one write, at boot, before any -/// hop opens. Idempotent by `OnceLock`: a second install is a no-op (the first wins). -pub fn install_egress_trust_host(host: Arc) { - let _ = EGRESS_TRUST.set(host); -} - -/// Raise a config commit to the installed capability, so the deployment's one destination guard -/// re-reads its metadata lists. Called at the commit (`InstalledLimits::keep`), so a rejected apply -/// leaves the lists in force; with no capability installed there is no guard to hear it. -pub(crate) fn destinations_applied(d: &Destinations) { - if let Some(host) = egress_trust_host() { - host.destinations_applied(d); - } -} - -/// The installed egress-trust capability, or `None` when none was installed (the dormant default). -#[must_use] -pub fn egress_trust_host() -> Option<&'static dyn EgressTrustHost> { - EGRESS_TRUST.get().map(AsRef::as_ref) -} - -// Test body externalised to `tests/egress_trust_tests.rs` (the sibling `identity`/`trust_anchor` -// convention) so this implementation file's length measures one thing — structure-lint:inline-tests. -#[cfg(test)] -#[path = "tests/egress_trust_tests.rs"] -mod egress_trust_tests; diff --git a/crates/busbar-kernel/src/plane_host/identity.rs b/crates/busbar-kernel/src/plane_host/identity.rs index c265c7f74a..9af224bf4d 100644 --- a/crates/busbar-kernel/src/plane_host/identity.rs +++ b/crates/busbar-kernel/src/plane_host/identity.rs @@ -17,7 +17,7 @@ //! //! ## Bounded retention //! -//! [`register`]'s signature is the ABI [`super::egress_trust::PassThroughEgressTrust`] calls through — +//! [`register`]'s signature is the one the HOT egress calls through — //! it carries no config-generation tag — so this module cannot tell a fresh hot-reload's registrations //! apart from an old one's by identity alone. What it CAN do, and what it does, is refuse to grow //! without bound: the registry retains only the [`MAX_RETAINED_IDENTITIES`] most-recently-registered diff --git a/crates/busbar-kernel/src/plane_host/mod.rs b/crates/busbar-kernel/src/plane_host/mod.rs index d779bd607a..d7f91b3f38 100644 --- a/crates/busbar-kernel/src/plane_host/mod.rs +++ b/crates/busbar-kernel/src/plane_host/mod.rs @@ -36,14 +36,12 @@ pub mod guard; // (process-atomic registries; no `App`, no engine, no FFI), so they live NEUTRALLY in // `busbar_kernel::plane_host` and are re-exported here — core's egress chokepoint names the // same `crate::plane_host::{identity, trust_anchor}` paths as before. The peer-SPKI DER walk's -// last in-core reader is gone with the engine cutover (the ENGINE computes the pin at connect -// and the chokepoint reads it off the response extensions), so `spki` is no longer re-exported; -// the A2A plane and the engine name the neutral `busbar_kernel::plane_host::spki` directly. +// walk is the connector's (`busbar_core_connector::tls::spki`): the connector owns pinning, and the +// engine reads the computed pin off its secured stream. pub(crate) mod identity_admit; pub mod journal; pub mod pipe; pub mod scope; -pub mod session_meter; pub mod trust; pub mod vtable; @@ -1816,1544 +1814,12 @@ mod tests; mod residual_tests; // ==== merged from busbar-substrate (W4.b P2 engine drain) ==== -// THE NEUTRAL LLM-RUNTIME BUILD CARRIER (1.6.0 money-path Phase 3-4 C): the single-compiled `PlaneBuildInput` -// DTO `busbar-core`'s `appbuild` populates and hands to the LLM plane's `build_runtime` seam. -pub mod build_input; -// The mTLS client-identity registry, the extra-root trust-anchor registry and the peer-certificate -// SPKI DER walk — PURE host-side TLS helpers (process-atomic registries + an RFC 5280 length-skip; no -// `App`, no engine, no FFI). They live here so the host egress chokepoint and the A2A plane both name -// one neutral home; core re-exports them under their historical `crate::plane_host::{identity, -// trust_anchor,spki}` paths. +// The mTLS client-identity registry and the extra-root trust-anchor registry — process-atomic +// registries the HOT egress resolves its refs against; they go with it. pub mod identity; -// THE COMPOSITION-ROOT-OWNED EGRESS-TRUST SEAM (HOST-CAPS S3, DECISIONS #26): the `EgressTrustHost` -// trait naming client-identity / trust-anchor / peer-SPKI as ONE host capability, with a byte-for-byte -// pass-through impl and a composition-root install/get. Additive and DORMANT — no shipped call site -// consults it yet (W2 flips the egress chokepoint onto it). -pub mod egress_trust; -// `PlaneSlots` through any pointer to a slot holder (`Arc`, load guard, borrow), so a plane's slot -// readers can take `&impl PlaneSlots` instead of a concrete snapshot type without touching callers. -pub mod slots_through; -pub mod spki; pub mod trust_anchor; -use crate::breaker::CanonicalSignal; -use crate::plane::approvals::Sealer; -use crate::plane::calllog::CallInput; -pub use crate::plane_host::build_input::{ - AffinityInput, AuthStyleInput, BreakerInput, ClientSettingsInput, FailoverInput, HealthInput, - HealthModeInput, LaneInput, OnExhaustedInput, PlaneBuildInput, PoolInput, PoolMemberInput, - TripInput, TripModeInput, -}; -use crate::store::Unavailable; -use crate::trust::validate::{Lapsed, Standing}; -use crate::trust::TrustState; -use busbar_contract::abi::hot::{AdmissionId, Signal, StatusClass}; -use busbar_contract::auth::{AuthPrincipal, IdentityRefusal}; -use busbar_contract::records::{PlaneRequestCtx, VirtualKey}; - -/// The outcome of a refusal-fidelity admit driven over the host `govern_admit_reason` seam. -#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] -pub enum GovAdmit { - /// Admitted — the RAII grant is registered in the arena the caller passed. - Admitted, - /// A limit blocked — the RENDERED reason (byte-identical to the plane's own - /// `format!("{blocked:?}")`) and the block's recovery floor in whole seconds. - Blocked { - /// The rendered reason bytes the host copied out (the exact `{blocked:?}` the plane surfaces). - reason: String, - /// The recovery floor in whole seconds (`0` when the block does not self-recover / never rolls). - retry_after_secs: u64, - }, -} - -/// The verdict of a request-admission gate fired over the host `gate_decide` seam. -#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] -pub enum GateOutcome { - /// No gate objected (or none is attached) — the request proceeds. - Proceed, - /// A gate refused the request. Reconstructed from the `GateVerdictOut` header + the copied-out - /// buffers, byte-identical to the in-process `GateVerdict::Reject`. - Reject { - /// The hook's refusal status, already clamped to the 4xx band by the gate. - status: u16, - /// The hook's own refusal message (empty on a fail-closed refusal). - message: String, - /// The transport/policy name, for the audit row and the log line (empty on a fail-closed refusal). - hook: String, - }, -} - -/// The verdict of a request-admission TRANSFORM (`prompt: rw` rewrite) chain fired over the host -/// `transform_over` seam — the TAP/observe-transform half of the hook surface, the twin of -/// [`GateOutcome`] for the rewrite pass. When no rewrite hook is attached the plane never calls the -/// seam (it guards on [`AdmissionHost::tap_attached`]), so the request/response path is BYTE-IDENTICAL -/// to a deployment with the seam absent: the tap is a no-op absent hooks. -#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] -pub enum TransformVerdict { - /// The transform chain ran. `args_json` is the (possibly rewritten) payload the plane should send - /// upstream; `applied` is `true` IFF a hook actually committed a rewrite (so the plane can keep - /// its original bytes untouched — and therefore byte-identical — when no rewrite landed, even - /// though a chain of purely-abstaining hooks fired). Reconstructed byte-for-byte from the same - /// serde_json round-trip the gate seam uses (`preserve_order` OFF ⇒ a `Value` object is a - /// sorted-stable `BTreeMap`). - Proceed { - /// Whether ANY hook in the chain committed a rewrite to the payload. - applied: bool, - /// The payload bytes to send upstream — the rewritten `arguments`/`params` when `applied`, or - /// a faithful re-serialization of the original otherwise (the plane uses its own bytes then). - args_json: Vec, - }, - /// A `prompt: rw` gate REJECTED the request on the transform path (reject > rewrite > abstain). - /// Same clamped/sanitized semantics as a decide-path reject, reconstructed identically to - /// [`GateOutcome::Reject`]. - Reject { - /// The hook's refusal status, already clamped to the 4xx band. - status: u16, - /// The hook's own refusal message (empty on a fail-closed refusal). - message: String, - /// The transport/policy name, for the audit row and the log line (empty on a fail-closed refusal). - hook: String, - }, -} - -/// What could be established about a presented bearer's RFC 8707 audience binding — the outcome of -/// the host `identity_audience_binding` pre-filter, for credentials busbar did not mint. -/// -/// Relocated here from `busbar_kernel::auth::audience` so a plane reads the pre-filter verdict without -/// naming the core auth module; the binding JUDGEMENT (which reaches core's governance token prefix) -/// stays core behind [`EngineHost::identity_audience_binding`]. Core re-exports this at -/// `crate::auth::audience::Binding`, so its own callers and the enum's variants are unchanged. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AudienceBinding { - /// A busbar-signed token. The real audience check happens in the verifier, which has the - /// signature and the claims; the pre-filter must not pre-judge it. - Deferred, - /// A JWT whose `aud` includes the expected value. Not an admission — the chain still verifies it. - Bound, - /// A JWT whose `aud` does not include the expected value, or which carries no `aud` at all. Both - /// are refused for the same reason: minted for someone else, or for nobody in particular. - Mismatch, - /// Not a JWT and not a busbar token: nothing to read. Refused. - Opaque, -} - -/// The raw wire outcome of a host-driven completion: the pipeline's HTTP status and body bytes, -/// for the plane to shape into its protocol's own result. Neutral — no axum `Response`, no `App`. -#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] -pub struct HostCompletion { - /// The pipeline's HTTP status. - pub status: u16, - /// The pipeline's response body bytes (bounded by the `max_body_bytes` the caller passed). - pub body: bytes::Bytes, -} - -/// Why the host produced no completion. NEUTRAL: the kernel names the fact, and the plane that asked -/// words the refusal in its own vocabulary (lean-core: no plane word is a kernel literal). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CompletionRefusal { - /// No plane installed the resolved-completion ingress, so there is no dialect to drive. - NotInstalled, - /// The pipeline answered, but its body could not be read within `max_body_bytes`; carries the - /// read error's text. - BodyUnread(String), -} - -// ───────────────────────────────────────────────────────────────────────────────────────────── -// THE GAUNTLET SEAM — one shared request sequence every protocol plane rides. -// ───────────────────────────────────────────────────────────────────────────────────────────── - -/// The neutral per-request facts of ONE gauntlet traversal that the SHARED sequence -/// ([`run_gauntlet`]) reads and threads — the resolved identity, the destination the pre-admission -/// verify judges, and the correlation/timing a plane's stage-6 record joins on. Everything protocol- -/// or dialect-specific (the parsed body, the dialect handler, the wire framing, the plane's engine) -/// lives in the plane's own [`GauntletPlane`] value, NEVER here — so this names no plane type. -pub struct GauntletRequest<'a> { - /// Stage 1 — the resolved caller identity/scope, threaded from the auth layer that ran upstream. - pub gov: &'a busbar_contract::records::PlaneRequestCtx, - /// The destination key the pre-admission `verify_destination` judges — a model for the LLM plane, - /// a tool/server for the MCP plane. Opaque to the shared sequence; each plane spells its meaning. - pub destination: &'a str, - /// The per-request correlation id the plane's stage-6 audit record joins on (single-accounting). - pub correlation_id: u64, - /// The header-arrival epoch (whole seconds) the request was admitted at — the metering window base. - pub charged_at: u64, - /// The monotonic start instant for the request-duration metric. - pub started: std::time::Instant, -} - -/// The outcome of the pre-admission destination verification (stage 2). -pub enum VerifyOutcome { - /// The destination is permitted; the sequence proceeds to the plane's `drive`. - Proceed, - /// The destination is refused. The plane returns its OWN already-finished, protocol-native - /// response (metrics/webhook already emitted plane-side); [`run_gauntlet`] returns it verbatim, - /// so refusal shaping stays byte-identical to the plane's in-place rejection. - Refuse(axum::response::Response), -} - -/// THE LOOP'S ANSWER SHAPE at the unified boundary (DECISIONS #28), plane-neutral. -/// -/// When a rider runs on the kernel Teller loop (`busbar_kernel::teller::run_unit`), the loop returns -/// only the money/lifecycle `Ended`; the request's ANSWER bytes are read back out of the plane's own -/// `ctx.key`-keyed binding table (the admin template — `AdminUnitTable`). `PlaneAnswer` is what a -/// rider stashes into that table and the outer async handler serves. Two shapes, per #28: -/// -/// - [`PlaneAnswer::Unary`] — buffered status + headers + body, crosses the sync channel and settles -/// at Encode on the final byte count (the admin cleanliness caller and every unary verb, including -/// an SSE body materialised after dispatch). -/// - [`PlaneAnswer::Live`] — a live body the outer handler serves directly, governed at admit -/// (auth/verify/approve/admit/audit ran unary), bypassing the Encode-emits-bytes path (the llm -/// RouteLeg emits this). -/// -/// NEUTRAL BY CONSTRUCTION: nothing here names a plane, a dialect or a verb, and it lives in the -/// neutral substrate tier — NOT in `busbar-kernel`, which keeps naming zero planes and zero answer -/// shapes. It coexists with the shipped [`crate::plane_host`] `PlaneDispatch`-role carriers (#28): a -/// plane's arena (`DispatchScope`) rides its own loop value, so nothing in this shape carries it. -pub enum PlaneAnswer { - /// Buffered: status, headers, body bytes. Settles at Encode on the final byte count. - Unary( - axum::http::StatusCode, - axum::http::HeaderMap, - axum::body::Bytes, - ), - /// A live body the outer async handler serves directly, governed at admit. - Live(axum::response::Response), -} - -impl PlaneAnswer { - /// Materialise either shape into the one response the outer handler serves. `Unary` is rebuilt - /// status-for-header-for-byte so a buffered answer serves byte-identically to the response the - /// plane shaped; `Live` is handed through untouched. - pub fn into_response(self) -> axum::response::Response { - match self { - PlaneAnswer::Unary(status, headers, body) => { - let mut resp = axum::response::Response::new(axum::body::Body::from(body)); - *resp.status_mut() = status; - *resp.headers_mut() = headers; - resp - } - PlaneAnswer::Live(resp) => resp, - } - } -} - -/// THE PLANE-NEUTRAL IN-FLIGHT ANSWER TABLE (DECISIONS #28), keyed by the unit's `ctx.key`. -/// -/// The kernel Teller loop (`busbar_kernel::teller::run_unit`) returns only the money/lifecycle -/// `Ended`; a rider stashes its [`PlaneAnswer`] here at the step that produced it (Route, or a -/// pre-charge refusal) and the outer async handler reads it back by key AFTER the loop returns — -/// mirroring `AdminUnitTable` (the admin template, `admin_mount.rs`). NEUTRAL BY CONSTRUCTION: the -/// key is a bare `u64` (the raw `ctx.key`), so this names no plane, no dialect and no verb, and it -/// lives in the neutral substrate tier — every rider (mcp first, then a2a/voice/llm) reuses this ONE -/// table with zero new seam code. -#[derive(Default)] -pub struct PlaneInFlight { - slots: std::sync::Mutex>>, -} - -impl PlaneInFlight { - /// An empty table. - #[must_use] - pub fn new() -> Self { - PlaneInFlight::default() - } - - /// Open a slot for a unit about to run. Idempotent; a re-open clears any prior answer. - pub fn open(&self, key: u64) { - self.slots - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .insert(key, None); - } - - /// Record the unit's answer, at the step that produced it. - pub fn store(&self, key: u64, answer: PlaneAnswer) { - self.slots - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .insert(key, Some(answer)); - } - - /// Take the unit's answer out and forget the slot, after the loop returned. `None` if the unit - /// produced no answer (a path that never reached the step that stores one). - #[must_use] - pub fn take(&self, key: u64) -> Option { - self.slots - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .remove(&key) - .flatten() - } -} - -/// A protocol plane's contribution to the shared gauntlet: the pre-admission destination check -/// (stage 2, sync) and the byte-identical engine that admits, routes, meters and finishes the -/// request (stages 4+5, async). The SHARED sequence ([`run_gauntlet`]) owns only stage 1 (identity, -/// already resolved and threaded via `req.gov`) and the stage-2→drive ORDER — verify strictly before -/// any charge, so nothing can reject an already-charged request. The sequence pulls NOTHING out of -/// `drive`: the plane's admission/route/metering/finish stay inside it, byte-identical. Names are -/// neutral; a plane implements this in its own crate (`busbar-mcp`/`busbar-a2a`) or in core (the LLM -/// native plane) identically — they are siblings on this one seam. -#[async_trait::async_trait] -pub trait GauntletPlane: Send + Sync { - /// STAGE 2 — pre-admission destination verification. Sync; runs BEFORE `drive`. `Proceed` clears - /// the request to admission; `Refuse` carries the plane's OWN finished, protocol-native rejection. - fn verify_destination(&self, req: &GauntletRequest<'_>) -> VerifyOutcome; - - /// STAGES 4+5 — the plane's OWN engine: budget-admission, route/failover, egress, and the plane's - /// own metering, returning the (possibly streaming) response. Byte-identical to the plane's - /// in-place dispatch. Takes `self: Box` so the plane moves its owned per-request payload - /// (body/parsed form/grant) into the engine; object-safe, so `run_gauntlet` drives it as `dyn`. - async fn drive(self: Box, req: GauntletRequest<'_>) -> axum::response::Response; - - /// THIS PLANE'S CAPABILITY KEY, for the composition-tier host-selection seam - /// ([`register_gauntlet_runner`]). `None` (the DEFAULT) means "run me on the substrate loop, - /// exactly as today" — so with nothing overridden and nothing registered, every path is - /// byte-identical to the shipped release. A plane opts onto the unified kernel loop by returning - /// its key AND the composition root registering a runner under it (an oracle-gated flip, #29). - /// NEUTRAL: the trait names no plane; each plane spells its own key, and the seam only ever - /// compares strings. - fn capability_key(&self) -> Option<&str> { - None - } -} - -/// The successful OPEN-PASS ADMISSION result of the session gate in [`run_gauntlet_session`] — the request cleared the verify-before- -/// charge gate. Carries the per-request `correlation_id` so a SESSION opener ([`run_gauntlet_session`]) -/// can join its own later durable/audit rows on it. A one-shot [`run_gauntlet`] discards it and proceeds -/// straight to `drive`; a session opener returns it to the plane, which then reserves/binds/opens its -/// live carrier AFTER (nothing charged before the gate cleared). -#[cfg_attr(not(any(feature = "dispatch", feature = "relay")), allow(dead_code))] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Admitted { - /// The per-request correlation id the caller threads into its stage-6 / durable-session record. - pub correlation_id: u64, -} - -// THE SUBSTRATE TELLER LOOP IS GONE (W2.b, DECISIONS #28). The gauntlet's old seat on that loop — the -// `GauntletAdapter` (a pass-through `TellerPlane`), `gauntlet_unit`, and the `admit_open` open-pass gate — -// was deleted with `busbar-substrate/src/teller/`. Every shipped plane now rides the UNIFIED -// `busbar_kernel::teller` loop via a registered runner on the host-selection seam below; the fallback for -// an unregistered/test plane is the inline verify-then-drive in `run_gauntlet[_session]`, byte-identical -// to what that adapter loop produced (it wrapped nothing). - -// ───────────────────────────────────────────────────────────────────────────────────────────── -// THE COMPOSITION-TIER HOST-SELECTION SEAM (loop unification, DECISIONS #28). -// -// A per-capability-keyed registry of KERNEL-LOOP runners the composition root installs at boot -// (mirrors the admin-mount-seam, busbar-core/src/admin/seam.rs). The kernel loop lives in -// `busbar-kernel`, which this tier does not depend on, so the runners are FN-POINTERS root injects. -// The gauntlet free fns below consult the registry by the request's plane capability key: if a -// runner is registered, route to it; UNSET (the default) runs today's substrate loop, byte-identical. -// NEUTRAL: the seam names no plane — it is a `&str`-keyed table, exactly as `AdminUnitTable` is -// `ctx.key`-keyed. Per-plane flip = registering that one plane's runner, oracle-gated (#29). -// ───────────────────────────────────────────────────────────────────────────────────────────── - -/// A kernel-loop one-shot runner, as the composition root injects it: the kernel-loop twin of -/// [`run_gauntlet`], erased to a fn-pointer so this tier need not name `busbar-kernel`. -pub type GauntletRunner = for<'a> fn( - GauntletRequest<'a>, - Box, -) -> std::pin::Pin< - Box + Send + 'a>, ->; - -/// A kernel-loop session runner, the kernel-loop twin of [`run_gauntlet_session`]. -pub type SessionRunner = for<'a> fn( - GauntletRequest<'a>, - Box, -) -> Result; - -static ONE_SHOT_RUNNERS: std::sync::RwLock< - std::collections::BTreeMap<&'static str, GauntletRunner>, -> = std::sync::RwLock::new(std::collections::BTreeMap::new()); - -static SESSION_RUNNERS: std::sync::RwLock> = - std::sync::RwLock::new(std::collections::BTreeMap::new()); - -/// Register a kernel-loop one-shot runner for a plane capability key. Composition root only, at boot. -/// Last-write-wins and idempotent. Registering a key is the per-plane FLIP onto the unified loop. -pub fn register_gauntlet_runner(key: &'static str, runner: GauntletRunner) { - ONE_SHOT_RUNNERS - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .insert(key, runner); -} - -/// Register a kernel-loop session runner for a plane capability key. Composition root only, at boot. -pub fn register_session_runner(key: &'static str, runner: SessionRunner) { - SESSION_RUNNERS - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .insert(key, runner); -} - -/// Whether a one-shot kernel-loop runner is registered for this capability key — i.e. whether that -/// plane has been FLIPPED onto the unified kernel loop. The read-side twin of -/// [`register_gauntlet_runner`], for a composition-root boot assertion or a per-plane cutover -/// regression test. NEUTRAL: a bare `&str` lookup that names no plane. -#[must_use] -pub fn gauntlet_runner_registered(key: &str) -> bool { - one_shot_runner(key).is_some() -} - -/// Whether a SESSION kernel-loop runner is registered for this capability key — i.e. whether that -/// session plane (voice/streaming) has been FLIPPED onto the unified kernel loop's session admit. The -/// read-side twin of [`register_session_runner`], for a composition-root boot assertion or a per-plane -/// cutover regression test. NEUTRAL: a bare `&str` lookup that names no plane. -#[must_use] -pub fn session_runner_registered(key: &str) -> bool { - session_runner(key).is_some() -} - -fn one_shot_runner(key: &str) -> Option { - ONE_SHOT_RUNNERS - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .get(key) - .copied() -} - -fn session_runner(key: &str) -> Option { - SESSION_RUNNERS - .read() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .get(key) - .copied() -} - -/// THE SHARED GAUNTLET SEQUENCE — the ONE request path every protocol plane rides. It consults the -/// host-selection seam first: a plane whose capability key has a kernel-loop runner registered rides -/// the UNIFIED `busbar_kernel::teller` loop; every other plane (a not-yet-flipped or test plane) runs -/// the inline verify-then-`drive` fallback below, byte-identical to the deleted substrate Teller loop -/// this seam used to ride (that loop wrapped nothing — see the fallback comment). -/// Stage 1 identity is already resolved (threaded via `req.gov`); the path runs the plane's -/// `verify_destination` at Verify, in the correct PRE-ADMISSION position, and only if it proceeds the -/// plane's `drive` at Route (its own byte-identical engine + metering). Returns the plane's (possibly -/// streaming) response verbatim. The plane owns admission/route/metering/finish; the loop owns solely -/// the order (nothing may reject after a charge) — so all planes enforce that invariant in ONE place. -pub async fn run_gauntlet( - req: GauntletRequest<'_>, - plane: Box, -) -> axum::response::Response { - let selected = plane.capability_key().and_then(one_shot_runner); - if let Some(runner) = selected { - return runner(req, plane).await; - } - // NO kernel-loop runner registered for this key. With W2.b every shipped plane is flipped onto the - // unified kernel loop, so this arm is the DEFAULT only for a plane not (yet) flipped or a test - // plane. It runs the plane's OWN two-stage sequence inline — verify STRICTLY before drive — which - // is BYTE-IDENTICAL to the (now-deleted) substrate Teller loop the gauntlet used to ride: that loop - // proceeded every step, opened an empty hold, settled nothing, and returned the plane's `drive` - // response (or its own pre-charge refusal) verbatim. The loop wrapped nothing, so inlining it - // removes only the redundant loop, not any behaviour (the kernel-vs-substrate shadow-compare proved - // the two produced identical bytes before the substrate loop was removed). - match plane.verify_destination(&req) { - VerifyOutcome::Refuse(resp) => resp, - VerifyOutcome::Proceed => plane.drive(req).await, - } -} - -/// THE SESSION SIBLING of [`run_gauntlet`] — the OPEN-PASS admission for a live, session-oriented plane -/// (voice/duplex) that has no one-shot `drive`-shaped Response to return. It runs the SAME -/// verify-STRICTLY-before-charge gate (a registered kernel-loop session runner, else the inline -/// fallback) and returns the [`Admitted`] result instead of -/// driving a request: the plane's own reserve/bind/open + socket bind proceed only on `Ok`, so a `Refuse` -/// costs ZERO bytes and ZERO charge (nothing opened before the gate cleared). Distinct from -/// [`run_gauntlet`] (one Response) but a TRUE sibling — they share the one verify-before-charge gate, so -/// a refactor can neither inline nor foreclose this opener, and both enforce that one order. -/// -/// Synchronous: the admission gate is `verify_destination` (sync), so a session opener (a sync -/// `begin_session`) calls this directly — there is no async `drive` leg on the session path. -/// -/// (`result_large_err`: the `Err` is the plane's OWN finished refusal `Response`, carried BY VALUE so -/// refusal shaping stays byte-identical to [`run_gauntlet`]'s verbatim return — boxing it would diverge -/// the two siblings on the type they carry a refusal in.) -#[allow(clippy::result_large_err)] -pub fn run_gauntlet_session( - req: GauntletRequest<'_>, - plane: Box, -) -> Result { - let selected = plane.capability_key().and_then(session_runner); - if let Some(runner) = selected { - return runner(req, plane); - } - // NO kernel-loop session runner registered for this key — the inline open-pass admit, BYTE-IDENTICAL - // to the (now-deleted) substrate `admit_open`: it ran the SAME verify-STRICTLY-before-charge gate - // through the substrate Teller's `open_unit` over a pass-through adapter that opened an empty hold - // and settled nothing, returning `Admitted` (the request's own correlation) on a pass or the plane's - // OWN finished refusal verbatim. Inlining removes only the redundant loop: on `Refuse` NOTHING is - // opened (zero bytes, zero charge); on `Proceed` the caller opens its own carrier next. - match plane.verify_destination(&req) { - VerifyOutcome::Refuse(resp) => Err(resp), - VerifyOutcome::Proceed => Ok(Admitted { - correlation_id: req.correlation_id, - }), - } -} - -/// The `plane_slots` companion key under which a plane's ALWAYS-PRESENT per-generation runtime object -/// is carried — DERIVED from the plane's own decl `key` by the neutral `":runtime"` convention, -/// so core spells no plane token. It is deliberately distinct from the plane's config-conditional -/// dispatch slot (carried under the bare decl key): the runtime bundle exists on every generation -/// whereas the dispatch slot is absent when the plane's config block is unspecified, so folding them -/// onto one key would change the bare key's presence semantics (and the dispatch table `build_dispatch` -/// derives from it). Named by both core's `appbuild` (which composes the slot) and the owning plane -/// (which reads it back through [`EngineHost::plane_slot`]) — each passes its decl key and gets the -/// SAME interned `&'static str`, so it lives in the neutral substrate rather than either crate. -/// -/// Interned process-lifetime (leaked once per distinct key, bounded by the plane count) so the -/// companion key is a stable `&'static str` fit for the `plane_slots` map's key type without either -/// caller holding a hard-coded literal. -pub fn runtime_slot_key(plane_key: &str) -> &'static str { - static INTERNED: std::sync::Mutex> = - std::sync::Mutex::new(std::collections::BTreeMap::new()); - let composed = format!("{plane_key}:runtime"); - let mut interned = INTERNED.lock().unwrap_or_else(|e| e.into_inner()); - if let Some(k) = interned.get(&composed) { - return k; - } - let leaked: &'static str = Box::leak(composed.clone().into_boxed_str()); - interned.insert(composed, leaked); - leaked -} - -/// A source of freshly live-bound hosts: each call returns a host reading the current snapshot, -/// so a config swap between calls is seen. Handed to transports that re-mint per frame. -pub type LiveHostFactory = std::sync::Arc std::sync::Arc + Send + Sync>; - -// ───────────────────────────────────────────────────────────────────────────────────────────── -// OPAQUE GOVERNANCE HANDLES (App-retype WEDGE 2) — the neutral carrier tokens a plane HOLDS on its -// per-request sink so the sink's field types stop naming `busbar_kernel::governance::GovState` / -// `busbar_kernel::cost::CostModel` / `busbar_kernel::governance::AdmitGrant`. Each wraps an -// `Arc` the host minted over the concrete engine value; the plane never -// introspects it — it hands [`GovHandle`]/[`CostHandle`] BACK to the metering seams -// ([`EngineHost::meter_ledger`]/[`EngineHost::meter_series`]), which downcast host-side. This keeps -// the byte-identical `sink.gov`/`sink.cost` the plane minted (a custom test cost, the live app cost — -// whichever the sink was built from), NOT a re-read of the host snapshot. -// ───────────────────────────────────────────────────────────────────────────────────────────── - -/// An OPAQUE, cheaply-cloned (one `Arc` bump) handle to the deployment's governance state — minted -/// host-side ([`EngineHost::governance`]) and consumed host-side ([`EngineHost::meter_ledger`] / -/// [`EngineHost::meter_series`]). The plane holds it on its per-request sink WITHOUT naming -/// `busbar_kernel::governance::GovState`. -#[derive(Clone)] -pub struct GovHandle(pub Arc); - -/// The cost-model twin of [`GovHandle`] — an opaque handle to a resolved `CostModel`. KERNEL-SIDE -/// ONLY: it rides inside a [`MeterPin`] and no plane-facing signature names it (#43). -#[derive(Clone)] -pub struct CostHandle(pub Arc); - -/// THE METER PIN — what a plane holds on its per-request sink so its counts land against the -/// governance state AND the rate card that were in force when the request was admitted. OPAQUE: the -/// fields are private and only the kernel's host pins a card ([`BudgetHost::meter_pin`]), so a plane -/// carries its request's pricing context without naming a cost or price type (#43: a plane is -/// PRICING-BLIND; #71: its money obligation is one raw count per class). The plane hands the pin BACK -/// to the metering seams ([`BudgetHost::meter_ledger`], [`BudgetHost::rate_headroom`], -/// [`BudgetHost::budget_state`], [`BudgetHost::meter_series`]), which read it kernel-side. A plane -/// calls [`BudgetHost::meter_series`] UNCONDITIONALLY with the counts it observed (#43: no branch, no -/// knowledge of billing state) — #42's billing-off posture is the VIEW's job (the served figure reads -/// zero), not the write's; nothing kernel-side gates the row on the pinned card any more (that gate, -/// `meter_series_billed`, was dead code with no production caller after d09c2e0b0 and is gone). -/// -/// A plane-side host (a test double) pins NO card: its pin answers the no-card posture everywhere. -#[derive(Clone)] -pub struct MeterPin { - gov: GovHandle, - cost: CostHandle, -} - -impl MeterPin { - /// Pin `gov` and the card `cost` names. Kernel-side; a TEST that hand-builds a card pins it here. - #[cfg(any(test, feature = "test-support"))] - pub fn new(gov: GovHandle, cost: CostHandle) -> Self { - MeterPin { gov, cost } - } - - /// The governance handle the pin carries — what [`BudgetHost::meter_series`] takes. - pub fn gov(&self) -> &GovHandle { - &self.gov - } -} - -/// An opaque handle to a governance ADMISSION grant (`AdmitGrant`), held DROP-ONLY on a plane's -/// per-request sink so the admission's in-flight concurrency holds release when the last sink clone -/// drops. The plane never introspects it; it exists purely so its `Drop` (on the last clone) releases -/// the gauges — byte-identical to the sink's current `Option>`. -#[derive(Clone)] -pub struct AdmitHandle(pub Arc); - -/// BRAKE (audit D): the BREAKER-family slice of the host seam, split off `EngineHost` as a supertrait -/// so the circuit-breaker admission/settle/record cluster stays a cohesive, bounded ABI rather than -/// dissolving into the ~30-method god-trait. Groups the five `(pool, lane)` breaker seams a plane's -/// dispatch/failover legs drive: win a probe ([`breaker_admit`](Self::breaker_admit)), fold a leg's -/// outcome ([`breaker_settle`](Self::breaker_settle)), the in-place record fallbacks -/// ([`breaker_record_success`](Self::breaker_record_success) / -/// [`breaker_record_signal`](Self::breaker_record_signal)), and the cooldown read -/// ([`breaker_retry_after_secs`](Self::breaker_retry_after_secs)). PURE STRUCTURAL: every method keeps -/// its exact signature and same-dispatch body; a plane that names `EngineHost` still calls these -/// through the inherited supertrait bound. -pub trait BreakerHost: Send + Sync { - /// WIN ONE `(pool, lane)` breaker probe through the host `breaker_admit` seam, leaving the - /// settle-capable admission REGISTERED in `scope`'s arena and returning the POD [`AdmissionId`] — - /// or the store's own [`Unavailable`] refusal. Identical to - /// `busbar_kernel::plane_host::breaker::breaker_admit_over`. - fn breaker_admit( - &self, - scope: &DispatchScope, - pool: &[u8], - lane: u32, - ) -> Result; - - /// Fold a leg's classified outcome through the host `breaker_settle` seam over `admission` (looked - /// up in `scope`'s arena). `Ok` means the live admission was found and settled; a `Gone` means it - /// was already settled — the caller falls back to an in-place record. Byte-identical disposition - /// to the plane's own `record_signal`/`record_success`. - fn breaker_settle( - &self, - scope: &DispatchScope, - admission: AdmissionId, - signal: &Signal, - ) -> StatusClass; - - /// Record a SUCCESS against the `(pool, lane)` breaker cell in place — the fallback a settle leg - /// takes when no arena owns the probe (or a multi-round leg whose probe was already settled). - /// Identical to the plane's own `PlaneBreakers::record_success`. - fn breaker_record_success(&self, pool: &str, lane: usize); - - /// Record a canonical failure signal against the `(pool, lane)` breaker cell in place — the - /// fallback twin of [`breaker_record_success`](Self::breaker_record_success). Identical to the - /// plane's own `PlaneBreakers::record_signal`. - fn breaker_record_signal(&self, pool: &str, lane: usize, sig: &CanonicalSignal); - - /// The seconds until the `(pool, lane)` breaker cell's cooldown expires — the honest `Retry-After` - /// for a refused pooled dispatch, read PER MEMBER so a pool whose members trip independently - /// answers with the soonest. Identical to the plane's own `PlaneBreakers::retry_after_secs`; a - /// pure read, so it needs no `HostCtx`. - fn breaker_retry_after_secs(&self, pool: &str, lane: usize) -> u64; -} - -/// BRAKE (audit D): the LANE/POOL-family slice of the host seam, split off `EngineHost` as a supertrait -/// so the lane-runtime + pool-membership + probe-default cluster stays a cohesive, bounded ABI. Groups -/// the five seams a plane's routing/health machinery reads: the neutral breaker/lane store view -/// ([`lane_store`](Self::lane_store)), the two process-wide active-probe fallbacks -/// ([`default_probe_interval_secs`](Self::default_probe_interval_secs) / -/// [`default_probe_timeout_secs`](Self::default_probe_timeout_secs)), and the failover-pool membership -/// resolvers ([`pool_members_repeatable`](Self::pool_members_repeatable) / -/// [`plane_pool_members`](Self::plane_pool_members)). PURE STRUCTURAL: signatures and bodies are -/// unchanged; a plane that names `EngineHost` reaches these through the inherited supertrait bound. -pub trait LanePoolHost: Send + Sync { - /// The breaker/lane store this deployment routes through, as the NEUTRAL - /// [`busbar_kernel::store::LaneRuntime`](crate::store::LaneRuntime) view — the seam the engine's - /// `app.store` reads (select/health/pipeline lane admit/settle/snapshot) resolve to WITHOUT naming - /// core's `state::App`. A pure borrow of the bound snapshot's store, no `HostCtx`; the returned - /// `&dyn LaneRuntime` shares the live in-memory breaker engine, identical to `&*App::store`. - /// - /// WEDGE 2 (App-retype): additive — the transitional seam the wedge-3 `app.store → host.lane_store()` - /// flip targets. The `LaneRuntime` trait already lives in substrate (wedge 1), so this names no core type. - fn lane_store(&self) -> &dyn crate::store::LaneRuntime; - - /// The process-wide active-probe INTERVAL fallback (whole seconds) a lane with no - /// `health.interval_secs` inherits — the host-read form of `busbar_kernel::limits::default_probe_interval_secs`. - /// A pure read of the live limits registry, no `HostCtx`; byte-identical to that free fn. - /// - /// WEDGE 2 (App-retype): additive — the seam the wedge-3 `health.rs` probe-spawn flip targets. The - /// limits fns read core's runtime `LIMITS` global, so they CANNOT relocate to substrate by-identity; - /// this host seam is the neutral home instead. - fn default_probe_interval_secs(&self) -> u64; - - /// The process-wide active-probe TIMEOUT fallback (whole seconds) a lane with no `health.timeout_secs` - /// inherits — the host-read twin of [`default_probe_interval_secs`](Self::default_probe_interval_secs). - /// Byte-identical to `busbar_kernel::limits::default_probe_timeout_secs`. - fn default_probe_timeout_secs(&self) -> u64; - - /// The `(pool_name, members, repeatable)` of the failover pool `member` belongs to, off the BOUND - /// snapshot; `None` when `member` is un-pooled. `repeatable` is the pool's `repeatable:` operation - /// list (what `CandidatePoolCfg::repeatability` consults) — the extra tuple element that - /// distinguishes this seam from the 2-tuple [`plane_pool_members`](Self::plane_pool_members). - /// Identical to scanning the deployment's repeatable-carrying pool map. - fn pool_members_repeatable(&self, member: &str) -> Option<(String, Vec, Vec)>; - - /// The `(pool_name, members)` of the failover pool `member` belongs to on the plane identified by - /// the opaque registry `plane_key`, off the BOUND snapshot; `None` when `member` is un-pooled. The - /// member-selection walk derives each candidate's lane from the member's position in the returned - /// list, so name + members is the whole seam. Identical to scanning the plane's pool map. - fn plane_pool_members(&self, plane_key: &str, member: &str) -> Option<(String, Vec)>; -} - -/// THE KERNEL'S PRICING SIDE of a live carrier — the rate card that prices each increment. KERNEL-INTERNAL (#43: "a plane plugin NEVER sees its rate card or -/// fees"): it is NOT a supertrait of [`EngineHost`], so no plane-side host implements it and no plane -/// names it. A plane meters a live carrier through the kernel's count-only -/// [`SessionAccount`](session_meter::SessionAccount), which prices nothing (Q21b). (The hot-ABI -/// reserve-then-settle lease slots that once drove this trait were retired by KERNEL<>PLUGINS step 16: -/// a plane never hands the kernel a price, #43/#71.) Money-denominated end to end in `u128` -/// nanodollars; a plain Rust trait, not the frozen hot FFI. -pub trait MeteringHost: Send + Sync { - /// PRICE a turn's neutral [`billing::Usage`](crate::billing::Usage) counts for `model` into - /// nanodollars via the deployment's rate card — the SAME `CostModel` arithmetic the LLM - /// enforcement/derive path uses (a new ENTRY POINT over the same function). - /// - /// Semantics mirror the host's per-model rate lookup exactly: - /// - no rate card configured ⇒ `Some(0)` (pricing off — every model prices at 0, as core does); - /// - card present, `model` priced ⇒ `Some(nanos)`; - /// - card present, `model` UNKNOWN ⇒ `None` — the caller FAILS CLOSED (an unpriced passthrough model - /// must not meter as free). - fn price_usage(&self, model: &str, usage: &crate::billing::Usage) -> Option; -} - -// ───────────────────────────────────────────────────────────────────────────────────────────── -// CAPABILITY SLICES (M4 god-trait split) — the residual flat host seam, cut into cohesive capability -// supertraits so a plane depends ONLY on the slices it uses. `EngineHost` is the SUM (a supertrait of -// every slice), so an existing `Arc` caller is unaffected — it still reaches every -// method through the inherited bound — while a plane that needs a narrower capability (a voice/bytes -// port that must NOT name LLM-only `synthesize_completion`) can take `&dyn SliceX` instead. PURELY -// STRUCTURAL: every method keeps its exact signature, doc and same-dispatch body; nothing moves in the -// wire/ABI/money plane. The single `EngineHostImpl` (busbar-core) implements each slice. -// ───────────────────────────────────────────────────────────────────────────────────────────── - -/// The CLOCK slice: the host wall-clock reads a plane's timing/window arithmetic needs, in whole -/// seconds and milliseconds. Split off `EngineHost` as a supertrait; a plane that only needs the -/// clock can take `&dyn ClockHost`. -pub trait ClockHost: Send + Sync { - /// Read the host wall clock in whole SECONDS through the `clock_now` seam — the host-driven form - /// of a plane's in-place seconds clock. Identical to `busbar_kernel::plane_host::clock_now_secs_over`. - fn clock_now_secs(&self) -> u64; - - /// Read the host wall clock in MILLISECONDS through the `clock_now` seam — the host-driven form of - /// a plane's in-place millis clock. Identical to `busbar_kernel::plane_host::clock_now_ms_over`. - fn clock_now_ms(&self) -> u64; -} - -/// The TELEMETRY slice: the plane-labelled metric emits a plane fires to close a request out and to -/// count its dispatch/failover/translation events, plus the `pool_label` cardinality bound every emit -/// path runs first. Split off `EngineHost` as a supertrait; each emit is snapshot-scoped, no `HostCtx`. -pub trait TelemetryHost: Send + Sync { - /// Stamp the plane-labelled request-completion metric family for a MOUNTED plane through the host. - /// A neutral trait seam: the plane hands its own `(plane, ingress_protocol, pool, outcome, seconds)` - /// and the host records the completion, so the plane never names core's engine snapshot to close a - /// request out. Identical to `busbar_kernel::telemetry::request_finished` over the bound snapshot. - fn request_finished( - &self, - plane: &str, - ingress_protocol: &str, - pool: &str, - outcome: &'static str, - seconds: f64, - ); - - /// Count ONE dispatch ATTEMPT on `(pool_label, lane)` — the `busbar_upstream_attempts_total` - /// upstream-attempt metric — through the host, so the engine emits it without naming core's - /// telemetry module. `pool_label` is the bounded metric label (a named pool, or the routed model - /// name for the default `""` cell); `lane` is the lane index the host resolves the `lane` label - /// from off the bound snapshot. A pure snapshot-scoped metric emit, no `HostCtx`; identical to - /// `busbar_kernel::telemetry::upstream_attempt` over the bound snapshot. - fn telemetry_upstream_attempt(&self, pool_label: &str, lane: usize); - - /// Count ONE classified upstream FAILURE on `(pool_label, lane)` by `disposition` — the - /// `busbar_upstream_failures_total` metric — through the host. The telemetry twin of - /// [`telemetry_upstream_attempt`](Self::telemetry_upstream_attempt); identical to - /// `busbar_kernel::telemetry::upstream_failure` over the bound snapshot. - fn telemetry_upstream_failure(&self, pool_label: &str, lane: usize, disposition: &'static str); - - /// Count ONE logical Closed→Open breaker TRIP on `(pool_label, lane)` — the - /// `busbar_breaker_trips_total` metric — through the host. Identical to - /// `busbar_kernel::telemetry::breaker_trip` over the bound snapshot. - fn telemetry_breaker_trip(&self, pool_label: &str, lane: usize); - - /// Count ONE FAILOVER event on `pool_label` by `reason` — the `busbar_failovers_total` metric — - /// through the host. Identical to `busbar_kernel::telemetry::failover` over the bound snapshot. - fn telemetry_failover(&self, pool_label: &str, reason: &'static str); - - /// Count ONE cross-protocol TRANSLATION hop `from → to` — the `busbar_translations_total` - /// metric — through the host. Both names come from the fixed protocol vocabulary, so the emit is - /// snapshot-independent; identical to `busbar_kernel::telemetry::translation`. - fn telemetry_translation(&self, from: &str, to: &str); - - /// Map a client-supplied model/name string to the BOUNDED `pool` metric label through the host: - /// the string verbatim when it names a configured pool or by-model lane, else the fixed - /// `"unresolved"` sentinel. Bounds the Prometheus label cardinality on every finish/webhook path. - /// Identical to `busbar_kernel::ingress::pool_label` over the bound snapshot; the returned slice - /// borrows `model` (or a `'static` sentinel), independent of the host. - fn pool_label<'a>(&self, model: &'a str) -> &'a str; -} - -/// The audit action of a residual row ([`JournalHost::settle_residual`]): counts reported, never billed. -pub const USAGE_RESIDUAL_ACTION: &str = "usage.residual"; - -/// The JOURNAL slice: the durable admin-audit / call-log emits a plane writes as a side effect of the -/// mutation it records. All fire-and-forget (a store miss never fails the recorded action). Split off -/// `EngineHost` as a supertrait. -pub trait JournalHost: Send + Sync { - /// Emit ONE hostless admin-audit record `(action, resource, outcome, principal)` to the shared - /// admin audit log. Fire-and-forget, loudly: a store write failure NEVER fails the mutation it - /// records. Identical to `busbar_kernel::audit::auditlog::emit_admin_hostless_now` — this seam needs - /// no `HostCtx`, so it is a plain forward to that engine (which stays unchanged in core). - fn audit_emit(&self, action: &str, resource: &str, outcome: &str, principal: &str); - - /// Record ONE admin-audit event `(action, resource, outcome, principal)` through the IN-PROCESS - /// ADMIN RING (`busbar_kernel::admin::audit::AUDIT::record_by`), which seals it into the retained ring - /// AND cascades the SAME record onto the durable hostless journal seam. Distinct from - /// [`audit_emit`](Self::audit_emit) (durable-only): this is the seam the DATA-plane egress - /// audit-and-allow path (a dropped cross-dialect control) writes through, so the record lands in the - /// in-process ring the egress audit-trail assertions read. `outcome` is a fixed vocabulary literal. - /// - /// WEDGE 3 (App-retype): the neutral home of the engine's `AUDIT.record_by(...)` reach. - fn audit_record(&self, action: &str, resource: &str, outcome: &'static str, principal: &str); - - /// THE SETTLE STEP'S RESIDUAL ROW (MONEY LAW, owner 2026-10-02; ARCHITECT ruling 2026-10-02): a - /// usage count no billing class records is WARNed where the reader found it, never billed, and - /// recorded here as ONE `usage.residual` row per non-empty `residual` map on the DURABLE - /// PER-PRINCIPAL journal stream ([`crate::residual_log`]): store-backed, hash-chained per - /// `principal`, replayed at boot, never evicted. NOT the admin audit ring - /// ([`audit_record`](Self::audit_record)), which is operator-rate and bounded in RAM. The row names - /// every unit and its count, the plane, the dialect that reported them, the serving lane and the - /// request id, outcome `unbilled`; `principal` is the caller the units settled against. An empty - /// map writes nothing. The row is the kernel's, so a plane hands the counts over and never spells - /// the row itself. - fn settle_residual( - &self, - residual: &std::collections::BTreeMap, - plane: &str, - dialect: &str, - lane: &str, - request_id: u64, - principal: &str, - ) { - crate::residual_log::emit(residual, plane, dialect, lane, request_id, principal); - } - - /// Emit ONE per-call record through the durable MCP call-log engine. The transient `HostCtx` the - /// chain seam needs is minted INTERNALLY (a fresh per-call arena over the live engine — the append - /// registers no host handle, so the arena choice is immaterial). Identical to - /// `busbar_kernel::calllog::emit`. - fn call_log_emit(&self, principal: &str, input: CallInput); - - /// The DEFERRED-SITE twin of [`call_log_emit`](Self::call_log_emit): emit through the - /// HOSTLESS call-log path, for a client-leg site that has no `HostCtx` to open. Identical to - /// `busbar_kernel::calllog::emit_hostless`. - fn call_log_emit_hostless(&self, principal: &str, input: CallInput); -} - -/// The MOUNT slice: the pure mount-table reads that shape an arrival's dialect and its pre-collapse -/// fallback error. Split off `EngineHost` as a supertrait; both are pure snapshot reads, no `HostCtx`. -pub trait MountHost: Send + Sync { - /// The mount-aware dialect an answer to `path` is SHAPED in — the host-driven form of - /// `busbar_kernel::ingress::native::envelope_dialect(App::planes.ingress_of(path))`. A pure snapshot - /// mount-table read, no `HostCtx`. - /// - /// WEDGE 3 (App-retype — THE FLIP): the seam core's `ArrivalHost` impl reads instead of the dropped - /// `ArrivalPayload::app`; the neutral `ArrivalPayload` now carries only the host, so this mount read - /// crosses the host seam like every other. - fn arrival_envelope_dialect(&self, path: &str) -> &'static str; - - /// The pre-collapse fallback error SHAPE by `path` — the host-driven form of - /// `busbar_kernel::fallback_error_response(&App::planes, path, status, kind, message)`. Renders the - /// unmatched-path/404 envelope in the dialect the deployment mounted `path` under; a pure snapshot - /// mount-table read, no `HostCtx`. The twin of [`arrival_envelope_dialect`](Self::arrival_envelope_dialect). - fn arrival_fallback_error( - &self, - path: &str, - status: axum::http::StatusCode, - kind: &str, - message: &str, - ) -> axum::response::Response; -} - -/// The REGISTRY slice: the per-generation registry/snapshot reads a plane pulls off the bound (or -/// live) snapshot — its type-erased runtime slot, its type-erased defs, the request-id counter, the -/// neutral secret resolver, and the host-held card-signing key. Split off `EngineHost` as a supertrait. -pub trait RegistryHost: Send + Sync { - /// Stamp the NEXT per-request correlation id — one relaxed `fetch_add` on the host-owned counter. - /// Identical to `busbar_kernel::state::App::next_request_id` (the counter is boot-seeded and carried - /// across config swaps, so the value is engine-snapshot independent). - fn next_request_id(&self) -> u64; - - /// The plane's type-erased runtime object off the BOUND snapshot (the one this host was minted - /// over), owned (an `Arc` clone) so it outlives the call. `None` when the plane contributed no - /// slot under `key` this generation. Identical to `busbar_kernel::state::App::plane_slot(key)` - /// cloned — a pure `plane_slots` map read, no `HostCtx` (mirrors [`next_request_id`]). - /// - /// [`next_request_id`]: RegistryHost::next_request_id - fn plane_slot(&self, key: &str) -> Option>; - - /// The plane's slot off the CURRENT snapshot — re-reads the LIVE handle so a config swap AFTER - /// this host was minted is seen (the dispatch-time re-validation / per-round revocation / watch - /// loops depend on this). Falls back to the bound snapshot for a snapshot-only mint (one built - /// without a live handle). A pure map read, no `HostCtx`. - fn plane_slot_live(&self, key: &str) -> Option>; - - /// The deployment's NEUTRAL secret resolver, behind the `busbar_contract::secret::SecretResolve` seam, so a - /// plane mints a delegation credential (and loads its outbound TLS PEM) WITHOUT naming the - /// engine's concrete `SecretResolver`. A pure snapshot read of `App::secret_resolver`, no - /// `HostCtx`; the returned `Arc` shares the live resolver (built-ins plus any - /// wired `kind: secret` plugin), fail-closed exactly as core resolution. - fn secret_resolver(&self) -> Arc; - - /// Sign a plane-framed agent-card signing input, returning the 64-byte Ed25519 signature (None - /// when this deployment holds no card-signing key). The card subkey is derived and held HOST-side; - /// only the bytes to sign cross in and only the signature crosses out — no key material reaches the - /// plane. Mints its transient HostCtx internally over a fresh per-call DispatchScope, drives the - /// slot synchronously, returns owned bytes — no HostCtx crosses an `.await`. - fn subkey_sign(&self, signing_input: &[u8]) -> Option<[u8; 64]>; - - /// The deployment's type-erased plane definitions (`Arc` holding the - /// per-plane config object the owning plane downcasts), off the BOUND snapshot — the - /// `App::agent_defs` field that is NOT a `plane_slots` entry. Owned (an `Arc` clone) so it - /// outlives the call; a pure snapshot read, no `HostCtx` (mirrors [`secret_resolver`](Self::secret_resolver)). - fn plane_defs(&self) -> Arc; -} - -/// The HOOK/CONFIG-FACADE slice (App-retype WEDGE 2d): the residual `busbar-llm` request-path reads off -/// the resolved hook/config facades — the rewrite/tap/gate chains, the routing policy, the requested- -/// signal bitmask and the group-membership walk. Each is a pure borrow of the bound snapshot tied to -/// `&self`, no `HostCtx`. Split off `EngineHost` as a supertrait; every borrow is byte-identical to the -/// `App::X` field read the wedge-3 flip targets. -pub trait HookConfigHost: Send + Sync { - /// Whether `caller_group` sits within (any ancestor of) one of `hook_groups` — the group-membership - /// walk a hook's `groups:` filter consults, resolved against this deployment's group registry. - /// Identical to `busbar_kernel::config::caller_in_hook_groups(caller_group, hook_groups, &App::groups_registry)`; - /// a pure tree walk over the bound snapshot's registry, no `HostCtx`. - /// - /// WEDGE 2 (App-retype): additive — the seam the wedge-3 `pipeline.rs` flip targets. Folding the - /// `&App::groups_registry` argument HOST-side means the engine reads group membership without naming - /// `busbar_kernel::config` or `App::groups_registry`. - fn caller_in_hook_groups(&self, caller_group: Option<&str>, hook_groups: &[String]) -> bool; - - /// This pool's resolved REWRITE chain `(timeout, policy)` — the phase-1 transform hooks fired for - /// requests routed to `pool`, empty (the default) ⇒ no pool rewrites. Byte-identical to - /// `busbar_kernel::state::App::pool_rewrites(pool)`; a pure keyed map read, no `HostCtx`. The tuple is - /// purely neutral (`Duration`, the [`RoutingPolicy`](busbar_contract::hooks::RoutingPolicy) trait object — api). - fn pool_rewrites( - &self, - pool: &str, - ) -> &[( - std::time::Duration, - std::sync::Arc, - )]; - - /// The GLOBAL (all-pools) rewrite chain `(timeout, policy)` fired in the phase-1 transform pass - /// BEFORE any pool rewrites — the borrow of `App::rewrite_hooks`. Empty (the default) ⇒ no globals. - /// Same neutral tuple shape as [`pool_rewrites`](Self::pool_rewrites). - fn rewrite_hooks( - &self, - ) -> &[( - std::time::Duration, - std::sync::Arc, - )]; - - /// Whether ANY registered hook holds a prompt-CONTENT grant (`prompt: ro`/`rw`) this generation — - /// the deployment gate that decides whether the request IR is built for the hook seam. A single - /// bool load of `App::any_content_hook`, byte-identical; `false` (the default) is the whole - /// zero-cost-when-off property. - fn any_content_hook(&self) -> bool; - - /// A hook was handed a unit's content (and the caller's identity, when `identity`): leave the - /// one access amendment the kernel's own gate and rewrite seams leave, on the node journal. - /// `principal` is whose content it was; `op` the ingress label — data, never branched on. - /// PROVIDED, so a plane's own hook call sites record through the same seam as the kernel's. - fn hook_read(&self, name: &str, principal: Option<&str>, op: &str, identity: bool) { - crate::audit::amend::hook_read(name, principal, op, identity); - } - - /// The DECISION GATES the plane `plane_key` attached to its entry `container` this generation - /// (the plane's per-entry `hooks:`), as resolved; empty = none. A gate-first plane's units - /// are screened by them (`abi::plane::TAIL_HOOKS_GATED`). PROVIDED: a host with no per-plane - /// gate map attaches none. - fn plane_gates_of( - &self, - plane_key: &str, - container: &str, - ) -> Vec<(u16, crate::hooks::ResolvedPolicy)> { - let _ = (plane_key, container); - Vec::new() - } - - /// The REWRITE chain the plane `plane_key` attached to its entry `container` this generation; - /// empty = none. PROVIDED: a host with no per-plane rewrite map attaches none. - fn plane_rewrites_of( - &self, - plane_key: &str, - container: &str, - ) -> Vec<( - std::time::Duration, - std::sync::Arc, - )> { - let _ = (plane_key, container); - Vec::new() - } - - /// The gate's INCREMENTAL-SCAN substrate under the operator's opt-in: the node's session store - /// and the hook-config generation a clearance is bound to; `None` = every request is screened - /// whole. PROVIDED: `None`. - fn gate_scan(&self) -> Option<(std::sync::Arc, u64)> { - None - } - - /// The GLOBAL request-stage `kind: tap` observers — the borrow of `App::tap_hooks`. Each - /// [`TapEntry`](crate::hooks::TapEntry) is a neutral `(deadline, prompt-grant, transport, groups)` - /// tuple. Held BY REF across the forward await-loop (the tap-fire pass reads it after each hop), so - /// the borrow is deliberately tied to the stable host `&self`, not a per-call temporary. - fn tap_hooks(&self) -> &[crate::hooks::TapEntry]; - - /// The RESPONSE-stage tap observers — the borrow of `App::tap_hooks_response`. Same - /// [`TapEntry`](crate::hooks::TapEntry) shape as [`tap_hooks`](Self::tap_hooks); fired once the - /// upstream response outcome is known. - fn tap_hooks_response(&self) -> &[crate::hooks::TapEntry]; - - /// The ROUTING-stage tap observers — the borrow of `App::tap_hooks_routing`. Fired per failover hop - /// with the routing/attempt projection; same [`TapEntry`](crate::hooks::TapEntry) shape. - fn tap_hooks_routing(&self) -> &[crate::hooks::TapEntry]; - - /// The CANDIDATE-stage tap observers — the borrow of `App::tap_hooks_candidate`. Fired once the - /// decision reconcile has produced the final candidate set; same [`TapEntry`](crate::hooks::TapEntry) - /// shape. - fn tap_hooks_candidate(&self) -> &[crate::hooks::TapEntry]; - - /// This pool's resolved DECISION GATES `(priority, policy)` in config order — the borrow of - /// `App::pool_gates(pool)`. Empty ⇒ no pool gates. The [`ResolvedPolicy`](crate::hooks::ResolvedPolicy) - /// carrier is already neutral substrate; a pure keyed map read, no `HostCtx`. - fn pool_gates(&self, pool: &str) -> &[(u16, crate::hooks::ResolvedPolicy)]; - - /// The GLOBAL decision gates `(priority, policy)` fired alongside the pool gates in the phase-2 - /// reconcile — the borrow of `App::global_gates`. Empty (the default) ⇒ the phase-2 pass is skipped. - /// Same neutral `(u16, ResolvedPolicy)` shape as [`pool_gates`](Self::pool_gates). - fn global_gates(&self) -> &[(u16, crate::hooks::ResolvedPolicy)]; - - /// This pool's resolved routing POLICY, or `None` for the zero-cost weighted/SWRR default — the - /// borrow of `App::pool_policy(pool)`. The [`ResolvedPolicy`](crate::hooks::ResolvedPolicy) is - /// neutral substrate; a pure keyed map read, no `HostCtx`. - fn pool_policy(&self, pool: &str) -> Option<&crate::hooks::ResolvedPolicy>; - - /// The config generation's declared-signal bitmask — the borrow of `App::requested_signals`. A - /// single load of the neutral [`RequestedSignals`](crate::hooks::RequestedSignals) newtype the - /// candidate-signal loop gates on (`requested.is_empty()` / `requested.wants(sig)`); the all-zero - /// default short-circuits the whole loop. Byte-identical to the field read. - fn requested_signals(&self) -> &crate::hooks::RequestedSignals; -} - -/// The BUDGET/METERING slice: the money-path seams a plane drives — COUNTS in, VERDICTS out. The -/// governance handle and [`MeterPin`] mints, the record-usage / record-metering accruals, the -/// reserve-then-charge meter, and the pure headroom/budget projections (a live carrier's -/// [`SessionAccount`](session_meter::SessionAccount) is built over them). No method names a cost or price type: pricing is the kernel's -/// ([`MeteringHost`], #43), so a plane-side host implements this slice without naming one. Split off -/// `EngineHost` as a supertrait; the accrual seams read the pin kernel-side, no `HostCtx`. -pub trait BudgetHost: Send + Sync { - /// Whether governance is configured for this deployment. Identical to - /// `busbar_kernel::state::App::governance.is_some()`. - fn governance_enabled(&self) -> bool; - - /// Record ONE metered, attributed event through the host `meter_charge` seam over `scope`'s arena, - /// billed to `caller` — the request's middleware-resolved context, carried INTO the mint (DEC-SERVE - /// G1b); `usage` supplies the counts and the `(model, provider)` words, never the key. The - /// transient `HostCtx` is minted over `scope` and consumed SYNCHRONOUSLY inside the call. - /// Fire-and-forget: a store miss is not surfaced, exactly as the in-place `record_metering` was. - fn meter_charge( - &self, - scope: &DispatchScope, - caller: &PlaneRequestCtx, - usage: &busbar_contract::abi::hot::Usage, - ); - - /// The per-caller RATE HEADROOM (min fraction of remaining request/token budget across the key's - /// chain, `None` when unconstrained) — the host-driven form of - /// `gov.rate_headroom(&app.cost, key, pool, now)` over the governance state and card `pin` carries - /// (a pure observation, no cell mutation). No `HostCtx`. `key` is the already-neutral - /// [`VirtualKey`](busbar_contract::records::VirtualKey) (api). - fn rate_headroom( - &self, - pin: &MeterPin, - key: &busbar_contract::records::VirtualKey, - pool: Option<&str>, - now: u64, - ) -> Option; - - /// The HOOK-seam budget projection for `key`: `{bucket_id, spend_at_current_rate, remaining, window}` - /// per chain bucket, derived fresh from the token ledger × the card `pin` carries — the host-driven - /// form of `gov.budget_state(&app.cost, key, now)`. Returns the neutral - /// [`BudgetBucketState`](busbar_contract::hooks::BudgetBucketState) vec (empty when the key has no chain). No - /// `HostCtx`. - fn budget_state( - &self, - pin: &MeterPin, - key: &busbar_contract::records::VirtualKey, - now: u64, - ) -> Vec; - - /// Mint the OPAQUE [`GovHandle`] for this deployment's governance state — `Some` iff governance is - /// configured. One `Arc` bump, no `HostCtx`. Byte-identical to cloning `App::governance`. - fn governance(&self) -> Option; - - /// PIN this request's metering context — `Some` iff governance is configured. The plane holds the - /// pin on its per-request sink and hands it back to the metering seams. The kernel's host pins its - /// bound snapshot's card; a plane-side host keeps this default, which pins NO card (every card read - /// through the pin answers the no-card posture). - fn meter_pin(&self) -> Option { - let cost = CostHandle(Arc::new(())); - self.governance().map(|gov| MeterPin { gov, cost }) - } - - /// The pre-admission pricing guard's VERDICT: does a PRESENT rate card leave `model` unpriced? - /// `false` for every name when no card is configured (there is no card to miss). A verdict, not a - /// rate: the plane fails the request closed on `true` and never learns a figure. - fn cost_model_unpriced(&self, model: &str) -> bool; - - /// LEDGER one delivered response's tier-split counts against the key's budget chain — the - /// host-driven form of `sink.gov.record_usage(&sink.cost, key, pool, model, tokens, now)` over the - /// governance state and card `pin` carries. A no-op on an all-zero tier, exactly as - /// `record_usage`. No `HostCtx`. - fn meter_ledger( - &self, - pin: &MeterPin, - key: &busbar_contract::records::VirtualKey, - pool: &str, - model: &str, - usage: &crate::billing::Usage, - now: u64, - ); - - /// GIVE BACK ONE FEE UNIT `fee_unit` a plane's admission counted on its own fee lane, across the - /// buckets of the key's chain `pool` reaches, in the window `now` (the count's own clock reading) - /// reached: the host-driven form of `GovState::refund_fee_unit` over the state `pin` carries. The - /// budget book only; the metering row stays. Floored at 0. - fn meter_refund_fee( - &self, - pin: &MeterPin, - key: &busbar_contract::records::VirtualKey, - pool: &str, - plane: &str, - fee_unit: &str, - now: u64, - ); - - /// Record one delivered response's RAW consumption into the per-(key, bucket, model, provider) - /// metering series — the host-driven form of - /// `sink.gov.record_metering(key_id, model, provider, usage, now)`. `gov` is the opaque handle the - /// sink minted; the host downcasts it and drives the SAME write-behind accrual (a zero-token - /// response still counts its request). No `HostCtx`. - fn meter_series( - &self, - gov: &GovHandle, - key_id: &str, - model: &str, - provider: &str, - usage: Option<&crate::billing::TokenUsage>, - now: u64, - ); -} - -/// The IDENTITY/TRUST slice: inbound identity resolution + the trust/approval seams around it — the -/// audience pre-filter, the auth-chain admit, the standing re-ask, the ask-state sealer derivation, the -/// drift quarantine settle, the one-time approval redeem, and the test-only token verifier. Split off -/// `EngineHost` as a supertrait. `#[async_trait]` because [`identity_admit`](Self::identity_admit) is -/// the one async method (it awaits a `spawn_blocking` join over the host auth chain). -#[async_trait::async_trait] -pub trait IdentityHost: Send + Sync { - /// Settle a drift disposition for `subject` through the host `drift_quarantine` seam, pulling the - /// demotion store host-side. Returns whether the slot answered `Ok`; the settle is - /// fire-and-forget, so a non-`Ok` is a durability miss, not a refusal. Identical to - /// `busbar_kernel::plane_host::trust::quarantine_settle_over`. - fn quarantine_settle(&self, subject: &str, state: TrustState) -> bool; - - /// Redeem a one-time approval against the shared spent-approval ledger the host pulls, spending - /// against the seal's own `expires_at` and the caller's `now`. `true` iff this is the FIRST - /// redemption; `false` when already spent OR the durable ledger could not answer (fail-closed). - /// Identical to `busbar_kernel::plane_host::trust::approval_redeem_q`. - fn approval_redeem(&self, nonce: &str, expires_at: u64, now: u64) -> bool; - - /// TEST-ONLY raw-token → resolved `VirtualKey` resolution over this deployment's governance state - /// (the data-plane boundary: no audience). The host-driven form of - /// `App::governance.and_then(|g| g.verify_token(token, now, None))`, for the test paths that - /// exercise the routing-policy seam WITHOUT building a full `PlaneRequestCtx` (production always - /// threads a resolved key, so this never runs there). Gated to test / `test-support` builds so no - /// production binary carries a raw-token verifier on the neutral seam. - /// - /// DEFAULT `None` (resolves nothing): the method exists only when THIS crate's `test-support` is - /// on, and a downstream implementor cannot see that feature — cargo may unify it on through some - /// other crate's dev-deps. A default lets an implementor compile either way; a host that actually - /// verifies tokens overrides it (item 115). - #[cfg(any(test, feature = "test-support"))] - fn verify_token_test(&self, token: &str) -> Option> { - let _ = token; - None - } - - /// Establish what can be established about a presented bearer's RFC 8707 audience binding against - /// `expected_aud` — the fail-closed pre-filter a plane runs BEFORE the auth chain, for credentials - /// busbar did not mint. A pure judgement (it reaches only core's governance token prefix, no live - /// engine state), so it needs no `HostCtx`. Identical to `busbar_kernel::auth::audience::inspect_bearer`. - fn identity_audience_binding(&self, token: &str, expected_aud: &str) -> AudienceBinding; - - /// Resolve INBOUND data-plane identity: run the configured auth chain + the ONE verdict resolution - /// over the caller's OWN wire credential and the live governance state, returning the resolved - /// `(AuthPrincipal, PlaneRequestCtx)` or the specific [`IdentityRefusal`]. Identical to - /// `busbar_kernel::plane_host::identity_admit_over`. - /// - /// The ONE async method: the core impl awaits a `spawn_blocking` that mints AND consumes the - /// `HostCtx` INSIDE the blocking closure, so the `!Send` pointer never crosses this `.await` and - /// the future stays `Send`. Fail-closed: a join panic maps to [`IdentityRefusal::Denied`]. - async fn identity_admit( - &self, - token: Option, - audience: String, - resource: String, - ) -> Result<(AuthPrincipal, PlaneRequestCtx), IdentityRefusal>; - - /// RE-ASK a [`Standing`] permission against the LIVE governance registry: hand back the principal - /// AS IT IS NOW, or the [`Lapsed`] reason it no longer stands. Injects the host's `GovState` - /// (through the `GovResolve` seam) INTERNALLY, so the plane holds only the `Standing`. Identical to - /// `Standing::still_permitted(app.governance, live, now)`. - fn principal_standing( - &self, - standing: &Standing, - live_gen: u64, - now: u64, - ) -> Result>, Lapsed>; - - /// Derive this deployment's ask-state [`Sealer`] from governance's fleet-shared signing secret, - /// WITHOUT the raw secret crossing to the plane. `None` when governance is disabled. Identical to - /// `busbar_kernel::plane::approvals::ask_state_sealer(app.governance)` — the derivation stays core - /// behind this seam. - fn ask_state_sealer(&self) -> Option; -} - -/// The ADMISSION slice: the request-admission gauntlet seams — the gate decision + presence pre-filter, -/// the governance admit-reason, the destination guard, the budget-admission door, the audience-bound -/// mount read, and the post-admission/not-charged finishes. Split off `EngineHost` as a supertrait. -pub trait AdmissionHost: Send + Sync { - /// Fire the operator's REQUEST-ADMISSION hook gates over the host `gate_decide` seam and - /// reconstruct the [`GateOutcome`]. Identical to `busbar_kernel::plane_host::gate_decide_over`: - /// same reconstructed facts, same key identity, same gate decision. Drives the ASYNC gate on a - /// fresh runtime, so it MUST be called from a BLOCKING thread (`spawn_blocking`). - /// - /// `plane_key` is the opaque registry key (the plane's stable decl key) the host resolves the - /// gate set and the `ingress_protocol` label from. `key` is the caller's resolved `(id, name)`; - /// `session_id` is the caller's session, `Some` only when non-empty. - #[allow(clippy::too_many_arguments)] - fn gate_decide( - &self, - plane_key: &str, - container: &str, - request_id: u64, - tool: &str, - args_json: &[u8], - key: Option<(&str, &str)>, - session_id: Option<&str>, - ) -> GateOutcome; - - /// Cheap presence pre-filter: is any request-admission hook gate attached to `container` on the - /// plane identified by the opaque registry `plane_key` (the plane's stable decl key)? Lets a plane - /// skip the blocking `gate_decide` hop when nothing is attached. Identical to - /// `App::plane_gates(plane_key).contains_key(container)`. - fn gate_attached(&self, plane_key: &str, container: &str) -> bool; - - /// Cheap presence pre-filter for the TAP/TRANSFORM half: is any `prompt: rw` rewrite hook attached - /// to `container` on the plane identified by the opaque registry `plane_key`? Lets a plane skip the - /// blocking `transform_over` hop — and stay BYTE-IDENTICAL to a build without the seam — when - /// nothing is attached. Identical to `App::plane_rewrites(plane_key).get(container).is_some()`. - /// - /// The presence check is the whole zero-cost guarantee: absent a rewrite hook a plane never - /// serializes the payload, never spawns the blocking hop, and never touches its own bytes. - fn tap_attached(&self, plane_key: &str, container: &str) -> bool; - - /// Fire the operator's REQUEST-ADMISSION TRANSFORM (`
.hooks:` `prompt: rw`) chain over the - /// host `transform_over` seam and reconstruct the [`TransformVerdict`] — the TAP/observe-transform - /// twin of [`gate_decide`](Self::gate_decide). The host re-selects the rewrite chain by - /// `(plane_key, container)` (the Seam-B inversion: the plane body names no core hook symbol), builds - /// the SAME `InvokeReq` projection the gate builds from `(tool, args_json)`, runs each hook's - /// `transform` in priority order (each seeing the prior's output — a true transform chain), and - /// returns the rewritten payload or a reject. Drives the ASYNC hooks on a fresh runtime, so it MUST - /// be called from a BLOCKING thread (`spawn_blocking`), exactly like `gate_decide`. - /// - /// `plane_key`/`container`/`request_id`/`tool`/`args_json`/`key`/`session_id` carry the identical - /// meaning they do on [`gate_decide`](Self::gate_decide). - #[allow(clippy::too_many_arguments)] - fn transform_over( - &self, - plane_key: &str, - container: &str, - request_id: u64, - tool: &str, - args_json: &[u8], - key: Option<(&str, &str)>, - session_id: Option<&str>, - ) -> TransformVerdict; - - /// Admit one unit of work for `caller` (the request's middleware-resolved context, DEC-SERVE G1b) - /// over the host `govern_admit_reason` seam, REGISTERING the RAII grant in `scope`'s arena on - /// success and returning the RENDERED refusal reason on a blocked limit. - /// Identical to `busbar_kernel::plane_host::govern_admit_reason_over`. - fn govern_admit_reason( - &self, - scope: &DispatchScope, - caller: &PlaneRequestCtx, - pool: &[u8], - ) -> GovAdmit; - - /// STAGE 2 pre-admission DESTINATION guard through the host: the pool ACL, the fallback-pool ACL, - /// and the all-or-nothing unpriced-model gate. `Ok(())` admits; `Err` is the already-finished, - /// protocol-native rejection response (finished via the not-charged terminal). Identical to - /// `busbar_kernel::ingress::destination_guard` over the bound snapshot + `gov` scope. - fn destination_guard( - &self, - gov: &PlaneRequestCtx, - proto: &'static str, - pool: &str, - started: std::time::Instant, - charged_at: u64, - ) -> Result<(), Box>; - - /// STAGE 3–4 budget-admission door: charge the chain buckets for one request under `gov` on - /// `pool`, returning the ADMISSION grant (as the opaque [`AdmitHandle`] the sink holds Drop-only) - /// and any budget DOWNGRADE re-pool, or the already-finished not-charged rejection. Identical to - /// `busbar_kernel::ingress::admission_door` over the bound snapshot; the returned `AdmitHandle` - /// wraps the SAME `AdmitGrant` the in-place door produced, so `.is_some()` (charged?) and the - /// gauge-releasing `Drop` are byte-identical. No `HostCtx` on this path. - /// - /// WEDGE 2 (App-retype): additive — the seam the wedge-3 `native_ingress::drive` flip targets - /// (a host is already minted there). The `AdmitGrant`→[`AdmitHandle`] carrier-field retype - /// (`response_body.rs` / `native_ingress.rs`) rides the same wedge-3 governance flip. - fn admission_door( - &self, - gov: &PlaneRequestCtx, - proto: &'static str, - pool: &str, - started: std::time::Instant, - charged_at: u64, - ) -> Result<(Option, Option), Box>; - - /// THE SAME DOOR, WITHOUT THE TERMINAL — the check-and-charge on its own. - /// - /// [`admission_door`](Self::admission_door) is exactly this call followed by - /// `finish_rejected` on the refusing arm, which is right for a caller whose refusal ends there - /// and wrong for one whose terminal is a step of its own: posting the refusal at the door and - /// then posting it again at the terminal would put two links on one unit's chain. So a plane - /// that owns an Audit step takes this one and hands the refusal — bytes, not a posted record — - /// to that step, and a unit posts exactly one link whichever way it ended. - /// - /// Identical to `busbar_kernel::ingress::admit_check` over the bound snapshot: the same buckets, - /// the same charge, the same downgrade re-pool, and on the refusing arm the same - /// protocol-native bytes the door would have finished. The label the terminal records those - /// bytes under is [`pool_label`](EngineHost::pool_label) of the same `pool`, which is what the - /// door computes for itself before it finishes. - fn admission_check( - &self, - gov: &PlaneRequestCtx, - proto: &'static str, - pool: &str, - charged_at: u64, - ) -> Result<(Option, Option), Box>; - - /// POST-ADMISSION finish through the host: emit the per-request metric family + request-log - /// webhook and, on a NON-2xx outcome, REFUND the flat per-request fee IFF it actually landed at - /// admission (`charged`: the admission's own handle, `None` when nothing was charged) — from - /// exactly the cells that admission's charge reached. Identical to - /// `busbar_kernel::ingress::finish_admitted` over the bound snapshot + `gov` scope. - #[allow(clippy::too_many_arguments)] - 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; - - /// NOT-CHARGED (pre-charge turn-away) finish through the host: emit metrics + the webhook with NO - /// refund, for a request rejected BEFORE the admission charge ever ran (governance guard denial or - /// a pre-routing failure). Identical to `busbar_kernel::ingress::finish_rejected` over the bound - /// snapshot + `gov` scope. - #[allow(clippy::too_many_arguments)] - 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; - - /// Whether the plane identified by the opaque registry `plane_key` is mounted under an - /// AUDIENCE-BOUND door — the deployment gate a request path reads before it trusts an inbound - /// audience claim. Identical to - /// `App::planes.mount_of(plane_key).and_then(|m| App::planes.admission_for(m)).is_some()`. A pure - /// snapshot read, no `HostCtx`. - fn plane_audience_bound(&self, plane_key: &str) -> bool; -} - -/// The COMPLETION slice — LLM-ONLY. The single seam that drives a non-streaming chat completion through -/// the resolved ingress pipeline. Split off `EngineHost` as a supertrait SPECIFICALLY so a non-LLM plane -/// (a voice/bytes port) is NOT forced to name it: such a plane takes the slices it uses and never sees -/// this one. `#[async_trait]` because [`synthesize_completion`](Self::synthesize_completion) is async. -#[async_trait::async_trait] -pub trait CompletionHost: Send + Sync { - /// Synthesize ONE non-streaming chat completion by driving `body` through the ENTIRE resolved - /// ingress pipeline (governance → pools → breaker/failover → metering → request log) under `gov`, - /// on the operator's declared `model`, and return the raw wire outcome. The dialect the request is - /// driven as is NEUTRAL to this seam: the host resolves it from the registry's residual-default - /// chat protocol (`None` — no chat dialect installed — surfaces as - /// [`CompletionRefusal::NotInstalled`], not a hard-coded identity), so MCP's `sampling/complete` - /// bridge names no LLM dialect to reach a completion, and words the refusal itself. - /// - /// The ONE async method beside [`IdentityHost::identity_admit`] — but simpler: - /// the host drives a NATIVE core async fn (no C-ABI slot, no `spawn_blocking`), so this only - /// `.await`s it. No `HostCtx` crosses the `.await`; the future is `Send`. `max_body_bytes` - /// bounds the response body read. - async fn synthesize_completion( - &self, - gov: &busbar_contract::records::PlaneRequestCtx, - model: &str, - body: bytes::Bytes, - max_body_bytes: usize, - ) -> Result; -} - -/// The neutral HOST seam a plane calls to reach the engine's host-owned capabilities. -/// -/// A plane holds an `Arc` (minted core-side over the live engine) and calls these -/// typed methods rather than naming `busbar_kernel::plane_host::*_over(&App, …)`. Each method reaches -/// the SAME host vtable slot the in-core veneer drives, so the value is identical — this is a -/// same-dispatch relocation of the reach, not a new behaviour. -/// -/// `Send + Sync` because a plane carries the handle across `.await` and between threads (e.g. into a -/// `spawn_blocking` breaker leg). That is sound precisely because no method exposes the `!Send` -/// `HostCtx`: each mints it internally, uses it synchronously, and drops it before returning. -/// -/// ## M4 — `EngineHost` is the SUM of the capability slices -/// -/// The residual flat method set has been cut into cohesive capability SUPERTRAITS -/// ([`ClockHost`], [`TelemetryHost`], [`JournalHost`], [`MountHost`], [`RegistryHost`], -/// [`HookConfigHost`], [`BudgetHost`], [`IdentityHost`], [`AdmissionHost`], [`CompletionHost`]), -/// alongside the earlier braking slices ([`BreakerHost`], [`LanePoolHost`]). Every slice is -/// PLANE-FACING and pricing-blind; the kernel's pricing ([`MeteringHost`]) is deliberately NOT one of -/// them, so a plane-side host implements the whole sum without naming a cost or price type (#43). -/// `EngineHost` now declares NO methods of its own beyond the provided [`run_gauntlet`](Self::run_gauntlet) -/// ergonomic entry — it is the SUM (a supertrait bound of every slice). An existing -/// `Arc` caller is unaffected (it still reaches every method through the inherited -/// bound); a plane that needs a narrower capability takes `&dyn SliceX` and depends only on what it -/// uses — a voice/bytes port never names LLM-only [`CompletionHost::synthesize_completion`]. -/// -/// `#[async_trait]` is inherited via the async slices; `EngineHost` itself carries only the provided -/// `run_gauntlet` future, so the attribute leaves it a thin sum. -#[async_trait::async_trait] -pub trait EngineHost: - BreakerHost - + LanePoolHost - + ClockHost - + TelemetryHost - + JournalHost - + MountHost - + RegistryHost - + HookConfigHost - + BudgetHost - + IdentityHost - + AdmissionHost - + CompletionHost - + Send - + Sync -{ - // The cross-protocol translation seam's global-fallback max-output-tokens and effort→budget-table - // reads are GONE from this neutral host trait: they are LLM-plane vocabulary, so the engine now - // reads them off the LLM plane's own per-generation runtime (`NativeRuntime`) rather than through a - // neutral `PlaneHost` method over `App`. See busbar-llm `engine/wire.rs`. - - /// Run one request through THE shared gauntlet sequence ([`run_gauntlet`]) — the ergonomic entry - /// for a plane that already holds an `Arc`. A PROVIDED method: it delegates to the - /// free [`run_gauntlet`] (which needs no host — the sequence is verify→drive over the plane), so - /// every host impl shares one body and core's own callers can use the free fn directly. - async fn run_gauntlet<'a>( - &self, - req: GauntletRequest<'a>, - plane: Box, - ) -> axum::response::Response { - run_gauntlet(req, plane).await - } -} - -/// Compile-time witness for the M4 god-trait split: any `EngineHost` implementor IS every capability -/// slice. If a future edit dropped any supertrait bound (or an impl stopped satisfying it), this stops -/// compiling — so `EngineHost` cannot silently cease to equal the SUM of its slices (the families -/// cannot re-dissolve back into the flat god-trait). -const _: () = { - fn _assert_engine_host_is_sum_of_slices() { - fn _needs_breaker() {} - fn _needs_lane_pool() {} - fn _needs_clock() {} - fn _needs_telemetry() {} - fn _needs_journal() {} - fn _needs_mount() {} - fn _needs_registry() {} - fn _needs_hook_config() {} - fn _needs_budget() {} - fn _needs_identity() {} - fn _needs_admission() {} - fn _needs_completion() {} - _needs_breaker::(); - _needs_lane_pool::(); - _needs_clock::(); - _needs_telemetry::(); - _needs_journal::(); - _needs_mount::(); - _needs_registry::(); - _needs_hook_config::(); - _needs_budget::(); - _needs_identity::(); - _needs_admission::(); - _needs_completion::(); - } -}; - -/// THE NEUTRAL TYPE-ERASED SLOT-READ SEAM the core-owned `PlaneDecl` callbacks that today force a -/// `&busbar_kernel::state::App` are neutralised over — so a plane's `on_swap` / `registry_contains` / -/// `retain_verify_gates` hook reads its own per-generation runtime object off the snapshot WITHOUT -/// the callback fn-pointer signature naming a core type. Core `impl`s it for `App` as a thin delegate -/// to the inherent `App::plane_slot`; an EXTRACTED plane reaches only [`Self::plane_slot`] and -/// stays neutral. [`Self::as_any`] is the recovery hatch an in-core plane twin uses to downcast back -/// to its concrete snapshot for a field that does not live in `plane_slots` (`agent_defs`); an -/// extracted plane never calls it. -pub trait PlaneSlots { - /// The plane's type-erased runtime object for THIS generation, keyed by the plane's decl key — - /// a pure `plane_slots` map read, borrowed (mirrors the inherent `App::plane_slot`). - fn plane_slot(&self, key: &str) -> Option<&Arc>; - - /// Recover the concrete engine snapshot as `&dyn Any` — the hatch an in-core plane twin downcasts - /// through to reach the snapshot fields that are not `plane_slots` entries. An extracted plane - /// never names a concrete type through this, so it never calls it. - fn as_any(&self) -> &dyn std::any::Any; -} - -/// THE NEUTRAL `&mut` GATE-REBUILD SINK the core-owned `PlaneDecl::reresolve_gates` callback is -/// neutralised over — the config-swap re-resolution of a plane's per-registration hook gates, moved -/// behind a trait so the fn-pointer signature names no `&mut busbar_kernel::state::App`. A `PlaneSlots` -/// (its supertrait) so the plane can read its own registry object off the same `&mut` receiver before -/// it writes the resolved gates back. The resolve-and-store is ONE method (rather than the spec's -/// `resolve` + `set` pair) because the resolved gate map value type is core-owned -/// (`Vec<(u16, busbar_kernel::hooks::ResolvedPolicy)>`) and cannot be named in this crate — so the map -/// never crosses the seam; it is built and stored entirely core-side, keyed by `plane_key`. -pub trait ContainerGateSink: PlaneSlots { - /// Resolve `containers` (each `(name, its-own-hooks)`) unioned with `section_hooks` against this - /// snapshot's hook registry, and store the resolved per-container gates under the opaque registry - /// `plane_key` (the plane's stable decl key) in the generic `App::plane_gates` map. Byte-identical - /// to the old inline `next.plane_gates.insert(plane_key, next.resolve_container_gates(...))`. - fn reresolve_container_gates( - &mut self, - plane_key: &str, - containers: &[(&str, &[String])], - section_hooks: &[String], - ); -} - -// ───────────────────────────────────────────────────────────────────────────────────────────── -// 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.) -// ───────────────────────────────────────────────────────────────────────────────────────────── -#[cfg(test)] -#[path = "tests/gauntlet_session_tests.rs"] -mod gauntlet_session_tests; - -#[cfg(test)] -#[path = "tests/plane_answer_tests.rs"] -mod plane_answer_tests; +// The host capability vocabulary (`EngineHost` and its slices, `PlaneSlots`, the gauntlet) is +// `crate::plane::host`'s; re-exported here for the plane_host files and the legacy plane crates +// until they are deleted. +pub use crate::plane::host::*; diff --git a/crates/busbar-kernel/src/plane_host/scope.rs b/crates/busbar-kernel/src/plane_host/scope.rs index 6327300b51..583992b6ef 100644 --- a/crates/busbar-kernel/src/plane_host/scope.rs +++ b/crates/busbar-kernel/src/plane_host/scope.rs @@ -1,318 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! The lifecycle SCOPES a plane's host handles live at — RELOCATED to -//! [`busbar_kernel::plane_host::scope`], re-exported here so in-core call sites are unchanged. -//! -//! The scope types ([`DispatchScope`], [`DurableScope`], [`SessionScope`] and their supporting -//! `SettleAdmission` / `EgressFaultDetail` vocabulary) name only [`busbar_contract::abi::hot`] + `std`, so -//! they are neutral and now live in the substrate. This module re-exports them unchanged, so every -//! in-core call site (`plane_host`'s own veneers, `a2a`) and the host `HostState` that materializes -//! over a `DispatchScope` are untouched — the move is a pure relocation, not a code change. +//! The lifecycle SCOPES that outlive one dispatch: [`SessionScope`] (one live session's durable +//! handle) and [`DurableScope`] (handles handed off past the request future). The per-dispatch +//! arena they build on, [`DispatchScope`], is `crate::plane::dispatch_scope`'s, re-exported here. // ==== merged from busbar-substrate (W4.b P2 engine drain) ==== +pub use crate::plane::dispatch_scope::{DispatchScope, EgressFaultDetail, SettleAdmission}; use crate::plane::handle_engine::{ ChainPosition, DurableHandleEngine, HandleDenied, HandleEngineError, MutateError, Mutation, ScopedMutateError, SubmitRecord, SweepBounds, }; -use busbar_contract::abi::hot::{ - AdmissionId, EgressFailClass, EgressId, PipeId, Signal, StatusClass, VerifyLease, -}; +use busbar_contract::abi::hot::{AdmissionId, Signal, StatusClass}; use busbar_contract::records::RecordStoreError; use std::any::Any; -use std::sync::{Arc, Mutex}; - -/// The neutral FAILURE detail the host stashes when an `egress_open` fails, so the plane can read it -/// back through `egress_fault` and compose its OWN operator string. Held in the [`DispatchScope`] (not -/// a process global) so the bytes live exactly for the dispatch that produced them and are reclaimed -/// with it; the CAUSE (the flattened transport-error chain) and the URL are kept SEPARATE so each -/// plane chooses to include or strip the url. -#[derive(Clone, Debug)] -pub struct EgressFaultDetail { - /// The neutral failure class the plane maps to its own failover/refusal taxonomy. - pub class: EgressFailClass, - /// The observed status (0 when the failure was before a response head). - pub status: u16, - /// The flattened cause-message bytes (the transport-error chain), url-free. - pub cause: String, - /// The target url bytes, kept separate from the cause. - pub url: String, -} - -/// A reclaim action for a handle whose release is an explicit host call (close an egress, kill a -/// subprocess, drop a leadership lease). Phase 2 fills these with the real host-side calls; each runs -/// exactly once, when the [`DispatchScope`] drops. -type Reclaim = Box; - -/// A settle-capable breaker admission held in the arena — the leak-safety-critical resource of the -/// BREAKER family. Its `Drop` (run by [`DispatchScope::reclaim_all`]) releases the real single-flight -/// half-open probe, so a dropped/cancelled dispatch that never settled cannot wedge the cell in -/// `HalfOpen`; [`settle`](Self::settle) instead records the observed outcome against the breaker, -/// after which the guard's release `Drop` is a no-op. -/// -/// The concrete implementor (`plane_host::breaker::BreakerAdmission`) owns the real -/// `store::planes::Admission` RAII token; this trait lets the arena hold it behind a boxed object and -/// still drive its one settle, WITHOUT `scope` depending on the private breaker types. -pub trait SettleAdmission: Send { - /// Record the observed `signal` against the breaker exactly once and return the resulting ABI - /// [`StatusClass`]. Invoked at most once via [`DispatchScope::settle_admission`]; after it, the - /// guard's probe-release `Drop` becomes a no-op (the recorded outcome already consumed HalfOpen). - fn settle(&mut self, signal: &Signal) -> StatusClass; -} - -/// Which kind of host handle an [`Entry`] carries. Kept alongside the raw id so the Phase-2 fan-out -/// can resolve a plane-held handle-id back to its registered resource (e.g. `egress_write(id)`), and -/// so the same raw `u64` under two kinds never collides. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum HandleKind { - /// A breaker/failover admission grant. - Admission, - /// A one-shot / duplex governed egress. - Egress, - /// A subprocess (or raw-connection) duplex byte pipe. - Pipe, - /// A single-flight counterparty-verification leadership lease. - VerifyLease, -} - -/// One registered, reclaimable resource. Either an RAII guard whose `Drop` reclaims it (the shape the -/// real breaker `store::planes::Admission` takes in Phase 2 — boxed as `dyn Send` so this scaffold does -/// not reach a private type), or an explicit [`Reclaim`] closure the arena runs once on drop. -enum Resource { - /// An RAII guard: dropping the box runs the guard's `Drop`. Used for the real admission guard in - /// Phase 2 and by the arena's own unit test. - Guard(Box), - /// An explicit reclaim call, taken and run once on scope drop. - Closer(Option), - /// A breaker admission: dropping it releases the single-flight half-open probe (the leak-safety - /// reclaim), and it can be SETTLED once — recording the outcome — before the scope ends. Boxed - /// behind [`SettleAdmission`] so the arena never names the private breaker types. - Admission(Box), -} - -/// A registered handle: its kind, its raw id (what the plane holds), and the resource to reclaim. -struct Entry { - // `kind` + `raw` are the Phase-2 lookup key: the fan-out resolves a plane-held handle-id back to - // its registered resource (e.g. `egress_write(EgressId)` → this entry). The scaffold only RECLAIMS - // (which needs `res` alone), so they are write-only until that fan-out lands. - #[allow(dead_code)] - kind: HandleKind, - #[allow(dead_code)] - raw: u64, - res: Resource, -} - -/// The mutable interior of a [`DispatchScope`]. Guarded by a `Mutex` because every vtable fn holds -/// only a shared `&DispatchScope` (via the recovered `HostState`) yet must register/reclaim. -#[derive(Default)] -struct Registry { - entries: Vec, - /// Monotonic id source; `0` is the reserved `NONE` sentinel of every handle newtype, so ids start - /// at `1`. - next: u64, -} - -/// The per-dispatch-invocation arena of acquired host handles — the leak-safety keystone. -/// -/// Core opens ONE of these per dispatch, hands the plane a `HostCtx` that recovers a -/// `HostState` referencing it, and the plane's host calls register every handle they acquire here. On -/// `Drop` — whenever the dispatch future ends OR is dropped — [`reclaim_all`](Self::reclaim_all) -/// reclaims every registered handle, so a cancelled/dropped dispatch can never leak a bare host handle. -pub struct DispatchScope { - reg: Mutex, - /// The last failed `egress_open`'s neutral fault detail, stashed here so the plane reads it back - /// through `egress_fault` after a non-`Ok` open. Lives with the dispatch (reclaimed on drop); holds - /// only the LAST fault (the "read it immediately after the failing open" contract, like the govern - /// refusal reason). - egress_fault: Mutex>, -} - -impl Default for DispatchScope { - fn default() -> Self { - Self::new() - } -} - -impl DispatchScope { - /// Open an empty dispatch arena. - #[must_use] - pub fn new() -> Self { - DispatchScope { - reg: Mutex::new(Registry::default()), - egress_fault: Mutex::new(None), - } - } - - /// STASH the neutral fault detail of a just-failed `egress_open`, so a following `egress_fault` - /// hands it to the plane. Overwrites any prior unread fault (the last-fault contract). - pub fn stash_egress_fault(&self, detail: EgressFaultDetail) { - *self.egress_fault.lock().unwrap_or_else(|e| e.into_inner()) = Some(detail); - } - - /// TAKE (and clear) the stashed egress fault, or `None` when none is pending. Consuming so a stale - /// fault cannot be re-read against a later, unrelated open. - pub fn take_egress_fault(&self) -> Option { - self.egress_fault - .lock() - .unwrap_or_else(|e| e.into_inner()) - .take() - } - - /// Poison-recovering lock: a panic mid-register must not wedge the arena for the reclaim path, so - /// recover the guard rather than cascade the poison (same discipline as `store::*_recover`). - fn lock(&self) -> std::sync::MutexGuard<'_, Registry> { - self.reg.lock().unwrap_or_else(|e| e.into_inner()) - } - - /// Allocate the next non-zero raw handle id. - fn next_raw(reg: &mut Registry) -> u64 { - reg.next += 1; - reg.next - } - - /// Register a breaker admission as an RAII `guard` (the real `store::planes::Admission` in Phase 2; - /// a test guard here). Its `Drop` runs the actual release when the scope drops. - pub fn register_admission(&self, guard: Box) -> AdmissionId { - let mut reg = self.lock(); - let raw = Self::next_raw(&mut reg); - reg.entries.push(Entry { - kind: HandleKind::Admission, - raw, - res: Resource::Guard(guard), - }); - AdmissionId(raw) - } - - /// Register a settle-capable breaker admission (the real `store::planes::Admission` RAII token, - /// wrapped so it can also be settled) — the BREAKER family's leak-safety keystone. Returns the - /// arena's [`AdmissionId`]; the plane never holds the bare probe. On scope drop the guard's `Drop` - /// releases the probe; a prior [`settle_admission`](Self::settle_admission) makes that a no-op. - pub fn register_settling_admission(&self, guard: Box) -> AdmissionId { - let mut reg = self.lock(); - let raw = Self::next_raw(&mut reg); - reg.entries.push(Entry { - kind: HandleKind::Admission, - raw, - res: Resource::Admission(guard), - }); - AdmissionId(raw) - } - - /// Settle the breaker admission `id`: record the observed `signal` against the breaker and return - /// the resulting ABI [`StatusClass`], REMOVING the entry (so the guard's probe-release `Drop` runs - /// now, a no-op after the record). Returns `None` when no live admission carries `id` — a stale or - /// already-settled handle the caller maps to `Gone`. Recording runs with the lock released, matching - /// [`reclaim_all`](Self::reclaim_all)'s discipline. - pub fn settle_admission(&self, id: AdmissionId, signal: &Signal) -> Option { - if id.is_none() { - return None; - } - let entry = { - let mut reg = self.lock(); - let pos = reg - .entries - .iter() - .position(|e| e.raw == id.0 && matches!(e.res, Resource::Admission(_)))?; - reg.entries.remove(pos) - }; - match entry.res { - Resource::Admission(mut guard) => { - let class = guard.settle(signal); - drop(guard); // release the probe (a no-op now the outcome is recorded) - Some(class) - } - // Unreachable: the `matches!` above selected an `Admission` entry. - _ => None, - } - } - - /// Register an open governed egress with the `reclaim` that closes it (Phase 2: `egress_close`). - pub fn register_egress(&self, reclaim: Reclaim) -> EgressId { - let mut reg = self.lock(); - let raw = Self::next_raw(&mut reg); - reg.entries.push(Entry { - kind: HandleKind::Egress, - raw, - res: Resource::Closer(Some(reclaim)), - }); - EgressId(raw) - } - - /// Register a subprocess/raw-connection pipe with the `reclaim` that kills/closes it (Phase 2). - pub fn register_pipe(&self, reclaim: Reclaim) -> PipeId { - let mut reg = self.lock(); - let raw = Self::next_raw(&mut reg); - reg.entries.push(Entry { - kind: HandleKind::Pipe, - raw, - res: Resource::Closer(Some(reclaim)), - }); - PipeId(raw) - } - - /// Register a verification leadership lease with the `reclaim` that releases it (Phase 2). - pub fn register_lease(&self, reclaim: Reclaim) -> VerifyLease { - let mut reg = self.lock(); - let raw = Self::next_raw(&mut reg); - reg.entries.push(Entry { - kind: HandleKind::VerifyLease, - raw, - res: Resource::Closer(Some(reclaim)), - }); - VerifyLease(raw) - } - - /// How many handles are currently registered (test/observability hook). - #[must_use] - pub fn registered(&self) -> usize { - self.lock().entries.len() - } - - /// Reclaim EVERY registered handle NOW, in reverse (LIFO) acquisition order: drop each guard - /// (running its real `Drop`) and run each closer exactly once. Idempotent — a second call finds an - /// empty registry. Called by `Drop`; exposed so a test can assert synchronous reclaim on abort. - /// - /// EVERY ENTRY GETS ITS OWN BOUNDARY. Reclaiming runs code this loop does not own — a closer that - /// kills a subprocess, a guard's real `Drop` — and one of those panicking must not cost the - /// others. Unbounded, a single panic here skips every remaining entry, leaking exactly what this - /// arena exists to reclaim; and because the loop is reached from `Drop`, that panic unwinding out - /// of a drop already running during an unwind aborts the process. The boundary contains it to the - /// entry that raised it, and the loop carries on. - pub fn reclaim_all(&self) { - // Take the entries OUT under the lock, then reclaim with the lock released so a reclaim that - // re-enters the arena cannot deadlock. - let drained: Vec = { - let mut reg = self.lock(); - std::mem::take(&mut reg.entries) - }; - for entry in drained.into_iter().rev() { - let kind = entry.kind; - // `AssertUnwindSafe`: nothing observable survives a panicking reclaim. The entry is - // already out of the registry and is consumed here whichever way it ends, so there is no - // half-updated state left for a later reader to see. - let reclaimed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - match entry.res { - Resource::Guard(g) => drop(g), - Resource::Admission(g) => drop(g), // Drop releases the single-flight probe. - Resource::Closer(Some(reclaim)) => reclaim(), - Resource::Closer(None) => {} - } - })); - if reclaimed.is_err() { - tracing::warn!( - handle_kind = ?kind, - "a host-handle reclaim panicked; the remaining handles were reclaimed anyway" - ); - } - } - } -} - -impl Drop for DispatchScope { - fn drop(&mut self) { - self.reclaim_all(); - } -} +use std::sync::Arc; /// The CONNECTION-lifetime scope (DESIGN-v5-taxonomy Axis 2): ONE live stateful session that owns a /// single durable handle in the [`DurableHandleEngine`] for longer than one exchange (a duplex/session diff --git a/crates/busbar-kernel/src/plane_host/tests/egress_trust_tests.rs b/crates/busbar-kernel/src/plane_host/tests/egress_trust_tests.rs deleted file mode 100644 index 28836da2d1..0000000000 --- a/crates/busbar-kernel/src/plane_host/tests/egress_trust_tests.rs +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! Tests for `crates/busbar-substrate/src/plane_host/egress_trust.rs` — the HOST-CAPS S3 egress-trust -//! seam as a byte-for-byte pass-through to the `identity`/`trust_anchor`/`spki` primitives. - -use super::*; - -/// A self-signed client identity, from its parts (the PEM walk `ClientIdentity::from_pem` runs is the -/// connector's, proven over the real wrap in the connector's `tls/engine_tests.rs`). -fn an_identity() -> ClientIdentity { - use rcgen::{CertificateParams, KeyPair}; - let kp = KeyPair::generate().expect("a key pair"); - let params = CertificateParams::new(vec!["client.test".to_string()]).expect("params"); - let cert = params.self_signed(&kp).expect("self-signed"); - ClientIdentity::from_parts( - vec![cert.der().to_vec()], - crate::secure::KeyDer::Pkcs8(kp.serialize_der()), - ) -} - -/// A self-signed root certificate, parsed to DER the way the a2a boot resolver parses a -/// `trusting_root` PEM. -fn a_root() -> Vec { - use rcgen::{CertificateParams, KeyPair}; - let kp = KeyPair::generate().expect("a key pair"); - let params = CertificateParams::new(vec!["root.test".to_string()]).expect("params"); - let cert = params.self_signed(&kp).expect("self-signed"); - cert.der().to_vec() -} - -#[test] -fn client_identity_seam_is_a_pass_through_to_the_registry() { - let host = PassThroughEgressTrust; - // A registered identity resolves through the seam exactly as through the free registry. - let r = host.register_client_identity(an_identity()); - assert_ne!(r, 0, "a live ref is nonzero"); - assert!( - host.resolve_client_identity(r).is_some(), - "the seam resolves the ref it just minted" - ); - // The reserved and unknown refs give the SAME fail-to-no-cert answer the free fn gives. - assert!(host.resolve_client_identity(0).is_none()); - assert_eq!( - host.resolve_client_identity(0).is_none(), - super::super::identity::resolve(0).is_none(), - "seam and free fn agree on the reserved ref" - ); - assert!(host.resolve_client_identity(u64::MAX).is_none()); -} - -#[test] -fn trust_anchor_seam_is_a_pass_through_to_the_registry() { - let host = PassThroughEgressTrust; - let r = host.register_trust_anchor(vec![a_root()]); - assert_ne!(r, 0, "a live ref is nonzero"); - assert_eq!( - host.resolve_trust_anchor(r).len(), - 1, - "the seam resolves the ref it just minted to its one root" - ); - // Reserved and unknown refs add no extra roots — fail-closed, same as the free fn. - assert!(host.resolve_trust_anchor(0).is_empty()); - assert!(host.resolve_trust_anchor(u64::MAX).is_empty()); - assert_eq!( - host.resolve_trust_anchor(u64::MAX).len(), - super::super::trust_anchor::resolve(u64::MAX).len(), - "seam and free fn agree on an unknown ref" - ); -} - -#[test] -fn peer_leaf_pin_seam_is_byte_for_byte_the_free_walk() { - let host = PassThroughEgressTrust; - let der = a_root(); - // The seam's pin is the SAME string the free DER walk computes — the whole point of the seam - // being a pass-through and not a second answer to "what is this key's pin". - assert_eq!( - host.peer_leaf_pin(der.as_ref()), - super::super::spki::pin(der.as_ref()), - "the seam pins byte-for-byte identically to the free walk" - ); - assert!( - host.peer_leaf_pin(der.as_ref()) - .expect("a valid cert pins") - .starts_with("sha256/"), - "a real pin is spelled sha256/" - ); - // A non-certificate refuses, exactly as the free walk does. - assert!(host.peer_leaf_pin(b"not-a-cert").is_err()); -} - -#[test] -fn the_composition_root_install_hands_the_installed_capability_back() { - // Dormant by default: nothing installs the seam on the shipped path. This test installs its - // own and reads it back, exercising the OnceLock accessor the composition root uses. - install_egress_trust_host(std::sync::Arc::new(PassThroughEgressTrust)); - let installed = egress_trust_host().expect("the just-installed capability reads back"); - let der = a_root(); - assert_eq!( - installed.peer_leaf_pin(der.as_ref()), - super::super::spki::pin(der.as_ref()), - "the installed capability pins byte-identically to the free walk" - ); -} - -/// THE DESTINATION JUDGEMENT ON THE SEAM (ARCHITECT ruling (C)): the pass-through has no guard -/// behind it and refuses every answer (fail closed); the root's capability hands answers to the -/// connector's guard (`busbar` root `connector::GuardedEgressTrust`). -#[test] -fn the_pass_through_seam_fails_closed() { - let public: std::net::IpAddr = "93.184.216.34".parse().unwrap(); - let refused = PassThroughEgressTrust - .judge_answer("api.test", &[public], 0) - .unwrap_err(); - assert_eq!( - refused.verdict, - busbar_contract::abi::host::service::DEST_NO_HOST - ); -} - -/// The root's capability hands every answer to the guard behind it, deciding nothing itself. -#[test] -fn the_guarded_seam_judges_by_the_guard_behind_it() { - let judged = GuardedEgressTrust( - crate::egress::fixtures::private_refusing(&[]), - crate::egress::fixtures::TlsDouble::default().layer(), - ); - let private: std::net::IpAddr = "10.0.0.5".parse().unwrap(); - let public: std::net::IpAddr = "93.184.216.34".parse().unwrap(); - assert!(judged.judge_answer("db.test", &[private], 0).is_err()); - assert_eq!(judged.judge_answer("api.test", &[public], 0), Ok(())); -} diff --git a/crates/busbar-kernel/src/plane_host/tests/scope_tests.rs b/crates/busbar-kernel/src/plane_host/tests/scope_tests.rs index 0fb656e68d..16f91afa23 100644 --- a/crates/busbar-kernel/src/plane_host/tests/scope_tests.rs +++ b/crates/busbar-kernel/src/plane_host/tests/scope_tests.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! Tests for `crates/busbar-substrate/src/plane_host/scope.rs`. +//! Tests for `crates/busbar-kernel/src/plane_host/scope.rs`: the durable and session scopes. use super::*; use crate::plane::handle_engine::{HandleMeta, SealedEvent}; @@ -17,92 +17,6 @@ impl Drop for DropCounter { } } -#[test] -fn arena_reclaims_a_registered_guard_on_drop() { - let reclaimed = Arc::new(AtomicUsize::new(0)); - { - let scope = DispatchScope::new(); - let id = scope.register_admission(Box::new(DropCounter(reclaimed.clone()))); - assert_eq!(id, AdmissionId(1)); - assert_eq!(scope.registered(), 1); - // Not yet reclaimed while the scope is live. - assert_eq!(reclaimed.load(Ordering::SeqCst), 0); - } - // Scope dropped: the real guard `Drop` ran. - assert_eq!(reclaimed.load(Ordering::SeqCst), 1); -} - -#[test] -fn arena_reclaims_every_kind_and_runs_closers() { - let count = Arc::new(AtomicUsize::new(0)); - let scope = DispatchScope::new(); - let c = count.clone(); - scope.register_admission(Box::new(DropCounter(count.clone()))); - let cc = c.clone(); - scope.register_egress(Box::new(move || { - cc.fetch_add(1, Ordering::SeqCst); - })); - let ccc = c.clone(); - scope.register_pipe(Box::new(move || { - ccc.fetch_add(1, Ordering::SeqCst); - })); - let cccc = c.clone(); - scope.register_lease(Box::new(move || { - cccc.fetch_add(1, Ordering::SeqCst); - })); - assert_eq!(scope.registered(), 4); - // Explicit reclaim (the abort-path hardening assertion): synchronous, reclaims all four. - scope.reclaim_all(); - assert_eq!(count.load(Ordering::SeqCst), 4); - assert_eq!(scope.registered(), 0); - // Idempotent: a second reclaim (e.g. the Drop after an explicit reclaim) is a no-op. - scope.reclaim_all(); - assert_eq!(count.load(Ordering::SeqCst), 4); -} - -/// One closer that panics must not take the rest of the arena with it: the entries around it still -/// reclaim, and the panic does not escape a call that `Drop` also makes (escaping there, mid-unwind, -/// aborts the process rather than leaking). -#[test] -fn a_panicking_closer_does_not_strand_the_other_handles() { - let count = Arc::new(AtomicUsize::new(0)); - let reclaimed = Arc::new(AtomicUsize::new(0)); - let scope = DispatchScope::new(); - - // Registered first, reclaimed LAST (LIFO) — it is the one a panic ahead of it would strand. - scope.register_admission(Box::new(DropCounter(reclaimed.clone()))); - let c = count.clone(); - scope.register_egress(Box::new(move || { - c.fetch_add(1, Ordering::SeqCst); - })); - scope.register_pipe(Box::new(|| { - panic!("a closer that kills a subprocess went wrong") - })); - let c = count.clone(); - scope.register_lease(Box::new(move || { - c.fetch_add(1, Ordering::SeqCst); - })); - assert_eq!(scope.registered(), 4); - - // The panic is contained: `reclaim_all` returns rather than unwinding into its caller. - let quiet = std::panic::take_hook(); - std::panic::set_hook(Box::new(|_| {})); - scope.reclaim_all(); - std::panic::set_hook(quiet); - - assert_eq!( - count.load(Ordering::SeqCst), - 2, - "the closers on both sides of the panicking one still ran" - ); - assert_eq!( - reclaimed.load(Ordering::SeqCst), - 1, - "the guard behind the panicking closer was not stranded" - ); - assert_eq!(scope.registered(), 0, "the arena is empty either way"); -} - #[test] fn durable_scope_reclaims_a_handed_off_guard_on_its_own_drop() { let reclaimed = Arc::new(AtomicUsize::new(0)); @@ -138,16 +52,6 @@ fn durable_scope_handoff_is_lazy_and_reclaims_lifo() { assert_eq!(count.load(Ordering::SeqCst), 2); } -#[test] -fn handle_ids_are_nonzero_and_monotonic() { - let scope = DispatchScope::new(); - let a = scope.register_egress(Box::new(|| {})); - let b = scope.register_egress(Box::new(|| {})); - assert!(!a.is_none()); - assert_eq!(a, EgressId(1)); - assert_eq!(b, EgressId(2)); -} - // ── SessionScope over the durable-handle engine ───────────────────────────────────────────────── /// A stand-in session row the engine holds opaquely and never names — a local demo type, so these diff --git a/crates/busbar-kernel/src/plane_routes.rs b/crates/busbar-kernel/src/plane_routes.rs index b9d5048a5d..2964ec3644 100644 --- a/crates/busbar-kernel/src/plane_routes.rs +++ b/crates/busbar-kernel/src/plane_routes.rs @@ -115,7 +115,7 @@ pub struct PlaneReqCtx { /// by calling typed methods on it rather than naming `busbar_kernel::plane_host::*_over`. Carried /// alongside `engine` during the transition: `engine` is the residual downcast the per-subsystem /// App-sever removes, `host` is the durable seam that replaces it. - pub host: Arc, + pub host: Arc, /// The plane's own per-generation runtime slot (the same `Arc` the plane's `build` fn /// produced), so the handler reads its plane state without a host round-trip. pub slot: Arc, diff --git a/crates/busbar-kernel/src/probe.rs b/crates/busbar-kernel/src/probe.rs index d7bf823cbb..39eb844a68 100644 --- a/crates/busbar-kernel/src/probe.rs +++ b/crates/busbar-kernel/src/probe.rs @@ -25,7 +25,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Weak}; use std::time::Duration; -use crate::plane_host::HealthModeInput as HealthMode; +use crate::route_input::HealthModeInput as HealthMode; /// The probe deadline used when `now + timeout` is unrepresentable: 30 years, tokio's own /// `far_future` horizon and the config-validation ceiling on every duration (item 147). diff --git a/crates/busbar-kernel/src/plane_host/build_input.rs b/crates/busbar-kernel/src/route_input.rs similarity index 89% rename from crates/busbar-kernel/src/plane_host/build_input.rs rename to crates/busbar-kernel/src/route_input.rs index 357957d90d..55275f14e1 100644 --- a/crates/busbar-kernel/src/plane_host/build_input.rs +++ b/crates/busbar-kernel/src/route_input.rs @@ -1,36 +1,28 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! THE NEUTRAL LLM-RUNTIME BUILD CARRIER (1.6.0 money-path Phase 3-4 C) — the single-compiled DTO -//! `busbar-core`'s `appbuild` populates from the already-resolved `RootCfg` and hands to the LLM -//! plane's `build_runtime` seam (`PlaneDecl::build_runtime`, through `&dyn Any`), so the plane rebuilds -//! its `Lane`/`WeightedLane`/`PoolRuntime`/`NativeRuntime` tables WITHOUT core naming a single plane -//! type and WITHOUT a core type crossing the type-erased downcast. +//! THE ROUTING-TABLE BUILD INPUT: the resolved configuration, flattened to the lanes, pools and +//! failover settings the kernel's routing tables are built from ([`crate::route_tables::ConfigTables::of`]). +//! Route is the kernel's (spec Part 3, the outbound table, step 1: "KERNEL | route (pool walk, +//! member, breaker)"), so its input is kernel-owned and lives beside the tables it builds. +//! `appbuild` populates it from the already-resolved `RootCfg`. //! //! ## Why every field is a NEUTRAL SCALAR //! -//! The `build_runtime` fn-pointer is stored on the plane's `&'static PlaneDecl`, which — in -//! `busbar-core`'s own `cfg(test)` binary — is registered through the neutral substrate test seam -//! against a SECOND, independently-compiled copy of `busbar-core` (the plane crate's normal-dep core, -//! distinct from the `cfg(test)` core under test). A `busbar_kernel::` type erased to `&dyn Any` in one -//! and downcast in the other carries a DIFFERENT `TypeId`, so the downcast silently returns `None` — -//! the dual-compile hazard. This carrier therefore holds NO `busbar_kernel::` type: only owned `String`s, -//! numbers, `bool`s, `Vec`/`HashMap` of those, and the neutral `busbar_contract::config::UpstreamCreds`. It lives in -//! `busbar-substrate` (compiled ONCE for the whole workspace) so its own `TypeId` is stable across the -//! dual compile, and so a zero-plane binary that `git-rm`'d `busbar-llm` (the `plane-delete-test --all` -//! posture) still compiles `appbuild` — which populates this — without naming the plane crate. +//! A plane's `build_runtime` hook receives this input through `&dyn Any`. Only owned `String`s, +//! numbers, `bool`s, `Vec`/`HashMap` of those, and contract types cross that downcast, so the input +//! names no kernel runtime type (`Lane`, `EgressTarget`, `CredentialProvider`). //! //! ## What it does NOT carry //! -//! Pre-RESOLVED secrets and the rate-card-derived costs ARE carried (fidelity: the plane cannot +//! Pre-RESOLVED secrets and the rate-card-derived costs ARE carried (fidelity: a consumer cannot //! re-resolve a secret ref — it has no `SecretResolver` — nor re-price without the rate card). A -//! resolved secret is carried in `busbar_contract::redacted::Redacted`, never as a bare `String`: this carrier is +//! resolved secret is carried in `busbar_contract::redacted::Redacted`, never as a bare `String`: this input is //! formatted on the build path, so the wrapper is what keeps the credential out of a trace line. -//! Pool-hook ROUTING POLICIES are NOT: their resolved value is the core-owned +//! Pool-hook ROUTING POLICIES are NOT: their resolved value is the kernel-owned //! `busbar_kernel::hooks::ResolvedPolicy` (an `Arc` over a dlopen plugin), which -//! cannot be named here and must not cross the downcast — so, exactly as the container-plane gate -//! rebuild does (`ContainerGateSink`), pool policies stay resolved-and-read core-side behind the -//! `App::resolve_pool_*` down-facade and never enter this carrier. +//! must not cross the downcast — so pool policies stay resolved-and-read kernel-side behind the +//! `App::resolve_pool_*` facade and never enter this input. use std::collections::HashMap; @@ -343,5 +335,5 @@ pub struct PlaneBuildInput { } #[cfg(test)] -#[path = "tests/build_input_tests.rs"] +#[path = "tests/route_input_tests.rs"] mod tests; diff --git a/crates/busbar-kernel/src/route_tables.rs b/crates/busbar-kernel/src/route_tables.rs index 8a1e3b8172..e8eee8946e 100644 --- a/crates/busbar-kernel/src/route_tables.rs +++ b/crates/busbar-kernel/src/route_tables.rs @@ -167,8 +167,8 @@ impl ConfigTables { /// The tables of one resolved configuration generation. #[must_use] pub fn of( - lanes: &[crate::plane_host::LaneInput], - pools: &[crate::plane_host::PoolInput], + lanes: &[crate::route_input::LaneInput], + pools: &[crate::route_input::PoolInput], by_model: &std::collections::HashMap, upstream_credentials: busbar_contract::config::UpstreamCreds, ) -> Self { @@ -190,7 +190,7 @@ impl ConfigTables { fallbacks: pools .iter() .filter_map(|p| match &p.on_exhausted { - crate::plane_host::OnExhaustedInput::FallbackPool(to) => { + crate::route_input::OnExhaustedInput::FallbackPool(to) => { Some((p.name.clone(), to.clone())) } _ => None, diff --git a/crates/busbar-kernel/src/secure.rs b/crates/busbar-kernel/src/secure.rs index efd05212ed..fa33a97869 100644 --- a/crates/busbar-kernel/src/secure.rs +++ b/crates/busbar-kernel/src/secure.rs @@ -8,25 +8,29 @@ //! engine's https arm, the duplex dial's `wss` arm and the client-identity PEM parse each ask this //! seam, and `busbar-core-connector` (which depends on this crate, never the other way) answers it. //! -//! The answer arrives through the egress-trust capability the composition root already installs -//! once, at boot, before anything dials ([`crate::plane_host::egress_trust::EgressTrustHost::secure_layer`]): -//! the outbound trust a dial is secured with is that capability's business, so no second -//! process-wide seam is opened for it. +//! The answer arrives through the egress-trust capability the composition root installs once, at +//! boot, before anything dials ([`EgressTrustHost::secure_layer`]): the outbound trust a dial is +//! secured with is that capability's business, so no second process-wide seam is opened for it. //! //! What crosses is the posture a client is built for (trust anchors, a client identity, the ALPN //! offer — plain DER and bytes) and, per dial, a connected TCP stream going in and a secured stream //! coming out. The secured stream answers the two facts the engine reads off a handshake: the -//! protocol ALPN agreed and the peer's leaf certificate. No key type, no config type and no -//! certificate type of the TLS library is named on this side. +//! protocol ALPN agreed and the SPKI pin of the peer's leaf. The connector owns pinning, SPKI and +//! mTLS (spec Part 3), so the pin arrives computed: this crate never walks a certificate. No key +//! type, no config type and no certificate type of the TLS library is named on this side. //! //! A process whose capability carries no wrap (a tool, another crate's test binary) builds its //! clients all the same; only an https or `wss` dial fails, after the TCP connect, naming the //! missing wrap. use std::future::Future; +use std::net::IpAddr; use std::pin::Pin; use std::sync::Arc; +use crate::config::Destinations; +use crate::host_services::DestRefusal; + use tokio::net::TcpStream; type BoxError = Box; @@ -74,8 +78,11 @@ pub struct ClientTlsSpec<'a> { pub trait SecuredIo: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin { /// The protocol ALPN agreed; `None` when none was. fn alpn(&self) -> Option<&[u8]>; - /// The peer's leaf certificate, DER, from the verified handshake. - fn peer_leaf(&self) -> Option<&[u8]>; + /// The `sha256/` SubjectPublicKeyInfo pin of the peer's leaf certificate from the + /// verified handshake, computed once by the wrap at handshake time. `None` when the handshake + /// produced no leaf or the leaf does not walk as DER: "we could not look" is absence, never a + /// pin. + fn peer_spki(&self) -> Option<&str>; /// The TCP stream under the session (what a connection reports itself connected over). fn tcp(&self) -> &TcpStream; } @@ -112,7 +119,7 @@ pub trait SecureLayer: Send + Sync { /// client a TLS double of its own (`EngineSpec::tls`), or, in a test binary no composition root /// boots, installs a capability carrying one ([`install_test_tls`]). pub fn layer() -> Option> { - crate::plane_host::egress_trust::egress_trust_host().and_then(|host| host.secure_layer()) + egress_trust_host().and_then(|host| host.secure_layer()) } /// TEST SEAM: carry `layer` in the egress-trust capability of a test binary no composition root @@ -124,3 +131,72 @@ pub use crate::egress::fixtures::install_test_tls; /// TLS wrap. pub const NO_LAYER: &str = "no TLS layer is installed (the composition root installs busbar-core-connector's)"; + +/// THE COMPOSITION-ROOT-OWNED EGRESS-TRUST CAPABILITY (HOST-CAPS S3, DECISIONS #26): the outbound +/// trust every kernel dial is subject to, installed once at boot ([`install_egress_trust_host`]) and +/// read back through [`egress_trust_host`]. It carries the deployment's one destination guard and the +/// connector's TLS wrap; the root installs the connector's implementation +/// (`busbar_core_connector::tls::trust::GuardedEgressTrust`). `Send + Sync` so the installed capability +/// is a process-wide `&'static dyn`. +pub trait EgressTrustHost: Send + Sync { + /// An answer the kernel's own pooled client resolved for `host`, judged whole under `class` by + /// the deployment's destination guard (OWNER DESTINATION GUARD; the connector decides). INTERIM + /// for the eight pooled-client builders; struck when D1-D6 move onto `conns` (Phase B). + /// + /// # Errors + /// + /// The refusal of the first refused address; with no guard behind the capability (the + /// pass-through) every answer: FAIL CLOSED, never allowed. + fn judge_answer(&self, host: &str, _: &[IpAddr], _: u32) -> Result<(), DestRefusal> { + Err(DestRefusal { + verdict: busbar_contract::abi::host::service::DEST_NO_HOST, + reason: format!("host `{host}` was not dialled: no destination guard is installed"), + }) + } + /// A config commit: the deployment's destinations are now `d`, raised to the guard behind the + /// capability ([`crate::host_services::DestJudge::destinations_applied`]); the pass-through has none and keeps nothing. + fn destinations_applied(&self, _: &Destinations) {} + + /// THE TLS AN OUTBOUND CONNECTION IS SECURED WITH: the connector's wrap, the one TLS path (THE + /// DESIGN: TLS stays in the connector; this crate names no TLS library). `None` on the + /// pass-through, which has no TLS behind it: an https dial then fails, naming the missing wrap. + fn secure_layer(&self) -> Option> { + None + } +} + +/// The capability with nothing behind it: no guard (every answer refused) and no TLS wrap. What a +/// dial is judged by when no capability was installed. +pub struct PassThroughEgressTrust; + +impl EgressTrustHost for PassThroughEgressTrust {} + +/// THE PROCESS-WIDE egress-trust capability, installed once by the composition root +/// ([`install_egress_trust_host`]). Read back through [`egress_trust_host`]; `None` in a build that +/// installed none. +static EGRESS_TRUST: std::sync::OnceLock> = std::sync::OnceLock::new(); + +/// Install the process egress-trust capability — the composition root's one write, at boot, before any +/// hop opens. Idempotent by `OnceLock`: a second install is a no-op (the first wins). +pub fn install_egress_trust_host(host: Arc) { + let _ = EGRESS_TRUST.set(host); +} + +/// Raise a config commit to the installed capability, so the deployment's one destination guard +/// re-reads its metadata lists. Called at the commit (`InstalledLimits::keep`), so a rejected apply +/// leaves the lists in force; with no capability installed there is no guard to hear it. +pub(crate) fn destinations_applied(d: &Destinations) { + if let Some(host) = egress_trust_host() { + host.destinations_applied(d); + } +} + +/// The installed egress-trust capability, or `None` when none was installed. +#[must_use] +pub fn egress_trust_host() -> Option<&'static dyn EgressTrustHost> { + EGRESS_TRUST.get().map(AsRef::as_ref) +} + +#[cfg(test)] +#[path = "tests/secure_tests.rs"] +mod tests; diff --git a/crates/busbar-kernel/src/state.rs b/crates/busbar-kernel/src/state.rs index 1c8d44fdc5..7f21c75419 100644 --- a/crates/busbar-kernel/src/state.rs +++ b/crates/busbar-kernel/src/state.rs @@ -35,7 +35,7 @@ pub use busbar_kernel::topology::{worker_stripe, worker_stripes}; /// key would change the bare key's presence semantics, and with it the dispatch table /// `build_dispatch` derives from it). Composed into `plane_slots` by `appbuild` and read back by the /// owning plane, each passing its decl key — so this crate names no plane runtime type or token. -pub use busbar_kernel::plane_host::runtime_slot_key; +pub use busbar_kernel::plane::host::runtime_slot_key; /// One plane's per-container resolved submission-gate map: container name → resolved /// `(hook_id, ResolvedPolicy)` gate list. The value half of [`App::plane_gates`]. @@ -111,7 +111,7 @@ pub struct App { /// member keeps its degenerate single-member cell and no reroute exists to be had. pub tool_pools: std::collections::BTreeMap, /// THE PER-PLANE FAILOVER POOL MAPS reached through the GENERIC pool-member seam - /// ([`busbar_kernel::plane_host::LanePoolHost::plane_pool_members`]), keyed by the plane's stable + /// ([`busbar_kernel::plane::host::LanePoolHost::plane_pool_members`]), keyed by the plane's stable /// decl key (the opaque registry key) — a registry-keyed map in place of the former plane-named /// pool field, so core carries no plane vocabulary in its own field names. Each plane's /// entry is its own resolved pool-member set (member selection derives lanes from member position). @@ -185,7 +185,7 @@ pub struct App { /// field names. Each plane's entry maps container → resolved `(hook_id, ResolvedPolicy)` gate list /// (`
.hooks:` ∪ `
..hooks:`), same combine rule and zero-cost absence /// as before. Composed at config apply by `appbuild` (and re-resolved on swap through - /// [`busbar_kernel::plane_host::ContainerGateSink`]); read on the dispatch path by + /// [`busbar_kernel::plane::host::ContainerGateSink`]); read on the dispatch path by /// [`App::plane_gates`]. Empty for a plane that attaches nothing — the lookup costs one probe. // Read on the plane dispatch/admission gate paths; with BOTH planes compiled out nothing fires a // gate, so the map goes unread in that config alone. @@ -195,7 +195,7 @@ pub struct App { /// the TAP/observe-transform twin of [`Self::plane_gates`]. Each plane's entry maps container → /// resolved `(deadline, transport)` rewrite list (`
.hooks:` ∪ `
..hooks:`, /// the `prompt: rw` members only). Composed at config apply by `appbuild` and re-resolved on swap - /// through the same [`busbar_kernel::plane_host::ContainerGateSink`] callback the gates use. Empty + /// through the same [`busbar_kernel::plane::host::ContainerGateSink`] callback the gates use. Empty /// for a plane that attaches no rewrite hook — the tap firing site's lookup costs one probe and the /// path stays byte-identical. // Read on the plane transform/tap paths only; with BOTH planes compiled out nothing fires a tap, so @@ -711,10 +711,10 @@ impl App { } /// THE NEUTRAL SLOT-READ SEAM the plane `PlaneDecl` callbacks name instead of `&App`. A thin delegate -/// to the inherent [`App::plane_slot`]; [`as_any`](busbar_kernel::plane_host::PlaneSlots::as_any) +/// to the inherent [`App::plane_slot`]; [`as_any`](busbar_kernel::plane::host::PlaneSlots::as_any) /// hands the concrete snapshot back to the owning plane's own reader for the field (`agent_defs`) /// that is not a `plane_slots` entry. -impl busbar_kernel::plane_host::PlaneSlots for App { +impl busbar_kernel::plane::host::PlaneSlots for App { fn plane_slot(&self, key: &str) -> Option<&Arc> { App::plane_slot(self, key) } @@ -728,7 +728,7 @@ impl busbar_kernel::plane_host::PlaneSlots for App { /// [`App::resolve_container_gates`]) and stores them in the generic [`App::plane_gates`] map under the /// opaque registry `plane_key` — byte-identical to the old inline /// `next.plane_gates.insert(plane_key, next.resolve_container_gates(...))`. -impl busbar_kernel::plane_host::ContainerGateSink for App { +impl busbar_kernel::plane::host::ContainerGateSink for App { fn reresolve_container_gates( &mut self, plane_key: &str, @@ -867,8 +867,8 @@ impl AppHandle { for decl in crate::plane::registry::plane_decls() { if let Some(on_swap) = decl.on_swap { on_swap( - &*prior as &dyn busbar_kernel::plane_host::PlaneSlots, - &*next as &dyn busbar_kernel::plane_host::PlaneSlots, + &*prior as &dyn busbar_kernel::plane::host::PlaneSlots, + &*next as &dyn busbar_kernel::plane::host::PlaneSlots, ); } } diff --git a/crates/busbar-kernel/src/store/vocab.rs b/crates/busbar-kernel/src/store/vocab.rs index f35778e31e..a8e5205b16 100644 --- a/crates/busbar-kernel/src/store/vocab.rs +++ b/crates/busbar-kernel/src/store/vocab.rs @@ -265,16 +265,16 @@ impl BreakerCfg { /// `build_runtime` reconstructs from (money-path Phase 3-4 C). Lossless over every field the FSM /// reads. `honor_retry_after`/`bench_below_trip_threshold` are always `true` on the LLM path, /// carried anyway so a future divergence cannot silently drop. - pub fn to_breaker_input(&self) -> crate::plane_host::BreakerInput { - crate::plane_host::BreakerInput { + pub fn to_breaker_input(&self) -> crate::route_input::BreakerInput { + crate::route_input::BreakerInput { base_cooldown_secs: self.base_cooldown_secs, max_cooldown_secs: self.max_cooldown_secs, honor_retry_after: self.honor_retry_after, bench_below_trip_threshold: self.bench_below_trip_threshold, - trip: crate::plane_host::TripInput { + trip: crate::route_input::TripInput { mode: match self.trip.mode { - TripMode::ErrorRate => crate::plane_host::TripModeInput::ErrorRate, - TripMode::Consecutive => crate::plane_host::TripModeInput::Consecutive, + TripMode::ErrorRate => crate::route_input::TripModeInput::ErrorRate, + TripMode::Consecutive => crate::route_input::TripModeInput::Consecutive, }, window_s: self.trip.window_s, threshold: self.trip.threshold, @@ -289,7 +289,7 @@ impl BreakerCfg { /// names only this pub constructor and the neutral input type). /// /// [`to_breaker_input`]: Self::to_breaker_input - pub fn from_breaker_input(i: &crate::plane_host::BreakerInput) -> Self { + pub fn from_breaker_input(i: &crate::route_input::BreakerInput) -> Self { Self { base_cooldown_secs: i.base_cooldown_secs, max_cooldown_secs: i.max_cooldown_secs, @@ -297,8 +297,8 @@ impl BreakerCfg { bench_below_trip_threshold: i.bench_below_trip_threshold, trip: TripConfig { mode: match i.trip.mode { - crate::plane_host::TripModeInput::ErrorRate => TripMode::ErrorRate, - crate::plane_host::TripModeInput::Consecutive => TripMode::Consecutive, + crate::route_input::TripModeInput::ErrorRate => TripMode::ErrorRate, + crate::route_input::TripModeInput::Consecutive => TripMode::Consecutive, }, window_s: i.trip.window_s, threshold: i.trip.threshold, diff --git a/crates/busbar-kernel/src/test_support/engine_kit/core_impl.rs b/crates/busbar-kernel/src/test_support/engine_kit/core_impl.rs index d043d10e0f..0a3826a123 100644 --- a/crates/busbar-kernel/src/test_support/engine_kit/core_impl.rs +++ b/crates/busbar-kernel/src/test_support/engine_kit/core_impl.rs @@ -18,8 +18,8 @@ use busbar_contract::records::{AuditRecord, MeteringRow, RecordStore, VirtualKey use busbar_kernel::governance::signing::TokenSigner; use busbar_kernel::governance::NewKeySpec; use busbar_kernel::plane::calllog::CallRecorded; +use busbar_kernel::plane::host::{EngineHost, LiveHostFactory}; use busbar_kernel::plane::store::PlaneStore; -use busbar_kernel::plane_host::{EngineHost, LiveHostFactory}; use busbar_kernel::store::BreakerState; use std::any::Any; use std::collections::BTreeMap; diff --git a/crates/busbar-kernel/src/test_support/engine_kit/mod.rs b/crates/busbar-kernel/src/test_support/engine_kit/mod.rs index 4e36c303a3..9f6fc0c89c 100644 --- a/crates/busbar-kernel/src/test_support/engine_kit/mod.rs +++ b/crates/busbar-kernel/src/test_support/engine_kit/mod.rs @@ -33,10 +33,10 @@ use crate::config::sections::RateEntryCfg; use crate::governance::signing::TokenSigner; use crate::governance::{DerivedUsage, NewKeySpec}; use crate::plane::calllog::CallRecorded; +use crate::plane::host::{EngineHost, LiveHostFactory, PlaneSlots}; use crate::plane::registry::CardIssuer; use crate::plane::store::PlaneStore; use crate::plane::PlaneAdmission; -use crate::plane_host::{EngineHost, LiveHostFactory, PlaneSlots}; use crate::store::BreakerState; use crate::trust::validate::GovResolve; use busbar_contract::abi::hot::GuardClass; diff --git a/crates/busbar-kernel/src/test_support/fixtures.rs b/crates/busbar-kernel/src/test_support/fixtures.rs index f4d6d6a6b8..1de8b003cc 100644 --- a/crates/busbar-kernel/src/test_support/fixtures.rs +++ b/crates/busbar-kernel/src/test_support/fixtures.rs @@ -340,13 +340,13 @@ impl busbar_kernel::test_support::TestAppSeam for TestApp { impl busbar_kernel::test_support::BuiltAppSeam for crate::state::App { fn engine_host_of( app: &std::sync::Arc, - ) -> std::sync::Arc { + ) -> std::sync::Arc { crate::plane_host::engine_host(app) } fn engine_host_value_of( app: std::sync::Arc, - ) -> impl busbar_kernel::plane_host::EngineHost + 'static { + ) -> impl busbar_kernel::plane::host::EngineHost + 'static { crate::plane_host::engine_host_value(&app) } diff --git a/crates/busbar-kernel/src/test_support/mod.rs b/crates/busbar-kernel/src/test_support/mod.rs index 4ee2c96ac6..1297c881ad 100644 --- a/crates/busbar-kernel/src/test_support/mod.rs +++ b/crates/busbar-kernel/src/test_support/mod.rs @@ -685,14 +685,14 @@ impl LaneSpec { /// reconstructs a `Lane` from (money-path Phase 3-4 C). The fixture hands `PlaneBuildInput` to the /// registered `build_runtime` fn-pointer exactly as production `appbuild` does, so core's test /// fixture names no `Lane`/`NativeRuntime` — plane-agnostic, like the MCP/A2A test-kits. - fn to_lane_input(&self) -> busbar_kernel::plane_host::LaneInput { + fn to_lane_input(&self) -> busbar_kernel::route_input::LaneInput { let auth_style = match self.auth.as_deref() { - None => busbar_kernel::plane_host::AuthStyleInput::Default, - Some("api-key") => busbar_kernel::plane_host::AuthStyleInput::ApiKey, - Some("bearer") => busbar_kernel::plane_host::AuthStyleInput::Bearer, + None => busbar_kernel::route_input::AuthStyleInput::Default, + Some("api-key") => busbar_kernel::route_input::AuthStyleInput::ApiKey, + Some("bearer") => busbar_kernel::route_input::AuthStyleInput::Bearer, Some(other) => panic!("unexpected test auth style in LaneSpec: {other}"), }; - busbar_kernel::plane_host::LaneInput { + busbar_kernel::route_input::LaneInput { model: self.model.clone(), provider: self.provider.clone(), protocol: self.protocol.to_string(), @@ -711,16 +711,16 @@ impl LaneSpec { health: self .health .as_ref() - .map(|h| busbar_kernel::plane_host::HealthInput { + .map(|h| busbar_kernel::route_input::HealthInput { mode: match h.mode { crate::config::HealthMode::None => { - busbar_kernel::plane_host::HealthModeInput::None + busbar_kernel::route_input::HealthModeInput::None } crate::config::HealthMode::Dead => { - busbar_kernel::plane_host::HealthModeInput::Dead + busbar_kernel::route_input::HealthModeInput::Dead } crate::config::HealthMode::Active => { - busbar_kernel::plane_host::HealthModeInput::Active + busbar_kernel::route_input::HealthModeInput::Active } }, interval_secs: h.interval_secs, @@ -867,7 +867,7 @@ pub struct TestApp { /// (which named the plane's `PoolRuntime`) is replaced by the granular setters that fill these. pool_failover: std::collections::HashMap, pool_affinity: std::collections::HashMap, - pool_breaker: std::collections::HashMap, + pool_breaker: std::collections::HashMap, pool_upstream_creds: std::collections::HashMap, #[allow(clippy::type_complexity)] pool_member_meta: std::collections::HashMap< @@ -1715,7 +1715,7 @@ impl TestApp { // lowering production runs (egress targets, credentials, upstream client, probe schedule). let member_input = |pool: &str, idx: usize, weight: u32| { let meta = self.pool_member_meta.get(pool).and_then(|m| m.get(&idx)); - busbar_kernel::plane_host::PoolMemberInput { + busbar_kernel::route_input::PoolMemberInput { model: lane_inputs .get(idx) .map(|l| l.model.clone()) @@ -1736,36 +1736,36 @@ impl TestApp { for (name, members) in self.fallback_pools.iter().chain(self.pools.iter()) { pool_map.insert(name.clone(), members.clone()); } - let pool_inputs: Vec = pool_map + let pool_inputs: Vec = pool_map .into_iter() - .map(|(name, members)| busbar_kernel::plane_host::PoolInput { + .map(|(name, members)| busbar_kernel::route_input::PoolInput { members: members .iter() .map(|(idx, w)| member_input(&name, *idx, *w)) .collect(), failover: self.pool_failover.get(&name).map(|f| { - busbar_kernel::plane_host::FailoverInput { + busbar_kernel::route_input::FailoverInput { timeout_secs: f.timeout_secs, exclusions: f.exclusions.clone(), max_hops: f.max_hops, } }), affinity: self.pool_affinity.get(&name).map(|a| { - busbar_kernel::plane_host::AffinityInput { + busbar_kernel::route_input::AffinityInput { header_name: a.header_name.clone(), } }), on_exhausted: match self.on_exhausted_cfgs.get(&name) { Some(crate::config::OnExhausted::FallbackPool(p)) => { - busbar_kernel::plane_host::OnExhaustedInput::FallbackPool(p.clone()) + busbar_kernel::route_input::OnExhaustedInput::FallbackPool(p.clone()) } Some(crate::config::OnExhausted::LeastBad) => { - busbar_kernel::plane_host::OnExhaustedInput::LeastBad + busbar_kernel::route_input::OnExhaustedInput::LeastBad } Some(crate::config::OnExhausted::Queue { max_ms }) => { - busbar_kernel::plane_host::OnExhaustedInput::Queue { max_ms: *max_ms } + busbar_kernel::route_input::OnExhaustedInput::Queue { max_ms: *max_ms } } - _ => busbar_kernel::plane_host::OnExhaustedInput::Status503, + _ => busbar_kernel::route_input::OnExhaustedInput::Status503, }, upstream_credentials: self.pool_upstream_creds.get(&name).copied(), breaker: self.pool_breaker.get(&name).cloned(), @@ -1781,24 +1781,24 @@ impl TestApp { let default_failover = self .failover_cfg .as_ref() - .map(|f| busbar_kernel::plane_host::FailoverInput { + .map(|f| busbar_kernel::route_input::FailoverInput { timeout_secs: f.timeout_secs, exclusions: f.exclusions.clone(), max_hops: f.max_hops, }) - .unwrap_or(busbar_kernel::plane_host::FailoverInput { + .unwrap_or(busbar_kernel::route_input::FailoverInput { timeout_secs: crate::config::DEFAULT_FAILOVER_DEADLINE_SECS, exclusions: None, max_hops: crate::config::DEFAULT_FAILOVER_CAP, }); - let build_input = busbar_kernel::plane_host::PlaneBuildInput { + let build_input = busbar_kernel::route_input::PlaneBuildInput { lanes: lane_inputs, pools: pool_inputs, upstream_credentials: self.upstream_credentials, allow_metadata_hosts: Vec::new(), allow_all_metadata: false, blocked_metadata_hosts: Vec::new(), - client_settings: busbar_kernel::plane_host::ClientSettingsInput { + client_settings: busbar_kernel::route_input::ClientSettingsInput { upstream_request_timeout_secs: self.upstream_request_timeout_secs, pool_max_idle_per_host: 4, pool_idle_timeout_secs: 300, diff --git a/crates/busbar-kernel/src/test_support/seam.rs b/crates/busbar-kernel/src/test_support/seam.rs index 50ac7ca118..5aeb5079e2 100644 --- a/crates/busbar-kernel/src/test_support/seam.rs +++ b/crates/busbar-kernel/src/test_support/seam.rs @@ -18,9 +18,9 @@ //! plane can drive the handle as `&mut dyn TestAppSeam`): the generic scratch accessors live on the //! blanket [`TestAppSeamExt`] rather than on the object-safe base. +use crate::plane::host::{EngineHost, PlaneSlots}; use crate::plane::registry::CardIssuer; use crate::plane::PlaneAdmission; -use crate::plane_host::{EngineHost, PlaneSlots}; use std::any::Any; use std::sync::Arc; diff --git a/crates/busbar-kernel/src/tests/admin_verbs.rs b/crates/busbar-kernel/src/tests/admin_verbs.rs index 5150c4f2cb..12ad35ba77 100644 --- a/crates/busbar-kernel/src/tests/admin_verbs.rs +++ b/crates/busbar-kernel/src/tests/admin_verbs.rs @@ -3,7 +3,7 @@ //! super::*` reaches the private items it always did. use super::{connect_reply, AdminReply, AdminReqCtx, PlaneTrust, PlaneVerbError}; -use crate::plane_host::EngineHost; +use crate::plane::host::EngineHost; use crate::test_support::TestApp; use std::collections::BTreeMap; use std::sync::Arc; diff --git a/crates/busbar-kernel/src/tests/door_tests.rs b/crates/busbar-kernel/src/tests/door_tests.rs index 703e639e06..9a31644734 100644 --- a/crates/busbar-kernel/src/tests/door_tests.rs +++ b/crates/busbar-kernel/src/tests/door_tests.rs @@ -4,7 +4,7 @@ //! The door's ADMIT verdict: what a grant, a grantless admission and a refusal each answer. use super::*; -use crate::plane_host::AdmitHandle; +use crate::plane::host::AdmitHandle; use std::sync::Arc; fn grant() -> AdmitHandle { diff --git a/crates/busbar-kernel/src/tests/probe_tests.rs b/crates/busbar-kernel/src/tests/probe_tests.rs index 457cb85f46..7ffec6c5d8 100644 --- a/crates/busbar-kernel/src/tests/probe_tests.rs +++ b/crates/busbar-kernel/src/tests/probe_tests.rs @@ -8,11 +8,11 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; -use crate::plane_host::HealthModeInput as HealthMode; use crate::probe::{ advance_owned_deadline, due, probe_deadline, spawn_probers, ProbeCfg, ProbeMember, ProbeSchedule, ProbeTarget, NEVER_PROBE_DEADLINE, }; +use crate::route_input::HealthModeInput as HealthMode; #[derive(Default)] struct Double { diff --git a/crates/busbar-kernel/src/plane_host/tests/build_input_tests.rs b/crates/busbar-kernel/src/tests/route_input_tests.rs similarity index 95% rename from crates/busbar-kernel/src/plane_host/tests/build_input_tests.rs rename to crates/busbar-kernel/src/tests/route_input_tests.rs index cfe6f1f48d..9f3d55d6e8 100644 --- a/crates/busbar-kernel/src/plane_host/tests/build_input_tests.rs +++ b/crates/busbar-kernel/src/tests/route_input_tests.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! Tests for `crates/busbar-substrate/src/plane_host/build_input.rs` — chiefly that the resolved +//! Tests for `crates/busbar-kernel/src/route_input.rs` — chiefly that the resolved //! provider credential the carrier holds cannot be printed out of it. use super::*; diff --git a/crates/busbar-kernel/src/tests/route_tables_tests.rs b/crates/busbar-kernel/src/tests/route_tables_tests.rs index 4637c3d6eb..268db3084f 100644 --- a/crates/busbar-kernel/src/tests/route_tables_tests.rs +++ b/crates/busbar-kernel/src/tests/route_tables_tests.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use super::{ConfigTables, EngineTablesView}; -use crate::plane_host::{AuthStyleInput, LaneInput, OnExhaustedInput, PoolInput, PoolMemberInput}; +use crate::route_input::{AuthStyleInput, LaneInput, OnExhaustedInput, PoolInput, PoolMemberInput}; fn lane(model: &str, provider: &str) -> LaneInput { LaneInput { diff --git a/crates/busbar-kernel/src/tests/secure_tests.rs b/crates/busbar-kernel/src/tests/secure_tests.rs new file mode 100644 index 0000000000..e3a1373955 --- /dev/null +++ b/crates/busbar-kernel/src/tests/secure_tests.rs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-kernel/src/secure.rs`: the egress-trust capability's install and its +//! fail-closed pass-through. The guarded capability's judgement is the connector's +//! (`busbar_core_connector::tls::trust`). + +use super::*; + +#[test] +fn the_composition_root_install_hands_the_installed_capability_back() { + // This test installs its own and reads it back, exercising the OnceLock accessor the + // composition root uses. + install_egress_trust_host(Arc::new(PassThroughEgressTrust)); + assert!( + egress_trust_host().is_some(), + "the just-installed capability reads back" + ); +} + +/// THE DESTINATION JUDGEMENT ON THE SEAM (ARCHITECT ruling (C)): the pass-through has no guard +/// behind it and refuses every answer (fail closed). +#[test] +fn the_pass_through_seam_fails_closed() { + let public: IpAddr = "93.184.216.34".parse().unwrap(); + let refused = PassThroughEgressTrust + .judge_answer("api.test", &[public], 0) + .unwrap_err(); + assert_eq!( + refused.verdict, + busbar_contract::abi::host::service::DEST_NO_HOST + ); + assert!(PassThroughEgressTrust.secure_layer().is_none()); +} diff --git a/crates/busbar-kernel/tests/spentledger_cross_plane.rs b/crates/busbar-kernel/tests/spentledger_cross_plane.rs index a3b0aa5a34..12678b6643 100644 --- a/crates/busbar-kernel/tests/spentledger_cross_plane.rs +++ b/crates/busbar-kernel/tests/spentledger_cross_plane.rs @@ -164,7 +164,7 @@ fn configured(mut app: TestApp, cfg: &RootCfg, runtime: &Arc( req: GauntletRequest<'a>, diff --git a/crates/busbar/src/root/gauntlet_kernel.rs b/crates/busbar/src/root/gauntlet_kernel.rs index 7f100971f1..fc3756ffd3 100644 --- a/crates/busbar/src/root/gauntlet_kernel.rs +++ b/crates/busbar/src/root/gauntlet_kernel.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! THE KERNEL-LOOP RUNNER behind `busbar_kernel::plane_host::run_gauntlet` — LIVE. +//! THE KERNEL-LOOP RUNNER behind `busbar_kernel::plane::host::run_gauntlet` — LIVE. //! //! [`run_gauntlet_via_kernel`] drives the SAME [`GauntletPlane`] the substrate gauntlet drives, but //! through `busbar_kernel::teller::run_unit_async` (the ONE unified loop) instead of the substrate @@ -31,7 +31,7 @@ use busbar_contract::caps::{ VerifiedDestination, Verify, }; use busbar_contract::{AuditFacts, FinishClass, RoutePlan, ScopeFacts, UnitKey}; -use busbar_kernel::plane_host::{ +use busbar_kernel::plane::host::{ Admitted, GauntletPlane, GauntletRequest, PlaneAnswer, PlaneInFlight, VerifyOutcome, }; use busbar_kernel::slice::GroupLeaseSlip; @@ -317,7 +317,7 @@ fn principal_of(gov: &busbar_contract::records::PlaneRequestCtx) -> PrincipalId /// Run one gauntlet request through the UNIFIED kernel loop and return the plane's response verbatim. /// -/// The kernel-loop twin of [`busbar_kernel::plane_host::run_gauntlet`]: same plane, same +/// The kernel-loop twin of [`busbar_kernel::plane::host::run_gauntlet`]: same plane, same /// verify-before-charge order, same bytes out — driven through `busbar_kernel::teller::run_unit_async` /// over an ephemeral per-request kernel harness (uncapped in-flight table, node-wide gauge, canary). /// The loop's `Ended` is discarded: this rider binds no book and settles nothing (see the module @@ -405,7 +405,7 @@ pub async fn run_gauntlet_via_kernel( } /// OPEN A SESSION through the UNIFIED kernel loop and return at the door — the kernel-loop twin of -/// `busbar_kernel::plane_host::run_gauntlet_session` (its `admit_open` seat). +/// `busbar_kernel::plane::host::run_gauntlet_session` (its `admit_open` seat). /// /// Runs the SAME plane's `verify_destination` in its verify-STRICTLY-before-charge position through /// `busbar_kernel::teller::open_unit` (governance-to-door, no Route, no settling exit), over an diff --git a/crates/busbar/src/root/linked.rs b/crates/busbar/src/root/linked.rs index 30e34c191c..8e070bd765 100644 --- a/crates/busbar/src/root/linked.rs +++ b/crates/busbar/src/root/linked.rs @@ -35,10 +35,10 @@ use busbar_contract::abi::hot::StatusClass; use busbar_contract::abi::mechanism::route::{RouteAuth, RouteMethod}; use busbar_contract::ids::OpClassId; use busbar_contract::plane::{MetricFamily, ServedOpClass}; +use busbar_kernel::plane::host::LiveHostFactory; use busbar_kernel::plane::registry::PlaneDecl; use busbar_kernel::plane::registry::{BillableClass, BuildCtx, PlaneDeclaration, PlaneHooks}; use busbar_kernel::plane::PlaneAdmission; -use busbar_kernel::plane_host::LiveHostFactory; use busbar_kernel::plane_routes::{PlaneReqCtx, PlaneResponse, PlaneRouteSpec}; use busbar_kernel::preflight::{LinkedAuth, LinkedStore, RootInstall}; @@ -967,7 +967,7 @@ fn hot_dispatch( }; }; let app = handle.load(); - let scope = busbar_kernel::plane_host::DispatchScope::new(); + let scope = busbar_kernel::plane::dispatch_scope::DispatchScope::new(); busbar_kernel::plane_host::with_plane_door( slot.served.plane().name(), ctx.gov.as_ref(), diff --git a/crates/busbar/src/root/model_egress.rs b/crates/busbar/src/root/model_egress.rs index 6c3837dfe6..53438db808 100644 --- a/crates/busbar/src/root/model_egress.rs +++ b/crates/busbar/src/root/model_egress.rs @@ -164,13 +164,13 @@ impl ModelPools { busbar_kernel::probe::ProbeCfg::resolve( match health.mode { busbar_kernel::config::HealthMode::None => { - busbar_kernel::plane_host::HealthModeInput::None + busbar_kernel::route_input::HealthModeInput::None } busbar_kernel::config::HealthMode::Dead => { - busbar_kernel::plane_host::HealthModeInput::Dead + busbar_kernel::route_input::HealthModeInput::Dead } busbar_kernel::config::HealthMode::Active => { - busbar_kernel::plane_host::HealthModeInput::Active + busbar_kernel::route_input::HealthModeInput::Active } }, health.interval_secs, diff --git a/crates/busbar/src/root/tests/gauntlet_kernel.rs b/crates/busbar/src/root/tests/gauntlet_kernel.rs index b44cb8a873..1963205770 100644 --- a/crates/busbar/src/root/tests/gauntlet_kernel.rs +++ b/crates/busbar/src/root/tests/gauntlet_kernel.rs @@ -15,7 +15,7 @@ use std::time::Instant; use axum::body::{to_bytes, Body}; use axum::http::StatusCode; use axum::response::Response; -use busbar_kernel::plane_host::{ +use busbar_kernel::plane::host::{ register_gauntlet_runner, register_session_runner, run_gauntlet, run_gauntlet_session, Admitted, GauntletPlane, GauntletRequest, VerifyOutcome, }; @@ -300,7 +300,7 @@ fn install_flips_every_linked_gauntlet_row_onto_the_runner_its_axis_names() { "`{key}` is on the gauntlet-one-shot axis and is no linked plane" ); assert!( - busbar_kernel::plane_host::gauntlet_runner_registered(key), + busbar_kernel::plane::host::gauntlet_runner_registered(key), "install() must flip the linked plane `{key}` onto the unified kernel loop's one-shot runner" ); } @@ -310,7 +310,7 @@ fn install_flips_every_linked_gauntlet_row_onto_the_runner_its_axis_names() { "`{key}` is on the gauntlet-session axis and is no linked plane" ); assert!( - busbar_kernel::plane_host::session_runner_registered(key), + busbar_kernel::plane::host::session_runner_registered(key), "install() must flip the linked plane `{key}` onto the unified kernel loop's session runner" ); } @@ -363,8 +363,8 @@ fn every_linked_plane_the_teller_ledger_runs_rides_the_kernel_loop() { gauntlet axis: install() never flips it onto the unified kernel loop" ); assert!( - busbar_kernel::plane_host::gauntlet_runner_registered(plane) - || busbar_kernel::plane_host::session_runner_registered(plane), + busbar_kernel::plane::host::gauntlet_runner_registered(plane) + || busbar_kernel::plane::host::session_runner_registered(plane), "the linked plane `{plane}` has no kernel-loop runner registered after install()" ); } diff --git a/crates/busbar/src/root/tests/linked.rs b/crates/busbar/src/root/tests/linked.rs index 45d9ef0cae..09dd30af99 100644 --- a/crates/busbar/src/root/tests/linked.rs +++ b/crates/busbar/src/root/tests/linked.rs @@ -421,7 +421,7 @@ struct JournalQuery { /// The example plane's audit rows, read back through the host's own `journal_read` slot — verified /// chain, then `seq · prev_hash · hash · content` per row — as hex. fn audit_rows(app: &busbar_kernel::state::App) -> String { - let scope = busbar_kernel::plane_host::DispatchScope::new(); + let scope = busbar_kernel::plane::dispatch_scope::DispatchScope::new(); busbar_kernel::plane_host::with_borrowed_host(app, &scope, |host, vt| { let query = JournalQuery { size: core::mem::size_of::() as u32, diff --git a/crates/busbar/src/root/tests/metric_family_conformance.rs b/crates/busbar/src/root/tests/metric_family_conformance.rs index 08b2b61de2..b7ba9a7277 100644 --- a/crates/busbar/src/root/tests/metric_family_conformance.rs +++ b/crates/busbar/src/root/tests/metric_family_conformance.rs @@ -48,7 +48,7 @@ fn row(linked_door: bool, dropped_in: Vec) -> &'static PlaneDecl { fn add(row: &'static PlaneDecl, family: &str, values: &[&'static str]) -> StatusClass { let _registry = TestRegistryIsolation::seeded(&[row]); let app = busbar_kernel::test_support::TestApp::new().build(); - let scope = busbar_kernel::plane_host::DispatchScope::new(); + let scope = busbar_kernel::plane::dispatch_scope::DispatchScope::new(); let values: Vec = values.iter().map(|v| DeclStr::new(v)).collect(); busbar_kernel::plane_host::with_borrowed_host_as(row.key, &app, &scope, |host, vt| { let add = vt.counter_add.expect("the host wires counter_add"); diff --git a/crates/busbar/src/root/tests/percall_meter_shadow.rs b/crates/busbar/src/root/tests/percall_meter_shadow.rs index 47b5076839..ec063c9e11 100644 --- a/crates/busbar/src/root/tests/percall_meter_shadow.rs +++ b/crates/busbar/src/root/tests/percall_meter_shadow.rs @@ -8,7 +8,7 @@ //! nothing. This one proves the other half of the thin-verbatim-rider claim (DECISIONS #28/#29): //! a plane whose ONE flat per-call charge fires INSIDE `drive` (the shape of the A2A invoke plane's //! `meter_request`, `busbar-a2a`'s `receive.rs`) is driven through BOTH loops — `leg_legacy` = -//! `busbar_kernel::plane_host::run_gauntlet` (the shipped authority), `leg_loop` = +//! `busbar_kernel::plane::host::run_gauntlet` (the shipped authority), `leg_loop` = //! `run_gauntlet_via_kernel` (the plane-neutral bridge `gauntlet_install::install()` registers as //! the runner) — on the same input, and the test asserts (1) byte-identical status/headers/body AND //! (2) identical `meter_charge` rows. Any difference is a divergence (STOP + report). @@ -30,7 +30,7 @@ use std::time::Instant; use axum::body::{to_bytes, Body}; use axum::http::StatusCode; use axum::response::Response; -use busbar_kernel::plane_host::{run_gauntlet, GauntletPlane, GauntletRequest, VerifyOutcome}; +use busbar_kernel::plane::host::{run_gauntlet, GauntletPlane, GauntletRequest, VerifyOutcome}; use crate::root::gauntlet_kernel::run_gauntlet_via_kernel; diff --git a/crates/busbar/src/root/tests/plane_node.rs b/crates/busbar/src/root/tests/plane_node.rs index 1b0a032def..9fb1368cc8 100644 --- a/crates/busbar/src/root/tests/plane_node.rs +++ b/crates/busbar/src/root/tests/plane_node.rs @@ -139,7 +139,7 @@ impl Rig { } } - fn host(&self) -> Arc { + fn host(&self) -> Arc { busbar_kernel::plane_host::engine_host(&self.app) } } diff --git a/crates/busbar/tests/plane_host_universal_purity.rs b/crates/busbar/tests/plane_host_universal_purity.rs index 129758b009..6bfa08e4f0 100644 --- a/crates/busbar/tests/plane_host_universal_purity.rs +++ b/crates/busbar/tests/plane_host_universal_purity.rs @@ -60,7 +60,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; /// The host-seam trait definition file. Its slice supertraits + `EngineHost` are the universe scanned. -const HOST_TRAIT_FILE: &str = "crates/busbar-kernel/src/plane_host/mod.rs"; +const HOST_TRAIT_FILE: &str = "crates/busbar-kernel/src/plane/host.rs"; /// The capability-slice supertraits of `EngineHost`, plus `EngineHost` itself (its provided /// `run_gauntlet`). A slice added/removed on the universal sum is ONE edit here — and the enumeration @@ -414,7 +414,7 @@ struct Scan { fn run_scan() -> Scan { let root = repo_root(); let host_src = std::fs::read_to_string(root.join(HOST_TRAIT_FILE)) - .expect("the plane_host trait file must be readable"); + .expect("the host trait file must be readable"); let host_stripped = strip_source(&host_src); let methods = universal_methods(&host_stripped); let sources = plane_sources(&root); diff --git a/crates/busbar/tests/plane_meter_seam_reachability.rs b/crates/busbar/tests/plane_meter_seam_reachability.rs index 6ba00b4cb5..77e77da99a 100644 --- a/crates/busbar/tests/plane_meter_seam_reachability.rs +++ b/crates/busbar/tests/plane_meter_seam_reachability.rs @@ -639,7 +639,7 @@ fn billing_plane_session_legs() -> &'static [SessionLeg] { const SESSION_ACCOUNT_CALL: &str = "account.report_turn("; /// Where the kernel's session account lives, relative to `crates/`, and the step of it that must /// reach the host's ledger seam. -const SESSION_ACCOUNT_FILE: &str = "busbar-kernel/src/plane_host/session_meter.rs"; +const SESSION_ACCOUNT_FILE: &str = "busbar-kernel/src/governance/session_meter.rs"; const SESSION_ACCOUNT_STEP: &str = "fn report_turn("; /// What is wrong with one session leg, or nothing: the plane's step lines, the lines of the file diff --git a/docs/security/1.6.0-security-posture.md b/docs/security/1.6.0-security-posture.md index efa45ef4dd..a9ca042727 100644 --- a/docs/security/1.6.0-security-posture.md +++ b/docs/security/1.6.0-security-posture.md @@ -136,8 +136,8 @@ references it and does not duplicate it.) `crates/busbar-contract/src/tests/redacted_no_serde.rs`, wired at `redacted.rs:145-149`) so it cannot enter a JSON/audit payload implicitly; it zeroizes on drop (`:72-76`); the only escape is an explicit `expose_secret()` (`:46-48`); `PartialEq` is constant-time (`:92-96`). Broadly adopted: provider `api_key` on `Lane` - (`crates/busbar-llm/src/engine/tables.rs:47`), the pre-resolved credential on the neutral plane - carrier (`crates/busbar-kernel/src/plane_host/build_input.rs:106`), admin token, OAuth + (`crates/busbar-llm/src/engine/tables.rs:47`), the pre-resolved credential on the kernel's routing + input (`crates/busbar-kernel/src/route_input.rs:103`), admin token, OAuth `client_secret` (`crates/busbar-kernel/src/egress_auth/oauth_client_credentials.rs:23`), bearer token, browser-login secret, submitted passwords (`crates/api/src/auth.rs:193`), and the whole MCP egress credential set. diff --git a/qa/construction.toml b/qa/construction.toml index 226f2eccbb..211bc4ab69 100644 --- a/qa/construction.toml +++ b/qa/construction.toml @@ -925,7 +925,7 @@ known_sites = [ "crates/busbar-kernel/src/ingress/mod.rs :: The model '{model}' does not exist or you do not have access to it.", "crates/busbar-kernel/src/metrics/mod.rs :: Per-(bucket, model, tier) tokens in the bucket's current budget window (key and budget-group buckets; derived from the token ledger at scrape time)", "crates/busbar-kernel/src/metrics/money.rs :: model", - "crates/busbar-kernel/src/plane_host/build_input.rs :: model", + "crates/busbar-kernel/src/route_input.rs :: model", "crates/busbar-kernel/src/store/vocab.rs :: agent:{agent}", "crates/busbar-kernel/src/test_support/mod.rs :: /v1/chat/completions", ] @@ -1238,11 +1238,14 @@ known_sites = [ # enclosing function — the escape is at :858, the fn opens at :820. "crates/busbar-kernel/src/plane/handle_engine.rs::sweep", # was crates/busbar-substrate/src/plane_host/mod.rs — MERGED into the core-descended kernel file - # of the same name by 5fa320208 (+1775 lines). The one escape there (:2433) is substrate's. - "crates/busbar-kernel/src/plane_host/mod.rs", - # was crates/busbar-substrate/src/plane_host/scope.rs::reclaim_all — merged the same way (+596). - # Still named BY FUNCTION, so the rest of the merged file is still held to the rule. - "crates/busbar-kernel/src/plane_host/scope.rs::reclaim_all", + # of the same name by 5fa320208 (+1775 lines). The one escape there (:2433) is substrate's: the + # `runtime_slot_key` interner, which moved verbatim with the host vocabulary to `plane/host.rs` + # (D2 step 3). Now named BY FUNCTION, so the rest of that file is held to the rule. + "crates/busbar-kernel/src/plane/host.rs::runtime_slot_key", + # was crates/busbar-substrate/src/plane_host/scope.rs::reclaim_all — merged the same way (+596), + # then moved verbatim with `DispatchScope` to `plane/dispatch_scope.rs` (D2 step 3). Still named BY + # FUNCTION, so the rest of the file is still held to the rule. + "crates/busbar-kernel/src/plane/dispatch_scope.rs::reclaim_all", # was crates/busbar-substrate-values/src/proto.rs — the protocol registry moved to the kernel # verbatim (#83a SD-8). "crates/busbar-kernel/src/proto/installed.rs", diff --git a/qa/door-only.toml b/qa/door-only.toml index 19392f2bc5..840b9008ca 100644 --- a/qa/door-only.toml +++ b/qa/door-only.toml @@ -164,7 +164,7 @@ item = "PLANE_SECTIONS" # destination guard through this seam; no new static.) [[finding]] rule = "door-only:static-seam" -file = "crates/busbar-kernel/src/plane_host/egress_trust.rs" +file = "crates/busbar-kernel/src/secure.rs" item = "EGRESS_TRUST" [[finding]]