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}