everruns_core/session_services.rs
1//! Neutral session-scoped storage, schedule, and resource contracts.
2
3use crate::error::Result;
4use crate::leased_resource::{LeasedResource, UpsertLeasedResource};
5use crate::session_schedule::SessionSchedule;
6use crate::typed_id::{ScheduleId, SessionId};
7use async_trait::async_trait;
8
9/// Info about a stored key (without its value)
10#[derive(Debug, Clone)]
11pub struct KeyInfo {
12 pub key: String,
13 pub created_at: chrono::DateTime<chrono::Utc>,
14 pub updated_at: chrono::DateTime<chrono::Utc>,
15}
16
17/// Info about a stored secret (without its value)
18#[derive(Debug, Clone)]
19pub struct SecretInfo {
20 pub name: String,
21 pub created_at: chrono::DateTime<chrono::Utc>,
22 pub updated_at: chrono::DateTime<chrono::Utc>,
23}
24
25/// Trait for session key/value and secret storage operations
26///
27/// This trait abstracts storage operations for tools that need to persist
28/// data within a session. Implementations can:
29/// - Store data in a database (production)
30/// - Use in-memory storage for testing
31///
32/// Storage for session-scoped key/value pairs and secrets.
33///
34/// Key/value storage is for general data that doesn't need encryption.
35/// Secret storage is for sensitive data that is encrypted at rest.
36#[async_trait]
37pub trait SessionStorageStore: Send + Sync {
38 // Key/Value operations (plain text)
39
40 /// Set a key/value pair (creates or updates)
41 async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
42
43 /// Get a value by key
44 async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
45
46 /// Delete a key/value pair
47 async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
48
49 /// List all keys in a session
50 async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
51
52 // Secret operations (encrypted)
53
54 /// Set a secret (creates or updates, value is encrypted before storage)
55 async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
56
57 /// Get a secret by name (value is decrypted before returning)
58 async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
59
60 /// Delete a secret
61 async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
62
63 /// List all secret names in a session (without values)
64 async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
65}
66
67// ============================================================================
68// SessionScheduleStore - For session-scoped schedule operations
69// ============================================================================
70
71/// Trait for session schedule CRUD operations.
72///
73/// Used by scheduling tools to create, cancel, and list schedules.
74#[async_trait]
75pub trait SessionScheduleStore: Send + Sync {
76 /// Create a new schedule for a session.
77 async fn create_schedule(
78 &self,
79 session_id: SessionId,
80 description: String,
81 cron_expression: Option<String>,
82 scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
83 timezone: String,
84 ) -> Result<SessionSchedule>;
85
86 /// Create a new schedule after enforcing create-time limits in the same
87 /// store operation. Backends with shared mutable state must override this
88 /// to make the check-and-create sequence atomic.
89 async fn create_schedule_enforcing_limits(
90 &self,
91 session_id: SessionId,
92 description: String,
93 cron_expression: Option<String>,
94 scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
95 timezone: String,
96 ) -> std::result::Result<SessionSchedule, crate::session_schedule::ScheduleLimitError> {
97 let per_session = self
98 .count_active_schedules(session_id)
99 .await
100 .map_err(crate::session_schedule::ScheduleLimitError::Store)?;
101 if per_session >= crate::session_schedule::MAX_ACTIVE_SCHEDULES_PER_SESSION {
102 return Err(crate::session_schedule::ScheduleLimitError::Rejected(
103 format!(
104 "Maximum {} active schedules per session. Cancel an existing schedule first.",
105 crate::session_schedule::MAX_ACTIVE_SCHEDULES_PER_SESSION
106 ),
107 ));
108 }
109
110 let max_per_org = crate::session_schedule::DEFAULT_MAX_SCHEDULES_PER_ORG;
111 let per_org = self
112 .count_active_org_schedules()
113 .await
114 .map_err(crate::session_schedule::ScheduleLimitError::Store)?;
115 if i64::from(per_org) >= max_per_org {
116 return Err(crate::session_schedule::ScheduleLimitError::Rejected(
117 format!(
118 "Maximum {max_per_org} active schedules per org reached. Cancel an existing schedule first."
119 ),
120 ));
121 }
122
123 if let Some(cron) = cron_expression.as_deref() {
124 crate::session_schedule::validate_cron_min_interval(cron)
125 .map_err(crate::session_schedule::ScheduleLimitError::Rejected)?;
126 }
127
128 self.create_schedule(
129 session_id,
130 description,
131 cron_expression,
132 scheduled_at,
133 timezone,
134 )
135 .await
136 .map_err(crate::session_schedule::ScheduleLimitError::Store)
137 }
138
139 /// Cancel (disable) a schedule.
140 async fn cancel_schedule(
141 &self,
142 session_id: SessionId,
143 schedule_id: ScheduleId,
144 ) -> Result<SessionSchedule>;
145
146 /// List schedules for a session.
147 async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
148
149 /// Count active (enabled) schedules for a session.
150 async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
151
152 /// Count active (enabled) schedules across the whole org this store is
153 /// scoped to. Used to enforce a per-org cap independent of session count:
154 /// `count_active_schedules` only bounds one session, so unlimited sessions
155 /// would otherwise imply unlimited active schedules per org.
156 async fn count_active_org_schedules(&self) -> Result<u32>;
157}
158
159// ============================================================================
160// SessionResourceRegistry - Generic session-scoped resource registry
161// ============================================================================
162
163/// Generic registry of resources active alongside a session.
164///
165/// Capabilities register resources here (sandboxes, subagents, browser sessions).
166/// Agents query it ("what's running?"), infrastructure scans it for cleanup.
167/// See `knowledge/runtime-resources/session-resources.md`.
168#[async_trait]
169pub trait SessionResourceRegistry: Send + Sync {
170 /// Register a resource (or update if resource_id already exists for this session).
171 async fn register(
172 &self,
173 entry: crate::session_resource::RegisterSessionResource,
174 ) -> Result<crate::session_resource::SessionResourceEntry>;
175
176 /// Update the status of a registered resource.
177 async fn update_status(
178 &self,
179 session_id: SessionId,
180 resource_id: &str,
181 status: crate::session_resource::SessionResourceStatus,
182 ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
183
184 /// Get a specific resource by ID.
185 async fn get(
186 &self,
187 session_id: SessionId,
188 resource_id: &str,
189 ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
190
191 /// List resources for a session, optionally filtered.
192 async fn list(
193 &self,
194 session_id: SessionId,
195 filter: Option<&crate::session_resource::SessionResourceFilter>,
196 ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
197
198 /// Remove a resource from the registry.
199 async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
200}
201
202// ============================================================================
203// LeasedResourceStore - For lifecycle-managed external resources
204// ============================================================================
205
206/// Trait for session-scoped leased resource operations.
207///
208/// Tools use this store to register or refresh leases when they create or use
209/// external provider resources. Cleanup workers operate through control-plane
210/// storage APIs directly so they can claim work across organizations.
211#[async_trait]
212pub trait LeasedResourceStore: Send + Sync {
213 /// Create or refresh a leased resource for a session.
214 ///
215 /// Implementations must treat this as an idempotent upsert keyed by the
216 /// provider-specific resource identity so repeated tool usage extends the
217 /// same lease instead of creating duplicate rows.
218 async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
219
220 /// Mark a leased resource as explicitly released.
221 ///
222 /// This is the fast path for explicit user intent such as "close browser"
223 /// or "delete sandbox". It should transition the resource to `released`
224 /// without waiting for the durable cleanup worker to observe lease expiry.
225 async fn release_resource(
226 &self,
227 session_id: SessionId,
228 provider: &str,
229 resource_type: &str,
230 external_id: &str,
231 ) -> Result<Option<LeasedResource>>;
232
233 /// List leased resources currently associated with a session.
234 ///
235 /// Session surfaces use this for visibility. Released resources remain
236 /// visible so operators can inspect cleanup outcomes and failure history.
237 async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
238}