agent_engine/tools/
confirm_latch.rs1use std::collections::BTreeSet;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Mutex;
21
22pub type LatchKey = (String, u64);
24
25pub fn latch_keys(sources: &[String], generations: &[u64]) -> Vec<LatchKey> {
27 sources
28 .iter()
29 .enumerate()
30 .map(|(i, s)| (s.clone(), generations.get(i).copied().unwrap_or(0)))
31 .collect()
32}
33
34#[derive(Debug, Default)]
35pub struct SessionConfirmLatch {
36 allowed: Mutex<BTreeSet<LatchKey>>,
37 revoked: AtomicBool,
38}
39
40impl SessionConfirmLatch {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 fn set(&self) -> std::sync::MutexGuard<'_, BTreeSet<LatchKey>> {
46 self.allowed.lock().unwrap_or_else(|e| e.into_inner())
47 }
48
49 pub fn allows_all(&self, keys: &[LatchKey]) -> bool {
51 if keys.is_empty() || self.revoked.load(Ordering::Acquire) {
52 return false;
53 }
54 let set = self.set();
55 keys.iter().all(|k| set.contains(k))
56 }
57
58 pub fn allow(&self, keys: &[LatchKey]) -> bool {
62 if keys.is_empty() || self.revoked.load(Ordering::Acquire) {
63 return false;
64 }
65 let mut set = self.set();
66 for k in keys {
67 set.insert(k.clone());
68 }
69 true
70 }
71
72 pub fn clear(&self) -> Vec<String> {
75 let taken = std::mem::take(&mut *self.set());
76 let mut ids: Vec<String> = taken.into_iter().map(|(id, _)| id).collect();
77 ids.dedup();
78 ids
79 }
80
81 pub fn revoke(&self) {
83 self.revoked.store(true, Ordering::Release);
84 self.clear();
85 }
86
87 pub fn extensions(&self) -> Vec<String> {
89 let mut ids: Vec<String> = self.set().iter().map(|(id, _)| id.clone()).collect();
90 ids.dedup();
91 ids
92 }
93
94 pub fn is_empty(&self) -> bool {
95 self.set().is_empty()
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn k(v: &[&str]) -> Vec<LatchKey> {
104 v.iter().map(|x| (x.to_string(), 0)).collect()
105 }
106
107 #[test]
108 fn latch_is_scoped_to_the_answering_extension() {
109 let latch = SessionConfirmLatch::new();
110 assert!(!latch.allows_all(&k(&["jev"])));
111 assert!(latch.allow(&k(&["jev"])));
112 assert!(latch.allows_all(&k(&["jev"])));
113 assert!(
114 !latch.allows_all(&k(&["other"])),
115 "other extensions still prompt"
116 );
117 assert!(
118 !latch.allows_all(&k(&["jev", "other"])),
119 "a multi-source confirm needs every source latched"
120 );
121 }
122
123 #[test]
124 fn reinstalled_extension_under_same_id_is_not_latched() {
125 let latch = SessionConfirmLatch::new();
126 latch.allow(&[("jev".to_string(), 0)]);
127 assert!(latch.allows_all(&[("jev".to_string(), 0)]));
128 assert!(
129 !latch.allows_all(&[("jev".to_string(), 1)]),
130 "a new load generation of the same id must prompt again"
131 );
132 assert_eq!(
133 latch_keys(&["a".into(), "b".into()], &[4]),
134 vec![("a".to_string(), 4), ("b".to_string(), 0)]
135 );
136 }
137
138 #[test]
139 fn unattributed_confirm_is_never_latched() {
140 let latch = SessionConfirmLatch::new();
141 assert!(!latch.allow(&[]));
142 assert!(latch.is_empty());
143 assert!(!latch.allows_all(&[]));
144 }
145
146 #[test]
147 fn clear_revokes_everything() {
148 let latch = SessionConfirmLatch::new();
149 latch.allow(&k(&["b", "a"]));
150 assert_eq!(latch.extensions(), vec!["a".to_string(), "b".to_string()]);
151 assert_eq!(latch.clear(), vec!["a".to_string(), "b".to_string()]);
152 assert!(latch.is_empty());
153 assert!(!latch.allows_all(&k(&["a"])));
154 assert!(latch.allow(&k(&["a"])), "reset latch can be armed again");
155 }
156
157 #[test]
158 fn revoked_latch_can_never_be_rearmed() {
159 let latch = SessionConfirmLatch::new();
160 latch.allow(&k(&["a"]));
161 latch.revoke();
162 assert!(latch.is_empty());
163 assert!(!latch.allow(&k(&["a"])), "stale clone cannot re-arm");
164 assert!(!latch.allows_all(&k(&["a"])));
165 }
166}