agentguard_core/
principal.rs1use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10use crate::ids::PrincipalId;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(tag = "type", rename_all = "snake_case")]
23#[non_exhaustive]
24pub enum Principal {
25 User {
30 uid: PrincipalId,
31 #[serde(default)]
32 attrs: IndexMap<String, serde_json::Value>,
33 },
34 Agent {
36 uid: PrincipalId,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 parent_uid: Option<PrincipalId>,
39 #[serde(default)]
40 attrs: IndexMap<String, serde_json::Value>,
41 },
42}
43
44impl Principal {
45 pub fn user(uid: impl Into<PrincipalId>) -> Self {
55 Principal::User {
56 uid: uid.into(),
57 attrs: IndexMap::new(),
58 }
59 }
60
61 pub fn agent(uid: impl Into<PrincipalId>) -> Self {
71 Principal::Agent {
72 uid: uid.into(),
73 parent_uid: None,
74 attrs: IndexMap::new(),
75 }
76 }
77
78 pub fn subagent(uid: impl Into<PrincipalId>, parent: impl Into<PrincipalId>) -> Self {
88 Principal::Agent {
89 uid: uid.into(),
90 parent_uid: Some(parent.into()),
91 attrs: IndexMap::new(),
92 }
93 }
94
95 pub fn with_attr(mut self, k: impl Into<String>, v: impl Into<serde_json::Value>) -> Self {
97 let map = match &mut self {
98 Principal::User { attrs, .. } => attrs,
99 Principal::Agent { attrs, .. } => attrs,
100 };
101 map.insert(k.into(), v.into());
102 self
103 }
104
105 pub fn entity_type(&self) -> &'static str {
107 match self {
108 Principal::User { .. } => "User",
109 Principal::Agent { .. } => "Agent",
110 }
111 }
112
113 #[deprecated(
120 since = "0.2.1",
121 note = "Use the Display impl to write into an existing buffer (no allocation)."
122 )]
123 pub fn entity_uid(&self) -> String {
124 format!("{}::\"{}\"", self.entity_type(), self.id())
125 }
126
127 pub fn id(&self) -> &PrincipalId {
129 match self {
130 Principal::User { uid, .. } | Principal::Agent { uid, .. } => uid,
131 }
132 }
133}
134
135impl fmt::Display for Principal {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}::\"{}\"", self.entity_type(), self.id())
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn user_principal_entity_uid() {
147 let p = Principal::user("alice").with_attr("role", "admin");
148 assert_eq!(p.entity_type(), "User");
149 assert_eq!(format!("{}", p), "User::\"alice\"");
150 }
151
152 #[test]
153 fn subagent_has_parent() {
154 let p = Principal::subagent("summarizer", "research");
155 assert_eq!(p.entity_type(), "Agent");
156 assert_eq!(format!("{}", p), "Agent::\"summarizer\"");
157 assert_eq!(p.id(), &PrincipalId::from("summarizer"));
158 }
159
160 #[test]
161 fn agent_no_parent_omits_parent_uid() {
162 let p = Principal::agent("solo");
163 assert_eq!(p.entity_type(), "Agent");
164 assert_eq!(p.id(), &PrincipalId::from("solo"));
165 let json = serde_json::to_value(&p).unwrap();
169 assert!(
170 json.get("parent_uid").is_none(),
171 "parent_uid leaked: {json}"
172 );
173 }
174
175 #[test]
176 fn with_attr_supports_multiple_attrs() {
177 let p = Principal::user("alice")
178 .with_attr("role", "admin")
179 .with_attr("tenant", "acme-corp")
180 .with_attr("level", "5");
181 let json = serde_json::to_value(&p).unwrap();
182 assert_eq!(json["attrs"]["role"], "admin");
183 assert_eq!(json["attrs"]["tenant"], "acme-corp");
184 assert_eq!(json["attrs"]["level"], "5");
185 }
186
187 #[test]
188 fn principal_serde_round_trips() {
189 let p = Principal::subagent("research-bot", "admin");
190 let json = serde_json::to_string(&p).unwrap();
191 let parsed: Principal = serde_json::from_str(&json).unwrap();
192 assert_eq!(parsed, p);
193 }
194
195 #[test]
196 fn user_and_agent_with_same_id_differ() {
197 let u = Principal::user("alice");
200 let a = Principal::agent("alice");
201 assert_ne!(u, a);
202 assert_eq!(u.entity_type(), "User");
203 assert_eq!(a.entity_type(), "Agent");
204 }
205
206 #[test]
207 fn user_display_uses_entity_uid() {
208 let p = Principal::user("alice");
209 assert_eq!(format!("{}", p), "User::\"alice\"");
210 }
211
212 #[test]
213 fn subagent_display_uses_entity_uid() {
214 let p = Principal::subagent("summarizer", "research");
215 assert_eq!(format!("{}", p), "Agent::\"summarizer\"");
216 }
217}