use std::path::Path;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::catalog::HarnessHomes;
use crate::HarnessId;
pub const ROUTES_SCHEMA: &str = "supercode.routes.v1";
pub const ROUTE_HARNESSES: &[&str] = &[
HarnessId::HERMES,
HarnessId::OPENCLAW,
HarnessId::ORCHESTRATOR,
];
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteMatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guild: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub team: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub roles: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteRow {
pub harness: String,
pub target: String,
#[serde(rename = "match")]
pub matcher: RouteMatch,
pub specificity: u32,
pub default: bool,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RouteError {
UnsupportedHarness { harness: String },
}
impl std::fmt::Display for RouteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RouteError::UnsupportedHarness { harness } => write!(
f,
"`{harness}` has no routing concept; `routes.list` is supported for: {}",
ROUTE_HARNESSES.join(", ")
),
}
}
}
impl std::error::Error for RouteError {}
pub fn list_routes(
homes: &HarnessHomes,
harness: Option<&str>,
target: Option<&str>,
) -> Result<Vec<RouteRow>, RouteError> {
let harnesses: Vec<&str> = match harness {
Some(id) if ROUTE_HARNESSES.contains(&id) => vec![id],
Some(id) => {
return Err(RouteError::UnsupportedHarness {
harness: id.to_string(),
})
}
None => ROUTE_HARNESSES.to_vec(),
};
use supercode_interchange::orchestration::codec::{
from_hermes, from_openclaw, load_home, Flavor,
};
let mut rows = Vec::new();
for id in harnesses {
match id {
HarnessId::HERMES => {
if let Ok(loaded) = from_hermes(homes.hermes.parent().unwrap_or(Path::new("."))) {
rows.extend(hermes_shaped_rows(HarnessId::HERMES, &loaded.orchestration));
}
}
HarnessId::OPENCLAW => {
if let Ok(loaded) = from_openclaw(&homes.openclaw) {
rows.extend(openclaw_rows(&loaded));
}
}
HarnessId::ORCHESTRATOR => {
if let Ok(loaded) = load_home(&homes.orchestrator, Flavor::Orchestrator) {
rows.extend(hermes_shaped_rows(
HarnessId::ORCHESTRATOR,
&loaded.orchestration,
));
}
}
_ => {}
}
}
if let Some(target) = target {
rows.retain(|row| row.target == target);
}
rows.sort_by(|a, b| {
a.harness
.cmp(&b.harness)
.then(b.specificity.cmp(&a.specificity))
.then(a.target.cmp(&b.target))
});
Ok(rows)
}
fn hermes_shaped_rows(
harness: &str,
orchestration: &supercode_interchange::orchestration::Orchestration,
) -> Vec<RouteRow> {
let mut rows = Vec::new();
let mut names: Vec<&String> = orchestration.profiles.keys().collect();
names.sort_by_key(|name| (name.as_str() != "default", name.as_str()));
for name in names {
let profile = &orchestration.profiles[name];
let source = profile.dir.join("config.yaml").display().to_string();
for route in &profile.routes {
let matcher = RouteMatch {
platform: Some(route.matches.platform.clone()).filter(|p| !p.is_empty()),
guild: route.matches.guild_id.clone(),
chat_id: route.matches.chat_id.clone(),
thread_id: route.matches.thread_id.clone(),
..RouteMatch::default()
};
let specificity = matcher.thread_id.as_ref().map_or(0, |_| 8)
+ matcher.chat_id.as_ref().map_or(0, |_| 4)
+ matcher.guild.as_ref().map_or(0, |_| 2);
rows.push(RouteRow {
harness: harness.into(),
target: route.profile.clone(),
matcher,
specificity,
default: false,
source: source.clone(),
});
}
if name == "default" {
rows.push(RouteRow {
harness: harness.into(),
target: "default".into(),
matcher: RouteMatch::default(),
specificity: 0,
default: true,
source,
});
}
}
rows
}
fn openclaw_rows(
loaded: &supercode_interchange::orchestration::codec::OpenclawLoaded,
) -> Vec<RouteRow> {
let source = loaded
.root
.state_dir
.join("openclaw.json")
.display()
.to_string();
let mut routes: Vec<_> = loaded
.orchestration
.profiles
.values()
.flat_map(|profile| profile.routes.iter())
.collect();
routes.sort_by_key(|route| route.residue.0.get("index").and_then(Value::as_u64));
let mut rows = Vec::new();
for route in routes {
let residue = &route.residue.0;
let m = residue.get("match").and_then(Value::as_object);
let text = |key: &str| {
m.and_then(|m| m.get(key)).and_then(|v| match v {
Value::String(s) => Some(s.clone()),
Value::Number(n) => Some(n.to_string()),
_ => None,
})
};
let peer_kind = m
.and_then(|m| m.get("peer"))
.and_then(|p| p.get("kind"))
.and_then(Value::as_str)
.map(str::to_string);
let roles: Vec<String> = m
.and_then(|m| m.get("roles"))
.and_then(Value::as_array)
.map(|list| {
list.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
let guild = route.matches.guild_id.clone();
let team = text("teamId");
let account = text("accountId");
let channel = Some(route.matches.platform.clone()).filter(|p| !p.is_empty());
let peer_id = route.matches.chat_id.clone();
let specificity = match (&peer_id, &peer_kind) {
(Some(id), _) if id == "*" => 6,
(Some(_), Some(kind)) if kind == "parent" => 7,
(Some(_), _) => 8,
_ if guild.is_some() && !roles.is_empty() => 5,
_ if guild.is_some() => 4,
_ if team.is_some() => 3,
_ if account.is_some() => 2,
_ if channel.is_some() => 1,
_ => 0,
};
let target = residue
.get("agent_id")
.and_then(Value::as_str)
.map(str::to_string)
.or_else(|| {
loaded
.profiles
.get(&route.profile)
.map(|io| io.agent_id.clone())
})
.unwrap_or_else(|| route.profile.clone());
rows.push(RouteRow {
harness: HarnessId::OPENCLAW.into(),
target,
matcher: RouteMatch {
platform: channel,
account,
guild,
team,
chat_id: peer_id,
peer_kind,
thread_id: None,
roles,
},
specificity,
default: false,
source: source.clone(),
});
}
rows.push(RouteRow {
harness: HarnessId::OPENCLAW.into(),
target: loaded.root.default_agent.clone(),
matcher: RouteMatch::default(),
specificity: 0,
default: true,
source,
});
rows
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(tag: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"supercode-routes-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn orchestrator_routes_are_read_per_profile_folder_with_one_default() {
let dir = scratch("orchestrator");
std::fs::create_dir_all(dir.join("profiles/ops")).unwrap();
std::fs::write(
dir.join("config.yaml"),
"gateway:\n profile_routes:\n - platform: slack\n chat_id: C1\n profile: ops\n",
)
.unwrap();
std::fs::write(
dir.join("profiles/ops/config.yaml"),
"gateway:\n profile_routes:\n - platform: telegram\n profile: ops\n",
)
.unwrap();
let homes = HarnessHomes {
orchestrator: dir.clone(),
..HarnessHomes::default()
};
let rows = list_routes(&homes, Some(HarnessId::ORCHESTRATOR), None).unwrap();
assert!(
rows.iter()
.all(|row| row.harness == HarnessId::ORCHESTRATOR),
"{rows:?}"
);
assert_eq!(rows.iter().filter(|row| row.default).count(), 1, "{rows:?}");
assert_eq!(
rows.iter().filter(|row| row.target == "ops").count(),
2,
"{rows:?}"
);
assert_eq!(rows[0].specificity, 4);
assert_eq!(rows[0].matcher.chat_id.as_deref(), Some("C1"));
assert!(
rows.iter()
.any(|row| row.matcher.platform.as_deref() == Some("telegram")
&& row.specificity == 0)
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn openclaw_bindings_follow_the_documented_cascade() {
let dir = scratch("openclaw");
std::fs::write(
dir.join("openclaw.json"),
r#"{ "agents": { "list": [ { "id": "main", "default": true }, { "id": "design" } ] },
"bindings": [
{ "type": "route", "agentId": "design", "match": { "channel": "slack" } },
{ "type": "route", "agentId": "ops", "match": { "channel": "discord", "guildId": "G1", "roles": ["admin"] } },
{ "type": "route", "agentId": "vip", "match": { "channel": "telegram", "peer": { "kind": "user", "id": "U1" } } }
] }"#,
)
.unwrap();
let rows = openclaw_rows(&dir);
let spec: Vec<(String, u32)> = rows
.iter()
.map(|r| (r.target.clone(), r.specificity))
.collect();
assert_eq!(
spec,
vec![
("design".into(), 1),
("ops".into(), 5),
("vip".into(), 8),
("main".into(), 0)
]
);
assert!(rows[3].default);
}
#[test]
fn unsupported_harness_is_refused() {
let err = list_routes(&HarnessHomes::default(), Some("codex"), None).unwrap_err();
assert!(err.to_string().contains("routes.list"));
}
}