1pub mod audit;
22pub mod identity;
23pub mod lifecycle;
24pub mod mcp;
25pub mod policy;
26pub mod rings;
27pub mod trust;
28pub mod types;
29
30pub use audit::AuditLogger;
31pub use identity::{AgentIdentity, PublicIdentity};
32pub use lifecycle::{LifecycleEvent, LifecycleManager, LifecycleState};
33pub use mcp::*;
34pub use policy::{PolicyEngine, PolicyError};
35pub use rings::{Ring, RingEnforcer};
36pub use trust::{TrustConfig, TrustManager};
37pub use types::{
38 AuditEntry, AuditFilter, CandidateDecision, ConflictResolutionStrategy, GovernanceResult,
39 PolicyDecision, PolicyScope, ResolutionResult, TrustScore, TrustTier,
40};
41
42use std::collections::HashMap;
43
44pub struct AgentMeshClient {
48 pub identity: AgentIdentity,
49 pub trust: TrustManager,
50 pub policy: PolicyEngine,
51 pub audit: AuditLogger,
52}
53
54#[derive(Default)]
56pub struct ClientOptions {
57 pub capabilities: Vec<String>,
58 pub trust_config: Option<TrustConfig>,
59 pub policy_yaml: Option<String>,
60}
61
62impl AgentMeshClient {
63 pub fn new(agent_id: &str) -> Result<Self, ClientError> {
65 Self::with_options(agent_id, ClientOptions::default())
66 }
67
68 pub fn with_options(agent_id: &str, opts: ClientOptions) -> Result<Self, ClientError> {
70 let identity =
71 AgentIdentity::generate(agent_id, opts.capabilities).map_err(ClientError::Identity)?;
72
73 let trust_config = opts.trust_config.unwrap_or_default();
74 let trust = TrustManager::new(trust_config);
75
76 let policy = PolicyEngine::new();
77 if let Some(yaml) = &opts.policy_yaml {
78 policy.load_from_yaml(yaml).map_err(ClientError::Policy)?;
79 }
80
81 Ok(Self {
82 identity,
83 trust,
84 policy,
85 audit: AuditLogger::new(),
86 })
87 }
88
89 pub fn execute_with_governance(
92 &self,
93 action: &str,
94 context: Option<&HashMap<String, serde_yaml::Value>>,
95 ) -> GovernanceResult {
96 let decision = self.policy.evaluate(action, context);
97 let audit_entry = self.audit.log(&self.identity.did, action, decision.label());
98 let trust_score = self.trust.get_trust_score(&self.identity.did);
99
100 match &decision {
101 PolicyDecision::Allow => self.trust.record_success(&self.identity.did),
102 PolicyDecision::Deny(_) => self.trust.record_failure(&self.identity.did),
103 _ => {}
104 }
105
106 GovernanceResult {
107 allowed: decision.is_allowed(),
108 decision,
109 trust_score,
110 audit_entry,
111 }
112 }
113}
114
115#[derive(Debug, thiserror::Error)]
117pub enum ClientError {
118 #[error("identity error: {0}")]
119 Identity(identity::IdentityError),
120 #[error("policy error: {0}")]
121 Policy(policy::PolicyError),
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_client_default_allows_everything() {
130 let client = AgentMeshClient::new("test-agent").unwrap();
131 let result = client.execute_with_governance("anything", None);
132 assert!(result.allowed);
133 assert_eq!(result.decision, PolicyDecision::Allow);
134 }
135
136 #[test]
137 fn test_client_with_policy() {
138 let yaml = r#"
139version: "1.0"
140agent: test
141policies:
142 - name: gate
143 type: capability
144 allowed_actions:
145 - "data.read"
146 denied_actions:
147 - "shell:*"
148"#;
149 let opts = ClientOptions {
150 policy_yaml: Some(yaml.to_string()),
151 ..Default::default()
152 };
153 let client = AgentMeshClient::with_options("test", opts).unwrap();
154
155 let r1 = client.execute_with_governance("data.read", None);
156 assert!(r1.allowed);
157
158 let r2 = client.execute_with_governance("shell:rm", None);
159 assert!(!r2.allowed);
160 assert!(matches!(r2.decision, PolicyDecision::Deny(_)));
161 }
162
163 #[test]
164 fn test_governance_updates_trust() {
165 let client = AgentMeshClient::new("trust-test").unwrap();
166 let did = client.identity.did.clone();
167
168 client.execute_with_governance("action1", None); client.execute_with_governance("action2", None); let score = client.trust.get_trust_score(&did);
171 assert!(score.score > 500);
172 }
173
174 #[test]
175 fn test_governance_creates_audit_chain() {
176 let client = AgentMeshClient::new("audit-test").unwrap();
177 client.execute_with_governance("a", None);
178 client.execute_with_governance("b", None);
179 client.execute_with_governance("c", None);
180 assert!(client.audit.verify());
181 assert_eq!(client.audit.entries().len(), 3);
182 }
183
184 #[test]
185 fn test_client_with_custom_trust_config() {
186 let opts = ClientOptions {
187 trust_config: Some(TrustConfig {
188 initial_score: 800,
189 threshold: 700,
190 reward: 20,
191 penalty: 100,
192 persist_path: None,
193 decay_rate: 0.95,
194 }),
195 ..Default::default()
196 };
197 let client = AgentMeshClient::with_options("custom-trust", opts).unwrap();
198 let score = client.trust.get_trust_score(&client.identity.did);
199 assert_eq!(score.score, 800);
200 assert_eq!(score.tier, TrustTier::Trusted);
201 }
202
203 #[test]
204 fn test_client_with_capabilities() {
205 let opts = ClientOptions {
206 capabilities: vec!["data.read".to_string(), "data.write".to_string()],
207 ..Default::default()
208 };
209 let client = AgentMeshClient::with_options("cap-agent", opts).unwrap();
210 assert_eq!(
211 client.identity.capabilities,
212 vec!["data.read", "data.write"]
213 );
214 }
215
216 #[test]
217 fn test_client_with_invalid_yaml_returns_error() {
218 let opts = ClientOptions {
219 policy_yaml: Some("not: valid: yaml: {{{{".to_string()),
220 ..Default::default()
221 };
222 let result = AgentMeshClient::with_options("bad-yaml", opts);
223 assert!(result.is_err());
224 match result {
225 Err(ClientError::Policy(_)) => {}
226 other => panic!("expected ClientError::Policy, got {:?}", other.err()),
227 }
228 }
229
230 #[test]
231 fn test_multiple_governance_executions_build_audit_chain() {
232 let client = AgentMeshClient::new("chain-agent").unwrap();
233 for i in 0..5 {
234 client.execute_with_governance(&format!("action.{}", i), None);
235 }
236 let entries = client.audit.entries();
237 assert_eq!(entries.len(), 5);
238 for i in 0..5 {
239 assert_eq!(entries[i].seq, i as u64);
240 }
241 for i in 1..5 {
243 assert_eq!(entries[i].previous_hash, entries[i - 1].hash);
244 }
245 assert!(client.audit.verify());
246 }
247
248 #[test]
249 fn test_governance_with_denied_action_decreases_trust() {
250 let yaml = r#"
251version: "1.0"
252agent: test
253policies:
254 - name: gate
255 type: capability
256 denied_actions:
257 - "dangerous.*"
258"#;
259 let opts = ClientOptions {
260 policy_yaml: Some(yaml.to_string()),
261 ..Default::default()
262 };
263 let client = AgentMeshClient::with_options("deny-trust", opts).unwrap();
264 let did = client.identity.did.clone();
265 let initial = client.trust.get_trust_score(&did).score;
266 client.execute_with_governance("dangerous.action", None);
267 let after = client.trust.get_trust_score(&did).score;
268 assert!(after < initial);
269 }
270
271 #[test]
272 fn test_governance_with_approval_required_action() {
273 let yaml = r#"
274version: "1.0"
275agent: test
276policies:
277 - name: deploy-gate
278 type: approval
279 actions:
280 - "deploy.*"
281 min_approvals: 3
282"#;
283 let opts = ClientOptions {
284 policy_yaml: Some(yaml.to_string()),
285 ..Default::default()
286 };
287 let client = AgentMeshClient::with_options("approval-test", opts).unwrap();
288 let result = client.execute_with_governance("deploy.prod", None);
289 assert!(!result.allowed);
290 assert!(matches!(
291 result.decision,
292 PolicyDecision::RequiresApproval(_)
293 ));
294 }
295
296 #[test]
297 fn test_governance_with_rate_limited_action() {
298 let yaml = r#"
299version: "1.0"
300agent: test
301policies:
302 - name: rate-gate
303 type: rate_limit
304 actions:
305 - "api.*"
306 max_calls: 2
307 window: "60s"
308"#;
309 let opts = ClientOptions {
310 policy_yaml: Some(yaml.to_string()),
311 ..Default::default()
312 };
313 let client = AgentMeshClient::with_options("rate-test", opts).unwrap();
314 let r1 = client.execute_with_governance("api.call", None);
315 assert!(r1.allowed);
316 let r2 = client.execute_with_governance("api.call", None);
317 assert!(r2.allowed);
318 let r3 = client.execute_with_governance("api.call", None);
319 assert!(!r3.allowed);
320 assert!(matches!(r3.decision, PolicyDecision::RateLimited { .. }));
321 }
322
323 #[test]
324 fn test_client_identity_did_is_correct() {
325 let client = AgentMeshClient::new("my-agent-42").unwrap();
326 assert_eq!(client.identity.did, "did:agentmesh:my-agent-42");
327 }
328
329 #[test]
330 fn test_audit_chain_integrity_after_mixed_allow_deny() {
331 let yaml = r#"
332version: "1.0"
333agent: test
334policies:
335 - name: gate
336 type: capability
337 allowed_actions:
338 - "safe.*"
339 denied_actions:
340 - "bad.*"
341"#;
342 let opts = ClientOptions {
343 policy_yaml: Some(yaml.to_string()),
344 ..Default::default()
345 };
346 let client = AgentMeshClient::with_options("mixed-test", opts).unwrap();
347 let r1 = client.execute_with_governance("safe.read", None);
348 assert!(r1.allowed);
349 let r2 = client.execute_with_governance("bad.delete", None);
350 assert!(!r2.allowed);
351 let r3 = client.execute_with_governance("safe.write", None);
352 assert!(r3.allowed);
353
354 assert!(client.audit.verify());
355 assert_eq!(client.audit.entries().len(), 3);
356
357 let entries = client.audit.entries();
358 assert_eq!(entries[0].decision, "allow");
359 assert_eq!(entries[1].decision, "deny");
360 assert_eq!(entries[2].decision, "allow");
361 }
362}