slicc-discovery 1.42.2

Discovery engine for agent-facing web standards
Documentation
use serde_json::{json, Map, Value};

use crate::classifier::{Classification, Validity};

const KNOWN_VERSIONS: &[&str] = &["2026-01-23", "2026-04-08", "2026-08-25"];
const TRANSPORTS: &[&str] = &["rest", "mcp", "a2a", "embedded"];
const LIST_CAP: usize = 50;
const URL_CAP: usize = 500;
const NAME_CAP: usize = 200;

fn invalid(reason: impl Into<String>) -> Classification {
    Classification {
        standard: "ucp".into(),
        status: Validity::Invalid,
        summary: Value::Null,
        reason: Some(reason.into()),
    }
}

fn is_date(text: &str) -> bool {
    let bytes = text.as_bytes();
    bytes.len() == 10
        && bytes.iter().enumerate().all(|(i, b)| match i {
            4 | 7 => *b == b'-',
            _ => b.is_ascii_digit(),
        })
}

fn capped(text: &str, max: usize) -> String {
    let mut end = text.len().min(max);
    while !text.is_char_boundary(end) {
        end -= 1;
    }
    text[..end].to_string()
}

fn clean_url(text: &str) -> String {
    let stripped = match url::Url::parse(text) {
        Ok(mut url) => {
            let _ = url.set_username("");
            let _ = url.set_password(None);
            url.to_string()
        }
        Err(_) => text.to_string(),
    };
    capped(&stripped, URL_CAP)
}

fn names_ap2(name: &str) -> bool {
    name.split(['.', '/']).any(|segment| segment.starts_with("ap2"))
}

fn check_service(name: &str, index: usize, entry: &Value) -> Result<(String, Option<String>), String> {
    let at = format!("services.{name}[{index}]");
    for key in ["version", "spec", "transport"] {
        if entry.get(key).and_then(Value::as_str).is_none() {
            return Err(format!("{at} is missing {key}"));
        }
    }
    let transport = entry["transport"].as_str().unwrap_or("").to_string();
    if !TRANSPORTS.contains(&transport.as_str()) {
        return Err(format!("{at} has unknown transport {}", capped(&transport, NAME_CAP)));
    }
    let endpoint = entry.get("endpoint").and_then(Value::as_str).map(clean_url);
    let schema = entry.get("schema").and_then(Value::as_str);
    if endpoint.is_none() && (transport != "embedded" || schema.is_none()) {
        return Err(format!("{at} has no endpoint"));
    }
    Ok((transport, endpoint))
}

fn named_entries(value: &Value) -> Vec<(&String, &Value)> {
    value.as_object().map(|map| map.iter().take(LIST_CAP).collect()).unwrap_or_default()
}

