use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use serde::Serialize;
use crate::harness::Harness;
pub const SUITE: &str = "acp_compat_1";
pub const SUITE_VERSION: &str = "1.0.0";
const PROBE_AUTH: &str = "brk_conformance_probe";
const PROBE_MODEL: &str = "anthropic/claude-opus-4.8";
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(45);
const ROUTE_TIMEOUT: Duration = Duration::from_secs(45);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Tier {
Handshake,
Routability,
}
impl Tier {
pub fn key(self) -> &'static str {
match self {
Self::Handshake => "handshake",
Self::Routability => "routability",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum Outcome {
Pass,
Fail { reason: String },
Skipped { reason: String },
}
impl Outcome {
fn label(&self) -> &'static str {
match self {
Self::Pass => "pass",
Self::Fail { .. } => "fail",
Self::Skipped { .. } => "skipped",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TierResult {
pub tier: Tier,
#[serde(flatten)]
pub outcome: Outcome,
pub duration_ms: u128,
}
#[derive(Debug, Clone, Serialize)]
pub struct Report {
pub agent: String,
pub suite: &'static str,
pub suite_version: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_version: Option<String>,
pub measured_by: &'static str,
pub as_of: String,
pub tiers: Vec<TierResult>,
}
impl Report {
pub fn passed(&self) -> bool {
self.tiers
.iter()
.all(|result| !matches!(result.outcome, Outcome::Fail { .. }))
}
pub fn registry_block(&self) -> String {
let mut out = String::from(" conformance:\n");
out.push_str(&format!(" {SUITE}:\n"));
for result in &self.tiers {
out.push_str(&format!(
" {}: {}\n",
result.tier.key(),
result.outcome.label()
));
}
out.push_str(&format!(" suite_version: {SUITE_VERSION}\n"));
if let Some(version) = &self.agent_version {
out.push_str(&format!(" agent_version: {version}\n"));
}
out.push_str(&format!(" measured_by: {}\n", self.measured_by));
out.push_str(&format!(" as_of: {}\n", self.as_of));
out
}
}
#[derive(Debug, Clone)]
pub struct SeenRequest {
pub method: String,
pub path: String,
pub headers: BTreeMap<String, String>,
pub body: String,
}
pub struct StubGateway {
base_url: String,
seen: Arc<Mutex<Vec<SeenRequest>>>,
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
}
impl StubGateway {
pub async fn start() -> anyhow::Result<Self> {
use axum::extract::State;
use axum::http::{HeaderMap, Uri};
use axum::routing::any;
let seen: Arc<Mutex<Vec<SeenRequest>>> = Arc::new(Mutex::new(Vec::new()));
let handler_seen = Arc::clone(&seen);
let app = axum::Router::new()
.fallback(any(
|State(seen): State<Arc<Mutex<Vec<SeenRequest>>>>,
method: axum::http::Method,
uri: Uri,
headers: HeaderMap,
body: String| async move {
let recorded = SeenRequest {
method: method.to_string(),
path: uri.path().to_string(),
headers: headers
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|value| (name.as_str().to_lowercase(), value.to_string()))
})
.collect(),
body,
};
let reply = canned_reply(&recorded.path);
seen.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(recorded);
axum::Json(reply)
},
))
.with_state(handler_seen);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let (shutdown, shutdown_rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let _ = axum::serve(listener, app)
.with_graceful_shutdown(async {
let _ = shutdown_rx.await;
})
.await;
});
Ok(Self {
base_url: format!("http://{addr}"),
seen,
shutdown: Some(shutdown),
})
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn seen(&self) -> Vec<SeenRequest> {
self.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
async fn wait_for_generation(&self, timeout: Duration) -> Option<SeenRequest> {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if let Some(request) = self.seen().into_iter().find(|request| {
request.method.eq_ignore_ascii_case("POST")
&& !request.path.trim_end_matches('/').ends_with("/models")
}) {
return Some(request);
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
None
}
}
impl Drop for StubGateway {
fn drop(&mut self) {
if let Some(shutdown) = self.shutdown.take() {
let _ = shutdown.send(());
}
}
}
fn canned_reply(path: &str) -> serde_json::Value {
if path.ends_with("/models") {
return serde_json::json!({
"data": [{ "id": PROBE_MODEL, "object": "model", "owned_by": "bitrouter" }]
});
}
if path.contains("/messages") {
return serde_json::json!({
"id": "msg_conformance",
"type": "message",
"role": "assistant",
"model": PROBE_MODEL,
"content": [{ "type": "text", "text": "ok" }],
"stop_reason": "end_turn",
"usage": { "input_tokens": 1, "output_tokens": 1 }
});
}
if path.contains("/responses") {
return serde_json::json!({
"id": "resp_conformance",
"object": "response",
"model": PROBE_MODEL,
"status": "completed",
"output": [{
"type": "message",
"role": "assistant",
"content": [{ "type": "output_text", "text": "ok" }]
}],
"usage": { "input_tokens": 1, "output_tokens": 1, "total_tokens": 2 }
});
}
serde_json::json!({
"id": "chatcmpl-conformance",
"object": "chat.completion",
"model": PROBE_MODEL,
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "ok" },
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2 }
})
}
pub async fn run(harness: &Harness, agent_id: &str, state_dir: &std::path::Path) -> Report {
run_inner(harness, agent_id, state_dir).await
}
async fn run_inner(harness: &Harness, agent_id: &str, state_dir: &std::path::Path) -> Report {
use bitrouter_sdk::acp::client::{AcpClient, ClientOptions};
use bitrouter_sdk::acp::up::AgentProcess;
let mut tiers = Vec::new();
let mut agent_version = None;
let finish = |tiers: Vec<TierResult>, agent_version: Option<String>| Report {
agent: agent_id.to_string(),
suite: SUITE,
suite_version: SUITE_VERSION,
agent_version,
measured_by: "bitrouter",
as_of: today(),
tiers,
};
let Some(command) = harness.acp_command else {
let reason = "the harness has no ACP adapter, so there is nothing to check".to_string();
return finish(
[Tier::Handshake, Tier::Routability]
.into_iter()
.map(|tier| TierResult {
tier,
outcome: Outcome::Skipped {
reason: reason.clone(),
},
duration_ms: 0,
})
.collect(),
None,
);
};
let gateway = match StubGateway::start().await {
Ok(gateway) => gateway,
Err(error) => {
return finish(
[Tier::Handshake, Tier::Routability]
.into_iter()
.map(|tier| TierResult {
tier,
outcome: Outcome::Fail {
reason: format!("could not start the stub gateway: {error}"),
},
duration_ms: 0,
})
.collect(),
None,
);
}
};
let overlay = match harness.launch_overlay(
gateway.base_url(),
PROBE_AUTH,
Some(PROBE_MODEL),
&[PROBE_MODEL.to_string()],
&[],
state_dir,
) {
Ok(overlay) => overlay,
Err(error) => {
return finish(
[Tier::Handshake, Tier::Routability]
.into_iter()
.map(|tier| TierResult {
tier,
outcome: Outcome::Fail {
reason: format!("could not render the routing overlay: {error}"),
},
duration_ms: 0,
})
.collect(),
None,
);
}
};
let env: HashMap<String, String> = overlay.env.iter().cloned().collect();
let mut args: Vec<String> = harness.acp_args.iter().map(|a| (*a).to_string()).collect();
if harness.env_args_routable() {
args.extend(overlay.args.iter().cloned());
}
let handshake_started = Instant::now();
let connect = AcpClient::connect(
AgentProcess::new(command, args, env),
ClientOptions {
turn_timeout: Some(ROUTE_TIMEOUT),
terminal_auth: false,
},
);
let client = match tokio::time::timeout(HANDSHAKE_TIMEOUT, connect).await {
Ok(Ok(client)) => client,
Ok(Err(error)) => {
tiers.push(TierResult {
tier: Tier::Handshake,
outcome: Outcome::Fail {
reason: format!("initialize failed: {error:#}"),
},
duration_ms: handshake_started.elapsed().as_millis(),
});
tiers.push(TierResult {
tier: Tier::Routability,
outcome: Outcome::Skipped {
reason: "the agent never completed the handshake".to_string(),
},
duration_ms: 0,
});
return finish(tiers, None);
}
Err(_) => {
tiers.push(TierResult {
tier: Tier::Handshake,
outcome: Outcome::Fail {
reason: format!("initialize did not answer within {HANDSHAKE_TIMEOUT:?}"),
},
duration_ms: handshake_started.elapsed().as_millis(),
});
tiers.push(TierResult {
tier: Tier::Routability,
outcome: Outcome::Skipped {
reason: "the agent never completed the handshake".to_string(),
},
duration_ms: 0,
});
return finish(tiers, None);
}
};
if let Some(info) = client.agent_info() {
agent_version = Some(info.version.clone());
}
let negotiated = client.protocol_version();
let declared = agent_client_protocol::schema::ProtocolVersion::from(
u16::try_from(harness.acp_protocol_version).unwrap_or(u16::MAX),
);
tiers.push(TierResult {
tier: Tier::Handshake,
outcome: if negotiated == declared {
Outcome::Pass
} else {
Outcome::Fail {
reason: format!(
"the entry declares ACP protocol version {} but the agent settled on \
{negotiated:?}",
harness.acp_protocol_version
),
}
},
duration_ms: handshake_started.elapsed().as_millis(),
});
let route_started = Instant::now();
let outcome = route_outcome(harness, &client, &gateway, state_dir).await;
tiers.push(TierResult {
tier: Tier::Routability,
outcome,
duration_ms: route_started.elapsed().as_millis(),
});
let _ = tokio::time::timeout(Duration::from_secs(5), client.shutdown()).await;
finish(tiers, agent_version)
}
async fn route_outcome(
harness: &Harness,
client: &bitrouter_sdk::acp::client::AcpClient,
gateway: &StubGateway,
state_dir: &std::path::Path,
) -> Outcome {
if matches!(harness.routing, crate::harness::Routing::OwnAuth) {
return Outcome::Skipped {
reason: "the harness authenticates against its own subscription and is never \
routed through BitRouter"
.to_string(),
};
}
if !harness.env_args_routable() {
return Outcome::Skipped {
reason: "this harness routes only on the interactive launch path; its ACP facet \
launches direct, so there is no routed ACP traffic to observe"
.to_string(),
};
}
let session = match client
.new_session(state_dir.to_path_buf(), Vec::new())
.await
{
Ok(session) => session,
Err(error) => {
return Outcome::Fail {
reason: format!("session/new failed, so no request could be provoked: {error:#}"),
};
}
};
let prompt = client.prompt(&session.acp_session_id, "Reply with the single word: ok");
let observed = tokio::select! {
biased;
request = gateway.wait_for_generation(ROUTE_TIMEOUT) => request,
_ = prompt => gateway.wait_for_generation(Duration::from_secs(2)).await,
};
let Some(request) = observed else {
return Outcome::Fail {
reason: format!(
"no generation request reached the gateway within {ROUTE_TIMEOUT:?} — the \
routing block did not redirect this harness"
),
};
};
let credential_found = if harness.auth_is_bearer() {
request
.headers
.get("authorization")
.is_some_and(|value| value == &format!("Bearer {PROBE_AUTH}"))
} else {
request
.headers
.values()
.any(|value| value.contains(PROBE_AUTH))
};
if !credential_found {
let scheme = if harness.auth_is_bearer() {
"Authorization: Bearer"
} else {
"a provider-native header"
};
return Outcome::Fail {
reason: format!(
"the request reached the gateway but carried no gateway credential as {scheme}; \
headers were {:?}",
request.headers.keys().collect::<Vec<_>>()
),
};
}
if !request.path.contains(PROBE_MODEL) && !request.body.contains(PROBE_MODEL) {
return Outcome::Fail {
reason: format!(
"a request reached the gateway with the right credential, but '{PROBE_MODEL}' \
appeared in neither its path nor its body, so it cannot be attributed to the \
routed model"
),
};
}
Outcome::Pass
}
fn today() -> String {
chrono::Utc::now().format("%Y-%m-%d").to_string()
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use crate::harness::Routing;
const ROUTED_STUB: &str = r#"
while read line; do
id=$(echo "$line" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
case "$line" in
*initialize*) printf '{"jsonrpc":"2.0","id":"%s","result":{"protocolVersion":1}}\n' "$id";;
*session/new*) printf '{"jsonrpc":"2.0","id":"%s","result":{"sessionId":"u1"}}\n' "$id";;
*session/prompt*)
curl -s -X POST -H "Authorization: Bearer $STUB_AUTH" \
-H 'content-type: application/json' \
-d "{\"model\":\"$STUB_MODEL\"}" \
"$STUB_BASE_URL/v1/chat/completions" > /dev/null 2>&1
printf '{"jsonrpc":"2.0","id":"%s","result":{"stopReason":"end_turn"}}\n' "$id";;
esac
done
"#;
const UNCREDENTIALED_STUB: &str = r#"
while read line; do
id=$(echo "$line" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
case "$line" in
*initialize*) printf '{"jsonrpc":"2.0","id":"%s","result":{"protocolVersion":1}}\n' "$id";;
*session/new*) printf '{"jsonrpc":"2.0","id":"%s","result":{"sessionId":"u1"}}\n' "$id";;
*session/prompt*)
curl -s -X POST -H 'content-type: application/json' \
-d "{\"model\":\"$STUB_MODEL\"}" \
"$STUB_BASE_URL/v1/chat/completions" > /dev/null 2>&1
printf '{"jsonrpc":"2.0","id":"%s","result":{"stopReason":"end_turn"}}\n' "$id";;
esac
done
"#;
fn stub_harness(script: &'static str, protocol_version: u32) -> Harness {
Harness {
id: "conformance-stub",
acp_protocol_version: protocol_version,
description: "stub",
project_url: "https://example.invalid/",
acp_command: Some("bash"),
acp_args: std::boxed::Box::leak(std::boxed::Box::new(["-c", script])),
package_marker: "conformance-stub",
interactive_binary: None,
routing: Routing::Env {
base_url_env: "STUB_BASE_URL",
auth_env: "STUB_AUTH",
bearer_auth: true,
model_env: Some("STUB_MODEL"),
extra: &[],
},
}
}
fn outcome_for(report: &Report, tier: Tier) -> Outcome {
report
.tiers
.iter()
.find(|result| result.tier == tier)
.map(|result| result.outcome.clone())
.unwrap_or(Outcome::Fail {
reason: "tier missing from the report".to_string(),
})
}
#[tokio::test]
async fn a_routed_agent_passes_both_tiers() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let report = run(&stub_harness(ROUTED_STUB, 1), "local/stub", dir.path()).await;
assert_eq!(
outcome_for(&report, Tier::Handshake),
Outcome::Pass,
"{report:?}"
);
assert_eq!(
outcome_for(&report, Tier::Routability),
Outcome::Pass,
"{report:?}"
);
assert!(report.passed());
Ok(())
}
#[tokio::test]
async fn traffic_without_the_gateway_credential_is_not_routable() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let report = run(
&stub_harness(UNCREDENTIALED_STUB, 1),
"local/stub",
dir.path(),
)
.await;
assert_eq!(outcome_for(&report, Tier::Handshake), Outcome::Pass);
let routability = outcome_for(&report, Tier::Routability);
assert!(
matches!(&routability, Outcome::Fail { reason } if reason.contains("credential")),
"{routability:?}"
);
assert!(!report.passed());
Ok(())
}
#[tokio::test]
async fn a_mismatched_protocol_version_fails_the_handshake() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let report = run(&stub_harness(ROUTED_STUB, 2), "local/stub", dir.path()).await;
let handshake = outcome_for(&report, Tier::Handshake);
assert!(
matches!(&handshake, Outcome::Fail { reason } if reason.contains("protocol version")),
"{handshake:?}"
);
Ok(())
}
#[test]
fn the_registry_block_is_what_a_contributor_pastes() {
let report = Report {
agent: "local/opencode".to_string(),
suite: SUITE,
suite_version: SUITE_VERSION,
agent_version: Some("1.17.15".to_string()),
measured_by: "bitrouter",
as_of: "2026-09-06".to_string(),
tiers: vec![
TierResult {
tier: Tier::Handshake,
outcome: Outcome::Pass,
duration_ms: 10,
},
TierResult {
tier: Tier::Routability,
outcome: Outcome::Pass,
duration_ms: 20,
},
],
};
let block = report.registry_block();
assert!(block.contains(" acp_compat_1:\n"), "{block}");
assert!(block.contains(" handshake: pass\n"), "{block}");
assert!(block.contains(" routability: pass\n"), "{block}");
assert!(
block.contains(" agent_version: 1.17.15\n"),
"{block}"
);
assert!(
block.contains(" measured_by: bitrouter\n"),
"{block}"
);
assert!(!block.contains("lifecycle"), "{block}");
}
}