1use crate::SessionMutator;
6use crate::{SessionFileSystemFactory, SessionFileSystemFactoryContext};
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use everruns_capability::CapabilityRef;
10use everruns_core::agent_definition::AgentDefinition;
11use everruns_core::harness_definition::HarnessDefinition;
12use everruns_core::session::ExecutionSession;
13use everruns_core::session_file::{
14 FileInfo, FileStat, GrepMatch, GrepOptions, GrepSearchResult, InitialFile, SessionFile,
15 build_grep_search_result,
16};
17use everruns_core::{
18 COMPACTION_CHECKPOINT_FORMAT_VERSION, CompactionCheckpoint, CompactionCheckpointStore,
19 ProactiveCompactionAttempt, ProactiveCompactionAttemptTracker, execution_loading::AgentStore,
20 execution_loading::HarnessStore, execution_loading::SessionStore,
21 provider_resolution::ProviderStore, session_files::SessionFileSystem,
22 session_services::KeyInfo, session_services::SecretInfo, session_services::SessionStorageStore,
23};
24use everruns_provider::credential_provider::CredentialProvider;
25use everruns_provider::error::{AgentLoopError, Result};
26use everruns_provider::model_spec::ModelSpec;
27use everruns_provider::provider::DriverId;
28use everruns_provider::runtime_provider::ProviderKey;
29use everruns_provider::typed_id::{AgentId, HarnessId, ModelId, SessionId};
30use std::collections::{BTreeSet, HashMap};
31use std::sync::Arc;
32use tokio::sync::RwLock;
33use uuid::Uuid;
34
35type CheckpointKey = (SessionId, String, String, u32);
36
37#[derive(Debug, Default)]
39pub struct InMemoryCompactionCheckpointStore {
40 checkpoints: RwLock<HashMap<CheckpointKey, CompactionCheckpoint>>,
41 proactive_attempts: ProactiveCompactionAttemptTracker,
42}
43
44#[async_trait]
45impl CompactionCheckpointStore for InMemoryCompactionCheckpointStore {
46 async fn get_latest(
47 &self,
48 session_id: SessionId,
49 provider_type: &str,
50 model: &str,
51 ) -> Result<Option<CompactionCheckpoint>> {
52 Ok(self
53 .checkpoints
54 .read()
55 .await
56 .get(&(
57 session_id,
58 provider_type.to_string(),
59 model.to_string(),
60 COMPACTION_CHECKPOINT_FORMAT_VERSION,
61 ))
62 .cloned())
63 }
64
65 async fn install(&self, checkpoint: CompactionCheckpoint) -> Result<bool> {
66 let key = (
67 checkpoint.session_id,
68 checkpoint.provider_type.clone(),
69 checkpoint.model.clone(),
70 checkpoint.format_version,
71 );
72 let mut checkpoints = self.checkpoints.write().await;
73 if checkpoints
74 .get(&key)
75 .is_some_and(|current| current.source_sequence >= checkpoint.source_sequence)
76 {
77 return Ok(false);
78 }
79 checkpoints.insert(key, checkpoint);
80 Ok(true)
81 }
82
83 async fn get_proactive_attempt(
84 &self,
85 session_id: SessionId,
86 provider_type: &str,
87 model: &str,
88 ) -> Result<Option<ProactiveCompactionAttempt>> {
89 Ok(self
90 .proactive_attempts
91 .get(session_id, provider_type, model)
92 .await)
93 }
94
95 async fn record_proactive_attempt(
96 &self,
97 session_id: SessionId,
98 provider_type: &str,
99 model: &str,
100 attempt: ProactiveCompactionAttempt,
101 ) -> Result<()> {
102 self.proactive_attempts
103 .record(session_id, provider_type, model, attempt)
104 .await;
105 Ok(())
106 }
107}
108
109#[derive(Debug, Default, Clone)]
111pub struct InMemoryAgentStore {
112 agents: Arc<RwLock<HashMap<AgentId, AgentDefinition>>>,
113}
114
115impl InMemoryAgentStore {
116 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub async fn add_agent(&self, agent: AgentDefinition) {
123 self.agents.write().await.insert(agent.id, agent);
124 }
125
126 pub async fn agent_ids(&self) -> Vec<AgentId> {
128 self.agents.read().await.keys().copied().collect()
129 }
130
131 pub async fn clear(&self) {
133 self.agents.write().await.clear();
134 }
135}
136
137#[async_trait]
138impl AgentStore for InMemoryAgentStore {
139 async fn get_agent(&self, agent_id: AgentId) -> Result<Option<AgentDefinition>> {
140 Ok(self.agents.read().await.get(&agent_id).cloned())
141 }
142}
143
144#[derive(Debug, Default, Clone)]
146pub struct InMemoryHarnessStore {
147 harnesses: Arc<RwLock<HashMap<HarnessId, HarnessDefinition>>>,
148}
149
150impl InMemoryHarnessStore {
151 pub fn new() -> Self {
153 Self::default()
154 }
155
156 pub async fn add_harness(&self, harness_id: HarnessId, harness: HarnessDefinition) {
158 self.harnesses.write().await.insert(harness_id, harness);
159 }
160}
161
162#[async_trait]
163impl HarnessStore for InMemoryHarnessStore {
164 async fn get_harness(&self, harness_id: HarnessId) -> Result<Option<HarnessDefinition>> {
165 Ok(self.harnesses.read().await.get(&harness_id).cloned())
166 }
167}
168
169#[derive(Debug, Default, Clone)]
171pub struct InMemoryProviderStore {
172 models: Arc<RwLock<HashMap<ModelId, ModelSpec>>>,
173 default_model: Arc<RwLock<Option<ModelSpec>>>,
174 provider_configs:
175 Arc<RwLock<HashMap<ProviderKey, everruns_provider::driver_registry::ProviderConfig>>>,
176}
177
178impl InMemoryProviderStore {
179 pub fn new() -> Self {
181 Self::default()
182 }
183
184 pub async fn from_credential_provider(provider: &dyn CredentialProvider) -> Self {
186 let store = Self::new();
187 if let Some(credentials) = provider
188 .resolve(&DriverId::OpenAI)
189 .filter(|credentials| credentials.api_key.is_some())
190 {
191 let config = everruns_provider::driver_registry::ProviderConfig::new(DriverId::OpenAI)
192 .with_api_key(credentials.api_key.expect("filtered above"));
193 let config = match credentials.base_url {
194 Some(base_url) => config.with_base_url(base_url),
195 None => config,
196 };
197 store.set_provider_config(config).await;
198 store
199 .set_default_model_spec(ModelSpec::on("openai", "gpt-5.4"))
200 .await;
201 } else if let Some(credentials) = provider
202 .resolve(&DriverId::Anthropic)
203 .filter(|credentials| credentials.api_key.is_some())
204 {
205 let config =
206 everruns_provider::driver_registry::ProviderConfig::new(DriverId::Anthropic)
207 .with_api_key(credentials.api_key.expect("filtered above"));
208 let config = match credentials.base_url {
209 Some(base_url) => config.with_base_url(base_url),
210 None => config,
211 };
212 store.set_provider_config(config).await;
213 store
214 .set_default_model_spec(ModelSpec::on("anthropic", "claude-sonnet-4-20250514"))
215 .await;
216 }
217 store
218 }
219
220 pub async fn with_default(model: ModelSpec) -> Self {
222 let store = Self::new();
223 store.set_default_model_spec(model).await;
224 store
225 }
226
227 pub async fn add_model(&self, model_id: ModelId, model: ModelSpec) {
229 self.models.write().await.insert(model_id, model);
230 }
231
232 pub async fn set_default_model_spec(&self, model: ModelSpec) {
234 *self.default_model.write().await = Some(model);
235 }
236
237 pub async fn set_provider_config(
239 &self,
240 config: everruns_provider::driver_registry::ProviderConfig,
241 ) {
242 self.provider_configs
243 .write()
244 .await
245 .insert(config.provider.clone(), config);
246 }
247
248 pub async fn clear(&self) {
250 self.models.write().await.clear();
251 *self.default_model.write().await = None;
252 self.provider_configs.write().await.clear();
253 }
254}
255
256#[async_trait]
257impl ProviderStore for InMemoryProviderStore {
258 async fn get_model_spec(&self, model_id: ModelId) -> Result<Option<ModelSpec>> {
259 Ok(self.models.read().await.get(&model_id).cloned())
260 }
261
262 async fn get_default_model_spec(&self) -> Result<Option<ModelSpec>> {
263 Ok(self.default_model.read().await.clone())
264 }
265
266 async fn get_provider_config(
267 &self,
268 provider: &ProviderKey,
269 ) -> Result<Option<everruns_provider::driver_registry::ProviderConfig>> {
270 Ok(self.provider_configs.read().await.get(provider).cloned())
271 }
272}
273
274#[derive(Debug, Default, Clone)]
280pub struct InMemorySessionStore {
281 sessions: Arc<RwLock<HashMap<SessionId, ExecutionSession>>>,
282}
283
284impl InMemorySessionStore {
285 pub fn new() -> Self {
287 Self {
288 sessions: Arc::new(RwLock::new(HashMap::new())),
289 }
290 }
291
292 pub async fn add_session(&self, session: ExecutionSession) {
294 self.sessions.write().await.insert(session.id, session);
295 }
296
297 pub async fn session_ids(&self) -> Vec<SessionId> {
299 self.sessions.read().await.keys().copied().collect()
300 }
301
302 pub async fn clear(&self) {
304 self.sessions.write().await.clear();
305 }
306}
307
308#[async_trait]
309impl SessionStore for InMemorySessionStore {
310 async fn get_session(&self, session_id: SessionId) -> Result<Option<ExecutionSession>> {
311 Ok(self.sessions.read().await.get(&session_id).cloned())
312 }
313}
314
315#[async_trait]
316impl SessionMutator for InMemorySessionStore {
317 async fn update_session_title(
318 &self,
319 session_id: SessionId,
320 title: String,
321 ) -> Result<ExecutionSession> {
322 let mut sessions = self.sessions.write().await;
323 let session = sessions
324 .get_mut(&session_id)
325 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
326 session.title = Some(title);
327 Ok(session.clone())
328 }
329
330 async fn upsert_session_capability(
331 &self,
332 session_id: SessionId,
333 capability: CapabilityRef,
334 ) -> Result<ExecutionSession> {
335 let mut sessions = self.sessions.write().await;
336 let session = sessions
337 .get_mut(&session_id)
338 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
339 if let Some(existing) = session
340 .capabilities
341 .iter_mut()
342 .find(|existing| existing.capability_id() == capability.capability_id())
343 {
344 *existing = capability;
345 } else {
346 session.capabilities.push(capability);
347 }
348 Ok(session.clone())
349 }
350
351 async fn remove_session_capability(
352 &self,
353 session_id: SessionId,
354 capability_id: &str,
355 ) -> Result<ExecutionSession> {
356 let mut sessions = self.sessions.write().await;
357 let session = sessions
358 .get_mut(&session_id)
359 .ok_or_else(|| AgentLoopError::store(format!("session not found: {session_id}")))?;
360 session
361 .capabilities
362 .retain(|capability| capability.capability_id() != capability_id);
363 Ok(session.clone())
364 }
365}
366
367#[derive(Debug, Clone)]
368struct FileEntry {
369 file: SessionFile,
370}
371
372#[derive(Debug, Default, Clone)]
377pub struct InMemorySessionFileStore {
378 files: Arc<RwLock<HashMap<(SessionId, String), FileEntry>>>,
379}
380
381#[derive(Debug, Clone, Default)]
383pub struct InMemorySessionFileSystemFactory;
384
385#[async_trait]
386impl SessionFileSystemFactory for InMemorySessionFileSystemFactory {
387 fn name(&self) -> &'static str {
388 "InMemorySessionFileSystemFactory"
389 }
390
391 async fn create_session_file_system(
392 &self,
393 _context: SessionFileSystemFactoryContext,
394 ) -> Result<Arc<dyn SessionFileSystem>> {
395 Ok(Arc::new(InMemorySessionFileStore::new()))
396 }
397}
398
399impl InMemorySessionFileStore {
400 pub fn new() -> Self {
402 Self {
403 files: Arc::new(RwLock::new(HashMap::new())),
404 }
405 }
406
407 pub async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
409 let path = normalize_path(&file.path);
410 self.ensure_parent_directories(session_id, &path).await?;
411 self.upsert_file(
412 session_id,
413 &path,
414 &file.content,
415 &file.encoding,
416 file.is_readonly,
417 )
418 .await
419 .map(|_| ())
420 }
421
422 async fn ensure_parent_directories(&self, session_id: SessionId, path: &str) -> Result<()> {
423 let mut current = String::new();
424 for segment in path.trim_start_matches('/').split('/').collect::<Vec<_>>() {
425 if segment.is_empty() {
426 continue;
427 }
428 current.push('/');
429 current.push_str(segment);
430 let is_leaf = current == path;
431 if is_leaf {
432 break;
433 }
434 self.insert_directory_if_missing(session_id, ¤t)
435 .await?;
436 }
437 Ok(())
438 }
439
440 async fn insert_directory_if_missing(&self, session_id: SessionId, path: &str) -> Result<()> {
441 let path = normalize_path(path);
442 if path == "/" {
443 return Ok(());
444 }
445
446 let mut files = self.files.write().await;
447 files
448 .entry((session_id, path.clone()))
449 .or_insert_with(|| FileEntry {
450 file: SessionFile {
451 id: Uuid::now_v7(),
452 session_id: session_id.uuid(),
453 path: path.clone(),
454 name: FileInfo::name_from_path(&path),
455 content: None,
456 encoding: "text".to_string(),
457 is_directory: true,
458 is_readonly: false,
459 size_bytes: 0,
460 created_at: Utc::now(),
461 updated_at: Utc::now(),
462 },
463 });
464 Ok(())
465 }
466
467 async fn upsert_file(
468 &self,
469 session_id: SessionId,
470 path: &str,
471 content: &str,
472 encoding: &str,
473 is_readonly: bool,
474 ) -> Result<SessionFile> {
475 let now = Utc::now();
476 let normalized = normalize_path(path);
477 let mut files = self.files.write().await;
478 let key = (session_id, normalized.clone());
479
480 let file = files
481 .entry(key)
482 .and_modify(|entry| {
483 entry.file.content = Some(content.to_string());
484 entry.file.encoding = encoding.to_string();
485 entry.file.is_directory = false;
486 entry.file.is_readonly = is_readonly;
487 entry.file.size_bytes = content.len() as i64;
488 entry.file.updated_at = now;
489 })
490 .or_insert_with(|| FileEntry {
491 file: SessionFile {
492 id: Uuid::now_v7(),
493 session_id: session_id.uuid(),
494 path: normalized.clone(),
495 name: FileInfo::name_from_path(&normalized),
496 content: Some(content.to_string()),
497 encoding: encoding.to_string(),
498 is_directory: false,
499 is_readonly,
500 size_bytes: content.len() as i64,
501 created_at: now,
502 updated_at: now,
503 },
504 })
505 .file
506 .clone();
507
508 Ok(file)
509 }
510
511 pub async fn read_text(&self, session_id: SessionId, path: &str) -> Option<String> {
513 self.read_file(session_id, path)
514 .await
515 .ok()
516 .flatten()
517 .and_then(|file| file.content)
518 }
519}
520
521#[async_trait]
522impl SessionFileSystem for InMemorySessionFileStore {
523 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
524 InMemorySessionFileStore::seed_initial_file(self, session_id, file).await
525 }
526
527 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
528 let normalized = normalize_path(path);
529 if normalized == "/" {
530 return Ok(Some(root_directory(session_id)));
531 }
532
533 Ok(self
534 .files
535 .read()
536 .await
537 .get(&(session_id, normalized))
538 .map(|entry| entry.file.clone()))
539 }
540
541 async fn write_file(
542 &self,
543 session_id: SessionId,
544 path: &str,
545 content: &str,
546 encoding: &str,
547 ) -> Result<SessionFile> {
548 let normalized = normalize_path(path);
549 self.ensure_parent_directories(session_id, &normalized)
550 .await?;
551
552 if let Some(existing) = self.read_file(session_id, &normalized).await?
553 && existing.is_readonly
554 {
555 return Err(AgentLoopError::tool(format!(
556 "file is read-only: {}",
557 normalized
558 )));
559 }
560
561 self.upsert_file(session_id, &normalized, content, encoding, false)
562 .await
563 }
564
565 async fn delete_file(
566 &self,
567 session_id: SessionId,
568 path: &str,
569 recursive: bool,
570 ) -> Result<bool> {
571 let normalized = normalize_path(path);
572 if normalized == "/" {
573 return Ok(false);
574 }
575
576 let mut files = self.files.write().await;
577 let key = (session_id, normalized.clone());
578 let Some(existing) = files.get(&key).cloned() else {
579 return Ok(false);
580 };
581
582 if existing.file.is_readonly {
583 return Ok(false);
584 }
585
586 if existing.file.is_directory {
587 let prefix = format!("{normalized}/");
588 let has_children = files
589 .keys()
590 .any(|(sid, candidate)| *sid == session_id && candidate.starts_with(&prefix));
591 if has_children && !recursive {
592 return Ok(false);
593 }
594 files.retain(|(sid, candidate), _| {
595 !(*sid == session_id
596 && (candidate == &normalized || candidate.starts_with(&prefix)))
597 });
598 return Ok(true);
599 }
600
601 Ok(files.remove(&key).is_some())
602 }
603
604 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
605 let normalized = normalize_path(path);
606 if normalized != "/" {
607 let Some(dir) = self.read_file(session_id, &normalized).await? else {
608 return Ok(vec![]);
609 };
610 if !dir.is_directory {
611 return Ok(vec![]);
612 }
613 }
614
615 let files = self.files.read().await;
616 let mut infos = Vec::new();
617 let mut seen = BTreeSet::new();
618
619 for ((sid, candidate), entry) in files.iter() {
620 if *sid != session_id {
621 continue;
622 }
623 if FileInfo::parent_path(candidate).as_deref() != Some(normalized.as_str()) {
624 continue;
625 }
626 if seen.insert(candidate.clone()) {
627 infos.push(file_info(&entry.file));
628 }
629 }
630
631 infos.sort_by(|a, b| a.path.cmp(&b.path));
632 Ok(infos)
633 }
634
635 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
636 let normalized = normalize_path(path);
637 if normalized == "/" {
638 let root = root_directory(session_id);
639 return Ok(Some(FileStat {
640 path: root.path,
641 name: root.name,
642 is_directory: true,
643 is_readonly: false,
644 size_bytes: 0,
645 created_at: root.created_at,
646 updated_at: root.updated_at,
647 }));
648 }
649
650 Ok(self
651 .files
652 .read()
653 .await
654 .get(&(session_id, normalized))
655 .map(|entry| FileStat {
656 path: entry.file.path.clone(),
657 name: entry.file.name.clone(),
658 is_directory: entry.file.is_directory,
659 is_readonly: entry.file.is_readonly,
660 size_bytes: entry.file.size_bytes,
661 created_at: entry.file.created_at,
662 updated_at: entry.file.updated_at,
663 }))
664 }
665
666 async fn grep_files(
667 &self,
668 session_id: SessionId,
669 pattern: &str,
670 path_pattern: Option<&str>,
671 ) -> Result<Vec<GrepMatch>> {
672 let regex = crate::grep_limits::build_regex(pattern)?;
673 crate::grep_limits::validate_path_pattern(path_pattern)?;
674 let path_pattern = path_pattern
675 .map(everruns_core::session_path::GrepPathPattern::new)
676 .transpose()?;
677 let files = self.files.read().await;
678 let mut matches = Vec::new();
679 let mut total_scanned = 0;
680
681 for ((sid, path), entry) in files.iter() {
682 if *sid != session_id || entry.file.is_directory || entry.file.encoding != "text" {
683 continue;
684 }
685 if let Some(path_pattern) = &path_pattern
686 && !path_pattern.is_match(path)
687 {
688 continue;
689 }
690
691 let Some(content) = &entry.file.content else {
692 continue;
693 };
694 if !crate::grep_limits::account_scan(&mut total_scanned, content.len())? {
695 continue;
696 }
697
698 for (idx, line) in content.lines().enumerate() {
699 if regex.is_match(line) {
700 matches.push(GrepMatch {
701 path: path.clone(),
702 line_number: idx + 1,
703 line: line.to_string(),
704 });
705 }
706 }
707 }
708
709 Ok(matches)
710 }
711
712 async fn grep_files_with_options(
713 &self,
714 session_id: SessionId,
715 pattern: &str,
716 options: &GrepOptions,
717 ) -> Result<GrepSearchResult> {
718 let regex = crate::grep_limits::build_regex(pattern)?;
719 crate::grep_limits::validate_path_pattern(options.path_pattern.as_deref())?;
720 let path_pattern = options
721 .path_pattern
722 .as_deref()
723 .map(everruns_core::session_path::GrepPathPattern::new)
724 .transpose()?;
725 let files = self.files.read().await;
726 let mut total_scanned = 0;
727 let mut text_files = Vec::new();
728 for ((_, path), entry) in files.iter().filter(|((sid, path), entry)| {
729 *sid == session_id
730 && !entry.file.is_directory
731 && entry.file.encoding == "text"
732 && path_pattern
733 .as_ref()
734 .is_none_or(|matcher| matcher.is_match(path))
735 }) {
736 let Some(content) = entry.file.content.as_ref() else {
737 continue;
738 };
739 if crate::grep_limits::account_scan(&mut total_scanned, content.len())? {
740 text_files.push((path.clone(), content.clone()));
741 }
742 }
743 Ok(build_grep_search_result(text_files, ®ex, options))
744 }
745
746 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
747 let normalized = normalize_path(path);
748 self.ensure_parent_directories(session_id, &normalized)
749 .await?;
750 self.insert_directory_if_missing(session_id, &normalized)
751 .await?;
752 let file = self
753 .read_file(session_id, &normalized)
754 .await?
755 .ok_or_else(|| AgentLoopError::store(format!("directory not found: {normalized}")))?;
756 Ok(file_info(&file))
757 }
758
759 fn is_mount_resolver(&self) -> bool {
760 false
761 }
762}
763
764#[derive(Debug, Default, Clone)]
766pub struct InMemorySessionStorageStore {
767 values: Arc<RwLock<HashMap<(SessionId, String), StorageValue>>>,
768 secrets: Arc<RwLock<HashMap<(SessionId, String), StorageValue>>>,
769}
770
771#[derive(Debug, Clone)]
772struct StorageValue {
773 value: String,
774 created_at: DateTime<Utc>,
775 updated_at: DateTime<Utc>,
776}
777
778impl InMemorySessionStorageStore {
779 pub fn new() -> Self {
781 Self {
782 values: Arc::new(RwLock::new(HashMap::new())),
783 secrets: Arc::new(RwLock::new(HashMap::new())),
784 }
785 }
786}
787
788#[async_trait]
789impl SessionStorageStore for InMemorySessionStorageStore {
790 async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()> {
791 upsert_storage(&self.values, session_id, key, value).await;
792 Ok(())
793 }
794
795 async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>> {
796 Ok(self
797 .values
798 .read()
799 .await
800 .get(&(session_id, key.to_string()))
801 .map(|value| value.value.clone()))
802 }
803
804 async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool> {
805 Ok(self
806 .values
807 .write()
808 .await
809 .remove(&(session_id, key.to_string()))
810 .is_some())
811 }
812
813 async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>> {
814 Ok(list_storage(&self.values, session_id)
815 .await
816 .into_iter()
817 .map(|(key, value)| KeyInfo {
818 key,
819 created_at: value.created_at,
820 updated_at: value.updated_at,
821 })
822 .collect())
823 }
824
825 async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()> {
826 upsert_storage(&self.secrets, session_id, name, value).await;
827 Ok(())
828 }
829
830 async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>> {
831 Ok(self
832 .secrets
833 .read()
834 .await
835 .get(&(session_id, name.to_string()))
836 .map(|value| value.value.clone()))
837 }
838
839 async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool> {
840 Ok(self
841 .secrets
842 .write()
843 .await
844 .remove(&(session_id, name.to_string()))
845 .is_some())
846 }
847
848 async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>> {
849 Ok(list_storage(&self.secrets, session_id)
850 .await
851 .into_iter()
852 .map(|(name, value)| SecretInfo {
853 name,
854 created_at: value.created_at,
855 updated_at: value.updated_at,
856 })
857 .collect())
858 }
859}
860
861async fn upsert_storage(
862 map: &Arc<RwLock<HashMap<(SessionId, String), StorageValue>>>,
863 session_id: SessionId,
864 key: &str,
865 value: &str,
866) {
867 let mut map = map.write().await;
868 let now = Utc::now();
869 map.entry((session_id, key.to_string()))
870 .and_modify(|stored| {
871 stored.value = value.to_string();
872 stored.updated_at = now;
873 })
874 .or_insert_with(|| StorageValue {
875 value: value.to_string(),
876 created_at: now,
877 updated_at: now,
878 });
879}
880
881async fn list_storage(
882 map: &Arc<RwLock<HashMap<(SessionId, String), StorageValue>>>,
883 session_id: SessionId,
884) -> Vec<(String, StorageValue)> {
885 let mut values: Vec<_> = map
886 .read()
887 .await
888 .iter()
889 .filter(|((sid, _), _)| *sid == session_id)
890 .map(|((_, key), value)| (key.clone(), value.clone()))
891 .collect();
892 values.sort_by(|a, b| a.0.cmp(&b.0));
893 values
894}
895
896fn normalize_path(path: &str) -> String {
897 everruns_core::session_path::to_session_path(path)
900}
901
902fn root_directory(session_id: SessionId) -> SessionFile {
903 let now = Utc::now();
904 SessionFile {
905 id: Uuid::nil(),
906 session_id: session_id.uuid(),
907 path: "/".to_string(),
908 name: "/".to_string(),
909 content: None,
910 encoding: "text".to_string(),
911 is_directory: true,
912 is_readonly: false,
913 size_bytes: 0,
914 created_at: now,
915 updated_at: now,
916 }
917}
918
919fn file_info(file: &SessionFile) -> FileInfo {
920 FileInfo {
921 id: file.id,
922 session_id: file.session_id,
923 path: file.path.clone(),
924 name: file.name.clone(),
925 is_directory: file.is_directory,
926 is_readonly: file.is_readonly,
927 size_bytes: file.size_bytes,
928 created_at: file.created_at,
929 updated_at: file.updated_at,
930 }
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936
937 #[tokio::test]
938 async fn grep_path_pattern_supports_globs_and_substring_compatibility() {
939 let store = InMemorySessionFileStore::new();
940 let session = SessionId::from_seed(1);
941 for path in [
942 "/src/lib.rs",
943 "/src/nested/mod.rs",
944 "/docs/readme.md",
945 "/notes.txt",
946 ] {
947 store
948 .write_file(session, path, "needle", "text")
949 .await
950 .unwrap();
951 }
952
953 let mut glob_paths: Vec<_> = store
954 .grep_files(session, "needle", Some("src/**/*.rs"))
955 .await
956 .unwrap()
957 .into_iter()
958 .map(|hit| hit.path)
959 .collect();
960 glob_paths.sort();
961 assert_eq!(glob_paths, vec!["/src/lib.rs", "/src/nested/mod.rs"]);
962
963 let substring_paths: Vec<_> = store
964 .grep_files(session, "needle", Some("docs"))
965 .await
966 .unwrap()
967 .into_iter()
968 .map(|hit| hit.path)
969 .collect();
970 assert_eq!(substring_paths, vec!["/docs/readme.md"]);
971 }
972}