Skip to main content

lit/identity/
trust.rs

1//! Agent Trust Scoring Engine
2//!
3//! Tracks reputation, reliability, and trust of agents based on observable
4//! behavior: commits, reviews, merges, violations, and delegation outcomes.
5
6use crate::errors::LitError;
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11/// Events that affect an agent's trust score
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum TrustEvent {
14    /// Agent committed code
15    Commit { hash: String },
16    /// Agent performed a code review
17    Review { target_hash: String },
18    /// Agent merged a branch
19    Merge { branch: String },
20    /// Agent's delegated task was completed successfully
21    DelegationCompleted { task_id: String },
22    /// Agent's delegated task failed or was abandoned
23    DelegationFailed { task_id: String },
24    /// Agent violated a policy (e.g., force-push to protected branch)
25    Violation { description: String },
26    /// Agent's token was revoked for cause
27    TokenRevoked { reason: String },
28    /// Peer vouched for this agent
29    PeerVouch { voucher_did: String },
30}
31
32impl TrustEvent {
33    /// Score impact of this event
34    fn score_delta(&self) -> f64 {
35        match self {
36            TrustEvent::Commit { .. } => 1.0,
37            TrustEvent::Review { .. } => 2.0,
38            TrustEvent::Merge { .. } => 1.5,
39            TrustEvent::DelegationCompleted { .. } => 3.0,
40            TrustEvent::DelegationFailed { .. } => -2.0,
41            TrustEvent::Violation { .. } => -10.0,
42            TrustEvent::TokenRevoked { .. } => -5.0,
43            TrustEvent::PeerVouch { .. } => 2.5,
44        }
45    }
46}
47
48/// Trust score record for an agent
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct TrustScore {
51    /// Agent's DID
52    pub did: String,
53    /// Current trust score (0.0 to 100.0, clamped)
54    pub score: f64,
55    /// Total events recorded
56    pub total_events: u64,
57    /// Trust level derived from score
58    pub level: TrustLevel,
59    /// Event history
60    pub events: Vec<TrustEventRecord>,
61    /// Last updated timestamp
62    pub updated: String,
63}
64
65/// Timestamped trust event record
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TrustEventRecord {
68    pub event: TrustEvent,
69    pub timestamp: String,
70    pub score_delta: f64,
71}
72
73/// Trust level labels
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub enum TrustLevel {
76    Untrusted,
77    Newcomer,
78    Contributor,
79    Trusted,
80    Maintainer,
81}
82
83impl std::fmt::Display for TrustLevel {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        match self {
86            TrustLevel::Untrusted => write!(f, "Untrusted"),
87            TrustLevel::Newcomer => write!(f, "Newcomer"),
88            TrustLevel::Contributor => write!(f, "Contributor"),
89            TrustLevel::Trusted => write!(f, "Trusted"),
90            TrustLevel::Maintainer => write!(f, "Maintainer"),
91        }
92    }
93}
94
95fn level_for_score(score: f64) -> TrustLevel {
96    if score < 10.0 {
97        TrustLevel::Untrusted
98    } else if score < 30.0 {
99        TrustLevel::Newcomer
100    } else if score < 60.0 {
101        TrustLevel::Contributor
102    } else if score < 85.0 {
103        TrustLevel::Trusted
104    } else {
105        TrustLevel::Maintainer
106    }
107}
108
109/// Trust scoring engine
110pub struct TrustEngine {
111    repo_root: std::path::PathBuf,
112}
113
114impl TrustEngine {
115    pub fn new(repo_root: &Path) -> Self {
116        TrustEngine {
117            repo_root: repo_root.to_path_buf(),
118        }
119    }
120
121    fn trust_dir(&self) -> std::path::PathBuf {
122        self.repo_root.join(".lit").join("trust")
123    }
124
125    fn score_path(&self, did: &str) -> std::path::PathBuf {
126        // Hash DID for safe filename
127        let safe_name: String = did
128            .chars()
129            .map(|c| if c.is_alphanumeric() { c } else { '_' })
130            .collect();
131        self.trust_dir().join(format!("{}.json", safe_name))
132    }
133
134    /// Get or initialize a trust score for an agent
135    pub fn get_score(&self, did: &str) -> Result<TrustScore, LitError> {
136        let path = self.score_path(did);
137        if path.exists() {
138            let json = fs::read_to_string(&path)
139                .map_err(|e| LitError::io(format!("Failed to read trust score: {}", e)))?;
140            serde_json::from_str(&json)
141                .map_err(|e| LitError::general(format!("Failed to parse trust score: {}", e)))
142        } else {
143            Ok(TrustScore {
144                did: did.to_string(),
145                score: 25.0, // Start as newcomer
146                total_events: 0,
147                level: TrustLevel::Newcomer,
148                events: Vec::new(),
149                updated: chrono::Utc::now().to_rfc3339(),
150            })
151        }
152    }
153
154    /// Record a trust event for an agent
155    pub fn record_event(&self, did: &str, event: TrustEvent) -> Result<TrustScore, LitError> {
156        let mut score = self.get_score(did)?;
157        let delta = event.score_delta();
158
159        score.events.push(TrustEventRecord {
160            event,
161            timestamp: chrono::Utc::now().to_rfc3339(),
162            score_delta: delta,
163        });
164
165        score.score = (score.score + delta).clamp(0.0, 100.0);
166        score.total_events += 1;
167        score.level = level_for_score(score.score);
168        score.updated = chrono::Utc::now().to_rfc3339();
169
170        self.save_score(&score)?;
171        Ok(score)
172    }
173
174    /// List all known agents with trust scores
175    pub fn list_agents(&self) -> Result<Vec<TrustScore>, LitError> {
176        let dir = self.trust_dir();
177        if !dir.exists() {
178            return Ok(Vec::new());
179        }
180
181        let mut scores = Vec::new();
182        for entry in fs::read_dir(&dir).map_err(|e| LitError::io(format!("IO error: {}", e)))? {
183            let entry = entry.map_err(|e| LitError::io(format!("IO error: {}", e)))?;
184            if entry.path().extension().is_some_and(|e| e == "json") {
185                if let Ok(json) = fs::read_to_string(entry.path()) {
186                    if let Ok(score) = serde_json::from_str::<TrustScore>(&json) {
187                        scores.push(score);
188                    }
189                }
190            }
191        }
192
193        scores.sort_by(|a, b| {
194            b.score
195                .partial_cmp(&a.score)
196                .unwrap_or(std::cmp::Ordering::Equal)
197        });
198        Ok(scores)
199    }
200
201    fn save_score(&self, score: &TrustScore) -> Result<(), LitError> {
202        let dir = self.trust_dir();
203        fs::create_dir_all(&dir)
204            .map_err(|e| LitError::io(format!("Failed to create trust dir: {}", e)))?;
205
206        let path = self.score_path(&score.did);
207        let json = serde_json::to_string_pretty(score)
208            .map_err(|e| LitError::general(format!("Failed to serialize trust score: {}", e)))?;
209        fs::write(&path, json)
210            .map_err(|e| LitError::io(format!("Failed to write trust score: {}", e)))?;
211        Ok(())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::path::PathBuf;
219    use std::sync::atomic::{AtomicU32, Ordering};
220
221    static COUNTER: AtomicU32 = AtomicU32::new(0);
222
223    /// Per-test scratch directory.
224    ///
225    /// Tests in this module run concurrently in one process and `TrustEngine`
226    /// persists scores to disk, so each needs its own root — otherwise a score
227    /// saved by one test is read back by another as its starting balance.
228    fn tmp_dir() -> PathBuf {
229        let n = COUNTER.fetch_add(1, Ordering::SeqCst);
230        let dir = std::env::temp_dir().join(format!("lit_trust_test_{}_{}", std::process::id(), n));
231        let _ = fs::remove_dir_all(&dir);
232        fs::create_dir_all(&dir).unwrap();
233        dir
234    }
235
236    #[test]
237    fn test_trust_score_default() {
238        let dir = tmp_dir();
239        let engine = TrustEngine::new(&dir);
240        let score = engine.get_score("did:lit:agent1").unwrap();
241        assert_eq!(score.score, 25.0);
242        assert_eq!(score.level, TrustLevel::Newcomer);
243        let _ = fs::remove_dir_all(&dir);
244    }
245
246    #[test]
247    fn test_trust_score_events() {
248        let dir = tmp_dir();
249        let engine = TrustEngine::new(&dir);
250
251        let score = engine
252            .record_event("did:lit:x", TrustEvent::Commit { hash: "abc".into() })
253            .unwrap();
254        assert_eq!(score.score, 26.0);
255
256        let score = engine
257            .record_event(
258                "did:lit:x",
259                TrustEvent::Violation {
260                    description: "force-push".into(),
261                },
262            )
263            .unwrap();
264        assert_eq!(score.score, 16.0);
265        assert_eq!(score.level, TrustLevel::Newcomer);
266
267        let _ = fs::remove_dir_all(&dir);
268    }
269
270    #[test]
271    fn test_trust_level_boundaries() {
272        assert_eq!(level_for_score(0.0), TrustLevel::Untrusted);
273        assert_eq!(level_for_score(25.0), TrustLevel::Newcomer);
274        assert_eq!(level_for_score(50.0), TrustLevel::Contributor);
275        assert_eq!(level_for_score(75.0), TrustLevel::Trusted);
276        assert_eq!(level_for_score(95.0), TrustLevel::Maintainer);
277    }
278}