Skip to main content

cedar_agent_schemas/
lib.rs

1//! Canonical Cedar agent action verbs with typed helpers.
2//!
3//! Rust equivalents of the TypeScript and Python builders in the sibling
4//! packages. The underlying schema file lives at
5//! `schemas/agent-actions.cedarschema.json` in the repository root.
6//!
7//! These helpers produce Cedar authorization request values that any
8//! Cedar evaluator (cedar-policy, cedar-wasm, or a remote evaluator) can
9//! consume. This crate does not itself evaluate policies; it is a
10//! typed-builder layer on top of the canonical schema.
11
12use serde::{Deserialize, Serialize};
13
14/// The canonical schema namespace for agent action verbs.
15pub const SCHEMA_NAMESPACE: &str = "Agent";
16
17/// The four canonical AI-agent action verbs.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum ActionVerb {
21    Exec,
22    Open,
23    Connect,
24    RequestTool,
25}
26
27impl ActionVerb {
28    /// The string form used inside Cedar action UIDs.
29    pub fn as_str(&self) -> &'static str {
30        match self {
31            ActionVerb::Exec => "exec",
32            ActionVerb::Open => "open",
33            ActionVerb::Connect => "connect",
34            ActionVerb::RequestTool => "request_tool",
35        }
36    }
37}
38
39/// Canonical `Agent::Principal` attributes.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct AgentPrincipal {
42    pub agent_id: String,
43    /// Decimal as a string for Cedar precision (e.g. "0.85").
44    pub trust_score: String,
45    pub ring: i64,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub session_id: Option<String>,
48}
49
50/// Canonical `Agent::File` attributes.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AgentFile {
53    pub path: String,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub owner_uid: Option<i64>,
56}
57
58/// Canonical `Agent::Endpoint` attributes.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AgentEndpoint {
61    pub host: String,
62    pub port: i64,
63    pub protocol: String,
64}
65
66/// Canonical `Agent::Tool` attributes.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AgentTool {
69    pub name: String,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub server: Option<String>,
72}
73
74/// Canonical `Agent::Executable` attributes.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct AgentExecutable {
77    pub path: String,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub trusted: Option<bool>,
80}
81
82/// Context for `Agent::Action::"exec"` requests.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ExecContext {
85    pub command: String,
86    pub argv: Vec<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub cwd: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub uid: Option<i64>,
91}
92
93/// Context for `Agent::Action::"open"` requests.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct OpenContext {
96    pub mode: String, // "read" | "write" | "append" | etc.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub size_bytes: Option<i64>,
99}
100
101/// Context for `Agent::Action::"connect"` requests.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ConnectContext {
104    pub tls: bool,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub method: Option<String>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub url: Option<String>,
109}
110
111/// Context for `Agent::Action::"request_tool"` requests.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct RequestToolContext {
114    pub args_hash: String,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub transport: Option<String>,
117}
118
119/// Canonical Cedar authorization request shape.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct CedarAuthorizationRequest {
122    pub principal: String,
123    pub action: String,
124    pub resource: String,
125    pub context: serde_json::Value,
126    pub entities: Vec<serde_json::Value>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub schema: Option<serde_json::Value>,
129}
130
131/// Build a fully-qualified Cedar entity UID string.
132pub fn entity_uid(type_: &str, id: &str) -> String {
133    format!("{SCHEMA_NAMESPACE}::{type_}::\"{id}\"")
134}
135
136/// Build a fully-qualified Cedar action UID string.
137pub fn action_uid(verb: ActionVerb) -> String {
138    format!("{SCHEMA_NAMESPACE}::Action::\"{}\"", verb.as_str())
139}
140
141/// Build a Cedar authorization request for an `exec` action.
142pub fn build_exec_request(
143    principal: &AgentPrincipal,
144    executable_id: &str,
145    context: &ExecContext,
146    entities: Option<Vec<serde_json::Value>>,
147) -> CedarAuthorizationRequest {
148    CedarAuthorizationRequest {
149        principal: entity_uid("Principal", &principal.agent_id),
150        action: action_uid(ActionVerb::Exec),
151        resource: entity_uid("Executable", executable_id),
152        context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
153        entities: entities.unwrap_or_default(),
154        schema: None,
155    }
156}
157
158/// Build a Cedar authorization request for an `open` action.
159pub fn build_open_request(
160    principal: &AgentPrincipal,
161    file_id: &str,
162    context: &OpenContext,
163    entities: Option<Vec<serde_json::Value>>,
164) -> CedarAuthorizationRequest {
165    CedarAuthorizationRequest {
166        principal: entity_uid("Principal", &principal.agent_id),
167        action: action_uid(ActionVerb::Open),
168        resource: entity_uid("File", file_id),
169        context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
170        entities: entities.unwrap_or_default(),
171        schema: None,
172    }
173}
174
175/// Build a Cedar authorization request for a `connect` action.
176pub fn build_connect_request(
177    principal: &AgentPrincipal,
178    endpoint_id: &str,
179    context: &ConnectContext,
180    entities: Option<Vec<serde_json::Value>>,
181) -> CedarAuthorizationRequest {
182    CedarAuthorizationRequest {
183        principal: entity_uid("Principal", &principal.agent_id),
184        action: action_uid(ActionVerb::Connect),
185        resource: entity_uid("Endpoint", endpoint_id),
186        context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
187        entities: entities.unwrap_or_default(),
188        schema: None,
189    }
190}
191
192/// Build a Cedar authorization request for a `request_tool` action.
193pub fn build_request_tool_request(
194    principal: &AgentPrincipal,
195    tool_id: &str,
196    context: &RequestToolContext,
197    entities: Option<Vec<serde_json::Value>>,
198) -> CedarAuthorizationRequest {
199    CedarAuthorizationRequest {
200        principal: entity_uid("Principal", &principal.agent_id),
201        action: action_uid(ActionVerb::RequestTool),
202        resource: entity_uid("Tool", tool_id),
203        context: serde_json::to_value(context).unwrap_or(serde_json::Value::Null),
204        entities: entities.unwrap_or_default(),
205        schema: None,
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn entity_uid_formats() {
215        assert_eq!(
216            entity_uid("Principal", "alice"),
217            r#"Agent::Principal::"alice""#
218        );
219        assert_eq!(entity_uid("Tool", "Bash"), r#"Agent::Tool::"Bash""#);
220    }
221
222    #[test]
223    fn action_uid_for_each_verb() {
224        assert_eq!(action_uid(ActionVerb::Exec), r#"Agent::Action::"exec""#);
225        assert_eq!(action_uid(ActionVerb::Open), r#"Agent::Action::"open""#);
226        assert_eq!(
227            action_uid(ActionVerb::Connect),
228            r#"Agent::Action::"connect""#
229        );
230        assert_eq!(
231            action_uid(ActionVerb::RequestTool),
232            r#"Agent::Action::"request_tool""#
233        );
234    }
235
236    #[test]
237    fn build_request_tool_request_shape() {
238        let principal = AgentPrincipal {
239            agent_id: "claude-code-3a2f".into(),
240            trust_score: "0.85".into(),
241            ring: 2,
242            session_id: Some("sess-9af21".into()),
243        };
244        let ctx = RequestToolContext {
245            args_hash: "sha256:e4d61f7a".into(),
246            transport: Some("mcp_stdio".into()),
247        };
248        let req = build_request_tool_request(&principal, "Bash", &ctx, None);
249
250        assert_eq!(req.principal, r#"Agent::Principal::"claude-code-3a2f""#);
251        assert_eq!(req.action, r#"Agent::Action::"request_tool""#);
252        assert_eq!(req.resource, r#"Agent::Tool::"Bash""#);
253        assert!(req.entities.is_empty());
254    }
255}