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    /// Whether this dispatch carries an explicit `confirm: true` from the
55    /// caller. The dispatch pipeline sets it before the Dharma gate runs so
56    /// that deliberate operator intent can pass the brain-wave strict-mode
57    /// block (Theta/Delta) for destructive tools — stressed-homeostasis
58    /// blocks stay absolute. 9.1.6.
59    pub explicit_confirm: bool,
60    /// Operation ID (monotonic time-ordered ID) grouping multi-step write
61    /// sequences (U6). Enables crash detection across multi-tool dispatches.
62    pub operation_id: Option<String>,
63    /// Subprocess spawn confinement policy (B2). The dispatcher resolves
64    /// it for tools that declare `Sandbox::Subprocess` (or `spawns`) and
65    /// tools build every external command through
66    /// [`SpawnPolicy::command`](crate::sandbox::SpawnPolicy::command).
67    pub spawn: SpawnPolicy,
68}
69
70impl Context {
71    /// Create a new context with the given brain-wave state.
72    #[must_use]
73    pub fn new(brain_wave: BrainWave) -> Self {
74        Self {
75            brain_wave,
76            session_id: None,
77            user_id: None,
78            meta: HashMap::new(),
79            scratchpad: HashMap::new(),
80            dharma_profile: None,
81            compartment: None,
82            karma_debt: 0.0,
83            intent_score: 1.0,
84            citta_coherence: 1.0,
85            citta_valence: 0.0,
86            self_model_confidence: 0.5,
87            drive_curiosity: 0.5,
88            drive_caution: 0.3,
89            drive_energy: 0.8,
90            drive_exploration_weight: 0.5,
91            drive_conservative_weight: 0.3,
92            last_gana: None,
93            readonly: false,
94            explicit_confirm: false,
95            operation_id: None,
96            spawn: SpawnPolicy::disabled(),
97        }
98    }
99
100    /// Set the operation ID for this context (U6).
101    #[must_use]
102    pub fn with_operation_id(mut self, op_id: impl Into<String>) -> Self {
103        self.operation_id = Some(op_id.into());
104        self
105    }
106
107    /// Get the current brain-wave state.
108    #[must_use]
109    pub const fn brain_wave(&self) -> BrainWave {
110        self.brain_wave
111    }
112
113    /// Set a scratchpad value.
114    pub fn set(&mut self, key: impl Into<String>, value: serde_json::Value) {
115        self.scratchpad.insert(key.into(), value);
116    }
117
118    /// Get a scratchpad value.
119    #[must_use]
120    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
121        self.scratchpad.get(key)
122    }
123
124    /// Check if this context has access to the given galaxy.
125    ///
126    /// Compartment-based access control:
127    /// - `None` (default): full access to all galaxies (backward compatible)
128    /// - `sandbox`: only Tutorial and Research (no production data)
129    /// - `production`: all memory galaxies except system (Karma, Dharma, Substrate)
130    /// - `secure`: all memory galaxies
131    #[must_use]
132    pub fn can_access_galaxy(&self, galaxy: Galaxy) -> bool {
133        match self.compartment.as_deref() {
134            None => true,
135            Some("sandbox") => matches!(galaxy, Galaxy::Tutorial | Galaxy::Research),
136            Some("production") => !matches!(
137                galaxy,
138                Galaxy::Karma
139                    | Galaxy::Dharma
140                    | Galaxy::Substrate
141                    | Galaxy::Associations
142                    | Galaxy::Embeddings
143            ),
144            Some("secure") => matches!(
145                galaxy,
146                Galaxy::Aria
147                    | Galaxy::Citta
148                    | Galaxy::Codex
149                    | Galaxy::Journals
150                    | Galaxy::Dreams
151                    | Galaxy::Research
152                    | Galaxy::Sessions
153                    | Galaxy::Substrate
154                    | Galaxy::Tutorial
155                    | Galaxy::Universal
156                    | Galaxy::Valkyrie
157            ),
158            // Unknown compartment values fail closed — no galaxy access.
159            Some(_) => false,
160        }
161    }
162
163    /// Check if this context allows write operations to the given galaxy.
164    ///
165    /// Sandbox is read-only for Research (can write to Tutorial only).
166    #[must_use]
167    pub fn can_write_galaxy(&self, galaxy: Galaxy) -> bool {
168        match self.compartment.as_deref() {
169            None => true,
170            Some("sandbox") => matches!(galaxy, Galaxy::Tutorial),
171            Some("production" | "secure") => self.can_access_galaxy(galaxy),
172            // Unknown compartment values fail closed — no write access.
173            Some(_) => false,
174        }
175    }
176}
177
178impl Default for Context {
179    fn default() -> Self {
180        Self::new(BrainWave::Gamma)
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::galaxy::Galaxy;
188
189    #[test]
190    fn no_compartment_has_full_access() {
191        let ctx = Context::default();
192        for g in Galaxy::all() {
193            assert!(
194                ctx.can_access_galaxy(g),
195                "No compartment should access {g:?}"
196            );
197            assert!(ctx.can_write_galaxy(g), "No compartment should write {g:?}");
198        }
199    }
200
201    fn make_ctx(compartment: &str) -> Context {
202        Context {
203            compartment: Some(compartment.into()),
204            ..Context::default()
205        }
206    }
207
208    #[test]
209    fn sandbox_can_only_read_tutorial_and_research() {
210        let ctx = make_ctx("sandbox");
211        assert!(ctx.can_access_galaxy(Galaxy::Tutorial));
212        assert!(ctx.can_access_galaxy(Galaxy::Research));
213        assert!(!ctx.can_access_galaxy(Galaxy::Codex));
214        assert!(!ctx.can_access_galaxy(Galaxy::Karma));
215        // Sandbox can only write to Tutorial
216        assert!(ctx.can_write_galaxy(Galaxy::Tutorial));
217        assert!(!ctx.can_write_galaxy(Galaxy::Research));
218    }
219
220    #[test]
221    fn production_cannot_access_system_galaxies() {
222        let ctx = make_ctx("production");
223        assert!(ctx.can_access_galaxy(Galaxy::Codex));
224        assert!(ctx.can_access_galaxy(Galaxy::Research));
225        assert!(!ctx.can_access_galaxy(Galaxy::Karma));
226        assert!(!ctx.can_access_galaxy(Galaxy::Dharma));
227        assert!(!ctx.can_access_galaxy(Galaxy::Substrate));
228        assert!(ctx.can_write_galaxy(Galaxy::Codex));
229    }
230
231    #[test]
232    fn secure_can_access_all_memory_galaxies() {
233        let ctx = make_ctx("secure");
234        assert!(ctx.can_access_galaxy(Galaxy::Codex));
235        assert!(ctx.can_access_galaxy(Galaxy::Substrate));
236        assert!(ctx.can_write_galaxy(Galaxy::Codex));
237        // Secure still can't access non-memory galaxies
238        assert!(!ctx.can_access_galaxy(Galaxy::Karma));
239    }
240
241    #[test]
242    fn unknown_compartment_fails_closed() {
243        let ctx = make_ctx("bogus");
244        for g in Galaxy::all() {
245            assert!(
246                !ctx.can_access_galaxy(g),
247                "unknown compartment must not read {g:?}"
248            );
249            assert!(
250                !ctx.can_write_galaxy(g),
251                "unknown compartment must not write {g:?}"
252            );
253        }
254    }
255
256    #[test]
257    fn operation_id_propagates_and_defaults_none() {
258        let ctx = Context::default();
259        assert_eq!(ctx.operation_id, None);
260
261        let ctx_with_op = ctx.with_operation_id("01H7XYZ0000000000000000000");
262        assert_eq!(
263            ctx_with_op.operation_id.as_deref(),
264            Some("01H7XYZ0000000000000000000")
265        );
266    }
267}