Skip to content

Commit 7746c8d

Browse files
committed
Added: Wire bubblewrap sandbox profiles into agent build pipeline
- Add factory module with create_sandbox, create_sandbox_with, create_temp_sandbox - Add SandboxDirs and TempSandboxDirs for borrowed and owned directory layouts - Add new_with_sandbox and new_with_temp_sandbox constructors to AgentBuildContext - Pass sandbox profile to BashTool during agent build - Enable default features on bubblewrap dependency in core crate
1 parent 06ec3c1 commit 7746c8d

8 files changed

Lines changed: 553 additions & 12 deletions

File tree

src/llm-coding-tools-bubblewrap/ARCHITECTURE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ llm-coding-tools-bubblewrap
1717
│ ├── mod.rs module root; re-exports public API surface
1818
│ ├── types.rs Profile, Preset, TmpBacking, Availability, etc.
1919
│ ├── builder.rs Builder + build() + static arg precomputation
20+
│ ├── factory.rs create_sandbox, create_sandbox_with, TempSandboxDirs (feature "default-sandbox")
2021
│ ├── presets.rs public_bot() & trusted_maintenance() constructors
2122
│ ├── validation.rs path/symlink/env/tmp validators
2223
│ └── layout.rs SandboxLayout — "is this host path visible inside?"

src/llm-coding-tools-bubblewrap/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ readme = "README.md"
1212
default = ["tokio"]
1313
tokio = ["dep:process-wrap", "process-wrap/tokio1", "process-wrap/process-group"]
1414
blocking = ["dep:process-wrap", "process-wrap/std", "process-wrap/process-group"]
15-
1615
[dependencies]
1716
parking_lot = "0.12"
1817
thiserror = "2.0"
1918
process-wrap = { version = "9", default-features = false, optional = true }
19+
tempfile = "3"
2020

2121
[dev-dependencies]
2222
rstest = "0.26"

src/llm-coding-tools-bubblewrap/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ pub mod wrap;
1313
mod test_helpers;
1414

