systemprompt-api 0.8.0

Axum-based HTTP server and API gateway for systemprompt.io AI governance infrastructure. Exposes governed agents, MCP, A2A, and admin endpoints with rate limiting and RBAC.
Documentation
pub(super) fn validate_resource_uri(resource: &str) -> Result<(), String> {
    let url = reqwest::Url::parse(resource)
        .map_err(|_| format!("Invalid resource URI: '{resource}' is not a valid absolute URI"))?;

    if url.scheme() != "https" && url.scheme() != "http" {
        return Err(format!(
            "Resource URI must use https or http scheme, got '{}'",
            url.scheme()
        ));
    }

    if url.fragment().is_some() {
        return Err("Resource URI must not contain a fragment".to_string());
    }

    if let Some(host) = url.host_str() {
        if is_forbidden_host(host) {
            return Err(
                "Resource URI must not target internal or private network addresses".to_string(),
            );
        }
    }

    Ok(())
}

fn is_forbidden_host(host: &str) -> bool {
    let lower = host.to_lowercase();

    if lower == "localhost" || lower == "127.0.0.1" || lower == "::1" || lower == "0.0.0.0" {
        return true;
    }

    if lower.ends_with(".internal")
        || std::path::Path::new(&lower)
            .extension()
            .is_some_and(|ext| ext.eq_ignore_ascii_case("local"))
    {
        return true;
    }

    if lower.starts_with("10.") || lower.starts_with("192.168.") || lower.starts_with("169.254.") {
        return true;
    }

    if lower.starts_with("172.") {
        if let Some(second_octet_str) = lower
            .strip_prefix("172.")
            .and_then(|rest| rest.split('.').next())
        {
            if let Ok(second_octet) = second_octet_str.parse::<u8>() {
                if (16..=31).contains(&second_octet) {
                    return true;
                }
            }
        }
    }

    false
}

pub(super) async fn resolve_resource_scopes(resource: &str) -> Option<String> {
    crate::routes::proxy::mcp::get_mcp_server_scopes_from_resource(resource)
        .await
        .map(|scopes| scopes.join(" "))
}