1use serde::{Deserialize, Serialize};
13
14pub const SCHEMA_NAMESPACE: &str = "Agent";
16
17#[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct AgentPrincipal {
42 pub agent_id: String,
43 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AgentEndpoint {
61 pub host: String,
62 pub port: i64,
63 pub protocol: String,
64}
65
66#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct OpenContext {
96 pub mode: String, #[serde(skip_serializing_if = "Option::is_none")]
98 pub size_bytes: Option<i64>,
99}
100
101#[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#[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#[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
131pub fn entity_uid(type_: &str, id: &str) -> String {
133 format!("{SCHEMA_NAMESPACE}::{type_}::\"{id}\"")
134}
135
136pub fn action_uid(verb: ActionVerb) -> String {
138 format!("{SCHEMA_NAMESPACE}::Action::\"{}\"", verb.as_str())
139}
140
141pub 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
158pub 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
175pub 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
192pub 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}