distri_types/
stores.rs

1use crate::{
2    ScratchpadEntry, ToolAuthStore, ToolResponse, configuration::PluginArtifact,
3    workflow::WorkflowStore,
4};
5use async_trait::async_trait;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8use serde_json::Value;
9use std::{collections::HashMap, sync::Arc};
10use tokio::sync::oneshot;
11use uuid::Uuid;
12
13use crate::{
14    AgentEvent, CreateThreadRequest, Message, Task, TaskMessage, TaskStatus, Thread,
15    UpdateThreadRequest,
16};
17
18// Redis and PostgreSQL stores moved to distri-stores crate
19
20/// Filter for listing threads
21#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22pub struct ThreadListFilter {
23    /// Filter by agent ID
24    pub agent_id: Option<String>,
25    /// Filter by external ID (for integration with external systems)
26    pub external_id: Option<String>,
27    /// Filter by thread attributes (JSON matching)
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub attributes: Option<serde_json::Value>,
30    /// Full-text search across title and last_message
31    pub search: Option<String>,
32    /// Filter threads updated after this time
33    pub from_date: Option<DateTime<Utc>>,
34    /// Filter threads updated before this time
35    pub to_date: Option<DateTime<Utc>>,
36    /// Filter by tags (array of tag strings to match)
37    pub tags: Option<Vec<String>>,
38}
39
40/// Paginated response for thread listing
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ThreadListResponse {
43    pub threads: Vec<crate::ThreadSummary>,
44    pub total: i64,
45    pub page: u32,
46    pub page_size: u32,
47}
48
49/// Agent usage information for sorting agents by thread count
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct AgentUsageInfo {
52    pub agent_id: String,
53    pub agent_name: String,
54    pub thread_count: i64,
55}
56
57/// Initialized store collection
58#[derive(Clone)]
59pub struct InitializedStores {
60    pub session_store: Arc<dyn SessionStore>,
61    pub agent_store: Arc<dyn AgentStore>,
62    pub task_store: Arc<dyn TaskStore>,
63    pub thread_store: Arc<dyn ThreadStore>,
64    pub tool_auth_store: Arc<dyn ToolAuthStore>,
65    pub scratchpad_store: Arc<dyn ScratchpadStore>,
66    pub workflow_store: Arc<dyn WorkflowStore>,
67    pub memory_store: Option<Arc<dyn MemoryStore>>,
68    pub crawl_store: Option<Arc<dyn CrawlStore>>,
69    pub external_tool_calls_store: Arc<dyn ExternalToolCallsStore>,
70    pub plugin_store: Arc<dyn PluginCatalogStore>,
71    pub prompt_template_store: Option<Arc<dyn PromptTemplateStore>>,
72    pub secret_store: Option<Arc<dyn SecretStore>>,
73    /// Plugin tool loader for dynamic tool resolution
74    /// OSS: uses registry-based loader (filesystem plugins)
75    /// Cloud: uses DB-based loader (tenant plugins)
76    pub plugin_tool_loader: Option<Arc<dyn PluginToolLoader>>,
77}
78impl InitializedStores {
79    pub fn set_tool_auth_store(&mut self, tool_auth_store: Arc<dyn ToolAuthStore>) {
80        self.tool_auth_store = tool_auth_store;
81    }
82
83    pub fn set_external_tool_calls_store(mut self, store: Arc<dyn ExternalToolCallsStore>) {
84        self.external_tool_calls_store = store;
85    }
86
87    pub fn set_session_store(&mut self, session_store: Arc<dyn SessionStore>) {
88        self.session_store = session_store;
89    }
90
91    pub fn set_agent_store(&mut self, agent_store: Arc<dyn AgentStore>) {
92        self.agent_store = agent_store;
93    }
94
95    pub fn with_task_store(&mut self, task_store: Arc<dyn TaskStore>) {
96        self.task_store = task_store;
97    }
98
99    pub fn with_thread_store(&mut self, thread_store: Arc<dyn ThreadStore>) {
100        self.thread_store = thread_store;
101    }
102
103    pub fn with_scratchpad_store(&mut self, scratchpad_store: Arc<dyn ScratchpadStore>) {
104        self.scratchpad_store = scratchpad_store;
105    }
106
107    pub fn with_workflow_store(mut self, workflow_store: Arc<dyn WorkflowStore>) {
108        self.workflow_store = workflow_store;
109    }
110
111    pub fn with_plugin_store(&mut self, plugin_store: Arc<dyn PluginCatalogStore>) {
112        self.plugin_store = plugin_store;
113    }
114}
115
116impl std::fmt::Debug for InitializedStores {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        f.debug_struct("InitializedStores").finish()
119    }
120}
121
122#[derive(Debug, Serialize, Deserialize, Clone)]
123pub struct SessionSummary {
124    pub session_id: String,
125    pub keys: Vec<String>,
126    pub key_count: usize,
127    pub updated_at: Option<DateTime<Utc>>,
128}
129
130// SessionStore trait - manages current conversation thread/run
131#[async_trait::async_trait]
132pub trait SessionStore: Send + Sync + std::fmt::Debug {
133    async fn clear_session(&self, namespace: &str) -> anyhow::Result<()>;
134
135    async fn set_value(&self, namespace: &str, key: &str, value: &Value) -> anyhow::Result<()>;
136
137    async fn set_value_with_expiry(
138        &self,
139        namespace: &str,
140        key: &str,
141        value: &Value,
142        expiry: Option<chrono::DateTime<chrono::Utc>>,
143    ) -> anyhow::Result<()>;
144
145    async fn get_value(&self, namespace: &str, key: &str) -> anyhow::Result<Option<Value>>;
146
147    async fn delete_value(&self, namespace: &str, key: &str) -> anyhow::Result<()>;
148
149    async fn get_all_values(&self, namespace: &str) -> anyhow::Result<HashMap<String, Value>>;
150
151    async fn list_sessions(
152        &self,
153        namespace: Option<&str>,
154        limit: Option<usize>,
155        offset: Option<usize>,
156    ) -> anyhow::Result<Vec<SessionSummary>>;
157}
158#[async_trait::async_trait]
159pub trait SessionStoreExt: SessionStore {
160    async fn set<T: Serialize + Sync>(
161        &self,
162        namespace: &str,
163        key: &str,
164        value: &T,
165    ) -> anyhow::Result<()> {
166        self.set_value(namespace, key, &serde_json::to_value(value)?)
167            .await
168    }
169    async fn set_with_expiry<T: Serialize + Sync>(
170        &self,
171        namespace: &str,
172        key: &str,
173        value: &T,
174        expiry: Option<chrono::DateTime<chrono::Utc>>,
175    ) -> anyhow::Result<()> {
176        self.set_value_with_expiry(namespace, key, &serde_json::to_value(value)?, expiry)
177            .await
178    }
179    async fn get<T: DeserializeOwned + Sync>(
180        &self,
181        namespace: &str,
182        key: &str,
183    ) -> anyhow::Result<Option<T>> {
184        match self.get_value(namespace, key).await? {
185            Some(b) => Ok(Some(serde_json::from_value(b)?)),
186            None => Ok(None),
187        }
188    }
189}
190impl<T: SessionStore + ?Sized> SessionStoreExt for T {}
191
192// Higher-level MemoryStore trait - manages cross-session permanent memory using user_id
193#[async_trait::async_trait]
194pub trait MemoryStore: Send + Sync {
195    /// Store permanent memory from a session for cross-session access
196    async fn store_memory(
197        &self,
198        user_id: &str,
199        session_memory: SessionMemory,
200    ) -> anyhow::Result<()>;
201
202    /// Search for relevant memories across sessions for a user
203    async fn search_memories(
204        &self,
205        user_id: &str,
206        query: &str,
207        limit: Option<usize>,
208    ) -> anyhow::Result<Vec<String>>;
209
210    /// Get all permanent memories for a user
211    async fn get_user_memories(&self, user_id: &str) -> anyhow::Result<Vec<String>>;
212
213    /// Clear all memories for a user
214    async fn clear_user_memories(&self, user_id: &str) -> anyhow::Result<()>;
215}
216
217#[derive(Debug, Clone)]
218pub struct SessionMemory {
219    pub agent_id: String,
220    pub thread_id: String,
221    pub session_summary: String,
222    pub key_insights: Vec<String>,
223    pub important_facts: Vec<String>,
224    pub timestamp: chrono::DateTime<chrono::Utc>,
225}
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
227#[serde(tag = "type", rename_all = "snake_case")]
228pub enum FilterMessageType {
229    Events,
230    Messages,
231    Artifacts,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct MessageFilter {
236    pub filter: Option<Vec<FilterMessageType>>,
237    pub limit: Option<usize>,
238    pub offset: Option<usize>,
239}
240
241// Task Store trait for A2A task management
242#[async_trait]
243pub trait TaskStore: Send + Sync {
244    fn init_task(
245        &self,
246        context_id: &str,
247        task_id: Option<&str>,
248        status: Option<TaskStatus>,
249    ) -> Task {
250        let task_id = task_id.unwrap_or(&Uuid::new_v4().to_string()).to_string();
251        Task {
252            id: task_id,
253            status: status.unwrap_or(TaskStatus::Pending),
254            created_at: chrono::Utc::now().timestamp_millis(),
255            updated_at: chrono::Utc::now().timestamp_millis(),
256            thread_id: context_id.to_string(),
257            parent_task_id: None,
258        }
259    }
260    async fn get_or_create_task(
261        &self,
262        thread_id: &str,
263        task_id: &str,
264    ) -> Result<(), anyhow::Error> {
265        match self.get_task(&task_id).await? {
266            Some(task) => task,
267            None => {
268                self.create_task(&thread_id, Some(&task_id), Some(TaskStatus::Running))
269                    .await?
270            }
271        };
272
273        Ok(())
274    }
275    async fn create_task(
276        &self,
277        context_id: &str,
278        task_id: Option<&str>,
279        task_status: Option<TaskStatus>,
280    ) -> anyhow::Result<Task>;
281    async fn get_task(&self, task_id: &str) -> anyhow::Result<Option<Task>>;
282    async fn update_task_status(&self, task_id: &str, status: TaskStatus) -> anyhow::Result<()>;
283    async fn add_event_to_task(&self, task_id: &str, event: AgentEvent) -> anyhow::Result<()>;
284    async fn add_message_to_task(&self, task_id: &str, message: &Message) -> anyhow::Result<()>;
285    async fn cancel_task(&self, task_id: &str) -> anyhow::Result<Task>;
286    async fn list_tasks(&self, thread_id: Option<&str>) -> anyhow::Result<Vec<Task>>;
287
288    async fn get_history(
289        &self,
290        thread_id: &str,
291        filter: Option<MessageFilter>,
292    ) -> anyhow::Result<Vec<(Task, Vec<TaskMessage>)>>;
293
294    async fn update_parent_task(
295        &self,
296        task_id: &str,
297        parent_task_id: Option<&str>,
298    ) -> anyhow::Result<()>;
299}
300
301#[derive(Debug, Clone)]
302pub struct PluginMetadataRecord {
303    pub package_name: String,
304    pub version: Option<String>,
305    pub object_prefix: String,
306    pub entrypoint: Option<String>,
307    pub artifact: PluginArtifact,
308    pub updated_at: chrono::DateTime<chrono::Utc>,
309}
310
311#[async_trait]
312pub trait PluginCatalogStore: Send + Sync {
313    async fn list_plugins(&self) -> anyhow::Result<Vec<PluginMetadataRecord>>;
314
315    async fn get_plugin(&self, package_name: &str) -> anyhow::Result<Option<PluginMetadataRecord>>;
316
317    async fn upsert_plugin(&self, record: &PluginMetadataRecord) -> anyhow::Result<()>;
318
319    async fn remove_plugin(&self, package_name: &str) -> anyhow::Result<()>;
320
321    async fn clear(&self) -> anyhow::Result<()>;
322}
323
324// Thread Store trait for thread management
325#[async_trait]
326pub trait ThreadStore: Send + Sync {
327    fn as_any(&self) -> &dyn std::any::Any;
328    async fn create_thread(&self, request: CreateThreadRequest) -> anyhow::Result<Thread>;
329    async fn get_thread(&self, thread_id: &str) -> anyhow::Result<Option<Thread>>;
330    async fn update_thread(
331        &self,
332        thread_id: &str,
333        request: UpdateThreadRequest,
334    ) -> anyhow::Result<Thread>;
335    async fn delete_thread(&self, thread_id: &str) -> anyhow::Result<()>;
336
337    /// List threads with pagination and filtering
338    /// Returns a paginated response with total count
339    async fn list_threads(
340        &self,
341        filter: &ThreadListFilter,
342        limit: Option<u32>,
343        offset: Option<u32>,
344    ) -> anyhow::Result<ThreadListResponse>;
345
346    async fn update_thread_with_message(
347        &self,
348        thread_id: &str,
349        message: &str,
350    ) -> anyhow::Result<()>;
351
352    /// Get aggregated home statistics
353    async fn get_home_stats(&self) -> anyhow::Result<HomeStats>;
354
355    /// Get agents sorted by thread count (most active first)
356    async fn get_agents_by_usage(&self) -> anyhow::Result<Vec<AgentUsageInfo>>;
357
358    /// Get a map of agent name -> stats for all agents with activity
359    async fn get_agent_stats_map(
360        &self,
361    ) -> anyhow::Result<std::collections::HashMap<String, AgentStatsInfo>>;
362}
363
364/// Home statistics for dashboard
365#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
366pub struct HomeStats {
367    pub total_agents: i64,
368    pub total_threads: i64,
369    pub total_messages: i64,
370    pub avg_run_time_ms: Option<f64>,
371    // Cloud-specific fields (optional)
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub total_owned_agents: Option<i64>,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub total_accessible_agents: Option<i64>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub most_active_agent: Option<MostActiveAgent>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub latest_threads: Option<Vec<LatestThreadInfo>>,
380    /// Recently used agents (last 10 by most recent thread activity)
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub recently_used_agents: Option<Vec<RecentlyUsedAgent>>,
383    /// Custom metrics that can be displayed in the stats overview
384    /// Key is the metric name (e.g., "usage"), value is the metric data
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub custom_metrics: Option<std::collections::HashMap<String, CustomMetric>>,
387}
388
389/// A custom metric for display in the stats overview
390#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
391pub struct CustomMetric {
392    /// Display label (e.g., "Monthly Calls")
393    pub label: String,
394    /// Current value as a string (formatted)
395    pub value: String,
396    /// Optional helper text below the value
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub helper: Option<String>,
399    /// Optional limit (for progress display)
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub limit: Option<String>,
402    /// Optional raw numeric value for calculations
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub raw_value: Option<i64>,
405    /// Optional raw limit for calculations
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub raw_limit: Option<i64>,
408}
409
410#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
411pub struct MostActiveAgent {
412    pub id: String,
413    pub name: String,
414    pub thread_count: i64,
415}
416
417/// Agent that was recently used (based on thread activity)
418#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
419pub struct RecentlyUsedAgent {
420    pub id: String,
421    pub name: String,
422    pub description: Option<String>,
423    pub last_used_at: chrono::DateTime<chrono::Utc>,
424}
425
426#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
427pub struct LatestThreadInfo {
428    pub id: String,
429    pub title: String,
430    pub agent_id: String,
431    pub agent_name: String,
432    pub updated_at: chrono::DateTime<chrono::Utc>,
433}
434
435/// Agent statistics for display
436#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
437pub struct AgentStatsInfo {
438    pub thread_count: i64,
439    pub sub_agent_usage_count: i64,
440    pub last_used_at: Option<chrono::DateTime<chrono::Utc>>,
441}
442
443#[async_trait]
444pub trait AgentStore: Send + Sync {
445    async fn list(
446        &self,
447        cursor: Option<String>,
448        limit: Option<usize>,
449    ) -> (Vec<crate::configuration::AgentConfig>, Option<String>);
450
451    async fn get(&self, name: &str) -> Option<crate::configuration::AgentConfig>;
452    async fn register(&self, config: crate::configuration::AgentConfig) -> anyhow::Result<()>;
453    /// Update an existing agent with new definition
454    async fn update(&self, config: crate::configuration::AgentConfig) -> anyhow::Result<()>;
455
456    async fn clear(&self) -> anyhow::Result<()>;
457}
458
459/// Store for managing scratchpad entries across conversations
460#[async_trait::async_trait]
461pub trait ScratchpadStore: Send + Sync + std::fmt::Debug {
462    /// Add a scratchpad entry for a specific thread
463    async fn add_entry(
464        &self,
465        thread_id: &str,
466        entry: ScratchpadEntry,
467    ) -> Result<(), crate::AgentError>;
468
469    /// Clear all scratchpad entries for a thread
470    async fn clear_entries(&self, thread_id: &str) -> Result<(), crate::AgentError>;
471
472    /// Get entries for a specific task within a thread
473    async fn get_entries(
474        &self,
475        thread_id: &str,
476        task_id: &str,
477        limit: Option<usize>,
478    ) -> Result<Vec<ScratchpadEntry>, crate::AgentError>;
479
480    async fn get_all_entries(
481        &self,
482        thread_id: &str,
483        limit: Option<usize>,
484    ) -> Result<Vec<ScratchpadEntry>, crate::AgentError>;
485}
486
487/// Web crawl result data
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct CrawlResult {
490    pub id: String,
491    pub url: String,
492    pub title: Option<String>,
493    pub content: String,
494    pub html: Option<String>,
495    pub metadata: serde_json::Value,
496    pub links: Vec<String>,
497    pub images: Vec<String>,
498    pub status_code: Option<u16>,
499    pub crawled_at: chrono::DateTime<chrono::Utc>,
500    pub processing_time_ms: Option<u64>,
501}
502
503/// Store for managing web crawl results
504#[async_trait]
505pub trait CrawlStore: Send + Sync {
506    /// Store a crawl result
507    async fn store_crawl_result(&self, result: CrawlResult) -> anyhow::Result<String>;
508
509    /// Get a crawl result by ID
510    async fn get_crawl_result(&self, id: &str) -> anyhow::Result<Option<CrawlResult>>;
511
512    /// Get crawl results for a specific URL
513    async fn get_crawl_results_by_url(&self, url: &str) -> anyhow::Result<Vec<CrawlResult>>;
514
515    /// Get recent crawl results (within time limit)
516    async fn get_recent_crawl_results(
517        &self,
518        limit: Option<usize>,
519        since: Option<chrono::DateTime<chrono::Utc>>,
520    ) -> anyhow::Result<Vec<CrawlResult>>;
521
522    /// Check if URL was recently crawled (within cache duration)
523    async fn is_url_recently_crawled(
524        &self,
525        url: &str,
526        cache_duration: chrono::Duration,
527    ) -> anyhow::Result<Option<CrawlResult>>;
528
529    /// Delete crawl result
530    async fn delete_crawl_result(&self, id: &str) -> anyhow::Result<()>;
531
532    /// Clear all crawl results older than specified date
533    async fn cleanup_old_results(
534        &self,
535        before: chrono::DateTime<chrono::Utc>,
536    ) -> anyhow::Result<usize>;
537}
538
539/// Store for managing external tool call completions using oneshot channels
540#[async_trait]
541pub trait ExternalToolCallsStore: Send + Sync + std::fmt::Debug {
542    /// Register a new external tool call session and return a receiver for the response
543    async fn register_external_tool_call(
544        &self,
545        session_id: &str,
546    ) -> anyhow::Result<oneshot::Receiver<ToolResponse>>;
547
548    /// Complete an external tool call by sending the response
549    async fn complete_external_tool_call(
550        &self,
551        session_id: &str,
552        tool_response: ToolResponse,
553    ) -> anyhow::Result<()>;
554
555    /// Remove a session (cleanup)
556    async fn remove_tool_call(&self, session_id: &str) -> anyhow::Result<()>;
557
558    /// List all pending sessions (for debugging)
559    async fn list_pending_tool_calls(&self) -> anyhow::Result<Vec<String>>;
560}
561
562// ========== Prompt Template Store ==========
563
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct PromptTemplateRecord {
566    pub id: String,
567    pub name: String,
568    pub template: String,
569    pub description: Option<String>,
570    pub version: Option<String>,
571    pub is_system: bool,
572    pub created_at: chrono::DateTime<chrono::Utc>,
573    pub updated_at: chrono::DateTime<chrono::Utc>,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct NewPromptTemplate {
578    pub name: String,
579    pub template: String,
580    pub description: Option<String>,
581    pub version: Option<String>,
582    #[serde(default)]
583    pub is_system: bool,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
587pub struct UpdatePromptTemplate {
588    pub name: String,
589    pub template: String,
590    pub description: Option<String>,
591}
592
593#[async_trait]
594pub trait PromptTemplateStore: Send + Sync {
595    async fn list(&self) -> anyhow::Result<Vec<PromptTemplateRecord>>;
596    async fn get(&self, id: &str) -> anyhow::Result<Option<PromptTemplateRecord>>;
597    async fn create(&self, template: NewPromptTemplate) -> anyhow::Result<PromptTemplateRecord>;
598    async fn update(
599        &self,
600        id: &str,
601        update: UpdatePromptTemplate,
602    ) -> anyhow::Result<PromptTemplateRecord>;
603    async fn delete(&self, id: &str) -> anyhow::Result<()>;
604    async fn clone_template(&self, id: &str) -> anyhow::Result<PromptTemplateRecord>;
605    async fn sync_system_templates(&self, templates: Vec<NewPromptTemplate>) -> anyhow::Result<()>;
606}
607
608// ========== Plugin Tool Loader ==========
609
610/// Trait for loading plugin tools dynamically.
611/// This abstraction allows different implementations for:
612/// - OSS: loads from filesystem/plugin registry
613/// - Cloud: loads from database (tenant plugins)
614///
615/// The loader is used by resolve_tools_config to get tools based on ToolsConfig.
616#[async_trait]
617pub trait PluginToolLoader: Send + Sync + std::fmt::Debug {
618    /// List all available package names (for wildcard resolution)
619    async fn list_packages(&self) -> anyhow::Result<Vec<String>>;
620
621    /// Get all tools for a specific package
622    async fn get_package_tools(&self, package_name: &str) -> anyhow::Result<Vec<Arc<dyn crate::Tool>>>;
623
624    /// Check if a package exists
625    async fn has_package(&self, package_name: &str) -> anyhow::Result<bool>;
626}
627
628// ========== Secret Store ==========
629
630#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct SecretRecord {
632    pub id: String,
633    pub key: String,
634    pub value: String,
635    pub created_at: chrono::DateTime<chrono::Utc>,
636    pub updated_at: chrono::DateTime<chrono::Utc>,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
640pub struct NewSecret {
641    pub key: String,
642    pub value: String,
643}
644
645#[async_trait]
646pub trait SecretStore: Send + Sync {
647    async fn list(&self) -> anyhow::Result<Vec<SecretRecord>>;
648    async fn get(&self, key: &str) -> anyhow::Result<Option<SecretRecord>>;
649    async fn create(&self, secret: NewSecret) -> anyhow::Result<SecretRecord>;
650    async fn update(&self, key: &str, value: &str) -> anyhow::Result<SecretRecord>;
651    async fn delete(&self, key: &str) -> anyhow::Result<()>;
652}