1mod governance;
20mod substrate;
21
22pub use governance::LoopGovernance;
23pub use substrate::{loop_state_of, AreevSubstrate, BorrowedSubstrate};
24
25use std::time::{SystemTime, UNIX_EPOCH};
26
27pub fn scopes_for(authz: &areev_core::authz::AuthzSet) -> areev_loop::ScopeSet {
35 use areev_loop::Scope;
36 use areev_loop::LOOP_NS;
37 use areev_core::authz::Verb;
38 let mut scopes = Vec::new();
39 for (verb, scope) in [
40 (Verb::Read, Scope::Read),
41 (Verb::Write, Scope::Write),
42 (Verb::LoopReview, Scope::Review),
43 (Verb::LoopApply, Scope::Apply),
44 (Verb::Admin, Scope::Admin),
45 ] {
46 if authz.allows(verb, LOOP_NS) {
47 scopes.push(scope);
48 }
49 }
50 areev_loop::ScopeSet::of(&scopes)
51}
52
53pub fn scopes_for_rec(
76 authz: &areev_core::authz::AuthzSet,
77 scope: &[String],
78) -> areev_loop::ScopeSet {
79 use areev_core::authz::Verb;
80 use areev_loop::Scope;
81 use areev_loop::LOOP_NS;
82 let whole_queue = scopes_for(authz);
84 if scope.is_empty() {
85 return whole_queue;
86 }
87 let mut scopes = Vec::new();
88 for (verb, s) in [
89 (Verb::Read, Scope::Read),
90 (Verb::Write, Scope::Write),
91 (Verb::LoopReview, Scope::Review),
92 (Verb::LoopApply, Scope::Apply),
93 (Verb::Admin, Scope::Admin),
94 ] {
95 if authz.allows(verb, LOOP_NS) || scope.iter().all(|ns| authz.allows(verb, ns)) {
100 scopes.push(s);
101 }
102 }
103 areev_loop::ScopeSet::of(&scopes)
104}
105
106pub fn covers_rec(authz: &areev_core::authz::AuthzSet, scope: &[String]) -> bool {
113 scopes_for_rec(authz, scope).has(areev_loop::Scope::Read)
114}
115
116pub fn visible_recommendations<S: areev_loop::OmsSubstrate>(
123 engine: &areev_loop::Engine,
124 sub: &S,
125 authz: &areev_core::authz::AuthzSet,
126 status: Option<areev_loop::RecStatus>,
127) -> areev_loop::Result<Vec<areev_loop::Recommendation>> {
128 let all = engine.recommendations(sub, status)?;
129 Ok(all.into_iter().filter(|r| covers_rec(authz, &r.scope)).collect())
130}
131
132pub fn observer_for_principal(actor: &str) -> areev_loop::ObserverType {
138 match areev_core::authz::observer_kind(actor) {
139 "agent" => areev_loop::ObserverType::Agent,
140 _ => areev_loop::ObserverType::Human,
141 }
142}
143
144pub fn now_ms() -> i64 {
155 if let Ok(v) = std::env::var("AREEV_LOOP_NOW_MS") {
156 return v
157 .trim()
158 .parse()
159 .unwrap_or_else(|_| panic!("AREEV_LOOP_NOW_MS is set but not epoch milliseconds: {v:?}"));
160 }
161 SystemTime::now()
162 .duration_since(UNIX_EPOCH)
163 .map(|d| d.as_millis() as i64)
164 .unwrap_or(0)
165}
166
167#[cfg(test)]
168mod authz_mapping_tests {
169 use super::*;
170 use areev_core::authz::{AuthzSet, Grant, Verb};
171
172 fn granted(principal: &str, verbs: Vec<Verb>, ns: &[&str]) -> AuthzSet {
173 AuthzSet::restricted(
174 principal,
175 vec![Grant {
176 verbs,
177 namespaces: ns.iter().map(|s| (*s).to_string()).collect(),
178 }],
179 )
180 }
181
182 #[test]
185 fn a_namespace_grant_covers_a_finding_derived_from_that_namespace() {
186 let amy = granted("user:amy", vec![Verb::Read, Verb::LoopReview], &["a"]);
190 let s = scopes_for_rec(&amy, &["a".into()]);
191 assert!(s.has(areev_loop::Scope::Read));
192 assert!(s.has(areev_loop::Scope::Review));
193 assert!(covers_rec(&amy, &["a".into()]));
194 }
195
196 #[test]
197 fn a_namespace_grant_does_not_cover_another_namespaces_finding() {
198 let amy = granted("user:amy", vec![Verb::Read, Verb::LoopReview], &["a"]);
199 assert!(!covers_rec(&amy, &["b".into()]));
200 let s = scopes_for_rec(&amy, &["b".into()]);
201 assert!(!s.has(areev_loop::Scope::Read));
202 assert!(!s.has(areev_loop::Scope::Review));
203 }
204
205 #[test]
206 fn coverage_needs_every_namespace_a_finding_was_derived_from() {
207 let amy = granted("user:amy", vec![Verb::Read], &["a"]);
210 assert!(!covers_rec(&amy, &["a".into(), "b".into()]));
211 let both = granted("user:both", vec![Verb::Read], &["a", "b"]);
212 assert!(covers_rec(&both, &["a".into(), "b".into()]));
213 }
214
215 #[test]
216 fn a_whole_queue_grant_still_means_the_whole_queue() {
217 let op = granted(
219 "user:op",
220 vec![Verb::Read, Verb::LoopReview],
221 &[areev_loop::LOOP_NS],
222 );
223 assert!(covers_rec(&op, &["a".into()]));
224 assert!(covers_rec(&op, &["a".into(), "b".into()]));
225 assert!(scopes_for_rec(&op, &["zzz".into()]).has(areev_loop::Scope::Review));
226 assert!(covers_rec(&AuthzSet::owner("user:local"), &["a".into()]));
227 }
228
229 #[test]
230 fn an_unscoped_finding_needs_a_whole_queue_grant() {
231 let amy = granted("user:amy", vec![Verb::Read, Verb::LoopReview], &["a"]);
234 assert!(!covers_rec(&amy, &[]));
235 let op = granted("user:op", vec![Verb::Read], &[areev_loop::LOOP_NS]);
236 assert!(covers_rec(&op, &[]));
237 assert!(covers_rec(&AuthzSet::owner("user:local"), &[]));
238 }
239
240 #[test]
241 fn coverage_is_per_verb() {
242 let amy = granted("user:amy", vec![Verb::Read], &["a"]);
244 let s = scopes_for_rec(&amy, &["a".into()]);
245 assert!(s.has(areev_loop::Scope::Read));
246 assert!(!s.has(areev_loop::Scope::Review));
247 assert!(!s.has(areev_loop::Scope::Apply));
248 }
249
250 #[test]
251 fn scope_normalization_is_order_and_duplicate_independent() {
252 use areev_loop::normalize_scope;
253 assert_eq!(
254 normalize_scope(&["b".into(), "a".into(), "a".into(), " ".into()]),
255 vec!["a".to_string(), "b".to_string()]
256 );
257 }
258
259 #[test]
260 fn owner_maps_to_every_scope() {
261 let scopes = scopes_for(&AuthzSet::owner("user:local"));
262 for s in [
263 areev_loop::Scope::Read,
264 areev_loop::Scope::Write,
265 areev_loop::Scope::Review,
266 areev_loop::Scope::Apply,
267 areev_loop::Scope::Admin,
268 ] {
269 assert!(scopes.has(s), "{s:?} missing for owner");
270 }
271 }
272
273 #[test]
274 fn loop_verbs_map_one_to_one_and_namespace_scoping_holds() {
275 let reviewer = AuthzSet::restricted(
277 "user:rev",
278 vec![Grant { verbs: vec![Verb::LoopReview], namespaces: vec!["*".into()] }],
279 );
280 let scopes = scopes_for(&reviewer);
281 assert!(scopes.has(areev_loop::Scope::Review));
282 for s in [
283 areev_loop::Scope::Read,
284 areev_loop::Scope::Write,
285 areev_loop::Scope::Apply,
286 areev_loop::Scope::Admin,
287 ] {
288 assert!(!scopes.has(s), "{s:?} must not be granted");
289 }
290
291 let elsewhere = AuthzSet::restricted(
294 "user:misscoped",
295 vec![Grant { verbs: vec![Verb::LoopReview], namespaces: vec!["caller".into()] }],
296 );
297 assert!(!scopes_for(&elsewhere).has(areev_loop::Scope::Review));
298 let on_loop_ns = AuthzSet::restricted(
299 "user:scoped",
300 vec![Grant {
301 verbs: vec![Verb::LoopReview],
302 namespaces: vec![areev_loop::LOOP_NS.into()],
303 }],
304 );
305 assert!(scopes_for(&on_loop_ns).has(areev_loop::Scope::Review));
306 }
307
308 #[test]
309 fn observer_derives_from_the_actor_label() {
310 use areev_loop::ObserverType;
311 assert_eq!(observer_for_principal("agent:mcp"), ObserverType::Agent);
312 assert_eq!(observer_for_principal("bot:sweeper"), ObserverType::Agent);
313 assert_eq!(observer_for_principal("job:retention"), ObserverType::Agent);
314 assert_eq!(observer_for_principal("engine:loop.llm/1"), ObserverType::Agent);
315 assert_eq!(observer_for_principal("user:anna"), ObserverType::Human);
316 assert_eq!(observer_for_principal("anna"), ObserverType::Human);
317 }
318}