Skip to main content

everruns_core/capabilities/
memory.rs

1//! Memory capability
2//!
3//! Mounts org-scoped Memories into session workspaces. See `specs/memory.md`
4//! for the durable design.
5//!
6//! This module registers the capability and validates the structural shape of
7//! its config (`mounts[]` entries: `mem_`-prefixed Memory IDs, paths under
8//! `/workspace`, no `..`, no `//`, no overlaps). Domain-level cross-validation
9//! (cross-org references, archived/deleted Memories, capability-mount overlaps,
10//! reserved system paths) and runtime mount resolution / read-only write
11//! protection / read-write writeback ship in follow-up vertical slices on top
12//! of this foundation.
13
14use serde_json::{Value, json};
15
16use super::{Capability, CapabilityLocalization, CapabilityStatus, RiskLevel};
17use crate::memory::{MemoryConfig, validate_memory_config};
18
19/// Stable string id for the memory capability.
20pub const MEMORY_CAPABILITY_ID: &str = "memory";
21
22pub struct MemoryCapability;
23
24impl Capability for MemoryCapability {
25    fn id(&self) -> &str {
26        MEMORY_CAPABILITY_ID
27    }
28
29    fn name(&self) -> &str {
30        "Memory"
31    }
32
33    fn description(&self) -> &str {
34        "Mount org-scoped, named Memories into the session workspace as \
35         read-only reference data or read-write shared working memory."
36    }
37
38    fn status(&self) -> CapabilityStatus {
39        CapabilityStatus::Available
40    }
41
42    fn icon(&self) -> Option<&str> {
43        Some("brain")
44    }
45
46    fn category(&self) -> Option<&str> {
47        Some("Memory")
48    }
49
50    fn dependencies(&self) -> Vec<&'static str> {
51        vec!["session_file_system"]
52    }
53
54    fn features(&self) -> Vec<&'static str> {
55        vec!["file_system"]
56    }
57
58    /// Read-write shared mounts let one session influence future sessions, so
59    /// classify as Medium risk per `specs/threat-model.md`.
60    fn risk_level(&self) -> RiskLevel {
61        RiskLevel::Medium
62    }
63
64    fn config_schema(&self) -> Option<Value> {
65        Some(json!({
66            "type": "object",
67            "properties": {
68                "mounts": {
69                    "type": "array",
70                    "title": "Mounts",
71                    "description": "Memories mounted into /workspace for sessions using this capability.",
72                    "items": {
73                        "type": "object",
74                        "required": ["memory", "path"],
75                        "properties": {
76                            "memory": {
77                                "type": "string",
78                                "title": "Memory",
79                                "description": "Memory ID (mem_<32-hex>) to mount.",
80                                "pattern": "^mem_[0-9a-f]{32}$"
81                            },
82                            "path": {
83                                "type": "string",
84                                "title": "Mount path",
85                                "description": "Absolute path under /workspace (e.g. /workspace/research).",
86                                "pattern": "^/workspace(/[^/\\0]+)*$"
87                            },
88                            "mode": {
89                                "type": "string",
90                                "title": "Access mode",
91                                "description": "Access mode for the mount.",
92                                "oneOf": [
93                                    { "const": "readonly", "title": "Read-only" },
94                                    { "const": "readwrite", "title": "Read-write" }
95                                ],
96                                "default": "readonly"
97                            }
98                        }
99                    }
100                }
101            }
102        }))
103    }
104
105    fn config_ui_schema(&self) -> Option<Value> {
106        Some(json!({
107            "mounts": {
108                "items": {
109                    "mode": { "ui:widget": "select" }
110                }
111            }
112        }))
113    }
114
115    fn validate_config(&self, config: &Value) -> Result<(), String> {
116        // Empty / null configs are valid (no mounts).
117        if config.is_null() {
118            return Ok(());
119        }
120        let typed: MemoryConfig = serde_json::from_value(config.clone())
121            .map_err(|e| format!("invalid memory config: {e}"))?;
122        validate_memory_config(&typed)
123    }
124
125    fn localizations(&self) -> Vec<CapabilityLocalization> {
126        vec![
127            CapabilityLocalization {
128                locale: "en",
129                name: None,
130                description: None,
131                config_description: Some(
132                    "Choose which Memories are mounted into /workspace and whether each \
133                     mount is read-only or read-write.",
134                ),
135                config_overlay: None,
136            },
137            CapabilityLocalization {
138                locale: "uk",
139                name: Some("Пам'ять"),
140                description: Some(
141                    "Підключає іменовані Пам'яті організації до робочого простору сесії як \
142                     довідкові дані лише для читання або як спільну робочу пам'ять із \
143                     читанням і записом.",
144                ),
145                config_description: Some(
146                    "Визначає, які Пам'яті монтуються у /workspace і чи доступні вони лише \
147                     для читання, чи для читання й запису.",
148                ),
149                config_overlay: Some(json!({
150                    "properties": {
151                        "mounts": {
152                            "title": "Монтування",
153                            "description": "Пам'яті, що монтуються у /workspace для сесій із цією можливістю.",
154                            "items": {
155                                "properties": {
156                                    "memory": {
157                                        "title": "Пам'ять",
158                                        "description": "Ідентифікатор Пам'яті (mem_<32-hex>) для монтування."
159                                    },
160                                    "path": {
161                                        "title": "Шлях монтування",
162                                        "description": "Абсолютний шлях у /workspace (наприклад, /workspace/research)."
163                                    },
164                                    "mode": {
165                                        "title": "Режим доступу",
166                                        "description": "Режим доступу до монтування.",
167                                        "enum_labels": {
168                                            "readonly": "Лише читання",
169                                            "readwrite": "Читання й запис"
170                                        }
171                                    }
172                                }
173                            }
174                        }
175                    }
176                })),
177            },
178        ]
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    // Metadata/dependency constants covered by builtin_capabilities_satisfy_registry_invariants.
187
188    #[test]
189    fn validate_accepts_empty_config() {
190        let cap = MemoryCapability;
191        assert!(cap.validate_config(&json!({})).is_ok());
192        assert!(cap.validate_config(&json!({ "mounts": [] })).is_ok());
193        assert!(cap.validate_config(&Value::Null).is_ok());
194    }
195
196    #[test]
197    fn validate_accepts_well_formed_mount() {
198        let cap = MemoryCapability;
199        let cfg = json!({
200            "mounts": [
201                {
202                    "memory": "mem_00000000000000000000000000000001",
203                    "path": "/workspace/research",
204                    "mode": "readonly"
205                }
206            ]
207        });
208        assert!(cap.validate_config(&cfg).is_ok());
209    }
210
211    #[test]
212    fn validate_rejects_overlapping_mounts() {
213        let cap = MemoryCapability;
214        let cfg = json!({
215            "mounts": [
216                {
217                    "memory": "mem_00000000000000000000000000000001",
218                    "path": "/workspace/data"
219                },
220                {
221                    "memory": "mem_00000000000000000000000000000002",
222                    "path": "/workspace/data/inner",
223                    "mode": "readwrite"
224                }
225            ]
226        });
227        let err = cap.validate_config(&cfg).unwrap_err();
228        assert!(err.contains("overlapping"));
229    }
230
231    #[test]
232    fn validate_rejects_path_outside_workspace() {
233        let cap = MemoryCapability;
234        let cfg = json!({
235            "mounts": [
236                { "memory": "mem_00000000000000000000000000000001", "path": "/etc/passwd" }
237            ]
238        });
239        assert!(cap.validate_config(&cfg).is_err());
240    }
241
242    #[test]
243    fn config_schema_is_present() {
244        let cap = MemoryCapability;
245        let schema = cap.config_schema().expect("config schema");
246        assert_eq!(schema["type"], "object");
247    }
248}