use serde::{Deserialize, Serialize};
pub const SCHEMA_NAMESPACE: &str = "Agent";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActionVerb {
Exec,
Open,
Connect,
RequestTool,
}
impl ActionVerb {
pub fn as_str(&self) -> &'static str {
match self {
ActionVerb::Exec => "exec",
ActionVerb::Open => "open",
ActionVerb::Connect => "connect",
ActionVerb::RequestTool => "request_tool",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentPrincipal {
pub agent_id: String,
pub trust_score: String,
pub ring: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentFile {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_uid: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentEndpoint {
pub host: String,
pub port: i64,
pub protocol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentTool {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub server: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentExecutable {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub trusted: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecContext {
pub command: String,
pub argv: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uid: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenContext {
pub mode: String, #[serde(skip_serializing_if = "Option::is_none")]
pub size_bytes: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectContext {
pub tls: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestToolContext {
pub args_hash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub transport: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CedarAuthorizationRequest {
pub principal: String,
pub action: String,
pub resource: String,
pub context: serde_json::Value,
pub entities: Vec<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<serde_json::Value>,
}
pub fn entity_uid(type_: &str, id: &str) -> String {
format!("{SCHEMA_NAMESPACE}::{type_}::\"{id}\"")
}
pub fn action_uid(verb: ActionVerb) -> String {
format!("{SCHEMA_NAMESPACE}::Action::\"{}\"", verb.as_str())
}
pub fn build_exec_request(
principal: &AgentPrincipal,
executable_id: &str,
context: &ExecContext,
entities: Option<Vec<serde_json::Value>>,
) -> CedarAuthorizationRequest {
CedarAuthorizationRequest {
principal: entity_uid("Principal", &principal.agent_id),
action: action_uid(ActionVerb::Exec),
resource: entity_uid("Executable", executable_id),
context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
entities: entities.unwrap_or_default(),
schema: None,
}
}
pub fn build_open_request(
principal: &AgentPrincipal,
file_id: &str,
context: &OpenContext,
entities: Option<Vec<serde_json::Value>>,
) -> CedarAuthorizationRequest {
CedarAuthorizationRequest {
principal: entity_uid("Principal", &principal.agent_id),
action: action_uid(ActionVerb::Open),
resource: entity_uid("File", file_id),
context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
entities: entities.unwrap_or_default(),
schema: None,
}
}
pub fn build_connect_request(
principal: &AgentPrincipal,
endpoint_id: &str,
context: &ConnectContext,
entities: Option<Vec<serde_json::Value>>,
) -> CedarAuthorizationRequest {
CedarAuthorizationRequest {
principal: entity_uid("Principal", &principal.agent_id),
action: action_uid(ActionVerb::Connect),
resource: entity_uid("Endpoint", endpoint_id),
context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
entities: entities.unwrap_or_default(),
schema: None,
}
}
pub fn build_request_tool_request(
principal: &AgentPrincipal,
tool_id: &str,
context: &RequestToolContext,
entities: Option<Vec<serde_json::Value>>,
) -> CedarAuthorizationRequest {
CedarAuthorizationRequest {
principal: entity_uid("Principal", &principal.agent_id),
action: action_uid(ActionVerb::RequestTool),
resource: entity_uid("Tool", tool_id),
context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
entities: entities.unwrap_or_default(),
schema: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entity_uid_formats() {
assert_eq!(
entity_uid("Principal", "alice"),
r#"Agent::Principal::"alice""#
);
assert_eq!(entity_uid("Tool", "Bash"), r#"Agent::Tool::"Bash""#);
}
#[test]
fn action_uid_for_each_verb() {
assert_eq!(action_uid(ActionVerb::Exec), r#"Agent::Action::"exec""#);
assert_eq!(action_uid(ActionVerb::Open), r#"Agent::Action::"open""#);
assert_eq!(
action_uid(ActionVerb::Connect),
r#"Agent::Action::"connect""#
);
assert_eq!(
action_uid(ActionVerb::RequestTool),
r#"Agent::Action::"request_tool""#
);
}
#[test]
fn build_request_tool_request_shape() {
let principal = AgentPrincipal {
agent_id: "claude-code-3a2f".into(),
trust_score: "0.85".into(),
ring: 2,
session_id: Some("sess-9af21".into()),
};
let ctx = RequestToolContext {
args_hash: "sha256:e4d61f7a".into(),
transport: Some("mcp_stdio".into()),
};
let req = build_request_tool_request(&principal, "Bash", &ctx, None);
assert_eq!(req.principal, r#"Agent::Principal::"claude-code-3a2f""#);
assert_eq!(req.action, r#"Agent::Action::"request_tool""#);
assert_eq!(req.resource, r#"Agent::Tool::"Bash""#);
assert!(req.entities.is_empty());
}
}