1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
//! The `Cua` entry object and its configuration.
use super::{Sandboxes, SpacesdClient, run, runtime};
use crate::{CuaError, Result};
use cua_daemon::{Runtime, RuntimeConfig, client::DaemonAddress, client::DaemonClient};
use std::{path::PathBuf, sync::Arc};
/// Configuration of an embedded SDK runtime.
#[derive(Debug, Clone, Default, PartialEq, Eq, uniffi::Record)]
pub struct CuaConfig {
/// State directory for sandbox state files (default `~/.cua/sandboxes`).
#[uniffi(default = None)]
pub state_dir: Option<String>,
/// spacesd probe timeout (default 15 s).
#[uniffi(default = None)]
pub env_probe_timeout_ms: Option<u32>,
/// Spaces registry directory (default `$CUA_HOME` or `~/.cua`).
#[uniffi(default = None)]
pub spaces_home: Option<String>,
/// Teleport reads app sessions under this home directory with no host
/// side effects (tests, CI). Default: the real host.
#[uniffi(default = None)]
pub teleport_home: Option<String>,
/// Use the signed-in session (`Cua.auth()`, `cua auth login`) from the
/// shared credential store, refreshed as needed, for the relay account
/// the runtime lists the account's machines with ("My machines").
#[uniffi(default = false)]
pub account_from_session: bool,
}
/// Where the SDK runtime lives.
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum CuaMode {
/// In this process.
Embedded,
/// In a `cua daemon`.
Daemon,
}
/// Identity of the SDK (and daemon, when connected).
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct CuaInfo {
/// SDK library version.
pub sdk_version: String,
/// Topology.
pub mode: CuaMode,
/// Daemon version (daemon mode).
pub daemon_version: Option<String>,
/// Daemon pid (daemon mode).
pub daemon_pid: Option<u32>,
/// Daemon socket (daemon mode).
pub socket_path: Option<String>,
/// Daemon loopback URL (daemon mode).
pub loopback_url: Option<String>,
/// Compiled-in modules.
pub features: Vec<String>,
}
#[derive(Clone)]
pub(crate) enum Backend {
Embedded(Runtime),
Daemon(DaemonClient),
}
pub(crate) fn features() -> Vec<String> {
let mut f = vec!["env".to_string(), "sandboxes".into(), "daemon".into()];
if cfg!(feature = "local") {
f.push("local".into());
}
if cfg!(feature = "spaces") {
f.push("spaces".into());
}
if cfg!(feature = "host") {
f.push("host".into());
}
if cfg!(feature = "media") {
f.push("media".into());
}
f
}
/// The entry point of the SDK: create one with `Cua::embedded` or
/// `Cua::connect`, then reach sandboxes, Spaces and the rest through its
/// accessors.
#[derive(uniffi::Object)]
pub struct Cua {
pub(crate) backend: Backend,
}
impl Cua {
/// Rust hosts: an embedded SDK over a caller-built runtime (tests, local
/// runtimes).
pub fn from_runtime(runtime: Runtime) -> Arc<Self> {
Arc::new(Self {
backend: Backend::Embedded(runtime),
})
}
/// Rust hosts: the embedded runtime, if any.
pub fn embedded_runtime(&self) -> Option<&Runtime> {
match &self.backend {
Backend::Embedded(r) => Some(r),
Backend::Daemon(_) => None,
}
}
}
/// The relay account `cua daemon` lists the account's machines with: the
/// signed-in session (read per call, so a later sign-in works without a
/// new runtime) on the relay this machine uses, as this enrolled device.
/// An embedded runtime on the session gets the same, so a Spaces app that
/// runs Spaces in-process (its daemon could not be started) still lists
/// "My machines".
#[cfg(all(feature = "spaces", feature = "host"))]
fn session_relay_account(home: &std::path::Path) -> cua_spaces::RelayAccount {
let url = cua_host::relay_url_for(None, home);
let session = Arc::new(cua_auth::Session::from_env());
let tokens: Arc<dyn cua_host::AccountTokens> =
Arc::new(super::host::SessionTokens(session.clone()));
let account = cua_spaces::RelayAccount::new(url.clone(), tokens.clone());
match cua_host::DeviceAuth::new(
&url,
tokens,
Arc::new(session.store().clone()),
cua_host::device_name(),
) {
Ok(device) => account.with_device(Arc::new(
device.with_pending_file(cua_host::device_pending_file(home)),
)),
Err(_) => account,
}
}
#[uniffi::export]
impl Cua {
/// Runs the SDK runtime in this process. Performs no I/O beyond
/// reading the credential store when `account_from_session` is set (for
/// the relay account).
#[uniffi::constructor]
pub fn embedded(config: CuaConfig) -> Result<Arc<Self>> {
let rc = RuntimeConfig {
state_dir: config.state_dir.clone().map(PathBuf::from),
local: None,
vmm: Some(Arc::new(cua_daemon::local::VmmLocal::default())),
env_probe_timeout: config.env_probe_timeout_ms.map(super::millis),
spaces_home: config.spaces_home.clone().map(PathBuf::from),
teleport_home: config.teleport_home.clone().map(PathBuf::from),
// Every contrib provider this build includes (none by default).
providers: None,
// Extensions: the host's registered ones (`cua_daemon::extension`),
// none in an MIT-only process.
..Default::default()
};
let rt = {
let _guard = runtime().enter();
Runtime::new(rc)?
};
#[cfg(all(feature = "spaces", feature = "host"))]
if config.account_from_session && std::env::var_os("CUA_DAEMON_NO_RELAY").is_none() {
let home = config
.spaces_home
.clone()
.map(PathBuf::from)
.unwrap_or_else(cua_daemon::cua_home);
rt.spaces().set_relay(Some(session_relay_account(&home)));
}
Ok(Arc::new(Self {
backend: Backend::Embedded(rt),
}))
}
/// The default topology: the `cua daemon` this machine runs when one
/// accepts connections (so the CLI, MCP clients and apps share
/// sandboxes and Spaces), else the runtime in this process with
/// `config`. A discovery file (`~/.cua/daemon.json`) or socket left by a
/// daemon that exited is not a running daemon: the files are removed
/// when their pid is provably dead, and this falls back to embedded.
/// A daemon that is starting (`~/.cua/daemon.starting`) is waited for,
/// up to 30 s. Probes with a local connect only (no RPC), except in a
/// process inside
/// an app bundle that ships its own `cua`: it uses only that build's
/// daemon, and fails with [`CuaError::DaemonNotRunning`] naming the
/// running one (another app's, or its own before a rebuild or update)
/// rather than silently use it; `<bundle>/Contents/MacOS/cua daemon
/// start` replaces it.
#[uniffi::constructor]
pub fn auto(config: CuaConfig) -> Result<Arc<Self>> {
// A daemon that is starting is waited for (bounded), not raced with
// an embedded runtime.
match cua_daemon::client::wait_for_starting(std::time::Duration::from_secs(30))? {
Some(addr) => {
let client = {
let _guard = runtime().enter();
DaemonClient::new(addr)?
};
if cua_daemon::identity::bundled_cua().is_some() {
check_identity(&client)?;
}
Ok(Arc::new(Self {
backend: Backend::Daemon(client),
}))
}
None => Self::embedded(config),
}
}
/// Connects to a running `cua daemon`. `address` is a socket path
/// (`unix:` prefix optional) or a loopback URL (then `token` is
/// required unless the discovery file has it); `None` uses
/// `~/.cua/daemon.json`, then `~/.cua/cua.sock`. Connects lazily: the
/// first call fails with `DaemonNotRunning` when no daemon runs; use
/// [`Cua::info`] to check, or [`Cua::auto`] to fall back to the runtime
/// in this process.
#[uniffi::constructor]
pub fn connect(address: Option<String>, token: Option<String>) -> Result<Arc<Self>> {
let addr = DaemonAddress::resolve(address.as_deref(), token)?;
let client = {
let _guard = runtime().enter();
DaemonClient::new(addr)?
};
Ok(Arc::new(Self {
backend: Backend::Daemon(client),
}))
}
/// Topology.
pub fn mode(&self) -> CuaMode {
match self.backend {
Backend::Embedded(_) => CuaMode::Embedded,
Backend::Daemon(_) => CuaMode::Daemon,
}
}
/// SDK (and daemon) identity. In daemon mode this is the connectivity
/// check.
pub async fn info(&self) -> Result<CuaInfo> {
let backend = self.backend.clone();
run(async move {
let mut info = CuaInfo {
sdk_version: crate::cua_sdk_version(),
mode: CuaMode::Embedded,
daemon_version: None,
daemon_pid: None,
socket_path: None,
loopback_url: None,
features: features(),
};
if let Backend::Daemon(d) = backend {
let i = d.info().await?;
let opt = |s: String| (!s.is_empty()).then_some(s);
info.mode = CuaMode::Daemon;
info.daemon_version = Some(i.version);
info.daemon_pid = Some(i.pid);
info.socket_path = opt(i.socket_path);
info.loopback_url = opt(i.loopback_url);
}
Ok(info)
})
.await
}
/// Sandboxes (local, direct, your own cloud).
pub fn sandboxes(&self) -> Arc<Sandboxes> {
Arc::new(Sandboxes {
backend: self.backend.clone(),
})
}
/// Connects to cua-spacesd at `url` (`host:port`, `http(s)://…` or a
/// relay URL) without a sandbox.
pub async fn spacesd(&self, url: String, token: Option<String>) -> Result<Arc<SpacesdClient>> {
let backend = self.backend.clone();
run(async move {
let client = match backend {
Backend::Embedded(rt) => rt.env_url(&url, token).await?,
Backend::Daemon(_) => {
let mut o = cua_spacesd_client::ConnectOptions::parse(&url)?;
o.token = token;
cua_spacesd_client::SpacesdClient::connect(o).await?
}
};
Ok(Arc::new(SpacesdClient::new(client, vec![])))
})
.await
}
/// Asks a connected daemon to stop (daemon mode only).
pub async fn shutdown_daemon(&self) -> Result<()> {
let backend = self.backend.clone();
run(async move {
match backend {
Backend::Daemon(d) => Ok(d.shutdown().await?),
Backend::Embedded(_) => Err(CuaError::Unsupported(
"shutdown_daemon needs a daemon connection".into(),
)),
}
})
.await
}
}
#[cfg(feature = "local")]
#[uniffi::export]
impl Cua {
/// Local runtimes (doctor and setup) and local images.
pub fn local(&self) -> Arc<super::Local> {
Arc::new(super::Local {
backend: self.backend.clone(),
})
}
}
#[cfg(feature = "spaces")]
#[uniffi::export]
impl Cua {
/// Spaces: the registry and every Space primitive.
pub fn spaces(&self) -> Arc<super::Spaces> {
Arc::new(super::Spaces::new(&self.backend))
}
}
/// [`cua_daemon::identity::check`] on the daemon `client` reaches, from a
/// thread of its own (so a caller already inside an async runtime can call
/// [`Cua::auto`]), bounded. A daemon that does not answer in time is left
/// to fail on first use, as without the check.
fn check_identity(client: &DaemonClient) -> Result<()> {
let client = client.clone();
let info = std::thread::spawn(move || {
runtime().block_on(async move {
tokio::time::timeout(std::time::Duration::from_secs(5), client.info()).await
})
})
.join()
.map_err(|_| CuaError::Internal("the daemon identity check panicked".into()))?;
match info {
Ok(Ok(info)) => Ok(cua_daemon::identity::check(&info)?),
_ => Ok(()),
}
}