Skip to main content

agent_governance/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! # Agent Governance Rust SDK
5//!
6//! Rust SDK for the [Agent Governance Toolkit](https://github.com/microsoft/agent-governance-toolkit)
7//! — policy evaluation, trust scoring, hash-chain audit logging, and Ed25519 agent identity.
8//!
9//! ## Quick Start
10//!
11//! ```rust
12//! use agent_governance::AgentMeshClient;
13//!
14//! let client = AgentMeshClient::new("my-agent")
15//!     .expect("failed to create client");
16//!
17//! let result = client.execute_with_governance("data.read", None);
18//! assert!(result.allowed);
19//! ```
20
21pub 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
44/// Unified governance client combining identity, policy, trust, and audit.
45///
46/// This is the primary entry point for most users.
47pub struct AgentMeshClient {
48    pub identity: AgentIdentity,
49    pub trust: TrustManager,
50    pub policy: PolicyEngine,
51    pub audit: AuditLogger,
52}
53
54/// Builder options for [`AgentMeshClient`].
55#[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    /// Create a new client with default configuration.
64    pub fn new(agent_id: &str) -> Result<Self, ClientError> {
65        Self::with_options(agent_id, ClientOptions::default())
66    }
67
68    /// Create a new client with custom options.
69    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    /// Run an action through the full governance pipeline:
90    /// policy → audit → trust update.
91    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/// Errors returned by [`AgentMeshClient`] construction.
116#[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); // allow → +trust
169        client.execute_with_governance("action2", None); // allow → +trust
170        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        // Each entry's prev_hash links to the previous entry's hash
242        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}