Skip to main content

claw_branch/sandbox/
environment.rs

1//! Isolated simulation environments backed by a forked branch.
2
3use std::sync::Arc;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use sqlx::{
8    sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
9    SqlitePool,
10};
11use uuid::Uuid;
12
13use crate::{
14    branch::lifecycle::BranchLifecycle,
15    config::BranchConfig,
16    error::BranchResult,
17    types::{Branch, SimulationOutcome},
18};
19
20/// Configures a simulation scenario run in an isolated sandbox branch.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SimulationScenario {
23    /// A short name for the scenario (used in the branch label).
24    pub name: String,
25    /// Human-readable description of what the scenario tests.
26    pub description: String,
27    /// Maximum number of write operations before the sandbox is halted.
28    pub max_ops: Option<u32>,
29    /// Timeout in seconds after which the run is aborted.
30    pub timeout_secs: Option<u64>,
31    /// Optional JSON seed data that may be loaded into the sandbox on setup.
32    pub seed_data: Option<serde_json::Value>,
33}
34
35/// Describes the current lifecycle phase of a sandbox environment.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub enum SandboxStatus {
38    /// The sandbox is actively running an agent.
39    Running,
40    /// The agent finished and produced an outcome.
41    Completed {
42        /// The captured simulation result.
43        outcome: SimulationOutcome,
44    },
45    /// The run failed with an error message.
46    Failed(String),
47    /// The environment was abandoned without completing.
48    Abandoned,
49}
50
51/// An isolated, forked branch environment used to run and evaluate agent simulations.
52///
53/// # Example
54/// ```rust,ignore
55/// let env = SimulationEnvironment::setup(&parent, scenario, config, lifecycle).await?;
56/// let pool = env.branch_pool()?;
57/// // run agent against pool ...
58/// env.teardown(lifecycle).await?;
59/// ```
60#[derive(Debug, Clone)]
61pub struct SimulationEnvironment {
62    /// A unique identifier for this simulation run.
63    pub id: Uuid,
64    /// A human-readable label for the sandbox branch.
65    pub label: String,
66    /// The isolated branch backing this environment.
67    pub branch: Branch,
68    /// The parent branch from which this sandbox was forked.
69    pub parent_branch_id: Uuid,
70    /// The timestamp the environment was initialized.
71    pub started_at: DateTime<Utc>,
72    /// The scenario definition that governs this run.
73    pub scenario: SimulationScenario,
74    /// The current lifecycle status of the sandbox.
75    pub status: SandboxStatus,
76}
77
78impl SimulationEnvironment {
79    /// Forks `parent` into a new sandbox branch and returns an initialized environment.
80    ///
81    /// The branch name follows the pattern `sandbox/{scenario.name}/{id_short}`.
82    pub async fn setup(
83        parent: &Branch,
84        scenario: SimulationScenario,
85        _config: Arc<BranchConfig>,
86        lifecycle: Arc<BranchLifecycle>,
87    ) -> BranchResult<Self> {
88        let id = Uuid::new_v4();
89        let id_short = &id.to_string()[..8];
90        let branch_name = format!("sandbox/{}/{}", scenario.name, id_short);
91        let label = branch_name.clone();
92
93        let branch = lifecycle
94            .fork(parent.id, &branch_name, Some(&scenario.description))
95            .await?;
96
97        Ok(Self {
98            id,
99            label,
100            branch,
101            parent_branch_id: parent.id,
102            started_at: Utc::now(),
103            scenario,
104            status: SandboxStatus::Running,
105        })
106    }
107
108    /// Discards the sandbox branch, freeing any on-disk state.
109    ///
110    /// After calling `teardown`, the branch database is marked as `Discarded` and
111    /// will be removed on the next GC pass.
112    pub async fn teardown(&mut self, lifecycle: Arc<BranchLifecycle>) -> BranchResult<()> {
113        lifecycle.discard(self.branch.id).await?;
114        self.status = SandboxStatus::Abandoned;
115        Ok(())
116    }
117
118    /// Opens a read-write SQLite pool connected to the sandbox branch database.
119    pub async fn branch_pool(&self) -> BranchResult<SqlitePool> {
120        SqlitePoolOptions::new()
121            .max_connections(4)
122            .connect_with(
123                SqliteConnectOptions::new()
124                    .filename(&self.branch.db_path)
125                    .create_if_missing(false)
126                    .journal_mode(SqliteJournalMode::Wal),
127            )
128            .await
129            .map_err(crate::error::BranchError::Database)
130    }
131}