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