acp-http-adapter 0.2.1

Minimal ACP HTTP-to-stdio adapter
Documentation
use std::collections::HashMap;
use std::path::PathBuf;

use serde::Deserialize;
use serde_json::Value;
use thiserror::Error;

#[derive(Debug, Clone)]
pub struct LaunchSpec {
    pub program: PathBuf,
    pub args: Vec<String>,
    pub env: HashMap<String, String>,
}

#[derive(Debug, Error)]
pub enum RegistryError {
    #[error("invalid registry json: {0}")]
    InvalidJson(#[from] serde_json::Error),
    #[error("unable to resolve registry entry from blob")]
    UnsupportedBlob,
    #[error("registry blob has agents[] but no --registry-agent-id was provided")]
    MissingAgentId,
    #[error("agent '{0}' was not found in registry blob")]
    AgentNotFound(String),
    #[error("registry entry has no supported launch target")]
    MissingLaunchTarget,
    #[error("platform '{0}' is not present in distribution.binary")]
    UnsupportedPlatform(String),
}

impl LaunchSpec {
    pub fn from_registry_blob(blob: &str, agent_id: Option<&str>) -> Result<Self, RegistryError> {
        let value: Value = serde_json::from_str(blob)?;
        Self::from_registry_value(value, agent_id)
    }

    fn from_registry_value(value: Value, agent_id: Option<&str>) -> Result<Self, RegistryError> {
        if value.get("agents").is_some() {
            let doc: RegistryDocument = serde_json::from_value(value)?;
            let wanted = agent_id.ok_or(RegistryError::MissingAgentId)?;
            let agent = doc
                .agents
                .into_iter()
                .find(|a| a.id == wanted)
                .ok_or_else(|| RegistryError::AgentNotFound(wanted.to_string()))?;
            return Self::from_distribution(agent.distribution);
        }

        if value.get("distribution").is_some() {
            let entry: RegistryAgent = serde_json::from_value(value)?;
            return Self::from_distribution(entry.distribution);
        }

        if value.get("npx").is_some() || value.get("binary").is_some() {
            let distribution: RegistryDistribution = serde_json::from_value(value)?;
            return Self::from_distribution(distribution);
        }

        Err(RegistryError::UnsupportedBlob)
    }

    fn from_distribution(distribution: RegistryDistribution) -> Result<Self, RegistryError> {
        if let Some(npx) = distribution.npx {
            let mut args = vec!["-y".to_string(), npx.package];
            args.extend(npx.args);
            return Ok(Self {
                program: PathBuf::from("npx"),
                args,
                env: npx.env,
            });
        }

        if let Some(binary) = distribution.binary {
            let platform = platform_key().ok_or(RegistryError::UnsupportedPlatform(format!(
                "{}/{}",
                std::env::consts::OS,
                std::env::consts::ARCH
            )))?;
            let target = binary
                .get(platform)
                .ok_or_else(|| RegistryError::UnsupportedPlatform(platform.to_string()))?;
            return Ok(Self {
                program: PathBuf::from(&target.cmd),
                args: target.args.clone(),
                env: target.env.clone(),
            });
        }

        Err(RegistryError::MissingLaunchTarget)
    }
}

fn platform_key() -> Option<&'static str> {
    match (std::env::consts::OS, std::env::consts::ARCH) {
        ("linux", "x86_64") => Some("linux-x86_64"),
        ("linux", "aarch64") => Some("linux-aarch64"),
        ("macos", "x86_64") => Some("darwin-x86_64"),
        ("macos", "aarch64") => Some("darwin-aarch64"),
        ("windows", "x86_64") => Some("windows-x86_64"),
        ("windows", "aarch64") => Some("windows-aarch64"),
        _ => None,
    }
}

#[derive(Debug, Deserialize)]
struct RegistryDocument {
    agents: Vec<RegistryAgent>,
}

#[derive(Debug, Deserialize)]
struct RegistryAgent {
    #[allow(dead_code)]
    id: String,
    distribution: RegistryDistribution,
}

#[derive(Debug, Deserialize)]
struct RegistryDistribution {
    #[serde(default)]
    npx: Option<RegistryNpx>,
    #[serde(default)]
    binary: Option<HashMap<String, RegistryBinaryTarget>>,
}

#[derive(Debug, Deserialize)]
struct RegistryNpx {
    package: String,
    #[serde(default)]
    args: Vec<String>,
    #[serde(default)]
    env: HashMap<String, String>,
}

#[derive(Debug, Deserialize)]
struct RegistryBinaryTarget {
    #[allow(dead_code)]
    archive: Option<String>,
    cmd: String,
    #[serde(default)]
    args: Vec<String>,
    #[serde(default)]
    env: HashMap<String, String>,
}