supercode-harness 0.5.45

The optional native Volter Harness agent and tool harness
Documentation
//! ORCH-15 (observed tier): the routing noun — which profile / agent a
//! surface tuple resolves to.
//!
//! * **Hermes** — `gateway.profile_routes` in `HERMES_HOME/config.yaml`: a list
//!   of `{platform, guild_id?, chat_id?, thread_id?, profile}` entries, most
//!   specific wins (thread 8 > chat 4 > guild 2 > platform 0), the default
//!   profile otherwise (`docs/HERMES-IDEAL-SUPPORT-DESIGN.md` §1).
//! * **OpenClaw** — `bindings[]` in `openclaw.json`: `{agentId, match{channel,
//!   accountId, peer{kind,id}, guildId, teamId, roles}}`, evaluated exact peer →
//!   parent peer → wildcard peer → guild+roles → guild → team → account →
//!   channel → default agent (§1b).
//!
//! Read-only. Editing a route stays the harness's own config edit.

use std::path::Path;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::HarnessId;
use supercode_interchange::catalog::HarnessHomes;

/// Wire schema of `harness.v1.routes.list`.
pub const ROUTES_SCHEMA: &str = "supercode.routes.v1";

/// Harnesses with a routing concept.
pub const ROUTE_HARNESSES: &[&str] = &[
    HarnessId::HERMES,
    HarnessId::OPENCLAW,
    HarnessId::ORCHESTRATOR,
];

/// The match side of a route, in the shared-noun vocabulary.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteMatch {
    /// Transport / platform (`slack`, `telegram`, …), or `None` for a catch-all.
    #[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>,
    /// Chat / channel / group id, or the peer id (OpenClaw).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub chat_id: Option<String>,
    /// OpenClaw peer kind (`user` | `channel` | `group` | `thread`).
    #[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>,
}

/// One routing entry.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouteRow {
    pub harness: String,
    /// Target profile (Hermes) or agent id (OpenClaw).
    pub target: String,
    #[serde(rename = "match")]
    pub matcher: RouteMatch,
    /// The harness's own precedence rank; higher wins.
    pub specificity: u32,
    /// The fallback route (no match fields).
    pub default: bool,
    /// Config file the route was read from.
    pub source: String,
}

/// Why a listing was refused.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RouteError {
    /// The harness has no routing concept.
    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 {}

/// List routes, optionally for one harness and/or one target.
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)
}

/// Read `gateway.profile_routes` out of one Hermes-shaped home.
///
/// `home` is the folder holding `config.yaml` (Hermes: HERMES_HOME, the
/// parent of `HarnessHomes::hermes`; the orchestrator: one profile folder).
/// `with_default` emits the catch-all row for the home that owns unmatched
/// traffic.
/// The `profile_routes` of a Hermes-shaped home as the orchestration codec
/// reads them: every profile's routes (the root's for Hermes, each folder's
/// for the orchestrator) weighted by what they name, plus the root's
/// catch-all row.
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
}

/// OpenClaw's `bindings` as the orchestration codec reads them (the match
/// vocabulary the orchestration does not model — account, team, peer kind,
/// roles — rides in the route's residue), weighted by the documented
/// cascade, plus the default agent's catch-all row.
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
}