Skip to main content

fluers_runtime/
env.rs

1//! The `SessionEnv` trait — the filesystem + process abstraction.
2//!
3//! This is the central abstraction that sandbox backends implement. Flue's
4//! built-in tools (`read`, `write`, `bash`, …) operate purely against a
5//! `SessionEnv`, so the same tools work unchanged over a virtual, local,
6//! or remote-container sandbox.
7
8use async_trait::async_trait;
9use std::path::Path;
10use tokio_util::sync::CancellationToken;
11
12use crate::error::RuntimeResult;
13
14/// The outcome of running a shell command.
15#[derive(Debug, Clone)]
16pub struct ShellResult {
17    /// Exit code (124 conventionally denotes a timeout, as in Flue).
18    pub exit_code: i32,
19    /// Captured stdout.
20    pub stdout: String,
21    /// Captured stderr.
22    pub stderr: String,
23}
24
25/// The environment a session runs in.
26///
27/// Every method is async and fallible so it can be backed by anything from a
28/// real local directory to a remote container API (E2B, Daytona, …).
29#[async_trait]
30pub trait SessionEnv: Send + Sync {
31    /// Read a file, bounded by `max_lines` / `max_bytes`.
32    async fn read_file(
33        &self,
34        path: &Path,
35        max_lines: usize,
36        max_bytes: usize,
37    ) -> RuntimeResult<String>;
38
39    /// Read a file **in full**, erroring (NOT truncating) if it exceeds
40    /// `max_bytes`.
41    ///
42    /// Use for tools that must operate on the complete file (e.g. `edit`, which
43    /// writes the file back): the bounded [`read_file`](Self::read_file) silently
44    /// truncates large files and would cause data loss on write-back. This method
45    /// checks the file size (via metadata, before reading) and returns
46    /// [`RuntimeError::FileTooLarge`] if the file is too big, so the caller never
47    /// operates on partial data. Path containment is enforced as for `read_file`.
48    async fn read_file_full(&self, path: &Path, max_bytes: usize) -> RuntimeResult<String>;
49
50    /// Write a file, creating parent directories as needed.
51    async fn write_file(&self, path: &Path, content: &str) -> RuntimeResult<()>;
52
53    /// Run a shell command, with a `timeout_ms` hint and cancellation.
54    ///
55    /// Implementations should `select!` on `cancel.cancelled()` and, for
56    /// child processes, send `SIGTERM` (then `SIGKILL` after a grace
57    /// period) on cancel.
58    async fn exec(
59        &self,
60        command: &str,
61        cwd: &Path,
62        timeout_ms: Option<u64>,
63        cancel: &CancellationToken,
64    ) -> RuntimeResult<ShellResult>;
65
66    /// List files matching a glob (bounded by `limit`).
67    async fn glob(&self, pattern: &str, limit: usize) -> RuntimeResult<Vec<String>>;
68
69    /// Grep for `pattern`, bounded by `max_matches`.
70    async fn grep(
71        &self,
72        pattern: &str,
73        paths: &[&str],
74        max_matches: usize,
75    ) -> RuntimeResult<Vec<String>>;
76}
77
78/// Flue's resource caps, applied uniformly across sandbox backends.
79///
80/// Values mirror `packages/runtime/src/agent.ts` constants.
81#[derive(Debug, Clone, Copy)]
82pub struct Limits {
83    /// Max lines returned by `read`.
84    pub max_read_lines: usize,
85    /// Max bytes returned by `read`.
86    pub max_read_bytes: usize,
87    /// Max grep matches.
88    pub max_grep_matches: usize,
89    /// Max glob results.
90    pub max_glob_results: usize,
91    /// Max line length before truncation.
92    pub max_grep_line_length: usize,
93    /// Max file size (bytes) for a non-truncating `edit` read. Files larger
94    /// than this are rejected with [`RuntimeError::FileTooLarge`] rather than
95    /// edited (which would risk data loss from a truncated read).
96    pub max_edit_bytes: usize,
97}
98
99impl Default for Limits {
100    fn default() -> Self {
101        // Mirror Flue's constants exactly so behavior matches.
102        Self {
103            max_read_lines: 2000,
104            max_read_bytes: 50 * 1024,
105            max_grep_matches: 100,
106            max_glob_results: 1000,
107            max_grep_line_length: 500,
108            max_edit_bytes: 256 * 1024,
109        }
110    }
111}
112
113/// Read an entire file ignoring the caps (used by internal helpers).
114pub async fn read_all(env: &dyn SessionEnv, path: &Path) -> RuntimeResult<String> {
115    env.read_file(path, usize::MAX, usize::MAX).await
116}