use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use systemprompt_identifiers::{McpToolName, PolicyId, SessionId, UserId};
use crate::authz::error::AuthzError;
use crate::authz::types::Decision;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretLocation {
pub kind: String,
pub path: String,
}
impl SecretLocation {
pub fn new(kind: impl Into<String>, path: impl Into<String>) -> Self {
Self {
kind: kind.into(),
path: path.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RateLimitWindow {
pub name: String,
pub seconds: u64,
pub limit: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AgentScope {
User { user_id: UserId },
System,
}
impl AgentScope {
#[must_use]
pub const fn user_id(&self) -> Option<&UserId> {
match self {
Self::User { user_id } => Some(user_id),
Self::System => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum AccessScope {
Admin,
User,
Unknown,
}
impl AccessScope {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Admin => "admin",
Self::User => "user",
Self::Unknown => "unknown",
}
}
}
impl fmt::Display for AccessScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for AccessScope {
type Err = AuthzError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"admin" => Ok(Self::Admin),
"user" => Ok(Self::User),
"unknown" | "" => Ok(Self::Unknown),
other => Err(AuthzError::Validation(format!(
"unknown access scope: {other}"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct McpToolInput(
serde_json::Value,
);
impl McpToolInput {
#[must_use]
pub const fn new(value: serde_json::Value) -> Self {
Self(value)
}
#[must_use]
pub const fn as_value(&self) -> &serde_json::Value {
&self.0
}
#[must_use]
pub fn as_str(&self, field: &str) -> Option<&str> {
self.0.get(field).and_then(serde_json::Value::as_str)
}
#[must_use]
pub fn as_path(&self, field: &str) -> Option<&str> {
self.as_str(field)
}
}
#[derive(Debug)]
pub struct PolicyContext<'a> {
pub tool: McpToolName,
pub agent_scope: AgentScope,
pub access_scope: AccessScope,
pub session_id: &'a SessionId,
pub user_id: &'a UserId,
pub tool_input: &'a McpToolInput,
}
pub trait GovernancePolicy: Send + Sync + fmt::Debug {
fn id(&self) -> PolicyId;
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision;
}
#[derive(Debug, Clone, Default)]
pub struct GovernanceChain {
entries: Vec<Arc<dyn GovernancePolicy>>,
}
impl GovernanceChain {
#[must_use]
pub const fn new(entries: Vec<Arc<dyn GovernancePolicy>>) -> Self {
Self { entries }
}
pub fn push(&mut self, policy: Arc<dyn GovernancePolicy>) {
self.entries.push(policy);
}
#[must_use]
pub fn entries(&self) -> &[Arc<dyn GovernancePolicy>] {
&self.entries
}
#[must_use]
pub fn evaluate(&self, ctx: &PolicyContext<'_>) -> Decision {
for policy in &self.entries {
if let deny @ Decision::Deny { .. } = policy.evaluate(ctx) {
return deny;
}
}
Decision::Allow {
matched_by: crate::authz::types::MatchedBy::DefaultIncluded,
}
}
}