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