Skip to main content

wm_core/
context.rs

1//! Context — Per-request execution environment
2//!
3//! The Context is passed to every tool call. It provides access to the
4//! memory store, current brain-wave state, session information, and
5//! a scratchpad for intermediate results.
6
7use crate::brain_wave::BrainWave;
8use crate::galaxy::Galaxy;
9use std::collections::HashMap;
10
11/// Per-request execution context.
12pub struct Context {
13    /// Current brain-wave state
14    pub brain_wave: BrainWave,
15    /// Session ID (if in a session)
16    pub session_id: Option<uuid::Uuid>,
17    /// User ID (for multi-user isolation)
18    pub user_id: Option<String>,
19    /// Request metadata (from MCP client)
20    pub meta: HashMap<String, serde_json::Value>,
21    /// Scratchpad for intermediate results within a single dispatch
22    pub scratchpad: HashMap<String, serde_json::Value>,
23    /// Whether this request is running under a Dharma governance profile
24    pub dharma_profile: Option<String>,
25    /// Mandala compartment (research/sandbox/production/secure)
26    pub compartment: Option<String>,
27    /// Cached karma debt (updated post-dispatch, synced periodically)
28    pub karma_debt: f32,
29    /// Intent score for this request (0.0 = low intent, 1.0 = high intent)
30    pub intent_score: f32,
31    /// Citta coherence at dispatch time (0.0–1.0). Low coherence blocks writes.
32    pub citta_coherence: f32,
33    /// Citta valence at dispatch time (−1.0 to 1.0). Negative = displeasure.
34    pub citta_valence: f32,
35    /// Self-model confidence at dispatch time (0.0–1.0).
36    /// Below 0.5 triggers conservative dispatch (prefer cached results).
37    pub self_model_confidence: f32,
38    /// Drive curiosity level (0.0–1.0). High curiosity → exploration bias.
39    pub drive_curiosity: f32,
40    /// Drive caution level (0.0–1.0). High caution → conservative bias.
41    pub drive_caution: f32,
42    /// Drive energy level (0.0–1.0). Low energy → lightweight tool bias.
43    pub drive_energy: f32,
44    /// Drive exploration weight (0.0–1.0). Derived from curiosity.
45    pub drive_exploration_weight: f32,
46    /// Drive conservative weight (0.0–1.0). Derived from caution.
47    pub drive_conservative_weight: f32,
48    /// Last Gana dispatched in this context (for co-usage tracking, Phase 6)
49    pub last_gana: Option<crate::Gana>,
50    /// Read-only server mode — the dispatch pipeline refuses any tool that
51    /// declares writes while this is set.
52    pub readonly: bool,
53    /// Operation ID (monotonic time-ordered ID) grouping multi-step write
54    /// sequences (U6). Enables crash detection across multi-tool dispatches.
55    pub operation_id: Option<String>,
56}
57
58impl Context {
59    /// Create a new context with the given brain-wave state.
60    #[must_use]
61    pub fn new(brain_wave: BrainWave) -> Self {
62        Self {
63            brain_wave,
64            session_id: None,
65            user_id: None,
66            meta: HashMap::new(),
67            scratchpad: HashMap::new(),
68            dharma_profile: None,
69            compartment: None,
70            karma_debt: 0.0,
71            intent_score: 1.0,
72            citta_coherence: 1.0,
73            citta_valence: 0.0,
74            self_model_confidence: 0.5,
75            drive_curiosity: 0.5,
76            drive_caution: 0.3,
77            drive_energy: 0.8,
78            drive_exploration_weight: 0.5,
79            drive_conservative_weight: 0.3,
80            last_gana: None,
81            readonly: false,
82            operation_id: None,
83        }
84    }
85
86    /// Set the operation ID for this context (U6).
87    #[must_use]
88    pub fn with_operation_id(mut self, op_id: impl Into<String>) -> Self {
89        self.operation_id = Some(op_id.into());
90        self
91    }
92
93    /// Get the current brain-wave state.
94    #[must_use]
95    pub const fn brain_wave(&self) -> BrainWave {
96        self.brain_wave
97    }
98
99    /// Set a scratchpad value.
100    pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
101        self.scratchpad.insert(key.into(), value);
102    }
103
104    /// Get a scratchpad value.
105    #[must_use]
106    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
107        self.scratchpad.get(key)
108    }
109
110    /// Check if this context has access to the given galaxy.
111    ///
112    /// Compartment-based access control:
113    /// - `None` (default): full access to all galaxies (backward compatible)
114    /// - `sandbox`: only Tutorial and Research (no production data)
115    /// - `production`: all memory galaxies except system (Karma, Dharma, Substrate)
116    /// - `secure`: all memory galaxies
117    #[must_use]
118    pub fn can_access_galaxy(&self, galaxy: Galaxy) -> bool {
119        match self.compartment.as_deref() {
120            None => true,
121            Some("sandbox") => matches!(galaxy, Galaxy::Tutorial | Galaxy::Research),
122            Some("production") => !matches!(
123                galaxy,
124                Galaxy::Karma
125                    | Galaxy::Dharma
126                    | Galaxy::Substrate
127                    | Galaxy::Associations
128                    | Galaxy::Embeddings
129            ),
130            Some("secure") => matches!(
131                galaxy,
132                Galaxy::Aria
133                    | Galaxy::Citta
134                    | Galaxy::Codex
135                    | Galaxy::Journals
136                    | Galaxy::Dreams
137                    | Galaxy::Research
138                    | Galaxy::Sessions
139                    | Galaxy::Substrate
140                    | Galaxy::Tutorial
141                    | Galaxy::Universal
142                    | Galaxy::Valkyrie
143            ),
144            // Unknown compartment values fail closed — no galaxy access.
145            Some(_) => false,
146        }
147    }
148
149    /// Check if this context allows write operations to the given galaxy.
150    ///
151    /// Sandbox is read-only for Research (can write to Tutorial only).
152    #[must_use]
153    pub fn can_write_galaxy(&self, galaxy: Galaxy) -> bool {
154        match self.compartment.as_deref() {
155            None => true,
156            Some("sandbox") => matches!(galaxy, Galaxy::Tutorial),
157            Some("production" | "secure") => self.can_access_galaxy(galaxy),
158            // Unknown compartment values fail closed — no write access.
159            Some(_) => false,
160        }
161    }
162}
163
164impl Default for Context {
165    fn default() -> Self {
166        Self::new(BrainWave::Gamma)
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::galaxy::Galaxy;
174
175    #[test]
176    fn no_compartment_has_full_access() {
177        let ctx = Context::default();
178        for g in Galaxy::all() {
179            assert!(
180                ctx.can_access_galaxy(g),
181                "No compartment should access {g:?}"
182            );
183            assert!(ctx.can_write_galaxy(g), "No compartment should write {g:?}");
184        }
185    }
186
187    fn make_ctx(compartment: &str) -> Context {
188        Context {
189            compartment: Some(compartment.into()),
190            ..Context::default()
191        }
192    }
193
194    #[test]
195    fn sandbox_can_only_read_tutorial_and_research() {
196        let ctx = make_ctx("sandbox");
197        assert!(ctx.can_access_galaxy(Galaxy::Tutorial));
198        assert!(ctx.can_access_galaxy(Galaxy::Research));
199        assert!(!ctx.can_access_galaxy(Galaxy::Codex));
200        assert!(!ctx.can_access_galaxy(Galaxy::Karma));
201        // Sandbox can only write to Tutorial
202        assert!(ctx.can_write_galaxy(Galaxy::Tutorial));
203        assert!(!ctx.can_write_galaxy(Galaxy::Research));
204    }
205
206    #[test]
207    fn production_cannot_access_system_galaxies() {
208        let ctx = make_ctx("production");
209        assert!(ctx.can_access_galaxy(Galaxy::Codex));
210        assert!(ctx.can_access_galaxy(Galaxy::Research));
211        assert!(!ctx.can_access_galaxy(Galaxy::Karma));
212        assert!(!ctx.can_access_galaxy(Galaxy::Dharma));
213        assert!(!ctx.can_access_galaxy(Galaxy::Substrate));
214        assert!(ctx.can_write_galaxy(Galaxy::Codex));
215    }
216
217    #[test]
218    fn secure_can_access_all_memory_galaxies() {
219        let ctx = make_ctx("secure");
220        assert!(ctx.can_access_galaxy(Galaxy::Codex));
221        assert!(ctx.can_access_galaxy(Galaxy::Substrate));
222        assert!(ctx.can_write_galaxy(Galaxy::Codex));
223        // Secure still can't access non-memory galaxies
224        assert!(!ctx.can_access_galaxy(Galaxy::Karma));
225    }
226
227    #[test]
228    fn unknown_compartment_fails_closed() {
229        let ctx = make_ctx("bogus");
230        for g in Galaxy::all() {
231            assert!(
232                !ctx.can_access_galaxy(g),
233                "unknown compartment must not read {g:?}"
234            );
235            assert!(
236                !ctx.can_write_galaxy(g),
237                "unknown compartment must not write {g:?}"
238            );
239        }
240    }
241
242    #[test]
243    fn operation_id_propagates_and_defaults_none() {
244        let ctx = Context::default();
245        assert_eq!(ctx.operation_id, None);
246
247        let ctx_with_op = ctx.with_operation_id("01H7XYZ0000000000000000000");
248        assert_eq!(
249            ctx_with_op.operation_id.as_deref(),
250            Some("01H7XYZ0000000000000000000")
251        );
252    }
253}