Skip to main content

ferrin_tool/sandbox/
mod.rs

1//! Sandbox abstraction (feature `sandbox`): a session that reads and writes
2//! files and runs commands on behalf of tools.
3//!
4//! Only the trait and [`LocalProcessSandbox`] (which offers **no isolation**
5//! and exists for tests and examples) live here; real sandboxes are provided
6//! by applications or separate crates.
7
8mod local;
9
10use std::collections::BTreeMap;
11use std::io;
12
13use bytes::Bytes;
14use ferrin_spec::BoxFuture;
15use ferrin_spec::BoxStream;
16pub use local::LocalProcessSandbox;
17use tokio_util::sync::CancellationToken;
18
19/// Byte stream of file or process output.
20pub type ByteStream = BoxStream<'static, io::Result<Bytes>>;
21
22/// Options for reading a file.
23#[derive(Debug, Clone)]
24pub struct ReadFileOptions {
25    /// Path inside the sandbox.
26    pub path: String,
27    /// Cancels the read.
28    pub cancellation: CancellationToken,
29}
30
31impl ReadFileOptions {
32    /// Reads `path`.
33    #[must_use]
34    pub fn new(path: impl Into<String>) -> Self {
35        Self {
36            path: path.into(),
37            cancellation: CancellationToken::new(),
38        }
39    }
40}
41
42/// Options for reading a text file.
43#[derive(Debug, Clone)]
44pub struct ReadTextFileOptions {
45    /// Path inside the sandbox.
46    pub path: String,
47    /// Text encoding (`utf-8` when absent).
48    pub encoding: Option<String>,
49    /// 1-based inclusive first line.
50    pub start_line: Option<usize>,
51    /// 1-based inclusive last line; past the end reads through EOF.
52    pub end_line: Option<usize>,
53    /// Cancels the read.
54    pub cancellation: CancellationToken,
55}
56
57impl ReadTextFileOptions {
58    /// Reads all of `path`.
59    #[must_use]
60    pub fn new(path: impl Into<String>) -> Self {
61        Self {
62            path: path.into(),
63            encoding: None,
64            start_line: None,
65            end_line: None,
66            cancellation: CancellationToken::new(),
67        }
68    }
69
70    /// Restricts to a line range.
71    #[must_use]
72    pub fn lines(mut self, start_line: usize, end_line: usize) -> Self {
73        self.start_line = Some(start_line);
74        self.end_line = Some(end_line);
75        self
76    }
77}
78
79/// Options for writing a file; `C` is the payload type.
80#[derive(Debug)]
81pub struct WriteFileOptions<C> {
82    /// Path inside the sandbox.
83    pub path: String,
84    /// Content to write.
85    pub content: C,
86    /// Cancels the write.
87    pub cancellation: CancellationToken,
88}
89
90impl<C> WriteFileOptions<C> {
91    /// Writes `content` to `path`.
92    #[must_use]
93    pub fn new(path: impl Into<String>, content: C) -> Self {
94        Self {
95            path: path.into(),
96            content,
97            cancellation: CancellationToken::new(),
98        }
99    }
100}
101
102/// Options for running a command.
103#[derive(Debug, Clone)]
104pub struct ProcessOptions {
105    /// Shell command line.
106    pub command: String,
107    /// Working directory inside the sandbox.
108    pub working_directory: Option<String>,
109    /// Extra environment variables (override the sandbox defaults).
110    pub env: BTreeMap<String, String>,
111    /// Kills the process when triggered.
112    pub cancellation: CancellationToken,
113}
114
115impl ProcessOptions {
116    /// Runs `command`.
117    #[must_use]
118    pub fn new(command: impl Into<String>) -> Self {
119        Self {
120            command: command.into(),
121            working_directory: None,
122            env: BTreeMap::new(),
123            cancellation: CancellationToken::new(),
124        }
125    }
126
127    /// Sets the working directory.
128    #[must_use]
129    pub fn in_directory(mut self, directory: impl Into<String>) -> Self {
130        self.working_directory = Some(directory.into());
131        self
132    }
133
134    /// Adds an environment variable.
135    #[must_use]
136    pub fn env(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
137        self.env.insert(name.into(), value.into());
138        self
139    }
140}
141
142/// Result of a completed command.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct ProcessResult {
145    /// Exit code (`-1` when the process was killed by a signal).
146    pub exit_code: i32,
147    /// Captured standard output.
148    pub stdout: String,
149    /// Captured standard error.
150    pub stderr: String,
151}
152
153/// A running process started with [`Sandbox::spawn`].
154///
155/// Implementations must make `kill` idempotent and let `wait` be called
156/// after the output streams were taken.
157pub trait SandboxProcess: Send {
158    /// Process id, when known.
159    fn pid(&self) -> Option<u32>;
160    /// Takes the standard output stream (once).
161    fn take_stdout(&mut self) -> Option<ByteStream>;
162    /// Takes the standard error stream (once).
163    fn take_stderr(&mut self) -> Option<ByteStream>;
164    /// Waits for exit, returning the exit code.
165    fn wait(&mut self) -> BoxFuture<'_, io::Result<i32>>;
166    /// Terminates the process.
167    fn kill(&mut self) -> BoxFuture<'_, io::Result<()>>;
168}
169
170/// A sandbox session.
171///
172/// Implement this to run tools inside a container, VM or remote worker.
173/// Reads return `Ok(None)` for missing files; writes create parent
174/// directories and overwrite; `run` is `spawn` plus collecting both output
175/// streams.
176pub trait Sandbox: Send + Sync {
177    /// Human-readable description added to agent instructions.
178    fn description(&self) -> &str;
179    /// Streams a file's bytes.
180    fn read_file(&self, options: ReadFileOptions) -> BoxFuture<'_, io::Result<Option<ByteStream>>>;
181    /// Reads a file into memory.
182    fn read_binary_file(
183        &self,
184        options: ReadFileOptions,
185    ) -> BoxFuture<'_, io::Result<Option<Bytes>>>;
186    /// Reads a text file, optionally a line range.
187    fn read_text_file(
188        &self,
189        options: ReadTextFileOptions,
190    ) -> BoxFuture<'_, io::Result<Option<String>>>;
191    /// Writes a file from a byte stream.
192    fn write_file(&self, options: WriteFileOptions<ByteStream>) -> BoxFuture<'_, io::Result<()>>;
193    /// Writes a file from bytes.
194    fn write_binary_file(&self, options: WriteFileOptions<Bytes>) -> BoxFuture<'_, io::Result<()>>;
195    /// Writes a text file.
196    fn write_text_file(&self, options: WriteFileOptions<String>) -> BoxFuture<'_, io::Result<()>>;
197    /// Starts a process.
198    fn spawn(&self, options: ProcessOptions) -> BoxFuture<'_, io::Result<Box<dyn SandboxProcess>>>;
199    /// Runs a command to completion.
200    fn run(&self, options: ProcessOptions) -> BoxFuture<'_, io::Result<ProcessResult>>;
201}