1use crate::action::AgentAction;
4use crate::context::AgentContext;
5use crate::error::{Error, Result};
6use crate::observability::TraceContext;
7use crate::principal::Principal;
8use crate::resource::Resource;
9use cedar_policy::{Context, EntityId, EntityTypeName, EntityUid, Request};
10use serde::{Deserialize, Serialize};
11use std::str::FromStr;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
18#[non_exhaustive]
19pub struct AgentRequest {
20 pub principal: Principal,
21 pub action: AgentAction,
22 pub resource: Resource,
23 pub context: AgentContext,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub request_id: Option<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub trace: Option<TraceContext>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub tenant_id: Option<String>,
38}
39
40impl AgentRequest {
41 pub fn new(
43 principal: Principal,
44 action: AgentAction,
45 resource: Resource,
46 context: AgentContext,
47 ) -> Self {
48 Self {
49 principal,
50 action,
51 resource,
52 context,
53 request_id: Some(uuid::Uuid::now_v7().to_string()),
54 trace: None,
55 tenant_id: None,
56 }
57 }
58
59 pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
61 self.request_id = Some(id.into());
62 self
63 }
64
65 pub fn with_trace(mut self, trace: TraceContext) -> Self {
67 self.trace = Some(trace);
68 self
69 }
70
71 pub fn with_tenant_id(mut self, tid: impl Into<String>) -> Self {
74 let s = tid.into();
75 self.tenant_id = if s.trim().is_empty() {
76 None
77 } else {
78 Some(s.trim().to_string())
79 };
80 self
81 }
82
83 pub fn to_cedar_request(&self, schema: Option<&cedar_policy::Schema>) -> Result<Request> {
92 let principal_eid = EntityId::new(&**self.principal.id());
93 let principal_etype =
94 EntityTypeName::from_str(self.principal.entity_type()).map_err(|e| {
95 Error::InvalidPrincipal(format!("{}: {}", self.principal.entity_type(), e))
96 })?;
97 let principal_uid = EntityUid::from_type_name_and_id(principal_etype, principal_eid);
98
99 let action_eid = EntityId::new(self.action.action_id());
100 let action_etype = EntityTypeName::from_str("Action")
101 .map_err(|e| Error::InvalidPrincipal(format!("Action: {}", e)))?;
102 let action_uid = EntityUid::from_type_name_and_id(action_etype, action_eid);
103
104 let resource_eid = EntityId::new(&self.resource.uid);
105 let resource_etype = EntityTypeName::from_str(&self.resource.entity_type)
106 .map_err(|e| Error::InvalidResource(format!("{}: {}", self.resource.entity_type, e)))?;
107 let resource_uid = EntityUid::from_type_name_and_id(resource_etype, resource_eid);
108
109 let mut ctx_map = self.context.to_json_object();
111 let ctx_json = serde_json::Value::Object(std::mem::take(&mut ctx_map));
112
113 let context = match schema {
114 Some(s) => {
115 let json_str = serde_json::to_string(&ctx_json)?;
116 Context::from_json_str(&json_str, Some((s, &action_uid)))
117 .map_err(|e| Error::InvalidContext(e.to_string()))?
118 }
119 None => Context::from_json_str(&serde_json::to_string(&ctx_json)?, None)
120 .map_err(|e| Error::InvalidContext(e.to_string()))?,
121 };
122
123 let req = Request::new(principal_uid, action_uid, resource_uid, context, schema)
124 .map_err(|e| Error::InvalidContext(e.to_string()))?;
125
126 Ok(req)
127 }
128}
129
130#[derive(Debug, Clone)]
142pub struct AgentRequestBuilder {
143 principal: Principal,
144 action: Option<AgentAction>,
145 resource: Option<Resource>,
146 context: AgentContext,
147 request_id: Option<String>,
148 trace: Option<TraceContext>,
149 tenant_id: Option<String>,
150}
151
152impl AgentRequestBuilder {
153 pub fn new(principal: impl Into<Principal>) -> Self {
155 Self {
156 principal: principal.into(),
157 action: None,
158 resource: None,
159 context: AgentContext::new(),
160 request_id: None,
161 trace: None,
162 tenant_id: None,
163 }
164 }
165
166 pub fn action(mut self, a: impl Into<AgentAction>) -> Self {
168 self.action = Some(a.into());
169 self
170 }
171
172 pub fn resource(mut self, r: impl Into<Resource>) -> Self {
174 self.resource = Some(r.into());
175 self
176 }
177
178 pub fn context(mut self, c: impl Into<AgentContext>) -> Self {
180 self.context = c.into();
181 self
182 }
183
184 pub fn request_id(mut self, id: impl Into<String>) -> Self {
186 self.request_id = Some(id.into());
187 self
188 }
189
190 pub fn trace(mut self, trace: TraceContext) -> Self {
192 self.trace = Some(trace);
193 self
194 }
195
196 pub fn traceparent(mut self, tp: &str) -> Result<Self> {
202 let trace: TraceContext = tp.parse()?;
203 self.trace = Some(trace);
204 Ok(self)
205 }
206
207 pub fn tenant_id(mut self, tid: impl Into<String>) -> Self {
210 let s = tid.into();
211 self.tenant_id = if s.trim().is_empty() {
212 None
213 } else {
214 Some(s.trim().to_string())
215 };
216 self
217 }
218
219 pub fn build(self) -> Result<AgentRequest> {
221 let action = self
222 .action
223 .ok_or_else(|| Error::InvalidPrincipal("action is required".into()))?;
224 let resource = self
225 .resource
226 .ok_or_else(|| Error::InvalidResource("resource is required".into()))?;
227 let mut req = AgentRequest::new(self.principal, action, resource, self.context);
228 if let Some(id) = self.request_id {
229 req.request_id = Some(id);
230 }
231 req.trace = self.trace;
232 req.tenant_id = self.tenant_id;
233 Ok(req)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::AgentAction;
241
242 #[test]
243 fn builder_sets_all_fields() {
244 let req = AgentRequestBuilder::new(Principal::user("alice"))
245 .action(AgentAction::tool("send_email"))
246 .resource(Resource::new("Mailbox", "alice@acme"))
247 .context(AgentContext::new().with_arg("to", "[email protected]"))
248 .build()
249 .unwrap();
250 assert_eq!(format!("{}", req.principal), "User::\"alice\"");
251 assert_eq!(
252 format!("{}", req.action),
253 "Action::\"ToolCall::send_email\""
254 );
255 assert_eq!(format!("{}", req.resource), "Mailbox::\"alice@acme\"");
256 assert!(req.request_id.is_some());
257 }
258
259 #[test]
260 fn builder_requires_action() {
261 let res = AgentRequestBuilder::new(Principal::user("alice"))
262 .resource(Resource::new("Mailbox", "alice@acme"))
263 .build();
264 assert!(res.is_err());
265 }
266
267 #[test]
268 fn builder_requires_resource() {
269 let res = AgentRequestBuilder::new(Principal::user("alice"))
270 .action(AgentAction::tool("send_email"))
271 .build();
272 assert!(res.is_err());
273 }
274
275 #[test]
276 fn new_auto_assigns_request_id() {
277 let req = AgentRequest::new(
278 Principal::user("alice"),
279 AgentAction::tool("send_email"),
280 Resource::new("Mailbox", "alice@acme"),
281 AgentContext::new(),
282 );
283 assert!(req.request_id.is_some());
284 }
285
286 #[test]
287 fn tenant_id_round_trips() {
288 let req = AgentRequestBuilder::new(Principal::user("alice"))
289 .action(AgentAction::tool("send_email"))
290 .resource(Resource::new("Mailbox", "alice@acme"))
291 .tenant_id("acme-corp")
292 .build()
293 .unwrap();
294 assert_eq!(req.tenant_id.as_deref(), Some("acme-corp"));
295
296 let req = AgentRequestBuilder::new(Principal::user("alice"))
298 .action(AgentAction::tool("send_email"))
299 .resource(Resource::new("Mailbox", "alice@acme"))
300 .tenant_id(" ")
301 .build()
302 .unwrap();
303 assert_eq!(req.tenant_id, None);
304
305 let req = AgentRequestBuilder::new(Principal::user("alice"))
307 .action(AgentAction::tool("send_email"))
308 .resource(Resource::new("Mailbox", "alice@acme"))
309 .tenant_id(" acme-corp ")
310 .build()
311 .unwrap();
312 assert_eq!(req.tenant_id.as_deref(), Some("acme-corp"));
313
314 let json = serde_json::to_string(&req).unwrap();
316 let parsed: AgentRequest = serde_json::from_str(&json).unwrap();
317 assert_eq!(parsed.tenant_id.as_deref(), Some("acme-corp"));
318 }
319
320 #[test]
323 fn full_serde_round_trip() {
324 let req = AgentRequestBuilder::new(Principal::user("alice"))
325 .action(AgentAction::tool_op("send", "email"))
326 .resource(Resource::new("Mailbox", "alice@acme"))
327 .context(AgentContext::new().with_arg("to", "[email protected]"))
328 .request_id("req-123")
329 .tenant_id("acme-corp")
330 .build()
331 .unwrap();
332 let json = serde_json::to_string(&req).unwrap();
333 let parsed: AgentRequest = serde_json::from_str(&json).unwrap();
334 assert_eq!(parsed, req);
335 }
336}