diff --git a/crates/busbar-kernel/src/plane_driver/far_end.rs b/crates/busbar-kernel/src/plane_driver/far_end.rs index d49183e3f9..1e00a5006b 100644 --- a/crates/busbar-kernel/src/plane_driver/far_end.rs +++ b/crates/busbar-kernel/src/plane_driver/far_end.rs @@ -590,7 +590,23 @@ fn class_code(class: Option) -> u32 { /// `base_url` + the plane's target: the base's trailing `/` trimmed, then the target as the plane /// answered it. +/// +/// A web base URL's `\` is read as `/` first, as the dialler's WHATWG reader reads it and as v1.5.5 +/// folded it before signing (`crates/busbar/src/proxy/egress.rs:18`, `host_from_base`), so the +/// authority the auth call signs, the head's path and the host dialled come off ONE spelling: a +/// backslash-blind split of `https://evil.example\@victim.example/` signed one host while the +/// stack dialled another. fn join(base: &str, target: &[u8]) -> String { + let base: std::borrow::Cow<'_, str> = match base.split_once("://") { + Some((scheme, _)) + if base.contains('\\') + && busbar_contract::net::UrlFamily::of(&scheme.to_ascii_lowercase()) + == busbar_contract::net::UrlFamily::Web => + { + std::borrow::Cow::Owned(base.replace('\\', "/")) + } + _ => std::borrow::Cow::Borrowed(base), + }; format!( "{}{}", base.trim_end_matches('/'), diff --git a/crates/busbar-kernel/src/plane_driver/route.rs b/crates/busbar-kernel/src/plane_driver/route.rs index 142b639941..8209a3ebed 100644 --- a/crates/busbar-kernel/src/plane_driver/route.rs +++ b/crates/busbar-kernel/src/plane_driver/route.rs @@ -1184,17 +1184,13 @@ impl super::PlaneUnits<'_, S, F, C> { // hook chose the serving member, after the plane's own fields, as 1.5.5 // stamped them; nothing on the default path or when no hook chose. let chosen = self.lock().route_policy; - if let Some(name) = chosen.filter(|_| { - !bufs.member.is_empty() && crate::proxy::route_policy_headers_enabled() - }) { - fields.push(( - crate::proxy::HDR_ROUTE_POLICY.as_bytes().to_vec(), - name.as_bytes().to_vec(), - )); - fields.push(( - crate::proxy::HDR_ROUTE_TARGET.as_bytes().to_vec(), - bufs.member.clone(), - )); + if chosen.is_some() { + route_policy_fields( + &mut fields, + crate::proxy::route_policy_headers_enabled(), + chosen, + &bufs.member, + ); } self.caller.head(out.reply_status, fields); // The answer's head is the `response` stage (1.5.5 fires it at head time, @@ -1243,6 +1239,29 @@ impl super::PlaneUnits<'_, S, F, C> { } } +/// THE ROUTE-POLICY TRANSPARENCY FIELDS (opt-in, `advanced.response_headers.route_policy`), as +/// 1.5.5's `maybe_attach_route_policy_gated` gated them: the pair goes after the plane's own fields +/// only when the operator opted in (`enabled`, the outer gate) AND a non-default policy chose the +/// serving member (`policy`, the inner gate), naming the policy and the member; a local answer +/// (no member) and every other combination add nothing. +fn route_policy_fields( + fields: &mut HeadFields, + enabled: bool, + policy: Option<&str>, + member: &[u8], +) { + if let Some(name) = policy.filter(|_| enabled && !member.is_empty()) { + fields.push(( + crate::proxy::HDR_ROUTE_POLICY.as_bytes().to_vec(), + name.as_bytes().to_vec(), + )); + fields.push(( + crate::proxy::HDR_ROUTE_TARGET.as_bytes().to_vec(), + member.to_vec(), + )); + } +} + /// The duplex session's two sides, on this pump (K6): a child module, so it drives the pump's own /// pieces and push. #[path = "session.rs"] diff --git a/crates/busbar-kernel/src/plane_driver/tests/far_end_tests.rs b/crates/busbar-kernel/src/plane_driver/tests/far_end_tests.rs index f786af27bc..db19322915 100644 --- a/crates/busbar-kernel/src/plane_driver/tests/far_end_tests.rs +++ b/crates/busbar-kernel/src/plane_driver/tests/far_end_tests.rs @@ -1112,6 +1112,91 @@ async fn a_plane_field_name_that_is_not_a_token_is_refused_before_the_dial() { assert!(r.book.observed.lock().unwrap().is_empty()); } +/// F25 row 75: rehomed from crates/busbar-llm/src/engine/tests/auth_style_tests.rs:: +/// test_host_from_base_backslash_authority_matches_wire_host (its signed-host half; the dialler's +/// reading of each URL is pinned in busbar-kernel-egress's `net_tests`). A WHATWG reader ends a web +/// URL's authority at the first `\` exactly as at `/`, so the host the auth call signs must be the +/// host the connection dials, byte for byte, never the post-`@` host a backslash-blind split reads +/// (v1.5.5 `crates/busbar/src/proxy/egress.rs:18`, `host_from_base`); the port survives and the +/// backslash path rides the head as a path. +#[tokio::test] +async fn test_host_from_base_backslash_authority_matches_wire_host() { + for (base, script_host, authority, host, port, head_path) in [ + ( + "https://evil.example.com\\@victim.example/path", + "evil.example.com", + "evil.example.com", + "evil.example.com", + None, + "/@victim.example/path/chat", + ), + ( + "https://host.example.com\\some\\path", + "host.example.com", + "host.example.com", + "host.example.com", + None, + "/some/path/chat", + ), + ( + "https://host.example.com:8443\\v1\\foo", + "host.example.com:8443", + "host.example.com:8443", + "host.example.com", + Some(8443), + "/v1/foo/chat", + ), + ] { + let mut r = rig( + &[(script_host, Script::Answer(200, None, vec![b"ok"]))], + OnExhausted::Status503, + None, + ); + r.egress + .routes + .get_mut(&DestinationId::new(1)) + .expect("m0's route") + .base_url = base.to_string(); + let t = token(); + let far = r.egress.unit(route()); + assert!(matches!(far.member(&t, 1).await, Pick::Member { .. })); + assert!(far.send(&t, request()).await, "{base}"); + let pieces = drain(&far, &t).await; + assert_eq!(pieces[0].status, Some((200, 1)), "{base}"); + drop(far); + + let signed = r.auth.facts.lock().unwrap()[0].1.clone(); + assert_eq!(signed, authority, "{base}: the signed host"); + let opened = r.table.opened.lock().unwrap().clone(); + assert_eq!(opened.len(), 1, "{base}"); + let dialled = + busbar_contract::net::parse_url(&opened[0].0).unwrap_or_else(|e| panic!("{base}: {e}")); + assert_eq!( + (dialled.host.as_str(), dialled.port), + (host, port), + "{base}: the dialled host" + ); + let dialled_authority = match dialled.port { + Some(p) => format!("{}:{p}", dialled.host), + None => dialled.host.clone(), + }; + assert_eq!( + signed, dialled_authority, + "{base}: the signed host is the dialled host" + ); + assert_eq!( + r.table.words.lock().unwrap()[0], + (b"POST".to_vec(), head_path.as_bytes().to_vec()), + "{base}: the head's path" + ); + assert_eq!( + r.auth.facts.lock().unwrap()[0].2, + head_path.as_bytes().to_vec(), + "{base}: the signed path is the head's path" + ); + } +} + /// An auth binding that must wait (a refresh that never comes back) is awaited no longer than the /// attempt's cap: the attempt ends without a dial, nothing recorded against the member. #[tokio::test] diff --git a/crates/busbar-kernel/src/plane_driver/tests/route_tests.rs b/crates/busbar-kernel/src/plane_driver/tests/route_tests.rs index 2531cfc617..81666ebb44 100644 --- a/crates/busbar-kernel/src/plane_driver/tests/route_tests.rs +++ b/crates/busbar-kernel/src/plane_driver/tests/route_tests.rs @@ -103,6 +103,97 @@ fn a_node_without_signing_material_lends_no_caller_reference() { assert!(i.caller_ref.ptr.is_null() && i.caller_ref.len == 0); } +// ── F25 rows 630-633: rehomed from crates/busbar-llm/src/engine/tests/wire_tests.rs. The +// route-policy transparency pair as the pump stamps it +// (`route_policy_fields`, 1.5.5's `maybe_attach_route_policy_gated`), over the full 2x2 matrix of +// the outer (operator opt-in) and inner (a non-default policy chose) gates. + +/// Both `x-busbar-route-*` fields the pump would stamp after the plane's own `fields` for +/// `(enabled, policy, member)`, as a `(policy, target)` pair (empty strings when absent); the +/// plane's own fields stay first and untouched. +fn built_route_fields(enabled: bool, policy: Option<&str>, member: &[u8]) -> (String, String) { + let mut fields: HeadFields = vec![(b"content-type".to_vec(), b"application/json".to_vec())]; + route_policy_fields(&mut fields, enabled, policy, member); + assert_eq!( + fields[0], + (b"content-type".to_vec(), b"application/json".to_vec()), + "the plane's own fields lead" + ); + let get = |name: &str| { + fields + .iter() + .find(|(n, _)| n.as_slice() == name.as_bytes()) + .map(|(_, v)| String::from_utf8(v.clone()).expect("utf8")) + .unwrap_or_default() + }; + ( + get(crate::proxy::HDR_ROUTE_POLICY), + get(crate::proxy::HDR_ROUTE_TARGET), + ) +} + +/// F25 row 630: rehomed from crates/busbar-llm/src/engine/tests/wire_tests.rs:: +/// route_policy_headers_suppressed_when_outer_gate_disabled_even_with_a_policy_name. Even with a +/// policy name present, the outer gate off suppresses BOTH fields. +#[test] +fn route_policy_headers_suppressed_when_outer_gate_disabled_even_with_a_policy_name() { + let (policy, target) = built_route_fields(false, Some("cheapest"), b"claude"); + assert_eq!( + policy, "", + "route-policy header must be absent when the outer gate is off" + ); + assert_eq!( + target, "", + "route-target header must be absent when the outer gate is off" + ); +} + +/// F25 row 631: rehomed from crates/busbar-llm/src/engine/tests/wire_tests.rs:: +/// route_policy_headers_absent_for_a_default_policy_even_when_outer_gate_enabled. With the outer +/// gate ON, a default routing decision (no policy name) attaches nothing. +#[test] +fn route_policy_headers_absent_for_a_default_policy_even_when_outer_gate_enabled() { + let (policy, target) = built_route_fields(true, None, b"claude"); + assert_eq!( + policy, "", + "no policy name -> no header, regardless of the outer gate" + ); + assert_eq!( + target, "", + "no policy name -> no header, regardless of the outer gate" + ); +} + +/// F25 row 632: rehomed from crates/busbar-llm/src/engine/tests/wire_tests.rs:: +/// route_policy_headers_present_only_when_both_gates_open. Both gates open is the only combination +/// that emits the pair, carrying the exact policy name and target member. +#[test] +fn route_policy_headers_present_only_when_both_gates_open() { + let (policy, target) = built_route_fields(true, Some("cheapest"), b"claude"); + assert_eq!(policy, "cheapest"); + assert_eq!(target, "claude"); + let mut fields: HeadFields = Vec::new(); + route_policy_fields(&mut fields, true, Some("cheapest"), b"claude"); + assert_eq!( + fields, + vec![ + (b"x-busbar-route-policy".to_vec(), b"cheapest".to_vec()), + (b"x-busbar-route-target".to_vec(), b"claude".to_vec()), + ], + "exactly the pair, policy first" + ); +} + +/// F25 row 633: rehomed from crates/busbar-llm/src/engine/tests/wire_tests.rs:: +/// route_policy_headers_absent_when_both_gates_closed. The fourth combination (outer off, inner off) +/// is also silent, completing the 2x2 matrix. +#[test] +fn route_policy_headers_absent_when_both_gates_closed() { + let (policy, target) = built_route_fields(false, None, b"claude"); + assert_eq!(policy, ""); + assert_eq!(target, ""); +} + /// The reference the driver derives is the node's keyed digest of the principal, never the /// principal: 64 lower-case hex characters that do not contain it. #[test]