1515
pub use error::LinuxBwrapError;
16+
pub use profile::{
17+
create_sandbox, create_sandbox_with, create_temp_sandbox, CreateSandboxError, SandboxDirs,
18+
TempSandboxDirs,
19+
};
1620
pub use profile::{
1721
Availability, Builder, EnvVar, FileMount, NetworkPolicy, Preset, Profile, Symlink, TmpBacking,
1822
};
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
//! Convenience constructors for sandbox profiles.
2+
//!
3+
//! Use [`create_sandbox`] for preset profiles or [`create_sandbox_with`] for
4+
//! custom builders, passing a [`SandboxDirs`] that specifies the host
5+
//! directory paths. Use [`create_temp_sandbox`] for the common case where
6+
//! all directories are auto-managed temporaries.
7+
//!
8+
//! # Directory ownership
9+
//!
10+
//! [`SandboxDirs`] borrows its paths, so the backing storage must outlive
11+
//! the `create_sandbox` / `create_sandbox_with` call. When all directories
12+
//! are ephemeral, [`TempSandboxDirs`] owns the temp tree and
13+
//! [`TempSandboxDirs::as_dirs`] provides the borrowed view.
14+
15+
use super::builder::Builder;
16+
use super::types::{Availability, Preset, Profile};
17+
use crate::LinuxBwrapError;
18+
use std::path::Path;
19+
use std::sync::Arc;
20+
21+
/// Borrowed directory paths for sandbox construction.
22+
///
23+
/// Lightweight view over three host directories that a sandbox profile
24+
/// needs: a synthetic home, a cache root, and a host-tmp directory.
25+
/// Because the fields are borrowed references, the backing storage must
26+
/// outlive this value.
27+
///
28+
/// Use [`TempSandboxDirs::as_dirs`] when all directories come from an
29+
/// auto-managed temp tree, or construct this directly when mixing
30+
/// persisted and ephemeral directories.
31+
///
32+
/// # Examples
33+
///
34+
/// Mixed persistent cache with ephemeral home and host-tmp:
35+
///
36+
/// ```no_run
37+
/// use llm_coding_tools_bubblewrap::profile::SandboxDirs;
38+
/// use std::path::Path;
39+
///
40+
/// let temp = llm_coding_tools_bubblewrap::TempSandboxDirs::new().unwrap();
41+
/// let dirs = SandboxDirs::new(
42+
/// temp.home(), // ephemeral
43+
/// Path::new("/persistent/cache"), // survives across sessions
44+
/// temp.host_tmp(), // ephemeral
45+
/// );
46+
/// ```
47+
pub struct SandboxDirs<'a> {
48+
home: &'a Path,
49+
cache: &'a Path,
50+
host_tmp: &'a Path,
51+
}
52+
53+
impl<'a> SandboxDirs<'a> {
54+
/// Creates a new directory spec from borrowed host paths.
55+
///
56+
/// # Arguments
57+
/// - `home` - Host path to the synthetic home directory.
58+
/// - `cache` - Host path to the cache root directory.
59+
/// - `host_tmp` - Host path to the host-tmp directory.
60+
pub fn new(home: &'a Path, cache: &'a Path, host_tmp: &'a Path) -> Self {
61+
Self {
62+
home,
63+
cache,
64+
host_tmp,
65+
}
66+
}
67+
68+
/// Returns the host path to the synthetic home directory.
69+
pub fn home(&self) -> &'a Path {
70+
self.home
71+
}
72+
73+
/// Returns the host path to the cache root directory.
74+
pub fn cache(&self) -> &'a Path {
75+
self.cache
76+
}
77+
78+
/// Returns the host path to the host-tmp directory.
79+
pub fn host_tmp(&self) -> &'a Path {
80+
self.host_tmp
81+
}
82+
}
83+
84+
/// Auto-managed temp directory layout for sandbox construction.
85+
///
86+
/// Creates a temp directory with `home`, `cache`, and `host-tmp`
87+
/// subdirectories. The `cache` subdirectory also gets `xdg-cache` and
88+
/// `xdg-state` sub-subdirectories created by [`Builder::build`].
89+
///
90+
/// Wrapped in [`Arc`] when returned from [`create_temp_sandbox`] so it can
91+
/// be stored alongside the profile in shared state.
92+
pub struct TempSandboxDirs {
93+
tmpdir: tempfile::TempDir,
94+
home: Box<Path>,
95+
cache: Box<Path>,
96+
host_tmp: Box<Path>,
97+
}
98+
99+
impl TempSandboxDirs {
100+
/// Creates a new temp directory layout.
101+
///
102+
/// # Returns
103+
/// - `Ok(Self)`: A temp directory layout with `home`, `cache`, and
104+
/// `host-tmp` subdirectories created under a system temp directory.
105+
///
106+
/// # Errors
107+
/// - Returns [`std::io::Error`] when the system temp directory cannot be
108+
/// created (e.g., no writable temp dir, disk full, or permission denied),
109+
/// or when any subdirectory (`home`, `cache`, or `host-tmp`) cannot be
110+
/// created inside the temp directory.
111+
pub fn new() -> std::io::Result<Self> {
112+
let tmpdir = tempfile::Builder::new()
113+
.prefix("llm-coding-tools-sandbox-")
114+
.tempdir()?;
115+
116+
let home = tmpdir.path().join("home").into_boxed_path();
117+
let cache = tmpdir.path().join("cache").into_boxed_path();
118+
let host_tmp = tmpdir.path().join("host-tmp").into_boxed_path();
119+
120+
// Builder::build() validates directories exist, so create them first.
121+
std::fs::create_dir_all(&home)?;
122+
std::fs::create_dir_all(&cache)?;
123+
std::fs::create_dir_all(&host_tmp)?;
124+
125+
Ok(Self {
126+
tmpdir,
127+
home,
128+
cache,
129+
host_tmp,
130+
})
131+
}
132+
133+
/// Returns a [`SandboxDirs`] view over this temp directory layout.
134+
///
135+
/// Useful for passing to [`create_sandbox`] or [`create_sandbox_with`]
136+
/// when all directories come from this auto-managed temp tree.
137+
pub fn as_dirs(&self) -> SandboxDirs<'_> {
138+
SandboxDirs::new(self.home(), self.cache(), self.host_tmp())
139+
}
140+
141+
/// Returns the host path to the synthetic home directory.
142+
pub fn home(&self) -> &Path {
143+
&self.home
144+
}
145+
146+
/// Returns the host path to the cache root directory.
147+
pub fn cache(&self) -> &Path {
148+
&self.cache
149+
}
150+
151+
/// Returns the host path to the host-tmp directory.
152+
pub fn host_tmp(&self) -> &Path {
153+
&self.host_tmp
154+
}
155+
156+
/// Returns a reference to the underlying temp directory.
157+
pub fn temp_dir(&self) -> &tempfile::TempDir {
158+
&self.tmpdir
159+
}
160+
}
161+
162+
/// Errors that can occur while creating a sandbox profile.
163+
#[derive(Debug, thiserror::Error)]
164+
pub enum CreateSandboxError {
165+
/// Failed to create the sandbox directory layout.
166+
#[error("failed to create sandbox directories: {0}")]
167+
Dirs(#[source] std::io::Error),
168+
/// Bubblewrap is not available on the host.
169+
#[error("bubblewrap is not available: {0}")]
170+
Unavailable(String),
171+
/// Profile validation or assembly failed.
172+
#[error("profile validation failed: {0}")]
173+
Profile(#[from] LinuxBwrapError),
174+
}
175+
176+
/// Creates a sandbox from a preset and a directory spec.
177+
///
178+
/// # Arguments
179+
/// - `workspace`: Host path to the project directory mounted inside the sandbox.
180+
/// - `preset`: The sandbox preset controlling mount layout and permissions.
181+
/// - `dirs`: The host directory paths for home, cache, and host-tmp.
182+
///
183+
/// # Returns
184+
/// - `Ok(`[`Arc<Profile>`]`)`: A ready-to-use sandbox profile.
185+
///
186+
/// # Errors
187+
/// - Returns [`CreateSandboxError::Unavailable`] when `bwrap` is not found on
188+
/// `PATH` or is otherwise unusable on the host (see [`Availability::detect`]).
189+
/// - Returns [`CreateSandboxError::Profile`] when [`Builder::build`] returns
190+
/// [`LinuxBwrapError`] (e.g., invalid paths, missing host shell inside the
191+
/// sandbox).
192+
pub fn create_sandbox(
193+
workspace: &Path,
194+
preset: Preset,
195+
dirs: &SandboxDirs<'_>,
196+
) -> Result<Arc<Profile>, CreateSandboxError> {
197+
// Select the builder preset for the desired sandbox policy.
198+
let builder = match preset {
199+
Preset::TrustedMaintenance => {
200+
Builder::trusted_maintenance(workspace, dirs.home(), dirs.cache(), dirs.host_tmp())
201+
}
202+
Preset::PublicBot => Builder::public_bot(workspace, dirs.home(), dirs.cache(), None),
203+
};
204+
// Verify bubblewrap is present and usable on the host before building.
205+
let availability = Availability::detect();
206+
if !availability.is_available() {
207+
return Err(CreateSandboxError::Unavailable(
208+
availability
209+
.reason()
210+
.unwrap_or("unknown reason")
211+
.to_string(),
212+
));
213+
}
214+
// Assemble and validate the profile, surfacing any structural errors.
215+
let profile = builder
216+
.with_availability(availability)
217+
.build()
218+
.map_err(CreateSandboxError::Profile)?;
219+
Ok(Arc::new(profile))
220+
}
221+
222+
/// Creates a sandbox with a custom builder and a directory spec.
223+
///
224+
/// Availability detection is handled automatically.
225+
///
226+
/// # Arguments
227+
/// - `workspace`: Host path to the project directory mounted inside the sandbox.
228+
/// - `dirs`: The host directory paths for home, cache, and host-tmp.
229+
/// - `f`: Closure receiving the workspace path and a [`SandboxDirs`] view;
230+
/// must return a configured [`Builder`].
231+
///
232+
/// # Returns
233+
/// - `Ok(`[`Arc<Profile>`]`)`: A ready-to-use sandbox profile.
234+
///
235+
/// # Errors
236+
/// - Returns [`CreateSandboxError::Unavailable`] when `bwrap` is not found on
237+
/// `PATH` or is otherwise unusable on the host (see [`Availability::detect`]).
238+
/// - Returns [`CreateSandboxError::Profile`] when [`Builder::build`] returns
239+
/// [`LinuxBwrapError`] (e.g., invalid paths, missing host shell inside the
240+
/// sandbox).
241+
pub fn create_sandbox_with<F>(
242+
workspace: &Path,
243+
dirs: &SandboxDirs<'_>,
244+
f: F,
245+
) -> Result<Arc<Profile>, CreateSandboxError>
246+
where
247+
F: FnOnce(&Path, &SandboxDirs<'_>) -> Builder,
248+
{
249+
let builder = f(workspace, dirs);
250+
let availability = Availability::detect();
251+
if !availability.is_available() {
252+
return Err(CreateSandboxError::Unavailable(
253+
availability
254+
.reason()
255+
.unwrap_or("unknown reason")
256+
.to_string(),
257+
));
258+
}
259+
let profile = builder
260+
.with_availability(availability)
261+
.build()
262+
.map_err(CreateSandboxError::Profile)?;
263+
Ok(Arc::new(profile))
264+
}
265+
266+
/// Creates a sandbox from a preset with auto-managed temp directories.
267+
///
268+
/// Convenience wrapper that creates a [`TempSandboxDirs`] and delegates to
269+
/// [`create_sandbox`]. The temp directory is wrapped in [`Arc`] so it can be
270+
/// stored alongside the profile in shared state.
271+
///
272+
/// # Directory layout
273+
///
274+
/// See [`TempSandboxDirs`].
275+
///
276+
/// # Arguments
277+
/// - `workspace`: Host path to the project directory mounted inside the sandbox.
278+
/// - `preset`: The sandbox preset controlling mount layout and permissions.
279+
///
280+
/// # Returns
281+
/// - `Ok((`[`Arc<Profile>`]`, `[`Arc<TempSandboxDirs>`]`))`: A ready-to-use
282+
/// sandbox profile and its owning temp directory layout.
283+
///
284+
/// # Errors
285+
/// - Returns [`CreateSandboxError::Dirs`] when the system temp directory or
286+
/// any subdirectory cannot be created (see [`TempSandboxDirs::new`]).
287+
/// - Returns [`CreateSandboxError::Unavailable`] when `bwrap` is not found on
288+
/// `PATH` or is otherwise unusable on the host (see [`Availability::detect`]).
289+
/// - Returns [`CreateSandboxError::Profile`] when [`Builder::build`] returns
290+
/// [`LinuxBwrapError`] (e.g., invalid paths, missing host shell inside the
291+
/// sandbox).
292+
pub fn create_temp_sandbox(
293+
workspace: &Path,
294+
preset: Preset,
295+
) -> Result<(Arc<Profile>, Arc<TempSandboxDirs>), CreateSandboxError> {
296+
let dirs = TempSandboxDirs::new().map_err(CreateSandboxError::Dirs)?;
297+
let profile = create_sandbox(workspace, preset, &dirs.as_dirs())?;
298+
Ok((profile, Arc::new(dirs)))
299+
}

src/llm-coding-tools-bubblewrap/src/profile/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
//! - [`TmpBacking`] - how sandbox `/tmp` is mounted
88
99
mod builder;
10+
mod factory;
1011
pub(crate) mod layout;
1112
mod presets;
1213
mod types;
1314
pub(crate) mod validation;
1415

1516
pub use builder::Builder;
17+
pub use factory::{
18+
create_sandbox, create_sandbox_with, create_temp_sandbox, CreateSandboxError, SandboxDirs,
19+
TempSandboxDirs,
20+
};
1621
pub use types::{
1722
Availability, EnvVar, FileMount, FileOverlay, NetworkPolicy, Preset, Profile, Symlink,
1823
TmpBacking,

src/llm-coding-tools-core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ process-wrap = { version = "9.1", default-features = false }
103103
const_format = "0.2.35"
104104

105105
# Linux sandbox types and runtime
106-
llm-coding-tools-bubblewrap = { version = "0.1.0", path = "../llm-coding-tools-bubblewrap", optional = true, default-features = false }
106+
llm-coding-tools-bubblewrap = { version = "0.1.0", path = "../llm-coding-tools-bubblewrap", optional = true }
107107

108108
[dev-dependencies]
109109
serial_test = "3"

0 commit comments

Comments
 (0)