microvms_core/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2//! The MicroVMs client: the control plane, the in-VM daemon, the cost engine, and
3//! every trap closure, behind the one library crate the CLI and the bindings depend on
4//! (ARCH-1). It depends on none of them (ARCH-4), so a consumer of the library never carries the
5//! CLI's parser and runtime.
6//!
7//! # What this crate is for
8//!
9//! `docs/PLATFORM.md` records seventeen measured findings about AWS Lambda MicroVMs,
10//! fifteen of which a client can act on. Most of them are traps in the specific sense
11//! that the platform's answer points away from the cause: an unsupported region
12//! answers `AccessDeniedException` with a null message, a `clientToken` replay wedges
13//! an image in `CREATING` for fifteen hours with no error at all, a
14//! `minimumMemoryInMiB` of 512 produces a guest reporting 2 GB. Each finding cost a
15//! measurement, and this crate is where that measurement is spent once so no caller
16//! has to make it again.
17//!
18//! The Python client closed the same traps and stayed in the tree as the conformance
19//! oracle and the API reference until this port had driven the live suite green; it is
20//! git history now. The reason to port was that Rust can make several of those closures
21//! *unavailable* rather than merely rejected — see the strength ladder below.
22//!
23//! # How strongly a trap is closed
24//!
25//! The spec ranks each closure, strongest first:
26//!
27//! * **S1, inexpressible** — the mistake cannot be written down. [`region::Region`] is
28//! an enum over the five regions that carry MicroVMs, so a typo'd region is a
29//! compile error rather than a runtime check; [`sizing::SizeClass`] is closed over
30//! the five documented baselines; [`hooks::RunHookTimeout`] and
31//! [`hooks::BuildHookTimeout`] are separate types with no conversion between them,
32//! so a 3600-second build timeout cannot reach a field that caps at 60.
33//! * **S2, expressible but rejected** — the mistake can be written and the client
34//! refuses it locally, before any control-plane call, with an error naming the
35//! `docs/PLATFORM.md` finding. Weaker because the guard is code that can regress,
36//! but it costs seconds rather than a build cycle. Every boundary where a bare
37//! integer or string still has to be judged lands here:
38//! [`sizing::SizeClass::from_baseline_mib`], `Region::from_str`.
39//! * **S3, correct by default and overridable** — weakest, because it protects the
40//! caller who does nothing and abandons the one who overrides. An S3 closure must
41//! say what the override costs: [`region::Region::unlisted`] says it costs you the
42//! diagnostic.
43//!
44//! # Every error message names its finding
45//!
46//! A local reject explains itself by naming the `docs/PLATFORM.md` section that
47//! measured the behaviour, because the codes and the guards exist precisely so a
48//! reader can go to the measurement rather than to a constraint. "region
49//! 'eu-central-1' is invalid" sends someone to check their spelling; the message
50//! [`region::Region`] actually raises sends them to the null-message finding.
51//!
52//! # A guard that cannot fail is worse than no guard
53//!
54//! Carried over from the Python era unchanged: every guard here has a falsification —
55//! a specific plausible edit that must turn a specific test red. "Delete the feature
56//! and the test fails" does not count. The clearest case is TRAP-13 in
57//! [`sizing`]: every documented peak is exactly four times its baseline, so a test
58//! against the shipped table cannot tell a table lookup from `baseline * 4`, and the
59//! guard has to drive the lookup over a table where the pattern does not hold.
60//!
61//! # Layout
62//!
63//! This crate is the composition root (ARCH-8). The code lives in four crates below it, and
64//! every item they export is re-exported here at the path it had in 0.10:
65//!
66//! * `microvms-domain`: the rules and values, with no I/O (ARCH-6): [`error`], [`region`],
67//! [`sizing`], [`hooks`], [`constants`], [`cost`], and the pure halves of [`names`],
68//! [`preflight`], [`identity`] and [`provision`].
69//! * `microvms-app`: the use cases, written only against ports (ARCH-7): [`control`],
70//! [`session`], [`sandbox`], [`agents`], and the ports themselves, [`clock`], [`entropy`] and
71//! [`adapters`].
72//! * `microvms-edges`: the production port implementations: the signed transport, the reqwest
73//! backend, the sockets, the name registry on disk, [`provision`]'s fetch, and
74//! [`env`](mod@env), the one process-environment lookup.
75//! * `protocol`, the wire contract shared with the daemon.
76//!
77//! What this crate adds is the wiring: [`prelude`]'s constructors (`ControlPlane::new`,
78//! `Sandbox::new`, `Session::connect` and the rest) put the production transport, clock,
79//! entropy and adapters into each type's port-taking constructor, and the free functions that
80//! read the production clock or pool keep their paths here. `use microvms_core::prelude::*;`
81//! keeps a 0.10 call compiling. The shared test doubles are in `testing`, behind the
82//! `test-support` feature.
83
84// CLI-7: a print macro panics when its stream's reader has gone (#216). Every write goes
85// through a checked writer instead.
86#![deny(clippy::print_stdout, clippy::print_stderr)]
87
88pub mod identity;
89pub mod prelude;
90
91// The rules and values live in `microvms-domain`, which can't do I/O (ARCH-6). Re-exported
92// whole, so every `microvms_core::` path they had still resolves (ARCH-1).
93pub use microvms_domain::{constants, cost, duration, error, hooks, region, sizing};
94
95// Whole modules that live in one crate below.
96pub use microvms_app::{defaults, sandbox};
97pub use microvms_edges::{env, provision};
98
99// Re-exported so consumers name wire types through this crate rather than
100// depending on `protocol` directly — the CLI's thinness guard counts on that.
101pub use protocol;
102
103pub use error::{Error, ErrorKind, WireKind};
104pub use hooks::{BuildHookTimeout, RunHookTimeout};
105pub use region::Region;
106pub use sizing::SizeClass;
107
108/// The crate's own version, for a `doctor` or `manifest` command to report.
109pub const VERSION: &str = env!("CARGO_PKG_VERSION");
110
111// The README's example is the first code a crates.io reader copies, so it compiles as a
112// doctest (`no_run`: it launches a billed VM).
113#[cfg(doctest)]
114#[doc = include_str!("../README.md")]
115struct ReadmeExample;
116
117/// The edges' S3 and STS request helpers stay out of this crate's API: they were
118/// `pub(crate)` in 0.10, and one of them returns an `aws-sigv4` type, so exporting it would
119/// tie this crate's semver to that one's.
120///
121/// ```compile_fail
122/// let _ = microvms_core::control::services::s3_signing_settings();
123/// ```
124#[cfg(doctest)]
125struct EdgesHelpersStayPrivate;
126
127// Each module below is a use-case module from `microvms-app` with the production pieces
128// `microvms-edges` supplies for it, at the paths both had when they were one crate. A local
129// item or module shadows a glob, which is how a submodule that has halves in both crates is
130// merged.
131
132pub mod adapters {
133 //! The adapters port and its production implementation.
134 pub use microvms_app::adapters::*;
135 pub use microvms_edges::adapters::SystemAdapters;
136}
137
138pub mod clock {
139 //! The one clock port and tokio's clock.
140 pub use microvms_app::clock::*;
141 pub use microvms_edges::clock::{SystemClock, TokioClock};
142}
143
144pub mod entropy {
145 //! The entropy port and the OS random pool.
146 pub use microvms_app::entropy::*;
147 pub use microvms_edges::entropy::OsEntropy;
148}
149
150pub mod names {
151 //! Named MicroVMs: the record, the store port, and the CLI's registry on disk.
152 pub use microvms_app::names::*;
153 pub use microvms_edges::names::FileNameStore;
154}
155
156pub mod preflight {
157 //! Whether a harness can launch here, checked before it queues work (#223).
158 //!
159 //! The checks are `microvms_app::preflight`'s; [`preflight`] runs them over the
160 //! production control plane.
161 pub use microvms_app::preflight::*;
162
163 use crate::control::ControlPlane;
164 use crate::prelude::ControlPlaneExt as _;
165 use crate::region::Region;
166
167 /// Runs the three checks for `region`, or for the region the environment names.
168 ///
169 /// The bindings' `preflight`. See the module docs of `microvms_app::preflight` for what
170 /// it checks, what it does not, and why nothing it does bills.
171 pub async fn preflight(region: Option<Region>) -> PreflightReport {
172 let resolved = match region {
173 Some(region) => Ok(region),
174 None => Region::from_env(&crate::env::process),
175 };
176 preflight_with(resolved, ControlPlane::new).await
177 }
178}
179
180pub mod agents {
181 //! The coding-agent helpers `docs/AGENT-VMS.md` specifies, and the Bedrock minter.
182 pub use microvms_app::agents::*;
183
184 use crate::error::Error;
185
186 pub mod bedrock {
187 //! A Bedrock bearer token from the caller's own AWS credentials (AGENT-4).
188 pub use microvms_app::agents::bedrock::*;
189 pub use microvms_edges::agents::bedrock::*;
190 }
191
192 /// The start request for one task (AGENT-7), with an exec id from the production clock
193 /// when `options` carries none. [`prompt_request_with`] over [`crate::session::mint_exec_id`].
194 pub fn prompt_request(
195 spec: &AgentSpec,
196 task: &str,
197 options: &PromptOptions,
198 ) -> Result<crate::protocol::exec::StartRequest, Error> {
199 prompt_request_with(spec, task, options, crate::session::mint_exec_id)
200 }
201}
202
203pub mod control {
204 //! The control-plane client, with the signed transport and the build services.
205 pub use microvms_app::control::*;
206 pub use microvms_edges::clock::SystemClock;
207 pub use microvms_edges::control::SignedBuildServices;
208
209 pub mod context {
210 //! A task's build context, and reading one from a directory (#221, IMAGE-7).
211 pub use microvms_app::control::context::*;
212 pub use microvms_edges::control::context::from_dir;
213 }
214
215 pub mod project {
216 //! A project directory's dependency files: the pairing rule, and reading the pair
217 //! from a directory (#74, #264).
218 pub use microvms_app::control::project::*;
219 pub use microvms_edges::control::project::read_project_files;
220 }
221 pub use project::read_project_files;
222
223 pub mod services {
224 //! The STS and S3 calls `ensure_image` makes, and their SigV4 implementation.
225 pub use microvms_app::control::services::*;
226 // Named, not globbed: the edges' request helpers are `pub(crate)` there, and a glob
227 // would make the next one that goes `pub` (and its aws-sigv4 types) core's API too.
228 pub use microvms_edges::control::services::SignedBuildServices;
229 }
230
231 pub mod transport {
232 //! The control plane's calls, the transport port, and the signed transport.
233 pub use microvms_app::control::transport::*;
234 pub use microvms_edges::control::transport::SignedTransport;
235 }
236
237 pub mod token {
238 //! Idempotency tokens, unique per attempt by construction (TRAP-1).
239 pub use microvms_app::control::token::*;
240 use microvms_edges::entropy::OsEntropy;
241
242 /// An image-create idempotency token, unique per attempt, from the OS random pool.
243 ///
244 /// [`create_token_with`] over the OS pool. `scope` is a label only, folded in beside
245 /// the nonce; see the module docs of `microvms_app::control::token` for the
246 /// fifteen-hour wedge that closure exists to prevent. An unavailable pool panics. A
247 /// control plane mints through its own source instead, so a launch refuses rather
248 /// than panics.
249 pub fn create_token(scope: &str) -> String {
250 create_token_with(scope, &OsEntropy).expect("the OS random pool is available")
251 }
252
253 /// A run idempotency token, unique per attempt, from the OS random pool. Same rule as
254 /// [`create_token`].
255 pub fn run_token(scope: &str) -> String {
256 run_token_with(scope, &OsEntropy).expect("the OS random pool is available")
257 }
258 }
259}
260
261pub mod session {
262 //! The in-VM client, with the reqwest backend and the sockets.
263 pub use microvms_app::session::*;
264 pub use microvms_edges::clock::TokioClock;
265 pub use microvms_edges::session::*;
266
267 pub mod http {
268 //! The HTTP seam and the production backend.
269 pub use microvms_app::session::http::*;
270 pub use microvms_edges::session::http::ReqwestBackend;
271 }
272
273 pub mod proxy {
274 //! The two-header proxy auth and the mint schedule, on tokio's clock in production.
275 pub use microvms_app::session::proxy::*;
276 pub use microvms_edges::clock::TokioClock;
277 }
278
279 pub mod exec {
280 //! The exec handle and the cursor-driven stream, with the production exec id.
281 pub use microvms_app::session::exec::*;
282
283 pub use super::mint_exec_id;
284 }
285
286 /// A fresh exec id from the production clock: `x-` plus 16 hex characters.
287 ///
288 /// [`exec_id_at`] over tokio's clock. Code holding a [`Session`] mints through the
289 /// session's clock instead, so a test that pins the clock pins the id.
290 pub fn mint_exec_id() -> String {
291 use microvms_app::clock::Clock as _;
292 exec_id_at(TokioClock::new().unix_now())
293 }
294}
295
296pub mod workspace {
297 //! Directory transfer: the manifest, the diff and the sync pass, over the real filesystem's
298 //! walk, hash, pack and guarded extraction (#260).
299 pub use microvms_app::workspace::*;
300 pub use microvms_edges::workspace::*;
301}
302
303#[cfg(feature = "test-support")]
304pub mod testing {
305 //! The shared test doubles, for a dependent's tests: `microvms_app::testing`.
306 pub use microvms_app::testing::*;
307}
308
309#[cfg(test)]
310mod tests {
311 use std::collections::HashSet;
312
313 /// The OS-pool tokens are distinct per call. The nonce is the pool's draw, so this is the
314 /// pool's distinctness as the token functions see it (TRAP-1).
315 ///
316 /// **Falsification**: mint `create_token` from a fixed source and the set collapses to one.
317 #[test]
318 fn the_os_pool_tokens_never_repeat() {
319 let minted: HashSet<String> = (0..200)
320 .flat_map(|_| {
321 [
322 crate::control::token::create_token("img"),
323 crate::control::token::run_token("img"),
324 ]
325 })
326 .collect();
327 assert_eq!(minted.len(), 400);
328 }
329
330 /// Two production exec ids differ, even minted in one instant.
331 #[test]
332 fn production_exec_ids_are_distinct() {
333 let minted: HashSet<String> = (0..200).map(|_| crate::session::mint_exec_id()).collect();
334 assert_eq!(minted.len(), 200);
335 }
336}