use std::collections::BTreeMap;
use std::env;
use axum::extract::{Path, State};
use axum::response::IntoResponse;
use axum::Json;
use embacle::config::CliRunnerType;
use embacle::discovery::resolve_binary;
use serde::{Deserialize, Serialize};
use super::error::UhpFailure;
use super::state::UhpState;
use crate::runner::ALL_PROVIDERS;
use crate::state::SharedState;
#[derive(Debug, Clone, Serialize)]
pub struct Harness {
pub id: String,
pub object: &'static str,
pub name: String,
pub base: String,
#[serde(rename = "defaultModel", skip_serializing_if = "Option::is_none")]
pub default_model: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<Skill>,
#[serde(rename = "mcpServers", default, skip_serializing_if = "Vec::is_empty")]
pub mcp_servers: Vec<McpServer>,
#[serde(
rename = "disabledTools",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub disabled_tools: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillFile {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_b64: Option<String>,
#[serde(flatten)]
pub rest: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
pub name: String,
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub files: Vec<SkillFile>,
}
impl Skill {
pub const MANIFEST: &'static str = "SKILL.md";
#[must_use]
pub fn has_manifest(&self) -> bool {
self.files.iter().any(|f| f.path == Self::MANIFEST)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServer {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transport: Option<String>,
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct HarnessList {
pub harnesses: Vec<Harness>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Model {
pub id: String,
pub backend: String,
pub available: bool,
pub default: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct BackendModels {
pub default: String,
pub models: Vec<Model>,
}
#[derive(Debug, Clone, Serialize)]
pub struct HarnessModels {
pub harness_id: String,
pub backend: String,
pub default: String,
pub models: Vec<Model>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ModelCatalog {
pub backends: BTreeMap<String, BackendModels>,
}
fn harness_id(provider: &str) -> String {
let slug: String = provider
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
format!("chrn_{slug}")
}
fn installed(provider: CliRunnerType) -> bool {
let env_override = env::var(provider.env_override_key()).ok();
resolve_binary(provider.binary_name(), env_override.as_deref()).is_ok()
}
pub struct Discovered {
pub provider: CliRunnerType,
pub name: String,
pub harness: Harness,
pub models: Vec<String>,
}
pub async fn discover(state: &SharedState) -> Vec<Discovered> {
let mut found = Vec::new();
for &provider in ALL_PROVIDERS.iter() {
if !installed(provider) {
continue;
}
let Ok(runner) = state.get_runner(provider).await else {
continue;
};
let name = runner.name().to_owned();
let models = runner.available_models().to_vec();
let default_model = runner.default_model().to_owned();
found.push(Discovered {
provider,
harness: Harness {
id: harness_id(&name),
object: "harness",
name: name.clone(),
base: name.clone(),
default_model: Some(default_model),
skills: Vec::new(),
mcp_servers: Vec::new(),
disabled_tools: Vec::new(),
},
name,
models,
});
}
found
}
pub async fn list(State(state): State<SharedState>) -> impl IntoResponse {
let harnesses = discover(&state)
.await
.into_iter()
.map(|d| d.harness)
.collect();
Json(HarnessList { harnesses })
}
pub async fn get(
State(state): State<UhpState>,
Path(id): Path<String>,
) -> Result<Json<Harness>, UhpFailure> {
if let Some(configured) = state.configured.get(&id) {
return Ok(Json(configured));
}
discover(&state.shared)
.await
.into_iter()
.find(|d| d.harness.id == id)
.map(|d| Json(d.harness))
.ok_or_else(|| {
UhpFailure::not_found("harness_not_found", "no harness with that id is configured")
})
}
fn catalogue(found: Vec<Discovered>) -> ModelCatalog {
let mut backends = BTreeMap::new();
for d in found {
let name = d.name;
let default = d.harness.default_model.unwrap_or_default();
let models = d
.models
.into_iter()
.map(|id| Model {
default: id == default,
id,
backend: name.clone(),
available: true,
})
.collect();
backends.insert(name, BackendModels { default, models });
}
ModelCatalog { backends }
}
pub async fn models(State(state): State<UhpState>) -> impl IntoResponse {
Json(catalogue(discover(&state.shared).await))
}
pub async fn harness_models(
State(state): State<UhpState>,
Path(id): Path<String>,
) -> Result<Json<HarnessModels>, UhpFailure> {
let d = discover(&state.shared)
.await
.into_iter()
.find(|d| d.harness.id == id)
.ok_or_else(|| {
UhpFailure::not_found("harness_not_found", "no harness with that id is configured")
})?;
let name = d.name;
let default = d.harness.default_model.unwrap_or_default();
Ok(Json(HarnessModels {
harness_id: d.harness.id,
backend: name.clone(),
models: d
.models
.into_iter()
.map(|id| Model {
default: id == default,
id,
backend: name.clone(),
available: true,
})
.collect(),
default,
}))
}
pub async fn unknown_route() -> UhpFailure {
UhpFailure::not_found("not_found", "no such endpoint on this server")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_harness_id_satisfies_the_schema_pattern() {
assert!(harness_id("claude_code").starts_with("chrn_"));
assert_eq!(harness_id("claude-code"), "chrn_claude_code");
assert_eq!(harness_id("copilot"), "chrn_copilot");
}
#[test]
fn the_catalogue_groups_by_backend_and_marks_one_default() {
let found = vec![Discovered {
provider: CliRunnerType::Copilot,
name: "copilot".to_owned(),
harness: Harness {
id: harness_id("copilot"),
object: "harness",
name: "copilot".to_owned(),
base: "copilot".to_owned(),
default_model: Some("gpt-5".to_owned()),
skills: Vec::new(),
mcp_servers: Vec::new(),
disabled_tools: Vec::new(),
},
models: vec!["gpt-5".to_owned(), "claude-sonnet".to_owned()],
}];
let cat = catalogue(found);
let backend = cat.backends.get("copilot").expect("backend present"); assert_eq!(backend.default, "gpt-5");
assert_eq!(backend.models.len(), 2);
assert_eq!(
backend.models.iter().filter(|m| m.default).count(),
1,
"exactly one model is the default"
);
assert!(
backend.models.iter().all(|m| m.available),
"availability is a boolean on every model, never absent"
);
}
}