use super::{Backend, SpacesdClient, ms, run};
use crate::types::{ExitInfo, ImageFormat, ProcessOutput, Screenshot};
use crate::{CuaError, Result};
use cua_daemon::{CreateRequest, SandboxRecord};
use cua_sandbox_core::{Probe, ProviderKind as CoreProvider, Status};
use std::{collections::HashMap, sync::Arc};
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum SandboxStatus {
Running,
Suspended,
Stopped,
Provisioning,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum SandboxPhase {
Provisioning,
Starting,
Ready,
Stopped,
}
impl SandboxPhase {
fn of(status: SandboxStatus) -> Self {
match status {
SandboxStatus::Running => SandboxPhase::Ready,
SandboxStatus::Provisioning => SandboxPhase::Provisioning,
SandboxStatus::Suspended | SandboxStatus::Stopped => SandboxPhase::Stopped,
SandboxStatus::Unknown => SandboxPhase::Starting,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct ImageInfo {
pub reference: String,
pub pinned_ref: String,
pub digest: String,
pub variant: String,
pub arch: Option<String>,
pub os: String,
pub emulated: bool,
#[uniffi(default = None)]
pub spacesd: Option<bool>,
}
impl From<cua_sandbox_core::ImageInfo> for ImageInfo {
fn from(i: cua_sandbox_core::ImageInfo) -> Self {
Self {
reference: i.reference,
pinned_ref: i.pinned_ref,
digest: i.digest,
variant: i.variant,
arch: i.arch,
os: i.os,
emulated: i.emulated,
spacesd: i.spacesd,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct SandboxInfo {
pub name: String,
pub kind: String,
pub runtime: String,
pub runtime_type: String,
pub status: SandboxStatus,
pub status_detail: Option<String>,
pub ephemeral: bool,
pub services: HashMap<String, u16>,
pub endpoints: HashMap<String, String>,
pub image: Option<String>,
pub id: String,
pub phase: SandboxPhase,
pub location: String,
pub expires_at_unix: Option<i64>,
pub provider_details: HashMap<String, String>,
#[uniffi(default = None)]
pub image_info: Option<ImageInfo>,
#[uniffi(default = None)]
pub stop_reason: Option<String>,
}
impl From<SandboxRecord> for SandboxInfo {
fn from(r: SandboxRecord) -> Self {
let (status, detail) = match r.status {
Status::Running => (SandboxStatus::Running, None),
Status::Suspended => (SandboxStatus::Suspended, None),
Status::Stopped => (SandboxStatus::Stopped, None),
Status::Provisioning => (SandboxStatus::Provisioning, None),
Status::Unknown(u) => (SandboxStatus::Unknown, Some(u)),
};
SandboxInfo {
id: r.id,
phase: SandboxPhase::of(status),
location: r.location,
expires_at_unix: r.expires_at.map(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}),
provider_details: r.provider_details.into_iter().collect(),
name: r.name,
kind: r.kind,
runtime: r.runtime,
runtime_type: r.runtime_type,
status,
status_detail: detail,
ephemeral: r.ephemeral,
services: r.services.into_iter().collect(),
endpoints: r.endpoints.into_iter().collect(),
image: r.image,
image_info: r.image_info.map(Into::into),
stop_reason: r.stop_reason,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, uniffi::Record)]
pub struct ReadinessProbe {
#[uniffi(default = 0)]
pub port: u16,
#[uniffi(default = None)]
pub http_path: Option<String>,
#[uniffi(default = None)]
pub http_status: Option<u16>,
#[uniffi(default = None)]
pub service: Option<String>,
}
impl ReadinessProbe {
pub fn tcp(service: impl Into<String>) -> Self {
Self {
service: Some(service.into()),
..Default::default()
}
}
pub fn http(service: impl Into<String>, path: impl Into<String>) -> Self {
Self {
service: Some(service.into()),
http_path: Some(path.into()),
..Default::default()
}
}
pub(crate) fn resolve(&self, services: &HashMap<String, u16>) -> Result<Probe> {
let port = match self.service.as_deref().filter(|s| !s.is_empty()) {
Some(name) => match services.get(name).copied().filter(|p| *p > 0) {
Some(p) => p,
None if name == "env" => cua_sandbox_core::ENV_PORT,
None => {
return Err(CuaError::InvalidArgument(format!(
"readiness probe names service {name:?}, which has no known port \
(declared: {:?})",
services.keys().collect::<Vec<_>>()
)));
}
},
None => self.port,
};
if port == 0 {
return Err(CuaError::InvalidArgument(
"probe needs a port > 0 or a declared service".into(),
));
}
Ok(match &self.http_path {
None => Probe::Tcp(port),
Some(path) => Probe::Http {
port,
path: if path.starts_with('/') {
path.clone()
} else {
format!("/{path}")
},
status: self.http_status,
},
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct Container {
pub image: String,
#[uniffi(default = None)]
pub command: Option<Vec<String>>,
#[uniffi(default)]
pub env: HashMap<String, String>,
#[uniffi(default = [])]
pub ports: Vec<u16>,
#[uniffi(default = None)]
pub name: Option<String>,
}
impl Container {
pub(crate) fn to_core(&self) -> cua_sandbox_core::Sidecar {
let mut s = cua_sandbox_core::Sidecar::new(self.image.clone());
if let Some(n) = self.name.clone().filter(|n| !n.is_empty()) {
s.name = n;
}
s.command = self.command.clone().filter(|c| !c.is_empty());
s.env = self.env.clone().into_iter().collect();
s.ports = self.ports.clone();
s
}
}
#[derive(Clone, PartialEq, Eq, uniffi::Enum)]
pub enum RegistrySecret {
Basic {
username: String,
password: String,
registry: Option<String>,
},
FromEnv {
username_var: String,
password_var: String,
registry: Option<String>,
},
AwsEcr {
region: Option<String>,
},
}
impl std::fmt::Debug for RegistrySecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegistrySecret::Basic {
username, registry, ..
} => f
.debug_struct("Basic")
.field("username", username)
.field("password", &"<redacted>")
.field("registry", registry)
.finish(),
RegistrySecret::FromEnv {
username_var,
password_var,
registry,
} => f
.debug_struct("FromEnv")
.field("username_var", username_var)
.field("password_var", password_var)
.field("registry", registry)
.finish(),
RegistrySecret::AwsEcr { region } => {
f.debug_struct("AwsEcr").field("region", region).finish()
}
}
}
}
impl RegistrySecret {
pub(crate) async fn credentials(
&self,
image: &str,
) -> Result<cua_sandbox_core::RegistryCredentials> {
use cua_sandbox_core::RegistryCredentials as C;
let scope = |c: C, registry: &Option<String>| match registry.as_deref() {
Some(r) if !r.is_empty() => c.for_registry(r),
_ => c,
};
match self {
RegistrySecret::Basic {
username,
password,
registry,
} => {
if username.is_empty() || password.is_empty() {
return Err(CuaError::InvalidArgument(
"RegistrySecret needs a username and a password".into(),
));
}
Ok(scope(C::new(username, password), registry))
}
RegistrySecret::FromEnv {
username_var,
password_var,
registry,
} => {
let read = |var: &str| {
std::env::var(var)
.ok()
.filter(|v| !v.is_empty())
.ok_or_else(|| {
CuaError::InvalidArgument(format!(
"RegistrySecret.from_env: ${var} is not set"
))
})
};
Ok(scope(
C::new(read(username_var)?, read(password_var)?),
registry,
))
}
RegistrySecret::AwsEcr { region } => {
let host = cua_image::registry_of(image).to_string();
let region = region
.clone()
.filter(|r| !r.is_empty())
.or_else(|| cua_image::auth::ecr_region(&host).map(str::to_string))
.ok_or_else(|| {
CuaError::InvalidArgument(format!(
"RegistrySecret.aws_ecr: {image} is not a private ECR image \
(<account>.dkr.ecr.<region>.amazonaws.com); pass the region"
))
})?;
let token = cua_image::auth::aws_ecr_login(®ion).await.ok_or_else(|| {
CuaError::InvalidArgument(format!(
"RegistrySecret.aws_ecr: `aws ecr get-login-password --region {region}` \
failed (install the AWS CLI and sign in)"
))
})?;
Ok(C::new("AWS", token).for_registry(host))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum)]
pub enum ImageLayer {
AptInstall {
packages: Vec<String>,
},
PipInstall {
packages: Vec<String>,
},
UvInstall {
packages: Vec<String>,
},
Run {
command: String,
},
AppInstall {
app_id: String,
},
}
impl ImageLayer {
fn to_core(&self) -> cua_image::spec::ImageLayer {
use cua_image::spec::ImageLayer as L;
match self.clone() {
ImageLayer::AptInstall { packages } => L::AptInstall { packages },
ImageLayer::PipInstall { packages } => L::PipInstall { packages },
ImageLayer::UvInstall { packages } => L::UvInstall { packages },
ImageLayer::Run { command } => L::Run { command },
ImageLayer::AppInstall { app_id } => L::AppInstall { app_id },
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct BuildFile {
pub source: String,
pub destination: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, uniffi::Record)]
pub struct ImageBuild {
#[uniffi(default = [])]
pub layers: Vec<ImageLayer>,
#[uniffi(default)]
pub env: HashMap<String, String>,
#[uniffi(default = [])]
pub ports: Vec<u16>,
#[uniffi(default = [])]
pub files: Vec<BuildFile>,
#[uniffi(default = None)]
pub timeout_ms: Option<u32>,
}
impl ImageBuild {
pub(crate) fn to_core(&self) -> cua_sandbox_core::BuildSpec {
cua_sandbox_core::BuildSpec {
from: String::new(),
layers: self.layers.iter().map(ImageLayer::to_core).collect(),
env: self.env.clone().into_iter().collect(),
ports: self.ports.clone(),
files: self
.files
.iter()
.map(|f| cua_sandbox_core::BuildFile {
source: f.source.clone().into(),
destination: f.destination.clone(),
})
.collect(),
timeout: self.timeout_ms.map(super::millis),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct SandboxCreateOptions {
#[uniffi(default = None)]
pub on: Option<String>,
#[uniffi(default = None)]
pub kind: Option<String>,
#[uniffi(default = None)]
pub runtime: Option<String>,
#[uniffi(default = "")]
pub image: String,
#[uniffi(default = None)]
pub name: Option<String>,
#[uniffi(default = None)]
pub token: Option<String>,
#[uniffi(default = None)]
pub os: Option<String>,
#[uniffi(default = None)]
pub cpus: Option<u32>,
#[uniffi(default = None)]
pub memory_mb: Option<u64>,
#[uniffi(default = [])]
pub ports: Vec<u16>,
#[uniffi(default)]
pub services: HashMap<String, u16>,
#[uniffi(default = [])]
pub wait_for: Vec<ReadinessProbe>,
#[uniffi(default = None)]
pub ready_timeout_ms: Option<u32>,
#[uniffi(default)]
pub env: HashMap<String, String>,
#[uniffi(default = None)]
pub command: Option<Vec<String>>,
#[uniffi(default = [])]
pub sidecars: Vec<Container>,
#[uniffi(default = None)]
pub registry_secret: Option<RegistrySecret>,
#[uniffi(default = None)]
pub build: Option<ImageBuild>,
#[uniffi(default = None)]
pub network: Option<String>,
#[uniffi(default = [])]
pub overlays: Vec<super::Overlay>,
#[uniffi(default = false)]
pub keep_on_failure: bool,
#[uniffi(default = None)]
pub gpu: Option<String>,
}
impl Default for SandboxCreateOptions {
fn default() -> Self {
Self::auto("")
}
}
impl SandboxCreateOptions {
pub fn new(on: impl Into<String>, image: impl Into<String>) -> Self {
Self {
on: Some(on.into()),
..Self::auto(image)
}
}
pub fn auto(image: impl Into<String>) -> Self {
Self {
on: None,
kind: None,
runtime: None,
image: image.into(),
name: None,
token: None,
os: None,
cpus: None,
memory_mb: None,
ports: vec![],
services: HashMap::new(),
wait_for: vec![],
ready_timeout_ms: None,
env: HashMap::new(),
command: None,
sidecars: vec![],
registry_secret: None,
build: None,
network: None,
overlays: vec![],
keep_on_failure: false,
gpu: None,
}
}
pub fn placement(&self) -> Result<cua_sandbox_core::settings::Resolved> {
use cua_sandbox_core::placement::{Kind, On, Runtime};
let text = |v: &Option<String>| {
v.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
.map(str::to_string)
};
let place =
|e: cua_sandbox_core::placement::PlacementError| CuaError::InvalidPlacement(e.message);
let on = text(&self.on)
.map(|o| On::parse(&o))
.transpose()
.map_err(place)?;
let kind = text(&self.kind)
.map(|k| Kind::parse(&k))
.transpose()
.map_err(place)?;
let runtime = text(&self.runtime)
.map(|r| Runtime::parse(&r))
.transpose()
.map_err(place)?;
let settings = cua_sandbox_core::settings::Settings::load()
.map_err(|e| CuaError::InvalidArgument(e.to_string()))?;
let r = settings
.resolve(on, kind, runtime)
.map_err(|e| CuaError::InvalidArgument(e.to_string()))?;
if r.on == On::Cloud {
return Err(CuaError::Unsupported(cua_sandbox_core::CLOUD_ENDED.into()));
}
cua_sandbox_core::placement::validate(&r.on, r.kind, &r.runtime).map_err(place)?;
Ok(r)
}
fn to_request(&self) -> Result<CreateRequest> {
use cua_sandbox_core::placement::On;
let placement = self.placement()?;
let (provider, url) = match &placement.on {
On::Local => (Some(CoreProvider::Local), None),
On::Direct(a) => (Some(CoreProvider::Direct), Some(a.clone())),
On::Provider(_) => (Some(CoreProvider::Contrib), None),
On::Relay(_) | On::Host(_) | On::Cloud => (None, None),
};
Ok(CreateRequest {
location: Some(placement.on.to_string()),
kind: Some(placement.kind.to_string()),
runtime: Some(placement.runtime.to_string()),
provider,
url,
name: self.name.clone(),
image: self.image.clone(),
token: self.token.clone(),
os: self.os.clone(),
cpus: self.cpus,
memory_mb: self.memory_mb,
ports: self.ports.clone(),
services: self.services.clone().into_iter().collect(),
wait_for: self
.wait_for
.iter()
.map(|p| p.resolve(&self.services))
.collect::<Result<_>>()?,
ready_timeout: self.ready_timeout_ms.map(super::millis),
env: self.env.clone().into_iter().collect(),
command: self.command.clone().filter(|c| !c.is_empty()),
labels: Default::default(),
sidecars: self.sidecars.iter().map(Container::to_core).collect(),
registry_credentials: None,
build: self
.build
.as_ref()
.map(ImageBuild::to_core)
.filter(|b| !b.is_empty()),
container_runtime: None,
network: self.network.clone().filter(|n| !n.is_empty()),
owner_pid: Some(std::process::id()),
keep_on_failure: self.keep_on_failure,
gpu: self.gpu.clone().filter(|g| !g.trim().is_empty()),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct SandboxListing {
pub sandboxes: Vec<SandboxInfo>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct HttpHeader {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct ServiceEndpoint {
pub url: String,
pub headers: Vec<HttpHeader>,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct HttpResponse {
pub status: u16,
pub headers: Vec<HttpHeader>,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct ViewerOptions {
#[uniffi(default = None)]
pub ttl_seconds: Option<u32>,
#[uniffi(default = false)]
pub view_only: bool,
#[uniffi(default = true)]
pub clipboard: bool,
#[uniffi(default = None)]
pub files_root: Option<String>,
#[uniffi(default = true)]
pub microphone: bool,
}
impl Default for ViewerOptions {
fn default() -> Self {
Self {
ttl_seconds: None,
view_only: false,
clipboard: true,
files_root: None,
microphone: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct ViewerLink {
pub url: String,
pub expires_at_unix: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct PublicUrl {
pub id: String,
pub url: String,
pub expires_at_unix: i64,
pub service: String,
pub provider_details: HashMap<String, String>,
}
impl From<cua_daemon::shares::PublicUrl> for PublicUrl {
fn from(u: cua_daemon::shares::PublicUrl) -> Self {
PublicUrl {
id: u.id,
url: u.url,
expires_at_unix: u
.expires_at
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
service: u.service,
provider_details: u.provider_details.into_iter().collect(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct MediaBridge {
pub ws_url: String,
pub ticket: String,
pub open_media_response_json: String,
}
#[derive(uniffi::Object)]
pub struct Sandboxes {
pub(crate) backend: Backend,
}
impl Sandboxes {
async fn create_bare(
&self,
options: SandboxCreateOptions,
progress: Option<cua_sandbox_core::progress::Sink>,
) -> Result<Arc<Sandbox>> {
let mut req = options.to_request()?;
let backend = self.backend.clone();
let secret = options.registry_secret.clone();
let rec = run(async move {
if let Some(s) = secret {
req.registry_credentials = Some(s.credentials(&req.image).await?);
}
Ok(match (&backend, progress) {
(Backend::Embedded(rt), progress) => {
cua_sandbox_core::progress::carry(progress, rt.create(req)).await?
}
(Backend::Daemon(d), Some(progress)) => d.create_reporting(req, progress).await?,
(Backend::Daemon(d), None) => d.create(req).await?,
})
})
.await?;
Ok(self.handle(rec.into()))
}
pub async fn by_name(&self, name: String) -> Result<Arc<Sandbox>> {
let info = self.get(name).await?;
Ok(self.handle(info))
}
fn handle(&self, info: SandboxInfo) -> Arc<Sandbox> {
Arc::new(Sandbox {
backend: self.backend.clone(),
info,
})
}
}
#[uniffi::export]
impl Sandboxes {
pub async fn create(&self, options: SandboxCreateOptions) -> Result<Arc<Sandbox>> {
self.create_reported(options, None).await
}
}
impl Sandboxes {
pub async fn create_reporting(
&self,
options: SandboxCreateOptions,
progress: cua_sandbox_core::progress::Sink,
) -> Result<Arc<Sandbox>> {
self.create_reported(options, Some(progress)).await
}
async fn create_reported(
&self,
options: SandboxCreateOptions,
progress: Option<cua_sandbox_core::progress::Sink>,
) -> Result<Arc<Sandbox>> {
let facts = super::telemetry::CreateFacts::of(&options);
let mut options = options;
if facts.is_local() && cua_telemetry::global().is_enabled() {
options
.env
.entry("CUA_SPACESD_TELEMETRY".into())
.or_insert_with(|| "1".into());
}
let r = self.create_recorded(options, progress).await;
facts.record(&r);
r
}
async fn create_recorded(
&self,
options: SandboxCreateOptions,
progress: Option<cua_sandbox_core::progress::Sink>,
) -> Result<Arc<Sandbox>> {
let overlays = options.overlays.clone();
for o in &overlays {
o.validate()?;
}
let ready_timeout_ms = options.ready_timeout_ms;
let sandbox = self.create_bare(options, progress).await?;
if overlays.is_empty() {
return Ok(sandbox);
}
match sandbox.clone().overlay(overlays, ready_timeout_ms).await {
Ok(_) => Ok(sandbox),
Err(e) => {
let _ = sandbox.delete().await;
Err(e)
}
}
}
}
#[uniffi::export]
impl Sandboxes {
pub async fn connect_url(
&self,
url: String,
token: Option<String>,
name: Option<String>,
) -> Result<Arc<Sandbox>> {
let addr = url.trim().to_string();
if addr.is_empty() {
return Err(CuaError::InvalidArgument("connect_url needs a URL".into()));
}
self.create(SandboxCreateOptions {
name,
token,
..SandboxCreateOptions::new(format!("direct:{addr}"), "")
})
.await
}
pub async fn connect(&self, name: String) -> Result<Arc<Sandbox>> {
let started = std::time::Instant::now();
let backend = self.backend.clone();
let rec = run(async move {
Ok(match &backend {
Backend::Embedded(rt) => rt.connect(&name).await?,
Backend::Daemon(d) => d.connect(&name).await?,
})
})
.await;
super::telemetry::api("sandbox.connect", started, &rec);
Ok(self.handle(rec?.into()))
}
pub async fn list(&self, location: Option<String>) -> Result<Vec<SandboxInfo>> {
let listing = self.list_with_warnings(location).await?;
for w in &listing.warnings {
tracing::warn!("{w}");
}
Ok(listing.sandboxes)
}
pub async fn list_with_warnings(&self, location: Option<String>) -> Result<SandboxListing> {
let backend = self.backend.clone();
let p = location_filter(location.as_deref())?;
if location.as_deref().is_some_and(asks_for_cloud) {
return Ok(SandboxListing {
sandboxes: vec![],
warnings: vec![],
});
}
let listing = run(async move {
Ok(match &backend {
Backend::Embedded(rt) => rt.list_filtered(p).await?,
Backend::Daemon(d) => d.list_filtered(p).await?,
})
})
.await?;
Ok(SandboxListing {
sandboxes: listing.sandboxes.into_iter().map(Into::into).collect(),
warnings: listing.warnings,
})
}
pub async fn list_all(&self) -> Result<Vec<SandboxInfo>> {
self.list(None).await
}
pub async fn cancel_create(&self, name: String) -> Result<Option<String>> {
let backend = self.backend.clone();
run(async move {
Ok(match &backend {
Backend::Embedded(rt) => rt.cancel_create(&name).await?,
Backend::Daemon(d) => d.cancel_create(&name).await?,
})
})
.await
}
pub async fn get(&self, name: String) -> Result<SandboxInfo> {
let backend = self.backend.clone();
Ok(run(async move {
Ok(match &backend {
Backend::Embedded(rt) => rt.record(&name).await?,
Backend::Daemon(d) => d.get(&name).await?,
})
})
.await?
.into())
}
pub async fn delete(&self, name: String) -> Result<()> {
let backend = self.backend.clone();
let location = name
.split_once(':')
.map(|(l, _)| l.to_string())
.unwrap_or_default();
let r = run(async move { delete(&backend, &name).await }).await;
super::telemetry::sandbox_deleted(&location, &r);
r
}
}
impl Sandboxes {
pub async fn list_known(&self, location: Option<&str>) -> Result<Vec<SandboxInfo>> {
let backend = self.backend.clone();
let p = location_filter(location)?;
let listing = run(async move {
Ok(match &backend {
Backend::Embedded(rt) => rt.list_filtered(p).await?,
Backend::Daemon(d) => d.list_filtered(p).await?,
})
})
.await?;
Ok(listing.sandboxes.into_iter().map(Into::into).collect())
}
}
fn asks_for_cloud(location: &str) -> bool {
cua_sandbox_core::Location::parse(location.trim()) == Some(cua_sandbox_core::Location::Cloud)
}
fn location_filter(location: Option<&str>) -> Result<Option<CoreProvider>> {
let Some(l) = location.map(str::trim).filter(|l| !l.is_empty()) else {
return Ok(None);
};
match cua_sandbox_core::Location::parse(l) {
Some(cua_sandbox_core::Location::Local) => Ok(Some(CoreProvider::Local)),
Some(cua_sandbox_core::Location::Cloud) => Ok(None),
Some(cua_sandbox_core::Location::Direct) => Ok(Some(CoreProvider::Direct)),
_ => Err(CuaError::InvalidArgument(format!(
"location {l:?}: expected local or direct"
))),
}
}
async fn delete(backend: &Backend, name: &str) -> Result<()> {
match backend {
Backend::Embedded(rt) => Ok(rt.delete(name).await?),
Backend::Daemon(d) => Ok(d.delete(name).await?),
}
}
#[derive(uniffi::Object)]
pub struct Sandbox {
backend: Backend,
info: SandboxInfo,
}
#[uniffi::export]
impl Sandbox {
pub fn name(&self) -> String {
self.info.name.clone()
}
pub fn id(&self) -> String {
self.info.id.clone()
}
pub fn location(&self) -> String {
self.info.location.clone()
}
pub fn kind(&self) -> String {
self.info.kind.clone()
}
pub fn runtime(&self) -> String {
self.info.runtime.clone()
}
pub fn runtime_type(&self) -> String {
self.info.runtime_type.clone()
}
pub fn is_ephemeral(&self) -> bool {
self.info.ephemeral
}
pub fn services(&self) -> HashMap<String, u16> {
self.info.services.clone()
}
pub fn info(&self) -> SandboxInfo {
self.info.clone()
}
pub fn image_info(&self) -> Option<ImageInfo> {
self.info.image_info.clone()
}
pub async fn refresh(&self) -> Result<SandboxInfo> {
let backend = self.backend.clone();
let name = self.info.id.clone();
Ok(run(async move {
Ok(match &backend {
Backend::Embedded(rt) => rt.record(&name).await?,
Backend::Daemon(d) => d.get(&name).await?,
})
})
.await?
.into())
}
pub async fn spacesd(&self, probe_timeout_ms: Option<u32>) -> Result<Arc<SpacesdClient>> {
let backend = self.backend.clone();
let name = self.info.id.clone();
let location = self.info.location.clone();
let timeout = probe_timeout_ms.map(super::millis);
run(async move {
match &backend {
Backend::Embedded(rt) => {
let a = rt.env(&name, timeout).await?;
Ok(Arc::new(
SpacesdClient::new(a.client, a.ws_headers).at(&location),
))
}
Backend::Daemon(d) => {
let ep = d.env_endpoint(&name, timeout).await?;
let mut o = cua_spacesd_client::ConnectOptions::parse(&ep.url)?
.transport(cua_spacesd_client::TransportPreference::Native)
.probe(false);
o.token = Some(ep.token.clone());
let client = cua_spacesd_client::SpacesdClient::connect(o).await?;
Ok(Arc::new(
SpacesdClient::new(
client,
vec![("authorization".into(), format!("Bearer {}", ep.token))],
)
.at(&location),
))
}
}
})
.await
}
#[uniffi::method(default(options = None))]
pub async fn viewer_url(&self, options: Option<ViewerOptions>) -> Result<ViewerLink> {
let started = std::time::Instant::now();
let r: Result<ViewerLink> = async move {
let o = options.unwrap_or_default();
let ttl = o.ttl_seconds.unwrap_or(3600);
if !(60..=86_400).contains(&ttl) {
return Err(CuaError::InvalidArgument(
"ttl_seconds must be between 60 and 86400".into(),
));
}
let guest = self.spacesd(None).await?;
let base = self.service("env".into())?.url().await?;
run(async move {
use cua_spacesd_client::pb;
let files_root = if o.view_only {
String::new()
} else {
o.files_root.unwrap_or_else(|| "~".into())
};
let minted = guest
.client
.system()
.create_viewer_ticket(pb::CreateViewerTicketRequest {
ttl: Some(cua_proto::wkt::Duration {
seconds: i64::from(ttl),
nanos: 0,
}),
policy: if o.view_only {
pb::SessionPolicy::ViewOnly
} else {
pb::SessionPolicy::AllowActivation
} as i32,
clipboard: o.clipboard && !o.view_only,
files_root,
audio_uplink: o.microphone && !o.view_only,
principal: None,
})
.await
.map_err(cua_spacesd_client::Error::from)?
.into_inner();
Ok(ViewerLink {
url: format!("{}{}", base.trim_end_matches('/'), minted.viewer_path),
expires_at_unix: minted.expires_at.map(|t| t.seconds).unwrap_or_default(),
})
})
.await
}
.await;
super::telemetry::api("sandbox.viewer_url", started, &r);
r
}
pub async fn agents(&self) -> Result<Arc<super::Agents>> {
let guest = self.spacesd(None).await?;
super::Agents::over(guest.client.clone(), &self.info.location).await
}
pub fn service(&self, name: String) -> Result<Arc<Service>> {
if !self.info.services.contains_key(&name) && !self.info.endpoints.contains_key(&name) {
if !self.info.services.is_empty() {
return Err(CuaError::NotFound(format!(
"sandbox {} has no service {name:?} (known: {:?})",
self.info.name,
self.info.services.keys().collect::<Vec<_>>()
)));
}
}
Ok(Arc::new(Service {
backend: self.backend.clone(),
sandbox: self.info.id.clone(),
name,
}))
}
#[uniffi::method(default(path = None))]
pub async fn mcp_config(
&self,
service: String,
path: Option<String>,
) -> Result<super::McpConfig> {
self.service(service.clone())?.mcp_config(path).await
}
#[uniffi::method(default(path = None))]
pub async fn mcp(
&self,
service: String,
path: Option<String>,
) -> Result<Arc<super::McpClient>> {
self.service(service)?.mcp(path).await
}
#[uniffi::method(default(ttl_seconds = None, label = None))]
pub async fn public_url(
&self,
service: String,
ttl_seconds: Option<u32>,
label: Option<String>,
) -> Result<PublicUrl> {
let backend = self.backend.clone();
let name = self.info.id.clone();
let ttl = ttl_seconds.map(super::secs);
run(async move {
let u = match &backend {
Backend::Embedded(rt) => rt.public_url(&name, &service, ttl, label).await?,
Backend::Daemon(d) => d.public_url(&name, &service, ttl, label).await?,
};
Ok(u.into())
})
.await
}
pub async fn revoke_public_url(&self, id: String) -> Result<()> {
let backend = self.backend.clone();
let name = self.info.id.clone();
run(async move {
match &backend {
Backend::Embedded(rt) => Ok(rt.revoke_public_url(&name, &id).await?),
Backend::Daemon(d) => Ok(d.revoke_public_url(&name, &id).await?),
}
})
.await
}
pub async fn forward(&self, port: u16) -> Result<Arc<PortForward>> {
let started = std::time::Instant::now();
let r: Result<Arc<PortForward>> = async move {
let backend = self.backend.clone();
let name = self.info.id.clone();
let info = run({
let backend = backend.clone();
async move {
Ok(match &backend {
Backend::Embedded(rt) => rt.forward(&name, port).await?,
Backend::Daemon(d) => d.forward(&name, port).await?,
})
}
})
.await?;
Ok(Arc::new(PortForward {
backend,
info,
closed: std::sync::atomic::AtomicBool::new(false),
}))
}
.await;
super::telemetry::api("sandbox.forward", started, &r);
r
}
pub async fn wait_ready(&self, probes: Vec<ReadinessProbe>, timeout_ms: u32) -> Result<()> {
let probes = probes
.iter()
.map(|p| p.resolve(&self.info.services))
.collect::<Result<Vec<_>>>()?;
let backend = self.backend.clone();
let name = self.info.id.clone();
let t = super::millis(timeout_ms);
run(async move {
match &backend {
Backend::Embedded(rt) => Ok(rt.wait_ready(&name, &probes, t).await?),
Backend::Daemon(d) => Ok(d.wait_ready(&name, &probes, t).await?),
}
})
.await
}
pub async fn keep_alive(&self, seconds: u32) -> Result<()> {
let backend = self.backend.clone();
let name = self.info.id.clone();
let d = super::secs(seconds);
run(async move {
match &backend {
Backend::Embedded(rt) => Ok(rt.keep_alive(&name, d).await?),
Backend::Daemon(c) => Ok(c.keep_alive(&name, d).await?),
}
})
.await
}
pub async fn suspend(&self) -> Result<()> {
self.lifecycle(Op::Suspend).await
}
pub async fn resume(&self) -> Result<()> {
self.lifecycle(Op::Resume).await
}
pub async fn restart(&self) -> Result<()> {
self.lifecycle(Op::Restart).await
}
pub async fn delete(&self) -> Result<()> {
let backend = self.backend.clone();
let name = self.info.id.clone();
let r = run(async move { delete(&backend, &name).await }).await;
super::telemetry::sandbox_deleted(&self.info.location, &r);
r
}
pub async fn open_media_bridge(&self, open_media_json: Option<String>) -> Result<MediaBridge> {
let backend = self.backend.clone();
let name = self.info.id.clone();
run(async move {
match &backend {
Backend::Daemon(d) => {
let r = d.open_media_bridge(&name, open_media_json).await?;
Ok(MediaBridge {
ws_url: r.ws_url,
ticket: r.ticket,
open_media_response_json: r.open_media_response_json,
})
}
Backend::Embedded(_) => Err(CuaError::Unsupported(
"media bridges are served by `cua daemon`; use Cua.connect, or \
SpacesdClient.open_media in-process"
.into(),
)),
}
})
.await
}
}
enum Op {
Suspend,
Resume,
Restart,
}
impl Sandbox {
async fn lifecycle(&self, op: Op) -> Result<()> {
let backend = self.backend.clone();
let name = self.info.id.clone();
run(async move {
match (&backend, op) {
(Backend::Embedded(rt), Op::Suspend) => Ok(rt.suspend(&name).await?),
(Backend::Embedded(rt), Op::Resume) => Ok(rt.resume(&name).await?),
(Backend::Embedded(rt), Op::Restart) => Ok(rt.restart(&name).await?),
(Backend::Daemon(d), Op::Suspend) => Ok(d.suspend(&name).await?),
(Backend::Daemon(d), Op::Resume) => Ok(d.resume(&name).await?),
(Backend::Daemon(d), Op::Restart) => Ok(d.restart(&name).await?),
}
})
.await
}
}
#[derive(uniffi::Object)]
pub struct GuestDisplay {
inner: cua_sandbox_core::GuestDisplay,
}
impl std::fmt::Debug for GuestDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl std::fmt::Display for GuestDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.inner.redacted_url())
}
}
impl From<cua_sandbox_core::GuestDisplay> for GuestDisplay {
fn from(inner: cua_sandbox_core::GuestDisplay) -> Self {
GuestDisplay { inner }
}
}
#[uniffi::export]
impl GuestDisplay {
pub fn url(&self) -> String {
self.inner.redacted_url()
}
pub fn url_with_password(&self) -> String {
self.inner.url.clone()
}
pub fn via(&self) -> String {
self.inner.via.clone()
}
pub fn open_command(&self) -> Vec<String> {
self.inner.open_command.clone().unwrap_or_default()
}
}
fn core_err(e: cua_sandbox_core::Error) -> CuaError {
cua_daemon::Error::from(e).into()
}
fn daemon_local(info: &SandboxInfo, op: &str) -> Result<cua_daemon::local::VmmLocal> {
if info.location != "local" {
return Err(CuaError::Unsupported(format!(
"{op} without cua-spacesd is only available for local Lume sandboxes \
({} is {})",
info.name, info.location
)));
}
Ok(cua_daemon::local::VmmLocal::default())
}
impl Sandbox {
pub async fn guest_sh_streaming(
&self,
line: String,
timeout_ms: Option<u32>,
sink: tokio::sync::mpsc::Sender<cua_sandbox_core::GuestOutput>,
) -> Result<i64> {
use cua_sandbox_core::LocalRuntime as _;
let backend = self.backend.clone();
let info = self.info.clone();
let timeout = Some(super::millis(timeout_ms.unwrap_or(120_000)));
run(async move {
match &backend {
Backend::Embedded(rt) => rt
.handle(&info.id)
.await?
.guest_exec(&line, timeout, sink)
.await
.map_err(core_err),
Backend::Daemon(_) => daemon_local(&info, "exec")?
.guest_exec(&info.name, &line, timeout, sink)
.await
.map_err(|e| core_err(e.into())),
}
})
.await
}
}
#[uniffi::export]
impl Sandbox {
pub fn lacks_spacesd(&self) -> bool {
!self.info.services.contains_key("env")
|| self.info.image_info.as_ref().and_then(|i| i.spacesd) == Some(false)
}
#[uniffi::method(default(timeout_ms = None))]
pub async fn guest_sh(&self, line: String, timeout_ms: Option<u32>) -> Result<ProcessOutput> {
let (tx, mut rx) = tokio::sync::mpsc::channel(64);
let run = self.guest_sh_streaming(line, timeout_ms, tx);
let collect = async {
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
while let Some(c) = rx.recv().await {
match c {
cua_sandbox_core::GuestOutput::Stdout(b) => stdout.extend(b),
cua_sandbox_core::GuestOutput::Stderr(b) => stderr.extend(b),
}
}
(stdout, stderr)
};
let (code, (stdout, stderr)) = tokio::join!(run, collect);
let code = code?;
Ok(ProcessOutput {
exit: ExitInfo {
success: code == 0,
code: i32::try_from(code).ok(),
signal: None,
timed_out: false,
error: None,
},
stdout,
stderr,
pty: vec![],
})
}
pub async fn guest_screenshot(&self) -> Result<Screenshot> {
use cua_sandbox_core::LocalRuntime as _;
let backend = self.backend.clone();
let info = self.info.clone();
let s = run(async move {
match &backend {
Backend::Embedded(rt) => rt
.handle(&info.id)
.await?
.guest_screenshot()
.await
.map_err(core_err),
Backend::Daemon(_) => daemon_local(&info, "screenshots")?
.guest_screenshot(&info.name)
.await
.map_err(|e| core_err(e.into())),
}
})
.await?;
Ok(Screenshot {
image: s.png,
format: ImageFormat::Png,
width: s.width,
height: s.height,
scale: 1.0,
screenshot_id: String::new(),
})
}
pub async fn guest_display(&self) -> Result<Arc<GuestDisplay>> {
use cua_sandbox_core::LocalRuntime as _;
let backend = self.backend.clone();
let info = self.info.clone();
run(async move {
match &backend {
Backend::Embedded(rt) => rt
.handle(&info.id)
.await?
.guest_display()
.await
.map(|d| Arc::new(d.into()))
.map_err(core_err),
Backend::Daemon(_) => daemon_local(&info, "a display")?
.guest_display(&info.name)
.await
.map(|d| Arc::new(d.into()))
.map_err(|e| core_err(e.into())),
}
})
.await
}
}
#[derive(uniffi::Object)]
pub struct Service {
backend: Backend,
sandbox: String,
name: String,
}
#[uniffi::export]
impl Service {
pub fn name(&self) -> String {
self.name.clone()
}
pub async fn url(&self) -> Result<String> {
let backend = self.backend.clone();
let (sb, svc) = (self.sandbox.clone(), self.name.clone());
run(async move {
match &backend {
Backend::Embedded(rt) => Ok(rt.service_url(&sb, &svc).await?),
Backend::Daemon(d) => Ok(d.service_url(&sb, &svc).await?),
}
})
.await
}
#[uniffi::method(default(ttl_seconds = None, label = None))]
pub async fn public_url(
&self,
ttl_seconds: Option<u32>,
label: Option<String>,
) -> Result<PublicUrl> {
let backend = self.backend.clone();
let (sb, svc) = (self.sandbox.clone(), self.name.clone());
let ttl = ttl_seconds.map(super::secs);
run(async move {
let u = match &backend {
Backend::Embedded(rt) => rt.public_url(&sb, &svc, ttl, label).await?,
Backend::Daemon(d) => d.public_url(&sb, &svc, ttl, label).await?,
};
Ok(u.into())
})
.await
}
pub async fn endpoint(&self) -> Result<ServiceEndpoint> {
let (backend, sb, svc) = (
self.backend.clone(),
self.sandbox.clone(),
self.name.clone(),
);
let e = run(async move { super::mcp::service_endpoint(&backend, &sb, &svc).await }).await?;
Ok(ServiceEndpoint {
url: e.url,
headers: e
.headers
.into_iter()
.map(|(name, value)| HttpHeader { name, value })
.collect(),
})
}
#[uniffi::method(default(path = None))]
pub async fn mcp_config(&self, path: Option<String>) -> Result<super::McpConfig> {
let (backend, sb, svc) = (
self.backend.clone(),
self.sandbox.clone(),
self.name.clone(),
);
run(async move {
let e = super::mcp::service_endpoint(&backend, &sb, &svc).await?;
Ok(cua_sandbox_core::McpConfig::of(&e, path.as_deref()).into())
})
.await
}
#[uniffi::method(default(path = None))]
pub async fn mcp(&self, path: Option<String>) -> Result<Arc<super::McpClient>> {
let (backend, sb, svc) = (
self.backend.clone(),
self.sandbox.clone(),
self.name.clone(),
);
run(async move {
let e = super::mcp::service_endpoint(&backend, &sb, &svc).await?;
super::McpClient::connect(cua_sandbox_core::McpConfig::of(&e, path.as_deref())).await
})
.await
}
#[uniffi::method(default(headers = None))]
pub async fn request(
&self,
method: String,
path: String,
body: Option<Vec<u8>>,
timeout_ms: Option<u32>,
headers: Option<Vec<HttpHeader>>,
) -> Result<HttpResponse> {
let backend = self.backend.clone();
let (sb, svc) = (self.sandbox.clone(), self.name.clone());
let t = ms(timeout_ms, 30_000);
let headers: Vec<(String, String)> = headers
.unwrap_or_default()
.into_iter()
.map(|h| (h.name, h.value))
.collect();
run(async move {
match &backend {
Backend::Embedded(rt) => {
let r = rt
.service_request(&sb, &svc, &method, &path, &headers, body, t)
.await?;
Ok(HttpResponse {
status: r.status,
headers: r
.headers
.into_iter()
.map(|(name, value)| HttpHeader { name, value })
.collect(),
body: r.body,
})
}
Backend::Daemon(d) => {
let r = d
.service_request(&sb, &svc, &method, &path, &headers, body, t)
.await?;
Ok(HttpResponse {
status: r.status,
headers: r
.headers
.into_iter()
.map(|(name, value)| HttpHeader { name, value })
.collect(),
body: r.body,
})
}
}
})
.await
}
}
#[derive(uniffi::Object)]
pub struct PortForward {
backend: Backend,
info: cua_daemon::ForwardInfo,
closed: std::sync::atomic::AtomicBool,
}
#[uniffi::export]
impl PortForward {
pub fn guest_port(&self) -> u16 {
self.info.guest_port
}
pub fn local_addr(&self) -> Option<String> {
self.info.local_addr.clone()
}
pub fn url(&self) -> Option<String> {
self.info.url.clone()
}
pub async fn close(&self) -> Result<()> {
if self.closed.swap(true, std::sync::atomic::Ordering::SeqCst) || self.info.id.is_empty() {
return Ok(());
}
let backend = self.backend.clone();
let id = self.info.id.clone();
run(async move {
match &backend {
Backend::Embedded(rt) => Ok(rt.close_forward(&id)?),
Backend::Daemon(d) => Ok(d.close_forward(&id).await?),
}
})
.await
}
}
impl Drop for PortForward {
fn drop(&mut self) {
if self.closed.swap(true, std::sync::atomic::Ordering::SeqCst) || self.info.id.is_empty() {
return;
}
let backend = self.backend.clone();
let id = self.info.id.clone();
super::runtime().spawn(async move {
let _ = match &backend {
Backend::Embedded(rt) => rt.close_forward(&id),
Backend::Daemon(d) => d.close_forward(&id).await,
};
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_probes_resolve_against_declared_services() {
let services: HashMap<String, u16> = [("mcp".to_string(), 8765u16)].into();
assert_eq!(
ReadinessProbe::http("mcp", "health")
.resolve(&services)
.unwrap(),
Probe::Http {
port: 8765,
path: "/health".into(),
status: None
}
);
assert_eq!(
ReadinessProbe::tcp("mcp").resolve(&services).unwrap(),
Probe::Tcp(8765)
);
assert_eq!(
ReadinessProbe::tcp("env").resolve(&services).unwrap(),
Probe::Tcp(3211),
"env defaults to the spacesd port"
);
assert!(ReadinessProbe::tcp("nope").resolve(&services).is_err());
assert!(ReadinessProbe::default().resolve(&services).is_err());
let by_port = ReadinessProbe {
port: 22,
..Default::default()
};
assert_eq!(by_port.resolve(&services).unwrap(), Probe::Tcp(22));
}
#[test]
fn the_location_defaults_to_the_user_default() {
use cua_sandbox_core::placement::{Kind, On, Runtime};
let _env = super::super::test_env::EnvGuard::isolated();
let o = SandboxCreateOptions::auto("python:3.12-slim");
assert_eq!(o.placement().unwrap().on, On::Local);
assert_eq!(SandboxCreateOptions::default().on, None);
let r = o.to_request().unwrap();
assert_eq!(r.location.as_deref(), Some("local"));
assert_eq!(r.provider, Some(CoreProvider::Local));
let e = SandboxCreateOptions::new("cloud", "x")
.placement()
.unwrap_err();
assert_eq!(
e.to_string(),
format!("unsupported: {}", cua_sandbox_core::CLOUD_ENDED)
);
let o = SandboxCreateOptions::new("direct:127.0.0.1:3211", "");
let r = o.to_request().unwrap();
assert_eq!(r.url.as_deref(), Some("127.0.0.1:3211"));
assert_eq!(r.location.as_deref(), Some("direct:127.0.0.1:3211"));
let mut o = SandboxCreateOptions::new("local", "img");
o.kind = Some("vm".into());
let p = o.placement().unwrap();
assert_eq!((p.kind, p.runtime), (Kind::Vm, Runtime::Auto));
assert_eq!(o.to_request().unwrap().kind.as_deref(), Some("vm"));
}
#[test]
fn invalid_placements_are_typed_and_list_the_valid_values() {
let _env = super::super::test_env::EnvGuard::isolated();
let mut o = SandboxCreateOptions::new("local", "img");
o.runtime = Some("kubevirt".into());
let e = o.placement().unwrap_err();
let CuaError::InvalidPlacement(m) = &e else {
panic!("{e:?}")
};
assert!(m.contains("valid runtime: auto"), "{m}");
assert_eq!(e.variant(), "InvalidPlacement");
let mut o = SandboxCreateOptions::new("local", "img");
o.kind = Some("container".into());
o.runtime = Some("lume".into());
assert!(matches!(o.placement(), Err(CuaError::InvalidPlacement(_))));
let o = SandboxCreateOptions::new("qemu", "img");
let e = o.placement().unwrap_err();
assert!(e.to_string().contains("--runtime qemu"), "{e}");
}
#[test]
fn user_defaults_apply_below_explicit_values() {
use cua_sandbox_core::placement::On;
let env = super::super::test_env::EnvGuard::isolated();
let mut s = cua_sandbox_core::settings::Settings::load().unwrap();
s.set(
cua_sandbox_core::settings::key("default.on").unwrap(),
"aws",
)
.unwrap();
let o = SandboxCreateOptions::auto("img");
let p = o.placement().unwrap();
assert_eq!(p.on, On::Provider("aws".into()));
assert!(matches!(
p.on_source,
cua_sandbox_core::settings::Source::Config(_)
));
env.set("CUA_DEFAULT_ON", "local");
assert_eq!(o.placement().unwrap().on, On::Local);
let o = SandboxCreateOptions::new("local", "img");
assert_eq!(o.placement().unwrap().on, On::Local);
let o = SandboxCreateOptions::new("cloud", "img");
assert!(matches!(o.placement(), Err(CuaError::Unsupported(_))));
}
#[test]
fn on_words_pick_the_provider_and_the_location() {
let mut o = SandboxCreateOptions::auto("ghcr.io/trycua/linux:24.04");
o.on = Some("e2b".into());
let r = o.to_request().unwrap();
assert_eq!(r.provider, Some(CoreProvider::Contrib));
assert_eq!(r.location.as_deref(), Some("e2b"));
o.on = Some("cloud".into());
assert!(matches!(o.to_request(), Err(CuaError::Unsupported(_))));
o.on = Some("direct:127.0.0.1:3211".into());
assert_eq!(
o.to_request().unwrap().url.as_deref(),
Some("127.0.0.1:3211")
);
o.on = Some("nosuchcloud".into());
let err = o.to_request().unwrap_err().to_string();
assert!(err.contains("daytona"), "{err}");
}
#[test]
fn phases_are_portable() {
assert_eq!(
SandboxPhase::of(SandboxStatus::Running),
SandboxPhase::Ready
);
assert_eq!(
SandboxPhase::of(SandboxStatus::Suspended),
SandboxPhase::Stopped
);
assert_eq!(
SandboxPhase::of(SandboxStatus::Provisioning),
SandboxPhase::Provisioning
);
assert_eq!(
SandboxPhase::of(SandboxStatus::Unknown),
SandboxPhase::Starting
);
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct SandboxRefParts {
pub location: Option<String>,
pub name: String,
pub id: String,
}
#[uniffi::export]
pub fn sandbox_locations() -> Vec<String> {
let mut out: Vec<String> = ["local"]
.into_iter()
.chain(cua_sandbox_core::CONTRIB_LOCATIONS.iter().copied())
.map(str::to_string)
.collect();
for w in cua_sandbox_core::CLOUD_LOCATIONS {
if !out.iter().any(|o| o == w) {
out.push((*w).to_string());
}
}
out
}
#[uniffi::export]
pub fn contrib_providers_built() -> Vec<String> {
cua_daemon::contrib_provider_names()
}
#[uniffi::export]
pub fn parse_sandbox_ref(input: String) -> Result<SandboxRefParts> {
let r = cua_sandbox_core::SandboxRef::parse(&input)?;
Ok(SandboxRefParts {
location: r.location_word().map(str::to_string),
name: r.name().to_string(),
id: r.to_string(),
})
}
#[uniffi::export(default(local = None))]
pub fn qualify_sandbox_ref(name: String, local: Option<bool>) -> Result<String> {
use cua_sandbox_core::{Location, SandboxRef};
let location = local.map(|l| if l { Location::Local } else { Location::Cloud });
Ok(SandboxRef::parse(&name)?.narrow(location)?.to_string())
}
#[uniffi::export]
pub fn ambiguous_sandbox_candidates(message: String) -> Vec<String> {
message
.rsplit_once("use one of: ")
.map(|(_, list)| {
list.split(',')
.map(str::trim)
.filter(|c| !c.is_empty())
.map(str::to_string)
.collect()
})
.unwrap_or_default()
}