Skip to main content

everruns_integrations_sprites/
lib.rs

1//! Sprites Integration
2//!
3//! Persistent, hardware-isolated Linux microVMs via Sprites (Fly.io) REST API.
4//! Sprites are Firecracker VMs with full ext4 filesystems that persist across sessions.
5//!
6//! Decision: External integration crate, auto-registered via inventory plugin system
7//! Decision: API token resolved via user connections (Settings > Connections), not secrets store
8//! Decision: Per-sprite state stored in session secrets (encrypted at rest)
9//! Decision: Single API tier — all operations go through api.sprites.dev/v1
10//! Decision: Embrace persistence — sprites survive idle, expose HTTP endpoints, support checkpoints
11
12pub mod client;
13pub mod connection;
14pub mod state;
15mod tools;
16
17use everruns_core::LEASED_RESOURCES_FEATURE;
18use everruns_core::capabilities::{
19    Capability, CapabilityLocalization, CapabilityStatus, IntegrationPlugin, RiskLevel,
20};
21use everruns_core::connector::ConnectorPlugin;
22use everruns_core::tools::Tool;
23
24use std::sync::LazyLock;
25
26use connection::SpritesConnector;
27
28use tools::{
29    SpritesCheckpointTool, SpritesCreateSpriteTool, SpritesExecTool, SpritesListSpritesTool,
30    SpritesManageSpriteTool, SpritesReadFileTool, SpritesRestoreCheckpointTool,
31    SpritesServiceUrlTool, SpritesWriteFileTool,
32};
33
34// ============================================================================
35// Plugin Registration
36// ============================================================================
37
38inventory::submit! {
39    IntegrationPlugin {
40        experimental_only: false,
41        feature_flag: None,
42        factory: || Box::new(SpritesCapability),
43    }
44}
45
46inventory::submit! {
47    ConnectorPlugin {
48        experimental_only: true,
49        factory: || Box::new(SpritesConnector),
50    }
51}
52
53// ============================================================================
54// Constants
55// ============================================================================
56
57const SPRITES_API_BASE: &str = "https://api.sprites.dev/v1";
58const SPRITES_SECRET_PREFIX: &str = "sprites_sprite:";
59const EXEC_TIMEOUT_MS: u64 = 120_000;
60/// Default workspace path inside sprites
61const SPRITES_WORKSPACE_PATH: &str = "/home/sprite";
62
63// ============================================================================
64// SpritesCapability
65// ============================================================================
66
67static SYSTEM_PROMPT: LazyLock<String> = LazyLock::new(|| {
68    let mut prompt = String::from(
69        "Sprites are persistent Firecracker Linux VMs. Create or select a sprite before sprite-scoped operations; data survives idle/sleep, checkpoints can protect risky changes, services should listen on port 8080 for the public URL, and deleting avoids storage charges. Working directory is `/home/sprite`.",
70    );
71    prompt.push_str(everruns_core::tool_output_sanitizer::EXEC_OUTPUT_HINT);
72    prompt
73});
74
75pub struct SpritesCapability;
76
77impl Capability for SpritesCapability {
78    fn id(&self) -> &str {
79        "sprites"
80    }
81
82    fn name(&self) -> &str {
83        "Sprites"
84    }
85
86    fn description(&self) -> &str {
87        "Run code in persistent, hardware-isolated Linux microVMs powered by Sprites. \
88         Create multiple Firecracker VMs per session with full ext4 filesystems, \
89         execute commands, manage files, checkpoint/restore state, and expose HTTP services."
90    }
91
92    fn localizations(&self) -> Vec<CapabilityLocalization> {
93        vec![CapabilityLocalization::text(
94            "uk",
95            "Sprites",
96            "Запускайте код у постійних, апаратно ізольованих мікровіртуальних машинах Linux \
97             на основі Sprites. Створюйте кілька віртуальних машин Firecracker на сесію з \
98             повноцінними файловими системами ext4, виконуйте команди, керуйте файлами, \
99             зберігайте та відновлюйте стан через контрольні точки й публікуйте HTTP-сервіси.",
100        )]
101    }
102
103    fn status(&self) -> CapabilityStatus {
104        CapabilityStatus::Available
105    }
106
107    fn risk_level(&self) -> RiskLevel {
108        RiskLevel::High
109    }
110
111    fn icon(&self) -> Option<&str> {
112        Some("sprites")
113    }
114
115    fn category(&self) -> Option<&str> {
116        Some("Execution")
117    }
118
119    fn system_prompt_addition(&self) -> Option<&str> {
120        Some(&SYSTEM_PROMPT)
121    }
122
123    fn tools(&self) -> Vec<Box<dyn Tool>> {
124        vec![
125            Box::new(SpritesCreateSpriteTool),
126            Box::new(SpritesExecTool),
127            Box::new(SpritesReadFileTool),
128            Box::new(SpritesWriteFileTool),
129            Box::new(SpritesListSpritesTool),
130            Box::new(SpritesManageSpriteTool),
131            Box::new(SpritesCheckpointTool),
132            Box::new(SpritesRestoreCheckpointTool),
133            Box::new(SpritesServiceUrlTool),
134        ]
135    }
136
137    fn dependencies(&self) -> Vec<&'static str> {
138        vec!["session_storage"]
139    }
140
141    fn features(&self) -> Vec<&'static str> {
142        vec![LEASED_RESOURCES_FEATURE]
143    }
144}
145
146// ============================================================================
147// Tests
148// ============================================================================
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use everruns_core::capabilities::CapabilityStatus;
154
155    #[test]
156    fn test_capability_metadata() {
157        let cap = SpritesCapability;
158        assert_eq!(cap.id(), "sprites");
159        assert_eq!(cap.name(), "Sprites");
160        assert_eq!(cap.status(), CapabilityStatus::Available);
161        assert_eq!(cap.icon(), Some("sprites"));
162        assert_eq!(cap.category(), Some("Execution"));
163    }
164
165    #[test]
166    fn test_capability_has_all_tools() {
167        let cap = SpritesCapability;
168        let tools = cap.tools();
169        assert_eq!(tools.len(), 9);
170
171        let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
172        assert!(names.contains(&"sprites_create_sprite"));
173        assert!(names.contains(&"sprites_exec"));
174        assert!(names.contains(&"sprites_read_file"));
175        assert!(names.contains(&"sprites_write_file"));
176        assert!(names.contains(&"sprites_list_sprites"));
177        assert!(names.contains(&"sprites_manage_sprite"));
178        assert!(names.contains(&"sprites_checkpoint"));
179        assert!(names.contains(&"sprites_restore_checkpoint"));
180        assert!(names.contains(&"sprites_service_url"));
181    }
182
183    #[test]
184    fn test_capability_has_system_prompt() {
185        let cap = SpritesCapability;
186        let prompt = cap.system_prompt_addition().unwrap();
187        assert!(prompt.contains("Sprites"));
188        assert!(prompt.contains("persistent Firecracker"));
189        assert!(prompt.contains("checkpoints"));
190        assert!(prompt.contains("port 8080"));
191        assert!(prompt.contains("/home/sprite"));
192    }
193
194    #[tokio::test]
195    async fn system_prompt_within_budget() {
196        let cap = SpritesCapability;
197        let ctx = everruns_core::capabilities::SystemPromptContext::without_file_store(
198            everruns_core::SessionId::new(),
199        );
200        let prompt = cap.system_prompt_contribution(&ctx).await.unwrap();
201        // Bumped 1200 → 1400: EVE-778 grew the shared EXEC_OUTPUT_HINT with the
202        // single-read/contextual-search policy (+438 bytes), taking this
203        // contribution to 1361 bytes.
204        assert!(prompt.len() <= 1400, "prompt is {} bytes", prompt.len());
205    }
206
207    #[test]
208    fn test_all_tools_require_context() {
209        let cap = SpritesCapability;
210        for tool in cap.tools() {
211            assert!(
212                tool.requires_context(),
213                "Tool {} should require context",
214                tool.name()
215            );
216        }
217    }
218
219    #[test]
220    fn test_capability_dependencies() {
221        let cap = SpritesCapability;
222        assert_eq!(cap.dependencies(), vec!["session_storage"]);
223    }
224}