pub fn classify_ucp(body: &str) -> Classification {
    let value: Value = match serde_json::from_str(body) {
        Ok(value) => value,
        Err(err) => return invalid(err.to_string()),
    };
    let Some(ucp) = value.get("ucp").filter(|v| v.is_object()) else {
        return invalid("missing ucp");
    };
    let version = match ucp.get("version").and_then(Value::as_str) {
        Some(version) if is_date(version) => version.to_string(),
        Some(_) => return invalid("ucp.version is not YYYY-MM-DD"),
        None => return invalid("missing ucp.version"),
    };
    let Some(services) = ucp.get("services").filter(|v| v.is_object()) else {
        return invalid("missing ucp.services");
    };
    let Some(handlers) = ucp.get("payment_handlers").filter(|v| v.is_object()) else {
        return invalid("missing ucp.payment_handlers");
    };
    let mut service_summary = Map::new();
    let mut mcp = Vec::new();
    let mut a2a = Vec::new();
    for (name, entries) in named_entries(services) {
        let Some(entries) = entries.as_array() else {
            return invalid(format!("services.{} is not a list", capped(name, NAME_CAP)));
        };
        let mut listed = Vec::new();
        for (index, entry) in entries.iter().take(LIST_CAP).enumerate() {
            let (transport, endpoint) = match check_service(&capped(name, NAME_CAP), index, entry) {
                Ok(found) => found,
                Err(reason) => return invalid(reason),
            };
            if let Some(url) = &endpoint {
                if transport == "mcp" && mcp.len() < LIST_CAP {
                    mcp.push(Value::String(url.clone()));
                }
                if transport == "a2a" && a2a.len() < LIST_CAP {
                    a2a.push(Value::String(url.clone()));
                }
            }
            listed.push(json!({ "transport": transport, "endpoint": endpoint }));
        }
        service_summary.insert(capped(name, NAME_CAP), Value::Array(listed));
    }
    let mut capabilities = Vec::new();
    let mut ap2 = false;
    for (name, entries) in named_entries(ucp.get("capabilities").unwrap_or(&Value::Null)) {
        ap2 |= names_ap2(name);
        let versions: Vec<&str> = entries
            .as_array()
            .map(|list| list.iter().filter_map(|e| e.get("version").and_then(Value::as_str)).collect())
            .unwrap_or_default();
        if versions.is_empty() {
            capabilities.push(Value::String(capped(name, NAME_CAP)));
        }
        for v in versions {
            if capabilities.len() < LIST_CAP {
                capabilities.push(Value::String(format!("{}@{}", capped(name, NAME_CAP), capped(v, NAME_CAP))));
            }
        }
    }
    let payment_handlers: Vec<Value> = named_entries(handlers)
        .into_iter()
        .map(|(name, _)| {
            ap2 |= names_ap2(name);
            Value::String(capped(name, NAME_CAP))
        })
        .collect();
    let has_keys = value.get("keys").and_then(Value::as_array).is_some_and(|keys| !keys.is_empty());
    let mut summary = json!({
        "version": version,
        "services": service_summary,
        "capabilities": capabilities,
        "paymentHandlers": payment_handlers,
        "hasKeys": has_keys,
        "ap2": ap2,
        "mcp": mcp,
        "a2a": a2a,
    });
    if !KNOWN_VERSIONS.contains(&version.as_str()) {
        summary["note"] = Value::String("unknown version".into());
    }
    Classification {
        standard: "ucp".into(),
        status: Validity::Valid,
        summary,
        reason: None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn profile(ucp: Value) -> String {
        json!({ "ucp": ucp }).to_string()
    }

    #[test]
    fn a_profile_with_every_transport_validates_and_surfaces_mcp_a2a_and_ap2() {
        let body = json!({
            "ucp": {
                "version": "2026-08-25",
                "services": { "dev.ucp.shopping": [
                    { "version": "2026-08-25", "spec": "s", "transport": "rest", "endpoint": "https://u:p@shop.test/ucp" },
                    { "version": "2026-08-25", "spec": "s", "transport": "mcp", "endpoint": "https://user:pw@shop.test/mcp" },
                    { "version": "2026-08-25", "spec": "s", "transport": "a2a", "endpoint": "https://shop.test/agent-card.json" },
                    { "version": "2026-08-25", "spec": "s", "transport": "embedded", "schema": "https://x.test/e.json" }
                ]},
                "capabilities": {
                    "dev.ucp.shopping.checkout": [{ "version": "2026-08-25" }],
                    "dev.ucp.common.payment.ap2_mandate": [{ "version": "2026-08-25" }],
                    "dev.example.flag": {}
                },
                "payment_handlers": { "com.example.card": [{ "id": "c" }] }
            },
            "keys": [{ "kty": "OKP", "x": "secret-ish" }]
        })
        .to_string();
        let c = classify_ucp(&body);
        assert_eq!(c.status, Validity::Valid, "{:?}", c.reason);
        let s = &c.summary;
        assert_eq!(s["version"], "2026-08-25");
        assert_eq!(s["services"]["dev.ucp.shopping"][0]["endpoint"], "https://shop.test/ucp");
        assert_eq!(s["services"]["dev.ucp.shopping"][3]["endpoint"], Value::Null);
        assert_eq!(s["mcp"], json!(["https://shop.test/mcp"]));
        assert_eq!(s["services"]["dev.ucp.shopping"][1]["endpoint"], "https://shop.test/mcp");
        assert!(!s.to_string().contains("pw@"));
        assert_eq!(s["a2a"], json!(["https://shop.test/agent-card.json"]));
        assert_eq!(s["capabilities"], json!(["dev.example.flag", "dev.ucp.common.payment.ap2_mandate@2026-08-25", "dev.ucp.shopping.checkout@2026-08-25"]));
        assert_eq!(s["paymentHandlers"], json!(["com.example.card"]));
        assert_eq!(s["hasKeys"], true);
        assert_eq!(s["ap2"], true);
        assert!(s.get("note").is_none());
        assert!(!s.to_string().contains("secret-ish"));
    }

    #[test]
    fn an_unknown_version_is_valid_with_a_note_and_empty_registries_are_fine() {
        let c = classify_ucp(&profile(json!({ "version": "2027-01-01", "services": {}, "payment_handlers": { "dev.x.ap2_handler": [] } })));
        assert_eq!(c.status, Validity::Valid);
        assert_eq!(c.summary["note"], "unknown version");
        assert_eq!(c.summary["ap2"], true);
        assert_eq!(c.summary["hasKeys"], false);
    }

    #[test]
    fn a_broken_profile_is_invalid_with_a_reason() {
        let ok = json!({ "version": "2026-08-25", "spec": "s", "transport": "rest", "endpoint": "e" });
        let cases = [
            ("{", "EOF"),
            ("{}", "missing ucp"),
            (&profile(json!({ "services": {}, "payment_handlers": {} })), "missing ucp.version"),
            (&profile(json!({ "version": "v1", "services": {}, "payment_handlers": {} })), "ucp.version is not YYYY-MM-DD"),
            (&profile(json!({ "version": "2026-08-25", "payment_handlers": {} })), "missing ucp.services"),
            (&profile(json!({ "version": "2026-08-25", "services": {} })), "missing ucp.payment_handlers"),
            (&profile(json!({ "version": "2026-08-25", "services": { "a": {} }, "payment_handlers": {} })), "services.a is not a list"),
            (&profile(json!({ "version": "2026-08-25", "services": { "a": [{ "spec": "s", "transport": "rest" }] }, "payment_handlers": {} })), "services.a[0] is missing version"),
            (&profile(json!({ "version": "2026-08-25", "services": { "a": [ok, { "version": "v", "spec": "s", "transport": "smtp", "endpoint": "e" }] }, "payment_handlers": {} })), "services.a[1] has unknown transport smtp"),
            (&profile(json!({ "version": "2026-08-25", "services": { "a": [{ "version": "v", "spec": "s", "transport": "rest" }] }, "payment_handlers": {} })), "services.a[0] has no endpoint"),
            (&profile(json!({ "version": "2026-08-25", "services": { "a": [{ "version": "v", "spec": "s", "transport": "embedded" }] }, "payment_handlers": {} })), "services.a[0] has no endpoint"),
        ];
        for (body, reason) in cases {
            let c = classify_ucp(body);
            assert_eq!(c.status, Validity::Invalid, "{body}");
            assert!(c.reason.as_deref().unwrap_or("").contains(reason), "{body}: {:?}", c.reason);
        }
    }

    #[test]
    fn long_names_and_urls_are_capped_and_unparsable_urls_kept() {
        let long = "n".repeat(300);
        let body = profile(json!({
            "version": "2026-08-25",
            "services": { long.clone(): [{ "version": "v", "spec": "s", "transport": "mcp", "endpoint": format!("not a url {}", "é".repeat(400)) }] },
            "payment_handlers": {}
        }));
        let c = classify_ucp(&body);
        let endpoint = c.summary["mcp"][0].as_str().unwrap();
        assert!(endpoint.len() <= URL_CAP && endpoint.starts_with("not a url"));
        assert_eq!(c.summary["services"].as_object().unwrap().keys().next().unwrap().len(), NAME_CAP);
        assert!(is_date("2026-08-25") && !is_date("2026-8-25") && !is_date("2026x08-25"));
    }
}