systemprompt-api 0.14.2

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
//! Scope, audience, and `act`-chain resolution for token exchange.

use std::str::FromStr;

use anyhow::{Result, anyhow};
use systemprompt_identifiers::ClientId;
use systemprompt_models::Config;
use systemprompt_models::auth::{ActClaim, JwtAudience, Permission};

use super::super::super::TokenError;

// Requested permissions are filtered to those that exist in the subject
// token, the client's scope grant, and the client owner's role set. An
// empty client scope is treated as "no restriction beyond owner".
// Returned in descending hierarchy order. Errors with invalid_scope when
// the intersection is empty.
pub fn intersect_scopes(
    requested: &[Permission],
    subject_scope: &[Permission],
    client_scope: &[Permission],
    owner_scope: &[Permission],
) -> Result<Vec<Permission>> {
    let mut out: Vec<Permission> = requested
        .iter()
        .filter(|p| subject_scope.contains(p))
        .filter(|p| client_scope.is_empty() || client_scope.contains(p))
        .filter(|p| owner_scope.contains(p))
        .copied()
        .collect();
    out.sort_by_key(|p| std::cmp::Reverse(p.hierarchy_level()));
    out.dedup();
    if out.is_empty() {
        return Err(anyhow!(TokenError::InvalidRequest {
            field: "scope".to_owned(),
            message: "no overlap between subject, client, and owner permissions".to_owned(),
        }));
    }
    Ok(out)
}

pub(super) fn resolve_audience(
    requested: Option<&str>,
    global: &Config,
) -> Result<Vec<JwtAudience>> {
    if let Some(value) = requested {
        if !global
            .allowed_resource_audiences
            .iter()
            .any(|allowed| allowed == value)
        {
            return Err(anyhow!(TokenError::InvalidTarget {
                message: format!("audience '{value}' not in allowed_resource_audiences"),
            }));
        }
        let aud =
            JwtAudience::from_str(value).map_err(|e| anyhow!("Invalid audience '{value}': {e}"))?;
        return Ok(vec![aud]);
    }
    Ok(global.jwt_audiences.clone())
}

pub fn build_act_chain(client_id: &ClientId, issuer: &str, prior: Option<ActClaim>) -> ActClaim {
    ActClaim {
        iss: issuer.to_owned(),
        sub: client_id.to_string(),
        act: Box::new(prior),
    }
}