Skip to content

Commit af58790

Browse files
authored
feat(sdk): add sparse sandbox configuration patches (#1303)
## TL;DR Adds SDK-owned, sparse sandbox configuration patches and explicit YAML adapters for `msb run`, `msb create`, and `msb install`. Root and scoped config flags are repeatable, compose in exact command-line order, and remain lower precedence than explicit CLI arguments. Configuration is never auto-discovered. Closes #1292. ## Description - Add public Rust SDK patch types for images, resources, runtime, init, filesystem, network, secrets, and scripts. - Add `SandboxBuilder::configure(SandboxConfigPatch)` and generic `SandboxConfigPatch::overlay`, with direct conversions from every scoped patch. - Keep patches sparse and defer required-field validation until the final sandbox build, allowing separate sources to complete nested values. - Use right-hand replacement for scalars and lists and recursive merge for maps, including secret entries and other named objects. - Add repeatable `--conf`, `--net-conf`, `--resource-conf`, `--runtime-conf`, `--fs-conf`, `--secret-conf`, and `--script-conf` CLI file adapters over the SDK patch model. - Preserve one exact left-to-right overlay stream across repeated and interleaved config flag kinds, including in generated `msb install` aliases. - Apply built-in defaults, ordered config sources, and explicit CLI inputs in documented precedence order while preserving CLI network-profile behavior. - Parse YAML strictly, resolve contributing relative paths against their source files, and support only `${ENV}` interpolation. - Defer config-provided scripts until the final shell is known so a later CLI shell override selects the generated shebang. - Document the complete schema, SDK API, scoped workflow, precedence rules, and the requirement to pass every config path explicitly. - Exclude sharing, templates, loops or fleets, cross-layer subtree copying, and expression syntax beyond `${ENV}`. ## Examples ### Root configuration ```yaml # sandbox.yaml image: "python:3.12" cpus: 2 memory: "1G" workdir: "/app" env: MODE: "production" mounts: - "./src:/app" network: policy: public allow: - "api.openai.com" scripts: start: "python app.py" ``` ```bash msb run --conf sandbox.yaml -- start msb create --conf sandbox.yaml --name agent msb install --conf sandbox.yaml --name agent ``` ### Repeated and interleaved config sources ```yaml # base.yaml image: "python:3.12" memory: "1G" env: MODE: "base" ``` ```yaml # standard.yaml (--resource-conf) cpus: 2 memory: "2G" ``` ```yaml # project.yaml env: MODE: "project" REGION: "us-east-1" ``` ```yaml # large.yaml (--resource-conf) cpus: 4 memory: "4G" ``` ```bash # Files overlay left to right. The explicit --memory value wins last. msb run \ --conf base.yaml \ --resource-conf standard.yaml \ --conf project.yaml \ --resource-conf large.yaml \ --memory 8G ``` ### Scoped network configuration ```yaml # net-policy.yaml policy: public allow: - "api.openai.com" deny: - "169.254.169.254" ``` ```bash msb run python --net-conf net-policy.yaml ``` ### Rust SDK ```rust use std::collections::BTreeMap; use microsandbox::{ ResourceConfigPatch, RuntimeConfigPatch, Sandbox, SandboxConfigPatch, SandboxImagePatch, }; let patch = SandboxConfigPatch::new() .image(SandboxImagePatch::Image("python:3.12".into())) .overlay(ResourceConfigPatch::new().cpus(2).memory_mib(1024)) .overlay(RuntimeConfigPatch::new().env(BTreeMap::from([ ("MODE".into(), "production".into()), ]))); let sandbox = Sandbox::builder("agent") .configure(patch) .memory(2048) .create() .await?; ``` ## Test Plan - [x] `cargo fmt --all -- --check` - [x] `git diff --check` - [x] `cargo test -p microsandbox -p microsandbox-cli --lib` (848 passed, 3 ignored after rebasing onto `e0c0f9ba`) - [x] `cargo test -p microsandbox-cli --lib` (299 passed after adding ordered repeated-config coverage) - [x] `cargo check -p microsandbox-cli` - [x] `cargo check -p microsandbox --lib --no-default-features --features prebuilt` - [x] `cargo check -p microsandbox-cli --no-default-features --features prebuilt` (passes with five pre-existing no-network runtime warnings) - [x] `cargo clippy -p microsandbox -p microsandbox-network -p microsandbox-cli --lib --bins -- -D warnings -A clippy::items-after-test-module -A clippy::type-complexity -A clippy::needless-borrows-for-generic-args -A clippy::cmp-owned` - [x] `cargo clippy -p microsandbox-cli --lib -- -D warnings` - [x] `cargo doc -p microsandbox --no-deps` - [x] Build and codesign `msb` on macOS, verify the hypervisor entitlement, and run real Apple Hypervisor VMs. - [x] On macOS, verify root config, all scoped config files, CLI-over-file precedence, deferred script shell selection, network allow/deny enforcement, missing-image validation, and rejection of a wrapped `--net-conf`. - [x] Run the actual updated `msb install` CLI with `--resource-conf first --conf base --resource-conf second`; verify validation succeeds and the generated alias preserves that exact order with absolute paths. - [x] Clone this PR commit on Ubuntu 26.04 x86_64 with usable KVM, run `cargo build -p microsandbox-cli`, and execute the root, scoped, network, and validation scenarios in real Linux/KVM VMs. - [x] On Ubuntu 26.04/KVM, run the new repeated/interleaved scenario from commit `86aa3c66`: `root → resources → root → resources` resolved the final file value to 1 GiB (`MemTotal: 1045056 kB`); moving the second root last resolved 768 MiB (`MemTotal: 787008 kB`); adding explicit `--memory 896M` won over the complete file stream (`MemTotal: 916032 kB`). All guests also observed the later root map value `ORDER_TEST=project`, and the isolated home was empty after all three ephemeral runs. - [ ] Unmodified workspace-wide strict Clippy remains blocked by pre-existing warnings outside this diff; the touched SDK, network, and CLI targets pass the focused strict command above.
1 parent e0c0f9b commit af58790

20 files changed

Lines changed: 4843 additions & 103 deletions

Cargo.lock

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ scopeguard = "1.2"
121121
serde = { version = "1.0", features = ["derive"] }
122122
serde_bytes = "0.11"
123123
serde_json = "1.0"
124+
serde-saphyr = { version = "1.0.1", default-features = false, features = ["deserialize"] }
124125
sha2 = "0.11.0"
125126
smoltcp = { version = "0.13", default-features = false }
126127
socket2 = "0.6"

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@
5959
> ```sh
6060
> go get github.com/superradcompany/microsandbox/sdk/go # 🐹 Go
6161
> ```
62-
>
63-
> ```sh
64-
> gem install microsandbox # 💎 Ruby
65-
> ```
6662
#### <img height="14" src="https://octicons-col.vercel.app/download/A770EF">&nbsp;&nbsp;Install the CLI
6763
6864
> Boot a microVM in a single command:
@@ -325,6 +321,31 @@ The `msb` CLI provides a complete interface for managing sandboxes, images, and
325321
> msb image rm python # Remove an image
326322
> ```
327323
324+
#### <img height="14" src="https://octicons-col.vercel.app/file-code/A770EF">&nbsp;&nbsp;Configuration File
325+
326+
> ```sh
327+
> msb run --conf sandbox.yaml -- octocat
328+
> ```
329+
>
330+
> ```yaml
331+
> image: python:3.12
332+
> network:
333+
> allow:
334+
> - api.github.com
335+
> scripts:
336+
> octocat: |
337+
> python - <<'PY'
338+
> import urllib.request
339+
>
340+
> request = urllib.request.Request(
341+
> "https://api.github.com/octocat",
342+
> headers={"User-Agent": "microsandbox-example"},
343+
> )
344+
> with urllib.request.urlopen(request) as response:
345+
> print(response.read().decode())
346+
> PY
347+
> ```
348+
328349
#### <img height="14" src="https://octicons-col.vercel.app/download/A770EF">&nbsp;&nbsp;Install & Uninstall Sandboxes
329350
330351
> ```sh

crates/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ russh = { workspace = true, optional = true }
5858
sea-orm.workspace = true
5959
serde.workspace = true
6060
serde_json.workspace = true
61+
serde-saphyr.workspace = true
6162
tempfile.workspace = true
6263
thiserror = { workspace = true, optional = true }
6364
tokio.workspace = true

0 commit comments

Comments
 (0)