everruns_core/capabilities/
memory.rs1use serde_json::{Value, json};
15
16use super::{Capability, CapabilityLocalization, CapabilityStatus, RiskLevel};
17use crate::memory::{MemoryConfig, validate_memory_config};
18
19pub 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 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 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 #[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}