Skip to main content

microvms_core/
prelude.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The production wiring, under the method names it had in 0.10.
3//!
4//! Two kinds of method live here as extension traits. The domain can't read the clock or the OS
5//! random pool (ARCH-6), so the methods its types had that did moved here. And the use cases
6//! take their I/O through ports, so the constructors that built the production transport, HTTP
7//! backend, clock or entropy for their caller moved here too: `ControlPlane::new`,
8//! `Sandbox::new`, `Session::connect` and the rest. Each wires the production adapters into a
9//! port-taking constructor the type keeps, such as [`ControlPlane::from_ports`] or
10//! [`SessionBuilder::try_build`].
11//!
12//! With `use microvms_core::prelude::*;` in scope, a call written against 0.10, such as
13//! `CalendarDate::today_utc()` or `ControlPlane::new(region)`, compiles unchanged: a path to a
14//! type's associated function also finds the methods of traits in scope.
15//!
16//! Every async method returns a `Send` future, as the inherent `async fn` it replaces did, so
17//! the bindings can still hand one to a multi-threaded runtime.
18
19use 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
40/// [`CalendarDate`]'s clock read.
41pub trait CalendarDateExt {
42    /// Today, UTC, from the system clock.
43    ///
44    /// Falls back to the epoch if the clock is set before 1970 or after 9999, which is a
45    /// machine whose age arithmetic is already meaningless. That's what 0.10 did, and the
46    /// signature can't grow an error without breaking the callers the prelude exists for.
47    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
58/// [`NameRecord`]'s timestamp.
59pub trait NameRecordExt: Sized {
60    /// A record for a VM this process can already address, stamped with the current time.
61    ///
62    /// Refuses an illegal name or an empty id, endpoint, or token: a record missing any of
63    /// them resolves to a VM nobody can adopt.
64    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
86/// [`LaunchIdentity`]'s seeds, from the OS random pool.
87pub trait LaunchIdentityExt: Sized {
88    /// Fresh material from the OS random pool.
89    ///
90    /// An all-zero draw isn't reachable from a working pool. It's still refused, because
91    /// the protocol crate refuses a zero seed (a shared identity proves nothing), and this
92    /// constructor must never hand out material the daemon would reject at bootstrap.
93    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
102/// [`TunnelIdentity`]'s Noise initiator, which draws its ephemeral key from the OS pool.
103pub trait TunnelIdentityExt {
104    /// A Noise initiator bound to this identity: proves the host, verifies the pin.
105    ///
106    /// Built per connection. A `HandshakeState` carries nonces and must never be reused.
107    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
116/// [`ControlPlane`]'s production constructors.
117pub trait ControlPlaneExt: Sized {
118    /// Resolves credentials for `region` and returns a usable client.
119    ///
120    /// The region is a [`Region`], so its name was parsed before this is reached, which is
121    /// why there is no region check here. A region outside [`crate::region::MEASURED_REGIONS`]
122    /// is reported at the first null-message denial (TRAP-6).
123    fn new(region: Region) -> impl Future<Output = Result<Self, Error>> + Send;
124
125    /// A client over a caller-supplied transport and clock, with the OS random pool and the
126    /// production adapters.
127    ///
128    /// A caller who passes a real transport here gets exactly what `new` builds. The entropy
129    /// is the OS pool's even over a scripted transport, and that's deliberate: such a plane
130    /// still mints client tokens, and a deterministic default would mint repeating ones
131    /// (TRAP-1). [`ControlPlane::from_ports`] is how a test pins it.
132    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
161/// A production plane for `region`, on `port` when one is given.
162async 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
170/// [`Sandbox`]'s constructors that resolve credentials for a region.
171pub trait SandboxExt: Sized {
172    /// Resolves credentials for `region` and returns a sandbox with nothing launched.
173    ///
174    /// The region is a parsed [`Region`], which is why there is no region check here; a
175    /// region outside the measured list is reported at the first null-message denial (TRAP-6).
176    fn new(region: Region) -> impl Future<Output = Result<Self, Error>> + Send;
177
178    /// [`Sandbox::adopt`] over a plane resolved for `region`, on `port` when the image's
179    /// daemon listens somewhere other than the default. The bindings' entry point.
180    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    /// Adopts the VM registered as `name` in `store`: [`Sandbox::adopt`] from a record.
189    ///
190    /// `region`, when given, must match the record's; see [`crate::names::resolve`].
191    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        // Converted before the future is built, so it holds strings rather than the caller's
212        // argument types and stays `Send` whatever they were.
213        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
238/// [`AgentVm`]'s constructors that resolve credentials for a region.
239pub trait AgentVmExt: Sized {
240    /// Adopts the VM registered as `name` in `store`; see `Sandbox::from_name`
241    /// ([`SandboxExt::from_name`]).
242    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    /// [`AgentVm::adopt`] over a plane resolved for `region`; the bindings' entry point.
251    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        // The specs first, as the adopt path checks them: a set this client refuses costs no
270        // credential resolution and no call.
271        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
295/// [`Session`]'s constructors over the production HTTP backend.
296pub trait SessionExt: Sized {
297    /// A session against `endpoint`, minting proxy tokens through `minter`.
298    ///
299    /// Does not talk to the VM. Constructing a session is free and re-doable, and a
300    /// constructor that probed would make "do I have a session" mean "is the VM up",
301    /// which are different questions with different answers during a launch.
302    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    /// Reattach to an existing AWS VM using a privately retained agent token.
309    ///
310    /// Resolves AWS credentials but does not probe the guest or bootstrap it again.
311    /// The returned session is independent of any sandbox lock: use a separate attach
312    /// with a short request timeout for supervisor keepalives during long transfers.
313    /// Keep `agent_token` in private encrypted storage, never in public job status.
314    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    /// A session with no proxy auth, for a daemon reached directly.
324    ///
325    /// The conformance path and every local-binary test go through here. See
326    /// [`crate::session::Transport::proxy`] on why this is a supported shape rather than a
327    /// test-only escape hatch.
328    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        // Built now rather than inside the future, which is then `Send` whatever the caller's
338        // argument types were. Nothing here awaits.
339        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
377/// [`SessionBuilder`]'s build over the production adapters.
378pub trait SessionBuilderExt {
379    /// Builds the session, with reqwest for an unset backend and tokio's clock for an unset
380    /// clock. [`SessionBuilder::build_with`] over [`SystemAdapters`].
381    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
390/// [`ProxyAuth`]'s constructor on tokio's clock.
391pub trait ProxyAuthExt {
392    /// A [`ProxyAuth`] with the validated defaults: [`ProxyAuth::with_clock`] on tokio's clock.
393    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
402/// [`BuildContext`]'s directory read.
403pub trait BuildContextExt: Sized {
404    /// Reads `dir` as a build context; see [`crate::control::context::from_dir`].
405    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
414/// Production constructors for durable run ledgers.
415pub 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
431/// Production constructor for a VM's durable history.
432pub 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    /// A plane `with_transport` builds draws from the OS pool, never a deterministic source.
448    /// A scripted transport still gets real client tokens, which is what TRAP-1 needs.
449    ///
450    /// **Falsification**: wire `SequenceEntropy` into `with_transport` and two planes draw
451    /// the same bytes.
452    #[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    /// A name registered in another region, or not registered at all, is refused locally by
473    /// `Sandbox::from_name` and `AgentVm::from_name`, and the refusal never carries the token.
474    ///
475    /// **Falsification**: resolve the record without the caller's region and the foreign
476    /// record goes on to adopt, which fails some other way (run the break with no
477    /// credentials in the environment, so nothing reaches AWS).
478    #[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    /// The production session builds without a backend or a clock set: the prelude fills
522    /// both, as 0.10's `build` did.
523    ///
524    /// **Falsification**: make `SessionBuilderExt::build` call `try_build` and the direct
525    /// session is refused for having neither.
526    #[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    /// `ProxyAuth::new` keeps 0.10's defaults: the default refresh interval, and nothing
533    /// minted yet.
534    ///
535    /// **Falsification**: build it with any other interval and its `Debug` names that one.
536    #[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}