use std::collections::HashMap;
use std::sync::RwLock;
use std::time::Duration;
use chrono::{DateTime, Utc};
pub const DEFAULT_ROUTE_LEASE_TTL: Duration = Duration::from_secs(12 * 60 * 60);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RouteLease {
lease_id: String,
api_principal: String,
controller_instance_id: String,
session_id: String,
route: String,
expires_at: DateTime<Utc>,
}
impl RouteLease {
pub fn lease_id(&self) -> &str {
&self.lease_id
}
pub fn api_principal(&self) -> &str {
&self.api_principal
}
pub fn controller_instance_id(&self) -> &str {
&self.controller_instance_id
}
pub fn matched_session_id(&self) -> &str {
&self.session_id
}
pub fn route(&self) -> &str {
&self.route
}
pub fn expires_at(&self) -> DateTime<Utc> {
self.expires_at
}
}
type RouteKey = (String, String, String);
#[derive(Default)]
struct RuntimeState {
leases: HashMap<RouteKey, RouteLease>,
}
pub struct AcpRuntime {
state: RwLock<RuntimeState>,
lease_ttl: Duration,
}
impl Default for AcpRuntime {
fn default() -> Self {
Self::with_lease_ttl(DEFAULT_ROUTE_LEASE_TTL)
}
}
impl AcpRuntime {
pub fn new() -> Self {
Self::default()
}
pub fn with_lease_ttl(lease_ttl: Duration) -> Self {
Self {
state: RwLock::new(RuntimeState::default()),
lease_ttl,
}
}
pub fn set_route(
&self,
api_principal: &str,
controller_instance_id: &str,
session_id: &str,
route: &str,
) -> Result<RouteLease, String> {
let api_principal = api_principal.trim();
let controller_instance_id = controller_instance_id.trim();
let session_id = session_id.trim();
let route = route.trim();
if api_principal.is_empty()
|| controller_instance_id.is_empty()
|| session_id.is_empty()
|| route.is_empty()
{
return Err(
"API principal, controller id, session id, and route must not be empty".to_string(),
);
}
let ttl = chrono::Duration::from_std(self.lease_ttl)
.map_err(|error| format!("route lease ttl is out of range: {error}"))?;
let now = Utc::now();
let lease = RouteLease {
lease_id: format!("brlease_{}", uuid::Uuid::new_v4().simple()),
api_principal: api_principal.to_string(),
controller_instance_id: controller_instance_id.to_string(),
session_id: session_id.to_string(),
route: route.to_string(),
expires_at: now + ttl,
};
let mut state = self.write_state();
cleanup_expired(&mut state, now);
state.leases.insert(
route_key(api_principal, controller_instance_id, session_id),
lease.clone(),
);
Ok(lease)
}
pub fn reset_route(
&self,
api_principal: &str,
controller_instance_id: &str,
session_id: &str,
) -> Option<RouteLease> {
let mut state = self.write_state();
cleanup_expired(&mut state, Utc::now());
state.leases.remove(&route_key(
api_principal,
controller_instance_id,
session_id,
))
}
pub fn remove_controller(&self, api_principal: &str, controller_instance_id: &str) {
let mut state = self.write_state();
cleanup_expired(&mut state, Utc::now());
state.leases.retain(|(principal, controller, _), _| {
principal != api_principal || controller != controller_instance_id
});
}
pub fn resolve_route(
&self,
api_principal: &str,
controller_instance_id: &str,
session_candidates: &[&str],
) -> Option<RouteLease> {
let mut state = self.write_state();
cleanup_expired(&mut state, Utc::now());
session_candidates.iter().find_map(|session_id| {
state
.leases
.get(&route_key(
api_principal,
controller_instance_id,
session_id,
))
.cloned()
})
}
pub fn current_route(
&self,
api_principal: &str,
controller_instance_id: &str,
session_id: &str,
) -> Option<RouteLease> {
self.resolve_route(api_principal, controller_instance_id, &[session_id])
}
fn write_state(&self) -> std::sync::RwLockWriteGuard<'_, RuntimeState> {
match self.state.write() {
Ok(state) => state,
Err(poisoned) => poisoned.into_inner(),
}
}
}
fn route_key(api_principal: &str, controller_instance_id: &str, session_id: &str) -> RouteKey {
(
api_principal.to_string(),
controller_instance_id.to_string(),
session_id.to_string(),
)
}
fn cleanup_expired(state: &mut RuntimeState, now: DateTime<Utc>) {
state.leases.retain(|_, lease| lease.expires_at > now);
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::AcpRuntime;
#[test]
fn route_leases_are_isolated_by_api_principal_and_controller_claim() {
let runtime = AcpRuntime::new();
runtime
.set_route(
"principal-a",
"controller-a",
"root",
"anthropic:claude-sonnet",
)
.expect("principal a lease");
runtime
.set_route("principal-a", "controller-b", "root", "openai:gpt-5")
.expect("controller b lease");
runtime
.set_route("principal-b", "controller-a", "root", "google:gemini-3")
.expect("principal b lease");
assert_eq!(
runtime
.current_route("principal-a", "controller-a", "root")
.expect("principal a controller a route")
.route(),
"anthropic:claude-sonnet"
);
assert_eq!(
runtime
.current_route("principal-a", "controller-b", "root")
.expect("principal a controller b route")
.route(),
"openai:gpt-5"
);
assert_eq!(
runtime
.current_route("principal-b", "controller-a", "root")
.expect("principal b controller a route")
.route(),
"google:gemini-3"
);
assert!(
runtime
.current_route("principal-b", "controller-b", "root")
.is_none()
);
}
#[test]
fn routes_follow_ordered_native_candidates_and_cleanup_one_controller() {
let runtime = AcpRuntime::new();
runtime
.set_route("principal", "controller", "root", "anthropic:claude-sonnet")
.expect("root lease");
runtime
.set_route("principal", "controller", "child", "openai:gpt-5")
.expect("child lease");
runtime
.set_route("principal", "other", "root", "google:gemini-3")
.expect("other controller lease");
let exact = runtime
.resolve_route("principal", "controller", &["child", "root"])
.expect("exact child lease");
assert_eq!(exact.matched_session_id(), "child");
assert_eq!(exact.route(), "openai:gpt-5");
runtime.reset_route("principal", "controller", "child");
assert_eq!(
runtime
.resolve_route("principal", "controller", &["child", "root"])
.expect("root fallback")
.route(),
"anthropic:claude-sonnet"
);
runtime.remove_controller("principal", "controller");
assert!(
runtime
.current_route("principal", "controller", "root")
.is_none()
);
assert!(
runtime
.current_route("principal", "other", "root")
.is_some()
);
}
#[test]
fn route_lease_expiry_is_independent_per_lease() {
let runtime = AcpRuntime::with_lease_ttl(Duration::ZERO);
let lease = runtime
.set_route("principal", "controller", "session", "openai:gpt-5")
.expect("lease can be installed");
assert_eq!(lease.api_principal(), "principal");
assert_eq!(lease.controller_instance_id(), "controller");
assert!(
runtime
.current_route("principal", "controller", "session")
.is_none(),
"zero-TTL lease is removed on the next lookup"
);
}
#[test]
fn same_principal_can_deliberately_reuse_an_exact_claim_namespace() {
let runtime = AcpRuntime::new();
runtime
.set_route("shared", "controller", "session", "first")
.expect("first lease");
runtime
.set_route("shared", "controller", "session", "second")
.expect("replacement lease");
assert_eq!(
runtime
.current_route("shared", "controller", "session")
.expect("shared namespace route")
.route(),
"second"
);
}
}