Skip to main content

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}