1use std::future::Future;
20use std::path::Path;
21use std::sync::Arc;
22use std::time::Duration;
23
24use microvms_edges::identity::HandshakeState;
25
26use crate::adapters::SystemAdapters;
27use crate::agents::{AgentSpec, AgentVm, require_specs};
28use crate::clock::{Clock, SystemClock};
29use crate::control::transport::{SignedTransport, Transport};
30use crate::control::{BuildContext, ControlPlane};
31use crate::cost::CalendarDate;
32use crate::entropy::OsEntropy;
33use crate::error::Error;
34use crate::identity::{LaunchIdentity, TunnelIdentity};
35use crate::names::{NameRecord, NameStore};
36use crate::region::Region;
37use crate::sandbox::{ControlPlaneMinter, Sandbox};
38use crate::session::{ProxyAuth, Session, SessionBuilder, TokenMinter};
39
40pub trait CalendarDateExt {
42 fn today_utc() -> CalendarDate;
48}
49
50impl CalendarDateExt for CalendarDate {
51 fn today_utc() -> CalendarDate {
52 SystemClock::new()
53 .today_utc()
54 .unwrap_or(CalendarDate::from_ymd(1970, 1, 1))
55 }
56}
57
58pub trait NameRecordExt: Sized {
60 fn new(
65 name: impl Into<String>,
66 microvm_id: impl Into<String>,
67 endpoint: impl Into<String>,
68 agent_token: impl Into<String>,
69 region: impl Into<String>,
70 ) -> Result<Self, Error>;
71}
72
73impl NameRecordExt for NameRecord {
74 fn new(
75 name: impl Into<String>,
76 microvm_id: impl Into<String>,
77 endpoint: impl Into<String>,
78 agent_token: impl Into<String>,
79 region: impl Into<String>,
80 ) -> Result<Self, Error> {
81 let at = SystemClock::new().unix_now().as_secs();
82 NameRecord::new_at(name, microvm_id, endpoint, agent_token, region, at)
83 }
84}
85
86pub trait LaunchIdentityExt: Sized {
88 fn generate() -> Result<Self, Error>;
94}
95
96impl LaunchIdentityExt for LaunchIdentity {
97 fn generate() -> Result<Self, Error> {
98 crate::entropy::launch_identity(&OsEntropy)
99 }
100}
101
102pub trait TunnelIdentityExt {
104 fn initiator(&self) -> Result<HandshakeState, Error>;
108}
109
110impl TunnelIdentityExt for TunnelIdentity {
111 fn initiator(&self) -> Result<HandshakeState, Error> {
112 microvms_edges::identity::initiator(self)
113 }
114}
115
116pub trait ControlPlaneExt: Sized {
118 fn new(region: Region) -> impl Future<Output = Result<Self, Error>> + Send;
124
125 fn with_transport(transport: Arc<dyn Transport>, region: Region, clock: Arc<dyn Clock>)
133 -> Self;
134}
135
136impl ControlPlaneExt for ControlPlane {
137 async fn new(region: Region) -> Result<Self, Error> {
138 let transport = SignedTransport::new(region.clone()).await?;
139 Ok(Self::with_transport(
140 Arc::new(transport),
141 region,
142 Arc::new(SystemClock::new()),
143 ))
144 }
145
146 fn with_transport(
147 transport: Arc<dyn Transport>,
148 region: Region,
149 clock: Arc<dyn Clock>,
150 ) -> Self {
151 ControlPlane::from_ports(
152 transport,
153 region,
154 clock,
155 Arc::new(OsEntropy),
156 Arc::new(SystemAdapters),
157 )
158 }
159}
160
161async fn plane_for(region: Region, port: Option<u16>) -> Result<ControlPlane, Error> {
163 let control = ControlPlane::new(region).await?;
164 match port {
165 Some(port) => control.with_port(port),
166 None => Ok(control),
167 }
168}
169
170pub trait SandboxExt: Sized {
172 fn new(region: Region) -> impl Future<Output = Result<Self, Error>> + Send;
177
178 fn adopt_in(
181 region: Region,
182 microvm_id: impl Into<String>,
183 endpoint: impl Into<String>,
184 agent_token: impl Into<String>,
185 port: Option<u16>,
186 ) -> impl Future<Output = Result<Self, Error>> + Send;
187
188 fn from_name(
192 store: &dyn NameStore,
193 name: &str,
194 region: Option<Region>,
195 port: Option<u16>,
196 ) -> impl Future<Output = Result<Self, Error>> + Send;
197}
198
199impl SandboxExt for Sandbox {
200 async fn new(region: Region) -> Result<Self, Error> {
201 Ok(Self::with_control_plane(ControlPlane::new(region).await?))
202 }
203
204 fn adopt_in(
205 region: Region,
206 microvm_id: impl Into<String>,
207 endpoint: impl Into<String>,
208 agent_token: impl Into<String>,
209 port: Option<u16>,
210 ) -> impl Future<Output = Result<Self, Error>> + Send {
211 let (microvm_id, endpoint, agent_token) =
214 (microvm_id.into(), endpoint.into(), agent_token.into());
215 async move {
216 Self::adopt(
217 plane_for(region, port).await?,
218 microvm_id,
219 endpoint,
220 agent_token,
221 )
222 .await
223 }
224 }
225
226 async fn from_name(
227 store: &dyn NameStore,
228 name: &str,
229 region: Option<Region>,
230 port: Option<u16>,
231 ) -> Result<Self, Error> {
232 let record = crate::names::resolve(store, name, region.as_ref())?;
233 let control = plane_for(record.region(), port).await?;
234 Self::adopt_record(control, record).await
235 }
236}
237
238pub trait AgentVmExt: Sized {
240 fn from_name(
243 store: &dyn NameStore,
244 specs: Vec<AgentSpec>,
245 name: &str,
246 region: Option<Region>,
247 port: Option<u16>,
248 ) -> impl Future<Output = Result<Self, Error>> + Send;
249
250 fn adopt_in(
252 region: Region,
253 specs: Vec<AgentSpec>,
254 microvm_id: impl Into<String>,
255 endpoint: impl Into<String>,
256 agent_token: impl Into<String>,
257 port: Option<u16>,
258 ) -> impl Future<Output = Result<Self, Error>> + Send;
259}
260
261impl AgentVmExt for AgentVm {
262 async fn from_name(
263 store: &dyn NameStore,
264 specs: Vec<AgentSpec>,
265 name: &str,
266 region: Option<Region>,
267 port: Option<u16>,
268 ) -> Result<Self, Error> {
269 require_specs(&specs)?;
272 let sandbox = Sandbox::from_name(store, name, region, port).await?;
273 AgentVm::new(sandbox, specs)
274 }
275
276 fn adopt_in(
277 region: Region,
278 specs: Vec<AgentSpec>,
279 microvm_id: impl Into<String>,
280 endpoint: impl Into<String>,
281 agent_token: impl Into<String>,
282 port: Option<u16>,
283 ) -> impl Future<Output = Result<Self, Error>> + Send {
284 let (microvm_id, endpoint, agent_token) =
285 (microvm_id.into(), endpoint.into(), agent_token.into());
286 async move {
287 require_specs(&specs)?;
288 let sandbox =
289 Sandbox::adopt_in(region, microvm_id, endpoint, agent_token, port).await?;
290 AgentVm::new(sandbox, specs)
291 }
292 }
293}
294
295pub trait SessionExt: Sized {
297 fn connect(
303 endpoint: impl Into<String>,
304 agent_token: impl Into<String>,
305 minter: Arc<dyn TokenMinter>,
306 ) -> impl Future<Output = Result<Self, Error>> + Send;
307
308 fn attach(
315 region: Region,
316 microvm_id: impl Into<String>,
317 endpoint: impl Into<String>,
318 agent_token: impl Into<String>,
319 port: Option<u16>,
320 request_timeout: Option<Duration>,
321 ) -> impl Future<Output = Result<Self, Error>> + Send;
322
323 fn direct(endpoint: impl Into<String>, agent_token: impl Into<String>) -> Result<Self, Error>;
329}
330
331impl SessionExt for Session {
332 fn connect(
333 endpoint: impl Into<String>,
334 agent_token: impl Into<String>,
335 minter: Arc<dyn TokenMinter>,
336 ) -> impl Future<Output = Result<Self, Error>> + Send {
337 let session = Session::builder(endpoint, agent_token)
340 .with_minter(minter)
341 .build();
342 std::future::ready(session)
343 }
344
345 fn attach(
346 region: Region,
347 microvm_id: impl Into<String>,
348 endpoint: impl Into<String>,
349 agent_token: impl Into<String>,
350 port: Option<u16>,
351 request_timeout: Option<Duration>,
352 ) -> impl Future<Output = Result<Self, Error>> + Send {
353 let (microvm_id, endpoint, agent_token) =
354 (microvm_id.into(), endpoint.into(), agent_token.into());
355 async move {
356 if request_timeout.is_some_and(|timeout| timeout.is_zero()) {
357 return Err(Error::invalid_arg("request timeout must be positive"));
358 }
359 let control = plane_for(region, port).await?;
360 let port = control.port();
361 let minter = Arc::new(ControlPlaneMinter::new(Arc::new(control), microvm_id));
362 let mut builder = Session::builder(endpoint, agent_token)
363 .with_minter(minter)
364 .with_port(port);
365 if let Some(timeout) = request_timeout {
366 builder = builder.with_timeout(timeout);
367 }
368 builder.build()
369 }
370 }
371
372 fn direct(endpoint: impl Into<String>, agent_token: impl Into<String>) -> Result<Self, Error> {
373 Session::builder(endpoint, agent_token).build()
374 }
375}
376
377pub trait SessionBuilderExt {
379 fn build(self) -> Result<Session, Error>;
382}
383
384impl SessionBuilderExt for SessionBuilder {
385 fn build(self) -> Result<Session, Error> {
386 self.build_with(&SystemAdapters)
387 }
388}
389
390pub trait ProxyAuthExt {
392 fn new(minter: Arc<dyn TokenMinter>, port: u16) -> Self;
394}
395
396impl ProxyAuthExt for ProxyAuth {
397 fn new(minter: Arc<dyn TokenMinter>, port: u16) -> Self {
398 ProxyAuth::with_clock(minter, port, Arc::new(SystemClock::new()))
399 }
400}
401
402pub trait BuildContextExt: Sized {
404 fn from_dir(dir: impl AsRef<Path>) -> Result<Self, Error>;
406}
407
408impl BuildContextExt for BuildContext {
409 fn from_dir(dir: impl AsRef<Path>) -> Result<Self, Error> {
410 crate::control::context::from_dir(dir)
411 }
412}
413
414pub trait LedgerExt {
416 fn new(region: &str, root: &std::path::Path) -> Self;
417 fn open_all_for_vm(root: &std::path::Path, microvm_id: &str) -> Vec<Self>
418 where
419 Self: Sized;
420}
421
422impl LedgerExt for microvms_app::ledger::Ledger {
423 fn new(region: &str, root: &std::path::Path) -> Self {
424 crate::ledger::new(region, root)
425 }
426 fn open_all_for_vm(root: &std::path::Path, microvm_id: &str) -> Vec<Self> {
427 crate::ledger::open_all_for_vm(root, microvm_id)
428 }
429}
430
431pub trait HistoryExt {
433 fn for_vm(root: &std::path::Path, vm_id: &str) -> Self;
434}
435
436impl HistoryExt for microvms_app::history::History {
437 fn for_vm(root: &std::path::Path, vm_id: &str) -> Self {
438 crate::history::for_vm(root, vm_id)
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::error::ErrorKind;
446
447 #[test]
453 fn with_transport_draws_from_the_os_pool() {
454 let plane = || {
455 ControlPlane::with_transport(
456 Arc::new(microvms_app::testing::FakeControlPlane::new()),
457 Region::US_EAST_1,
458 Arc::new(microvms_app::testing::TestClock::new()),
459 )
460 };
461 let draw = |plane: &ControlPlane| {
462 let mut bytes = [0_u8; 16];
463 plane
464 .entropy()
465 .fill(&mut bytes)
466 .expect("the pool is available");
467 bytes
468 };
469 assert_ne!(draw(&plane()), draw(&plane()));
470 }
471
472 #[tokio::test]
479 async fn from_name_refuses_a_foreign_or_missing_name_before_any_plane() {
480 struct One(NameRecord);
481 impl NameStore for One {
482 fn get(&self, name: &str) -> Result<Option<NameRecord>, Error> {
483 Ok((name == self.0.name).then(|| self.0.clone()))
484 }
485 fn put(&self, _: &NameRecord) -> Result<(), Error> {
486 unreachable!()
487 }
488 fn delete(&self, _: &str) -> Result<bool, Error> {
489 unreachable!()
490 }
491 fn list(&self) -> Result<Vec<NameRecord>, Error> {
492 Ok(vec![self.0.clone()])
493 }
494 }
495 const TOKEN: &str = "from-name-canary-token";
496 let record = NameRecord::new(
497 "ci",
498 "mvm-abc123",
499 "https://mvm-abc123.microvm.us-west-2.amazonaws.com",
500 TOKEN,
501 "us-west-2",
502 )
503 .expect("record");
504 let store = One(record);
505 let foreign = Sandbox::from_name(&store, "ci", Some(Region::US_EAST_1), None)
506 .await
507 .expect_err("registered in another region");
508 assert_eq!(foreign.kind(), ErrorKind::InvalidArg);
509 let missing = Sandbox::from_name(&store, "nope", None, None)
510 .await
511 .expect_err("no such name");
512 assert_eq!(missing.kind(), ErrorKind::Precondition);
513 let specs = vec![AgentSpec::new(crate::agents::Agent::Codex)];
514 let agent = AgentVm::from_name(&store, specs, "ci", Some(Region::US_EAST_1), None)
515 .await
516 .expect_err("registered in another region");
517 assert_eq!(agent.kind(), ErrorKind::InvalidArg);
518 assert!(!format!("{foreign} {missing} {agent}").contains(TOKEN));
519 }
520
521 #[test]
527 fn the_production_build_fills_the_backend_and_the_clock() {
528 let session = Session::direct("http://127.0.0.1:9", "token").expect("a direct session");
529 assert!(session.proxy_auth().is_none());
530 }
531
532 #[test]
537 fn proxy_auth_new_keeps_the_defaults() {
538 let auth = ProxyAuth::new(
539 Arc::new(microvms_app::testing::CountingMinter::default()),
540 crate::session::DEFAULT_AGENT_PORT,
541 );
542 assert_eq!(auth.port(), crate::session::DEFAULT_AGENT_PORT);
543 assert_eq!(auth.mint_count(), 0);
544 assert!(!auth.is_cached());
545 let described = format!("{auth:?}");
546 assert!(
547 described.contains(&format!(
548 "refresh_after: {:?}",
549 crate::session::DEFAULT_REFRESH_AFTER
550 )),
551 "{described}"
552 );
553 }
554}