Skip to main content

areev_loop_adapter/
lib.rs

1//! # areev-loop-adapter
2//!
3//! The Areev substrate adapter for the [`areev_loop`] engine. It implements
4//! [`areev_loop::OmsSubstrate`] over [`areev_cal::AreevFacade`], so the governed
5//! self-improvement loop runs against real Areev `.mg`/Turso memory files.
6//!
7//! ```no_run
8//! use areev_loop_adapter::{AreevSubstrate, now_ms};
9//! use areev_store::Areev;
10//! use areev_loop::{Engine, RunOptions};
11//!
12//! let store = Areev::open("agent.db").unwrap();
13//! let mut sub = AreevSubstrate::new(store, None);
14//! let engine = Engine::with_builtins();
15//! let result = engine.run(&mut sub, &RunOptions::default(), now_ms()).unwrap();
16//! println!("proposed {} recommendation(s)", result.stored);
17//! ```
18
19mod governance;
20mod substrate;
21
22pub use governance::LoopGovernance;
23pub use substrate::{BorrowedSubstrate, AreevSubstrate};
24
25use std::time::{SystemTime, UNIX_EPOCH};
26
27/// Map a session's grants onto the loop engine's scope set — the one
28/// translation between Areev's verb model (`areev_core::authz`) and
29/// `areev_loop::Scope`, so surfaces stop handing out `ScopeSet::all()`
30/// unconditionally. Loop verbs are checked against the loop's own namespace
31/// (`areev_loop::LOOP_NS`), which a grant covers by naming it or `*`.
32/// An owner session maps to every scope — the CLI's local-root-of-trust
33/// behavior, unchanged.
34pub 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
53/// The observer type an actor label implies, used where no credential record
54/// declares one (the credential map will carry an explicit `observer` field
55/// when the multi-token surfaces land; this prefix heuristic is the interim
56/// derivation — a *statement* must never be able to claim humanity, so the
57/// answer always comes from the host-held actor label, not request text).
58pub fn observer_for_principal(actor: &str) -> areev_loop::ObserverType {
59    match areev_core::authz::observer_kind(actor) {
60        "agent" => areev_loop::ObserverType::Agent,
61        _ => areev_loop::ObserverType::Human,
62    }
63}
64
65/// Wall-clock now in epoch milliseconds — the `now_ms` the engine's `run`,
66/// `review`, `apply`, and `rollback` take. Kept out of `areev-loop` itself so the
67/// engine stays deterministic (the caller supplies the clock).
68///
69/// `AREEV_LOOP_NOW_MS` (epoch ms) overrides the wall clock — the simulation seam
70/// that makes a run through the real binary a pure function of (file, policy,
71/// time). The golden E2E suite uses it to pin analyzer output and to step time
72/// across outcome-review horizons and rejection cooldowns without sleeping.
73/// A set-but-unparseable value panics: the caller asked for simulated time,
74/// and silently running at wall time instead would defeat the point.
75pub fn now_ms() -> i64 {
76    if let Ok(v) = std::env::var("AREEV_LOOP_NOW_MS") {
77        return v
78            .trim()
79            .parse()
80            .unwrap_or_else(|_| panic!("AREEV_LOOP_NOW_MS is set but not epoch milliseconds: {v:?}"));
81    }
82    SystemTime::now()
83        .duration_since(UNIX_EPOCH)
84        .map(|d| d.as_millis() as i64)
85        .unwrap_or(0)
86}
87
88#[cfg(test)]
89mod authz_mapping_tests {
90    use super::*;
91    use areev_core::authz::{AuthzSet, Grant, Verb};
92
93    #[test]
94    fn owner_maps_to_every_scope() {
95        let scopes = scopes_for(&AuthzSet::owner("user:local"));
96        for s in [
97            areev_loop::Scope::Read,
98            areev_loop::Scope::Write,
99            areev_loop::Scope::Review,
100            areev_loop::Scope::Apply,
101            areev_loop::Scope::Admin,
102        ] {
103            assert!(scopes.has(s), "{s:?} missing for owner");
104        }
105    }
106
107    #[test]
108    fn loop_verbs_map_one_to_one_and_namespace_scoping_holds() {
109        // A reviewer granted on `*` gets exactly Review (plus nothing else).
110        let reviewer = AuthzSet::restricted(
111            "user:rev",
112            vec![Grant { verbs: vec![Verb::LoopReview], namespaces: vec!["*".into()] }],
113        );
114        let scopes = scopes_for(&reviewer);
115        assert!(scopes.has(areev_loop::Scope::Review));
116        for s in [
117            areev_loop::Scope::Read,
118            areev_loop::Scope::Write,
119            areev_loop::Scope::Apply,
120            areev_loop::Scope::Admin,
121        ] {
122            assert!(!scopes.has(s), "{s:?} must not be granted");
123        }
124
125        // Loop verbs are checked against the loop's own namespace — a grant
126        // scoped to some data namespace does not reach the review queue.
127        let elsewhere = AuthzSet::restricted(
128            "user:misscoped",
129            vec![Grant { verbs: vec![Verb::LoopReview], namespaces: vec!["caller".into()] }],
130        );
131        assert!(!scopes_for(&elsewhere).has(areev_loop::Scope::Review));
132        let on_loop_ns = AuthzSet::restricted(
133            "user:scoped",
134            vec![Grant {
135                verbs: vec![Verb::LoopReview],
136                namespaces: vec![areev_loop::LOOP_NS.into()],
137            }],
138        );
139        assert!(scopes_for(&on_loop_ns).has(areev_loop::Scope::Review));
140    }
141
142    #[test]
143    fn observer_derives_from_the_actor_label() {
144        use areev_loop::ObserverType;
145        assert_eq!(observer_for_principal("agent:mcp"), ObserverType::Agent);
146        assert_eq!(observer_for_principal("bot:sweeper"), ObserverType::Agent);
147        assert_eq!(observer_for_principal("job:retention"), ObserverType::Agent);
148        assert_eq!(observer_for_principal("engine:loop.llm/1"), ObserverType::Agent);
149        assert_eq!(observer_for_principal("user:anna"), ObserverType::Human);
150        assert_eq!(observer_for_principal("anna"), ObserverType::Human);
151    }
152}