1use areev_cal::governance::{GovernanceHost, LoopInfo, ReviewDecision, RunLoopOptions};
15use areev_cal::{CalStoreFacade, AreevFacade};
16use areev_core::error::{AreevError, Result};
17use areev_loop::{Decision, Engine, Policy, RunOptions};
18
19pub struct LoopGovernance {
24 policy: Option<Policy>,
25}
26
27impl LoopGovernance {
28 fn scopes_over(
36 &self,
37 facade: &areev_cal::AreevFacade,
38 sub: &mut crate::BorrowedSubstrate<'_>,
39 rec_hash: &str,
40 ) -> Result<areev_loop::ScopeSet> {
41 let authz = facade.authz();
42 let scope = self
43 .engine()
44 .recommendations(sub, None)
45 .ok()
46 .and_then(|recs| recs.into_iter().find(|r| r.hash == rec_hash))
47 .map(|r| r.scope)
48 .unwrap_or_default();
49 if !crate::covers_rec(&authz, &scope) {
50 return Err(Self::wrap(areev_loop::Error::NotFound(rec_hash.into())));
51 }
52 Ok(crate::scopes_for_rec(&authz, &scope))
53 }
54
55
56 pub fn new() -> Self {
57 LoopGovernance { policy: None }
58 }
59
60 pub fn with_policy(policy: Policy) -> Self {
64 LoopGovernance { policy: Some(policy) }
65 }
66
67 fn engine(&self) -> Engine {
68 match &self.policy {
69 Some(p) => Engine::with_builtins().with_policy(p.clone()),
70 None => Engine::with_builtins(),
71 }
72 }
73
74 fn facade<'a>(&self, store: &'a dyn CalStoreFacade) -> Result<&'a AreevFacade> {
78 store
79 .as_any()
80 .and_then(|a| a.downcast_ref::<AreevFacade>())
81 .ok_or_else(|| {
82 AreevError::Internal(
83 "governance requires the Areev facade — this store cannot host the loop"
84 .into(),
85 )
86 })
87 }
88
89 fn wrap(e: areev_loop::Error) -> AreevError {
90 AreevError::Validation(e.to_string())
93 }
94}
95
96impl Default for LoopGovernance {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102impl GovernanceHost for LoopGovernance {
103 fn run_loop(
104 &self,
105 store: &dyn CalStoreFacade,
106 opts: &RunLoopOptions,
107 ) -> Result<serde_json::Value> {
108 let facade = self.facade(store)?;
109 facade
116 .authz()
117 .check(areev_core::authz::Verb::LoopRun, areev_loop::LOOP_NS)?;
118 let mut sub = crate::BorrowedSubstrate::new(facade);
119 let run_opts = RunOptions {
120 min_new: opts.min_new,
121 if_stale_ms: opts.if_stale_ms,
122 full_sweep: opts.full_sweep,
123 triggering_actor: Some(facade.authz().principal().to_string()),
127 ..RunOptions::default()
128 };
129 let res = self
130 .engine()
131 .run(&mut sub, &run_opts, crate::now_ms())
132 .map_err(Self::wrap)?;
133 serde_json::to_value(&res).map_err(|e| AreevError::Internal(e.to_string()))
134 }
135
136
137 fn review(
138 &self,
139 store: &dyn CalStoreFacade,
140 rec_hash: &str,
141 decision: ReviewDecision,
142 because: &str,
143 ) -> Result<()> {
144 let facade = self.facade(store)?;
145 let actor = facade.authz().principal().to_string();
146 let observer = crate::observer_for_principal(&actor);
147 let mut sub = crate::BorrowedSubstrate::new(facade);
148 let scopes = self.scopes_over(facade, &mut sub, rec_hash)?;
149 let d = match decision {
150 ReviewDecision::Approve => Decision::Approve,
151 ReviewDecision::Reject => Decision::Reject,
152 };
153 self.engine()
154 .review(&mut sub, rec_hash, d, &actor, observer, &scopes, because, crate::now_ms())
155 .map_err(Self::wrap)
156 }
157
158 fn apply(
159 &self,
160 store: &dyn CalStoreFacade,
161 rec_hash: &str,
162 because: &str,
163 destructive_cap: bool,
164 ) -> Result<bool> {
165 let facade = self.facade(store)?;
166 let authz = facade.authz();
167 let actor = authz.principal().to_string();
168 let observer = crate::observer_for_principal(&actor);
169 let allow_destructive = destructive_cap
175 && (authz.allows(areev_core::authz::Verb::Delete, "*")
176 || authz.allows(areev_core::authz::Verb::Erase, "*"));
177 let mut sub = crate::BorrowedSubstrate::new(facade);
178 let scopes = self.scopes_over(facade, &mut sub, rec_hash)?;
179 let applied = self
180 .engine()
181 .apply(
182 &mut sub,
183 rec_hash,
184 &actor,
185 observer,
186 &scopes,
187 because,
188 allow_destructive,
189 crate::now_ms(),
190 )
191 .map_err(Self::wrap)?;
192 Ok(applied.rollbackable)
193 }
194
195 fn rollback(&self, store: &dyn CalStoreFacade, rec_hash: &str, because: &str) -> Result<()> {
196 let facade = self.facade(store)?;
197 let actor = facade.authz().principal().to_string();
198 let observer = crate::observer_for_principal(&actor);
199 let mut sub = crate::BorrowedSubstrate::new(facade);
200 let scopes = self.scopes_over(facade, &mut sub, rec_hash)?;
201 self.engine()
202 .rollback(&mut sub, rec_hash, &actor, observer, &scopes, because, crate::now_ms())
203 .map_err(Self::wrap)
204 }
205
206 fn describe(&self, store: &dyn CalStoreFacade, what: LoopInfo) -> Result<serde_json::Value> {
207 let facade = self.facade(store)?;
208 facade
211 .authz()
212 .check(areev_core::authz::Verb::Read, areev_loop::LOOP_NS)?;
213 let authz = facade.authz();
214 let sub = crate::BorrowedSubstrate::new(facade);
215 let engine = self.engine();
216 let v = match what {
217 LoopInfo::Loop => {
218 let health = engine.health(&sub, crate::now_ms()).map_err(Self::wrap)?;
222 let pending = crate::visible_recommendations(
225 &engine,
226 &sub,
227 &authz,
228 Some(areev_loop::RecStatus::Pending),
229 )
230 .map_err(Self::wrap)?;
231 serde_json::to_value(health).map(|mut v| {
232 v["pending_recommendations"] = serde_json::Value::Array(
233 pending
234 .iter()
235 .map(|r| {
236 serde_json::json!({
237 "hash": r.hash,
238 "analyzer": r.analyzer,
239 "severity": format!("{:?}", r.severity).to_lowercase(),
240 "summary": r.summary.render(),
241 })
242 })
243 .collect(),
244 );
245 v
246 })
247 }
248 LoopInfo::Analyzers => Ok(serde_json::Value::Array(
249 engine
250 .analyzers()
251 .iter()
252 .map(|a| {
253 let m = a.manifest();
254 serde_json::json!({
255 "id": m.id,
256 "family": m.family(),
257 "default_on": m.default_on,
258 })
259 })
260 .collect(),
261 )),
262 LoopInfo::Outcomes => {
263 serde_json::to_value(engine.outcomes(&sub).map_err(Self::wrap)?)
264 }
265 LoopInfo::Policy => Ok(match &self.policy {
266 None => serde_json::json!({
267 "attached": false,
268 "note": "no host policy attached — the closed default: nothing \
269 auto-applies, nothing is denied",
270 }),
271 Some(p) => serde_json::json!({
272 "attached": true,
273 "auto_apply_enabled": p.auto_apply_enabled,
274 "denied_families": p.deny,
275 }),
276 }),
277 };
278 v.map_err(|e| AreevError::Internal(e.to_string()))
279 }
280}