agentmux_srv/backend/storage/
conversation_trust_grants.rs1use rusqlite::params;
20
21use super::error::StoreError;
22use super::store::Store;
23
24use agentmux_common::time::now_secs;
25
26impl Store {
27 pub fn conversation_trust_grant_check(
33 &self,
34 agent_id: &str,
35 requester_agent_id: &str,
36 tier: &str,
37 ) -> Result<bool, StoreError> {
38 if tier.eq_ignore_ascii_case("wan") {
46 return Ok(false);
47 }
48 let agent_key = agent_id.to_lowercase();
49 let requester_key = requester_agent_id.to_lowercase();
50 let conn = self.conn.lock().unwrap();
51 let count: i64 = conn.query_row(
52 "SELECT COUNT(*) FROM db_conversation_trust_grants \
53 WHERE agent_id = ?1 AND granted_peer_agent_id = ?2 AND tier = ?3",
54 params![agent_key, requester_key, tier],
55 |row| row.get(0),
56 )?;
57 Ok(count > 0)
58 }
59
60 #[allow(dead_code)] pub fn conversation_trust_grant_add(
69 &self,
70 agent_id: &str,
71 requester_agent_id: &str,
72 tier: &str,
73 ) -> Result<(), StoreError> {
74 let agent_key = agent_id.to_lowercase();
75 let requester_key = requester_agent_id.to_lowercase();
76 let conn = self.conn.lock().unwrap();
77 conn.execute(
78 "INSERT OR REPLACE INTO db_conversation_trust_grants \
79 (agent_id, granted_peer_agent_id, tier, granted_at) VALUES (?1, ?2, ?3, ?4)",
80 params![agent_key, requester_key, tier, now_secs()],
81 )?;
82 Ok(())
83 }
84
85 #[allow(dead_code)] pub fn conversation_trust_grant_revoke(
89 &self,
90 agent_id: &str,
91 requester_agent_id: &str,
92 tier: &str,
93 ) -> Result<(), StoreError> {
94 let agent_key = agent_id.to_lowercase();
95 let requester_key = requester_agent_id.to_lowercase();
96 let conn = self.conn.lock().unwrap();
97 conn.execute(
98 "DELETE FROM db_conversation_trust_grants \
99 WHERE agent_id = ?1 AND granted_peer_agent_id = ?2 AND tier = ?3",
100 params![agent_key, requester_key, tier],
101 )?;
102 Ok(())
103 }
104
105 #[allow(dead_code)] pub fn conversation_trust_grant_list(
109 &self,
110 agent_id: &str,
111 ) -> Result<Vec<(String, String, i64)>, StoreError> {
112 let agent_key = agent_id.to_lowercase();
113 let conn = self.conn.lock().unwrap();
114 let mut stmt = conn.prepare(
115 "SELECT granted_peer_agent_id, tier, granted_at FROM db_conversation_trust_grants \
116 WHERE agent_id = ?1 ORDER BY granted_at DESC",
117 )?;
118 let rows = stmt.query_map(params![agent_key], |row| {
119 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
120 })?;
121 let mut out = Vec::new();
122 for r in rows {
123 out.push(r?);
124 }
125 Ok(out)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn object_store() -> Store {
134 Store::open(std::path::Path::new(":memory:")).unwrap()
135 }
136
137 #[test]
138 fn check_returns_false_when_ungranted() {
139 let store = object_store();
140 assert!(!store.conversation_trust_grant_check("korp", "loap", "lan").unwrap());
141 }
142
143 #[test]
144 fn add_then_check_returns_true() {
145 let store = object_store();
146 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
147 assert!(store.conversation_trust_grant_check("korp", "loap", "lan").unwrap());
148 }
149
150 #[test]
153 fn a_wan_grant_is_never_honoured() {
154 let store = object_store();
155 store.conversation_trust_grant_add("korp", "loap", "wan").unwrap();
156 assert!(!store.conversation_trust_grant_check("korp", "loap", "wan").unwrap());
157 assert!(!store.conversation_trust_grant_check("korp", "loap", "WAN").unwrap());
158 }
159
160 #[test]
161 fn grant_is_scoped_to_its_own_tier() {
162 let store = object_store();
163 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
164 assert!(
165 !store.conversation_trust_grant_check("korp", "loap", "wan").unwrap(),
166 "a LAN grant must not be assumed to cover WAN — different cryptographic identity guarantee"
167 );
168 }
169
170 #[test]
171 fn grant_is_scoped_to_its_own_responding_agent() {
172 let store = object_store();
173 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
174 assert!(
175 !store.conversation_trust_grant_check("agentx", "loap", "lan").unwrap(),
176 "a grant made by one agent must not apply to a different agent's own visibility check"
177 );
178 }
179
180 #[test]
181 fn revoke_removes_the_grant() {
182 let store = object_store();
183 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
184 store.conversation_trust_grant_revoke("korp", "loap", "lan").unwrap();
185 assert!(!store.conversation_trust_grant_check("korp", "loap", "lan").unwrap());
186 }
187
188 #[test]
189 fn revoke_of_a_never_granted_peer_is_a_harmless_no_op() {
190 let store = object_store();
191 store.conversation_trust_grant_revoke("korp", "loap", "lan").unwrap();
192 assert!(!store.conversation_trust_grant_check("korp", "loap", "lan").unwrap());
193 }
194
195 #[test]
196 fn granting_twice_is_idempotent() {
197 let store = object_store();
198 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
199 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
200 let list = store.conversation_trust_grant_list("korp").unwrap();
201 assert_eq!(list.len(), 1, "granting the same peer/tier twice must not duplicate the row");
202 }
203
204 #[test]
205 fn agent_id_lookup_is_case_insensitive() {
206 let store = object_store();
207 store.conversation_trust_grant_add("Korp", "Loap", "lan").unwrap();
208 assert!(store.conversation_trust_grant_check("korp", "loap", "lan").unwrap());
209 assert!(store.conversation_trust_grant_check("KORP", "LOAP", "lan").unwrap());
210 }
211
212 #[test]
213 fn list_returns_every_grant_for_the_agent_across_tiers() {
214 let store = object_store();
215 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
216 store.conversation_trust_grant_add("korp", "agentx", "wan").unwrap();
217 let list = store.conversation_trust_grant_list("korp").unwrap();
218 assert_eq!(list.len(), 2);
219 let peers: std::collections::HashSet<_> = list.iter().map(|(p, _, _)| p.clone()).collect();
220 assert_eq!(peers, ["loap", "agentx"].into_iter().map(String::from).collect());
221 }
222
223 #[test]
224 fn list_never_returns_a_different_agents_grants() {
225 let store = object_store();
226 store.conversation_trust_grant_add("korp", "loap", "lan").unwrap();
227 store.conversation_trust_grant_add("agentx", "loap", "lan").unwrap();
228 let list = store.conversation_trust_grant_list("korp").unwrap();
229 assert_eq!(list.len(), 1);
230 }
231}