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 pub fn new() -> Self {
29 LoopGovernance { policy: None }
30 }
31
32 pub fn with_policy(policy: Policy) -> Self {
36 LoopGovernance { policy: Some(policy) }
37 }
38
39 fn engine(&self) -> Engine {
40 match &self.policy {
41 Some(p) => Engine::with_builtins().with_policy(p.clone()),
42 None => Engine::with_builtins(),
43 }
44 }
45
46 fn facade<'a>(&self, store: &'a dyn CalStoreFacade) -> Result<&'a AreevFacade> {
50 store
51 .as_any()
52 .and_then(|a| a.downcast_ref::<AreevFacade>())
53 .ok_or_else(|| {
54 AreevError::Internal(
55 "governance requires the Areev facade — this store cannot host the loop"
56 .into(),
57 )
58 })
59 }
60
61 fn wrap(e: areev_loop::Error) -> AreevError {
62 AreevError::Validation(e.to_string())
65 }
66}
67
68impl Default for LoopGovernance {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl GovernanceHost for LoopGovernance {
75 fn run_loop(
76 &self,
77 store: &dyn CalStoreFacade,
78 opts: &RunLoopOptions,
79 ) -> Result<serde_json::Value> {
80 let facade = self.facade(store)?;
81 facade
88 .authz()
89 .check(areev_core::authz::Verb::LoopRun, areev_loop::LOOP_NS)?;
90 let mut sub = crate::BorrowedSubstrate::new(facade);
91 let run_opts = RunOptions {
92 min_new: opts.min_new,
93 if_stale_ms: opts.if_stale_ms,
94 full_sweep: opts.full_sweep,
95 triggering_actor: Some(facade.authz().principal().to_string()),
99 ..RunOptions::default()
100 };
101 let res = self
102 .engine()
103 .run(&mut sub, &run_opts, crate::now_ms())
104 .map_err(Self::wrap)?;
105 serde_json::to_value(&res).map_err(|e| AreevError::Internal(e.to_string()))
106 }
107
108 fn review(
109 &self,
110 store: &dyn CalStoreFacade,
111 rec_hash: &str,
112 decision: ReviewDecision,
113 because: &str,
114 ) -> Result<()> {
115 let facade = self.facade(store)?;
116 let actor = facade.authz().principal().to_string();
117 let scopes = crate::scopes_for(&facade.authz());
118 let observer = crate::observer_for_principal(&actor);
119 let mut sub = crate::BorrowedSubstrate::new(facade);
120 let d = match decision {
121 ReviewDecision::Approve => Decision::Approve,
122 ReviewDecision::Reject => Decision::Reject,
123 };
124 self.engine()
125 .review(&mut sub, rec_hash, d, &actor, observer, &scopes, because, crate::now_ms())
126 .map_err(Self::wrap)
127 }
128
129 fn apply(
130 &self,
131 store: &dyn CalStoreFacade,
132 rec_hash: &str,
133 because: &str,
134 destructive_cap: bool,
135 ) -> Result<bool> {
136 let facade = self.facade(store)?;
137 let authz = facade.authz();
138 let actor = authz.principal().to_string();
139 let scopes = crate::scopes_for(&authz);
140 let observer = crate::observer_for_principal(&actor);
141 let allow_destructive = destructive_cap
147 && (authz.allows(areev_core::authz::Verb::Delete, "*")
148 || authz.allows(areev_core::authz::Verb::Erase, "*"));
149 let mut sub = crate::BorrowedSubstrate::new(facade);
150 let applied = self
151 .engine()
152 .apply(
153 &mut sub,
154 rec_hash,
155 &actor,
156 observer,
157 &scopes,
158 because,
159 allow_destructive,
160 crate::now_ms(),
161 )
162 .map_err(Self::wrap)?;
163 Ok(applied.rollbackable)
164 }
165
166 fn rollback(&self, store: &dyn CalStoreFacade, rec_hash: &str, because: &str) -> Result<()> {
167 let facade = self.facade(store)?;
168 let actor = facade.authz().principal().to_string();
169 let scopes = crate::scopes_for(&facade.authz());
170 let observer = crate::observer_for_principal(&actor);
171 let mut sub = crate::BorrowedSubstrate::new(facade);
172 self.engine()
173 .rollback(&mut sub, rec_hash, &actor, observer, &scopes, because, crate::now_ms())
174 .map_err(Self::wrap)
175 }
176
177 fn describe(&self, store: &dyn CalStoreFacade, what: LoopInfo) -> Result<serde_json::Value> {
178 let facade = self.facade(store)?;
179 facade
182 .authz()
183 .check(areev_core::authz::Verb::Read, areev_loop::LOOP_NS)?;
184 let sub = crate::BorrowedSubstrate::new(facade);
185 let engine = self.engine();
186 let v = match what {
187 LoopInfo::Loop => {
188 let health = engine.health(&sub, crate::now_ms()).map_err(Self::wrap)?;
192 let pending = engine
193 .recommendations(&sub, Some(areev_loop::RecStatus::Pending))
194 .map_err(Self::wrap)?;
195 serde_json::to_value(health).map(|mut v| {
196 v["pending_recommendations"] = serde_json::Value::Array(
197 pending
198 .iter()
199 .map(|r| {
200 serde_json::json!({
201 "hash": r.hash,
202 "analyzer": r.analyzer,
203 "severity": format!("{:?}", r.severity).to_lowercase(),
204 "summary": r.summary.render(),
205 })
206 })
207 .collect(),
208 );
209 v
210 })
211 }
212 LoopInfo::Analyzers => Ok(serde_json::Value::Array(
213 engine
214 .analyzers()
215 .iter()
216 .map(|a| {
217 let m = a.manifest();
218 serde_json::json!({
219 "id": m.id,
220 "family": m.family(),
221 "default_on": m.default_on,
222 })
223 })
224 .collect(),
225 )),
226 LoopInfo::Outcomes => {
227 serde_json::to_value(engine.outcomes(&sub).map_err(Self::wrap)?)
228 }
229 LoopInfo::Policy => Ok(match &self.policy {
230 None => serde_json::json!({
231 "attached": false,
232 "note": "no host policy attached — the closed default: nothing \
233 auto-applies, nothing is denied",
234 }),
235 Some(p) => serde_json::json!({
236 "attached": true,
237 "auto_apply_enabled": p.auto_apply_enabled,
238 "denied_families": p.deny,
239 }),
240 }),
241 };
242 v.map_err(|e| AreevError::Internal(e.to_string()))
243 }
244}