fabro/lib/components/fabro-sandbox/tests/plugin_provider.rs
Bryan Helmkamp 4dcbcec400
Add the single sandbox provider construction function
fabro_sandbox::driver::connect_provider turns one
[server.sandbox.providers.<kind>] entry into an Arc<dyn SandboxProvider>
from the sandbox-driver crates. Bundled kinds link the driver's Host,
Docker, and Daytona providers in-process; Daytona receives its API key
and endpoint explicitly through connect_explicit with a
fabro-sandbox/<version> user agent, never from the process environment.
Any other kind launches the configured plugin executable through a
PluginSupervisor and returns a wrapper that replaces a crashed plugin
for new work only, never replaying a failed call. Disabled entries are
refused at the construction point.

The Host and Docker providers also ship as fabro-sandbox-host and
fabro-sandbox-docker plugin executables so CI can drive the bundled
providers over stdio and a deployment can move one out of process by
configuration alone. An integration test registers the Host executable
under the non-bundled kind `host`, creates a sandbox and runs a command
over the wire, then attaches to it by persisted id from a fresh plugin
process, and checks that a plugin declaring a different kind is refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 15:23:56 -06:00

110 lines
4 KiB
Rust

//! The construction function serves a non-bundled kind through a plugin
//! executable, and a sandbox created through one plugin generation is
//! reachable by persisted id from a fresh connection.
use std::collections::BTreeMap;
use fabro_sandbox::driver::{ProviderConnectOptions, connect_provider};
use fabro_types::SandboxProviderKind;
use fabro_types::settings::server::{SandboxPluginSettings, ServerSandboxProviderSettings};
use sandbox_driver::{ExecSpec, SandboxId, SandboxSource, SandboxSpec};
const HOST_PLUGIN: &str = env!("CARGO_BIN_EXE_fabro-sandbox-host");
fn host_plugin_settings(registry: &std::path::Path) -> ServerSandboxProviderSettings {
ServerSandboxProviderSettings {
enabled: true,
plugin: Some(SandboxPluginSettings {
path: Some(HOST_PLUGIN.to_string()),
sha256: None,
dev: true,
args: Vec::new(),
env: BTreeMap::from([(
"SANDBOX_DRIVER_HOST_REGISTRY".to_string(),
registry.display().to_string(),
)]),
inherit_env: Vec::new(),
}),
}
}
#[tokio::test]
async fn host_plugin_under_a_non_bundled_kind_creates_and_reattaches_by_persisted_id() {
let registry = tempfile::tempdir().expect("registry tempdir");
let workspace = tempfile::tempdir().expect("workspace tempdir");
let kind = SandboxProviderKind::try_new("host").expect("host is a valid kind");
assert_eq!(
kind.bundled(),
None,
"host is not one of fabro's bundled kinds"
);
let settings = host_plugin_settings(registry.path());
let persisted_id: SandboxId = {
let connected = connect_provider(&kind, &settings, &ProviderConnectOptions::default())
.await
.expect("plugin launches");
assert_eq!(connected.kind, kind);
assert_eq!(connected.provider.kind().as_str(), "host");
let spec = SandboxSpec::new(SandboxSource::HostDirectory)
.working_directory(workspace.path().display().to_string())
.label("sh.fabro.managed", "true");
let sandbox = connected
.provider
.create(&spec, None)
.await
.expect("create over the wire");
let result = sandbox
.exec()
.run(&ExecSpec::bash(
"printf hello > marker.txt && cat marker.txt",
))
.await
.expect("exec over the wire");
assert!(result.success(), "{result:?}");
assert_eq!(result.stdout_lossy(), "hello");
sandbox.id().clone()
};
// A fresh connection is a new plugin process; the id alone must be
// enough to find the sandbox again, exactly as run reconnect will do.
let connected = connect_provider(&kind, &settings, &ProviderConnectOptions::default())
.await
.expect("plugin relaunches");
let sandbox = connected
.provider
.attach(&persisted_id, None)
.await
.expect("attach by persisted id");
let content = sandbox
.fs()
.read("marker.txt")
.await
.expect("file survives across plugin generations");
assert_eq!(content, b"hello");
assert!(workspace.path().join("marker.txt").is_file());
sandbox.delete().await.expect("delete releases the handle");
assert!(
workspace.path().is_dir(),
"designated directories are never removed by delete"
);
}
#[tokio::test]
async fn a_plugin_that_declares_another_kind_is_rejected() {
let registry = tempfile::tempdir().expect("registry tempdir");
let kind = SandboxProviderKind::try_new("e2b").expect("valid kind");
let error = connect_provider(
&kind,
&host_plugin_settings(registry.path()),
&ProviderConnectOptions::default(),
)
.await
.err()
.expect("the host executable declares `host`, not `e2b`");
let rendered = format!(
"{error}: {:?}",
std::error::Error::source(&error).map(ToString::to_string)
);
assert!(rendered.contains("e2b"), "{rendered}");
}