From 26f50aa711a8e8d95c6b41839f3de68037186503 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 8 Sep 2026 14:29:25 -0700 Subject: [PATCH 01/41] docs: design NVX backend integration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../specs/2026-09-08-nvx-backend-design.md | 433 ++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-08-nvx-backend-design.md diff --git a/docs/superpowers/specs/2026-09-08-nvx-backend-design.md b/docs/superpowers/specs/2026-09-08-nvx-backend-design.md new file mode 100644 index 000000000..8e0b71725 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-nvx-backend-design.md @@ -0,0 +1,433 @@ +# NVX Backend for MXC + +## Status + +Approved design for an experimental MXC backend named `nvx`. + +## Assumptions + +1. NVX refers to `nanvix/nvx`, which boots a Linux microVM through OpenVMM and Windows Hypervisor Platform (WHP). It is distinct from MXC's existing NanVix-based `microvm` backend. +2. The first workload is a CPython-based agent image, but the execution contract remains command-oriented rather than Python-specific. +3. The first milestone supports one live NVX sandbox per user. +4. All requested host paths must be beneath one canonical common host root. +5. Correctness and observability take priority over a fixed startup-latency target. Cold-boot and warm-exec latency are measured but do not initially gate acceptance. +6. The backend remains behind `--experimental` until its limitations and policy behavior are proven. + +## Goal + +Add NVX as a new Windows containment backend with: + +- one-shot execution; +- state-aware `provision → start → exec* → stop → deprovision`; +- repeated execution in one warm microVM; +- live, binary-safe stdin/stdout/stderr; +- CPython/agent workload support; +- provision-time filesystem and network policy; +- fail-closed validation for unsupported MXC policy; +- an optional, separately versioned runtime package. + +The first milestone does not require snapshot-accelerated startup, arbitrary unrelated host path mappings, multiple simultaneous sandboxes per user, or per-host network filtering. + +## Current NVX Capability Baseline + +The baseline was verified in the NVX `Project build` session against commit `3fed8175dbd5dd04cb06a13ed79ba7882de4053f`. + +| Area | Available today | +|---|---| +| Isolation | Hardware-backed x86-64 microVM through OpenVMM on WHP | +| Host control | `python scripts\nvx.py run` foreground CLI | +| Compute | Fixed supported vCPU counts of 1, 2, 4, or 8 and configurable memory | +| Guest | Linux kernel plus Alpine initramfs | +| Filesystem | One virtio-fs export root with device-wide read-only or read-write mode | +| Network | Optional static portable network profile | +| Console | Portb plus optional bidirectional virtio-console | +| Execution | Interactive shell, kernel-command-line `nvx_exec`, and experimental single-workload sandbox bootstrap | +| Exit | Guest `nvx-exit` reports an 8-bit status through PMIO | +| Snapshot | Capture-and-terminate plus same-backend restore primitives | +| Build outputs | OpenVMM executable, kernel, initramfs, kernel config, and package manifest | +| Tests | Unit tests and WHP lifecycle coverage, plus broader declared microVM scenario matrices | + +These capabilities prove the VM foundation. They do not provide a production lifecycle or repeated-exec API for MXC. + +## Selected Architecture + +MXC owns the host-side integration and launches OpenVMM directly. It does not invoke `scripts\nvx.py` at runtime. + +```text +MXC SDK / wxc-exec + | + | containment: "nvx" + v +NVX backend crate + - schema/domain translation + - policy validation + - one-shot composition + | + | owner-only, versioned named-pipe protocol + v +MXC NVX daemon + - one sandbox per user + - lifecycle state machine + - OpenVMM process ownership + - runtime and attachment validation + - orphan recovery and teardown + | + | OpenVMM launch + dedicated virtio-console endpoint + v +OpenVMM / WHP microVM + | + | versioned framed guest protocol + v +NVX guest agent + - readiness and health + - repeated exec + - stream multiplexing and backpressure + - signals, cancellation, and exit metadata + - mount-namespace construction +``` + +### Why a daemon + +Each state-aware phase runs in a separate short-lived MXC process. A persistent per-user daemon must therefore own: + +- the live OpenVMM process; +- the guest control endpoint; +- provisioned configuration; +- lifecycle state; +- stream and cancellation routing; +- teardown after client or VM failure. + +The daemon follows the proven WSLc pattern for secure discovery, protocol-version checks, exact process-identity validation, transition locking, idle teardown, and typed errors. Its implementation is NVX-specific and does not share WSLc SDK state. + +### Why dedicated virtio-console + +The current NVX profile already has a bidirectional virtio-console with receive and transmit queues, host pipe/socket backends, input gating, and snapshot-aware state. + +For the MXC contract: + +- portb remains the kernel boot, panic, and recovery diagnostics channel; +- virtio-console is reserved exclusively for framed guest-agent traffic; +- kernel `printk` output must not be routed to the protocol console; +- a profile or contract version distinguishes the new behavior. + +This is safer and smaller than adding virtio-vsock for the first milestone. It avoids the fragility and throughput limits of console scraping or portb-based workload streaming. + +## Lifecycle Semantics + +| Phase | Behavior | +|---|---| +| `provision` | Validate runtime compatibility, MXC policy, common-root filesystem mappings, network mode, and resource settings. Persist provision metadata without booting the VM. Mint an `nvx:` sandbox ID. | +| `start` | Launch OpenVMM with the provisioned immutable attachments. Wait for the guest agent's launch-bound, version-compatible readiness message. | +| `exec` | Run a command repeatedly in the warm VM. Stream separate stdin/stdout/stderr, honor cwd/env/timeout, support cancellation, and return exit metadata. | +| `stop` | Request graceful guest shutdown, then force termination after a bounded deadline. Preserve provisioned host-side state for a later `start`. | +| `deprovision` | Ensure the VM is stopped, delete provision metadata and disposable sandbox state, and release daemon ownership. | + +The state machine rejects invalid transitions. Operations are idempotent only where success can be proven without hiding cleanup uncertainty. + +### One-shot composition + +One-shot execution uses the same backend and daemon primitives: + +```text +provision → start → exec → stop → deprovision +``` + +Cleanup runs after success, workload failure, timeout, cancellation, or VM failure. This keeps policy validation, protocol behavior, error mapping, and cleanup consistent across one-shot and state-aware surfaces. + +## Host-to-Daemon Protocol + +The daemon protocol is internal to the MXC distribution and separate from the public JSON schema. + +Requirements: + +- owner-only Windows named pipe; +- exact server PID and creation-time authentication; +- length-prefixed, bounded frames; +- explicit protocol version; +- typed request and response variants; +- one request ID per operation; +- stable machine-readable daemon error kinds; +- bounded readiness and operation timeouts; +- no unbounded allocation from a peer-controlled length; +- safe behavior when a stale daemon from another MXC version is present. + +Core operations: + +- `Ping` +- `Provision` +- `Start` +- `ExecOpen` +- `ExecInput` +- `ExecInputClosed` +- `ExecCancel` +- `Stop` +- `Deprovision` +- daemon-to-client stream and terminal events + +Only one provisioned sandbox is admitted per user in the first milestone. A second provision returns a typed busy/capacity error rather than replacing the existing sandbox. + +## Guest Protocol and Agent + +The guest agent is a production component in the NVX runtime image, not an extension of shell-output parsing. + +### Handshake + +The agent sends: + +- protocol version; +- runtime image version; +- launch nonce; +- supported feature bits; +- guest boot identity. + +The daemon rejects a mismatched nonce or incompatible version before marking the sandbox started. + +### Exec request + +An exec request carries: + +- request and stream IDs; +- executable or shell command representation; +- arguments without whitespace encoding restrictions; +- working directory; +- environment variables; +- timeout metadata; +- terminal/non-terminal mode; +- stdin availability. + +The initial execution identity is a fixed unprivileged guest account named `mxc`. Caller-selected UID/GID is rejected. + +### Stream frames + +The protocol distinguishes: + +- stdin bytes; +- stdin EOF; +- stdout bytes; +- stderr bytes; +- cancellation; +- process exit; +- timeout completion; +- protocol or agent failure. + +It must support binary payloads, bounded buffering, backpressure, high-volume output, and deterministic terminal delivery. A request cannot report both a timeout and a normal exit. + +### Process isolation + +For each exec, the agent creates the supported subset of: + +- mount namespace; +- PID namespace; +- UTS namespace; +- IPC namespace; +- private `/dev`; +- read-only `/sys`; +- cgroup v2 process and memory limits; +- `no_new_privs`; +- empty capability bounding, inheritable, and ambient sets. + +Execs share the VM's provisioned network namespace in the first milestone. Per-exec network +namespaces are deferred because they require a guest-side virtual network setup that NVX does not +currently provide; creating an empty namespace would silently break the accepted network policy. + +Seccomp or LSM enforcement is a P1 hardening item unless a safe, tested initial profile can be included without delaying the core contract. + +## Policy Honor Matrix + +Every rejected field fails with `policy_validation` before daemon or VM side effects. + +| Policy field | Provision | Start / stop / deprovision | Exec | +|---|---|---|---| +| `filesystem.readwritePaths` | Honored under one canonical common root | Rejected as immutable | Rejected as immutable | +| `filesystem.readonlyPaths` | Honored under one canonical common root | Rejected as immutable | Rejected as immutable | +| `filesystem.deniedPaths` | Rejected | Rejected | Rejected | +| Network default posture | `block`: no NIC; `allow`: portable NVX NIC | Rejected as immutable | Rejected as immutable | +| Allowed/blocked hosts | Rejected | Rejected | Rejected | +| `allowLocalNetwork` | Rejected | Rejected | Rejected | +| Network enforcement mode | Accept only capability-level semantics | Rejected | Rejected | +| URL-form proxy | Rejected as an exec concern | Rejected | Honored through scrubbed `HTTP_PROXY` and `HTTPS_PROXY` | +| Localhost/built-in proxy forms | Rejected | Rejected | Rejected | +| UI policy | Rejected by presence | Rejected by presence | Rejected by presence | +| Process command/args/cwd/env | Not applicable | Not applicable | Honored | +| Process timeout | Not applicable | Not applicable | Honored | +| Caller-selected user/group | Not applicable | Not applicable | Rejected | +| Lifecycle section on one-shot | Only values matching actual composed behavior may be accepted | Not applicable | Not applicable | + +### Filesystem enforcement + +NVX currently exposes one virtio-fs root and one device-wide access mode. The initial backend therefore requires all requested host paths to be under one canonical common root. + +The root is exported read-write to enable a mixture of requested modes. The guest agent then: + +1. mounts the exported root in an agent-only location; +2. creates a private workload mount namespace; +3. bind-mounts only declared child paths at their requested guest locations; +4. remounts read-only children read-only; +5. ensures the raw exported root is not reachable from the workload namespace. + +The backend rejects paths outside the common root, path aliases that escape after canonicalization, overlapping mappings with conflicting modes, and requests that cannot be represented safely. + +Arbitrary unrelated host paths require a future static aggregate virtio-fs contract and are P2. + +### Network enforcement + +At provision: + +- `block` omits the virtio-net device; +- `allow` configures NVX's portable network profile and waits for guest network readiness; +- static allocation details remain daemon-internal; +- per-host filtering is rejected. + +At exec, a URL-form proxy is cooperative environment injection. Documentation must state that raw sockets can bypass it when the network default is `allow`. + +## Runtime Package + +NVX artifacts are distributed separately from the main MXC package. + +The package contains: + +- version-matched `openvmm.exe`; +- NVX kernel; +- CPython/agent initramfs; +- guest-agent version; +- artifact manifest; +- cryptographic hashes; +- MXC/NVX protocol compatibility range; +- microVM profile/ABI version; +- package provenance and license metadata. + +The installer or runtime resolver must: + +- verify artifact integrity before use; +- probe WHP and required Windows features; +- avoid Docker as an end-user runtime dependency; +- support explicit local override for development; +- reject incompatible partial installations; +- define upgrade, rollback, and stale-daemon behavior. + +## Error Semantics + +| Condition | MXC error/outcome | +|---|---| +| Unsupported or wrong-phase policy | `policy_validation` | +| NVX runtime missing, incompatible, corrupt, or WHP unavailable | `backend_unavailable` | +| Unknown, stale, or deprovisioned sandbox ID | `not_provisioned` | +| Exec before start | `not_started` | +| Capacity reached or invalid concurrent transition | `backend_error` with stable daemon kind | +| Daemon transport or protocol failure | `backend_error` | +| Guest protocol mismatch, agent failure, or VM crash | `backend_error` with retained diagnostics | +| Exec deadline elapsed and workload confirmed gone | `TimedOut` outcome | +| Workload exited | `Exited(code)` outcome | +| Cleanup status uncertain | Error; never a success-shaped fallback | + +Diagnostics from portb, OpenVMM stderr, daemon logs, and guest-agent events must be correlated by sandbox and launch identity without mixing them into workload stdout/stderr. + +## Prioritized Gaps + +### P0: Required for the first experimental backend + +1. Production guest agent and versioned framed protocol over dedicated virtio-console. +2. MXC-owned per-user daemon with secure discovery, lifecycle durability, one-sandbox admission, and orphan cleanup. +3. Repeated exec with separate binary-safe streams, backpressure, EOF, cancellation, timeout, exit, and typed errors. +4. NVX/OpenVMM profile adjustment that reserves virtio-console for protocol traffic and keeps diagnostics on portb. +5. Common-root filesystem validation and guest mount-namespace enforcement for declared read-only/read-write children. +6. No-NIC versus portable-NIC lifecycle integration and URL-form cooperative proxy injection. +7. Optional versioned runtime package, integrity manifest, host probe, compatibility negotiation, and installation flow. +8. MXC schema, parser, domain model, engine, SDK, FFI, probe, build, packaging, documentation, and validation wiring for `nvx` and `nvx:` IDs. +9. Fixed unprivileged workload identity and phase-specific fail-closed policy validation. + +### P1: Hardening and performance follow-up + +1. Structured logs, crash classification, metrics, protocol tracing, recovery diagnostics, and daemon idle teardown. +2. Snapshot orchestration for faster startup, including compatibility, storage, and garbage-collection rules. +3. Seccomp/LSM hardening, resource quotas, stress tests, failure injection, and signed distribution. + +### P2: Expanded capability + +1. Arbitrary independent host paths through a static aggregate virtio-fs contract. +2. Multiple simultaneous NVX sandboxes per user. +3. Per-host network egress controls. +4. Virtio-vsock if measurements show the dedicated virtio-console cannot meet product needs. + +## Testing Strategy + +### Unit tests + +- public schema and generated type parity; +- containment and sandbox-ID routing; +- per-phase policy matrix; +- common-root canonicalization and overlap handling; +- lifecycle state transitions and idempotence; +- daemon and guest frame size limits; +- protocol version negotiation; +- stream multiplexing, buffering, EOF, and terminal-state exclusivity; +- daemon-to-MXC error mapping; +- package manifest and compatibility validation. + +### Host integration tests + +- real owner-only daemon named-pipe transport; +- daemon discovery, race prevention, and stale-version replacement; +- exact process identity and PID-reuse defense; +- fake guest endpoint for readiness, exec streams, cancellation, malformed frames, and disconnects; +- OpenVMM process crash and forced teardown; +- orphan recovery after phase-client or daemon restart; +- one-sandbox-per-user admission; +- idle teardown. + +### Guest tests + +- fixed unprivileged identity; +- repeated exec; +- command arguments containing spaces and binary data; +- cwd and environment; +- read-only/read-write mount behavior; +- raw export root is unreachable; +- proxy environment scrubbing and injection; +- high-volume stdout/stderr with backpressure; +- stdin EOF; +- graceful signal and forced cancellation; +- malformed frame handling; +- agent health and shutdown. + +### WHP end-to-end tests + +- one-shot CPython workload; +- full state-aware lifecycle; +- warm-state persistence across separate exec invocations; +- CPython agent workload using the optional runtime image; +- common-root read-only and read-write mappings; +- network block and allow; +- URL-form proxy; +- timeout with confirmed workload termination; +- VM crash and orphan cleanup; +- stale runtime and guest-protocol rejection; +- stop/restart and deprovision cleanup; +- measured cold-boot and warm-exec latency. + +## Acceptance Criteria + +The first milestone is complete when: + +1. `containment: "nvx"` is available only with experimental opt-in and host capability success. +2. Both one-shot and state-aware SDK/CLI paths execute CPython agent workloads. +3. A state-aware sandbox remains warm across repeated execs from separate MXC processes. +4. Workload stdout and stderr remain separate and binary-safe; stdin, EOF, cancellation, timeout, and exit are deterministic. +5. Provisioned common-root RO/RW mappings are enforced and immutable. +6. Network block/allow and URL proxy behavior match the documented honor matrix. +7. Unsupported fields fail before any daemon or VM side effects. +8. VM, agent, client, and daemon failure tests leave no unowned OpenVMM process or undisclosed cleanup uncertainty. +9. Runtime artifact integrity and protocol/profile compatibility are validated before launch. +10. Targeted unit, daemon integration, guest, and WHP E2E suites pass. + +## Deferred Decisions + +The following are intentionally outside the first milestone: + +- snapshot-restored startup as an acceptance requirement; +- aggregate virtio-fs and arbitrary unrelated host paths; +- multiple live sandboxes per user; +- per-host network filtering; +- virtio-vsock; +- caller-selected guest identities; +- capture-and-continue or cross-hypervisor restore. From 15155cc7f6d2df3a94fbf373953a3f9161dc95eb Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 8 Sep 2026 14:32:50 -0700 Subject: [PATCH 02/41] docs: define NVX sizing config Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../specs/2026-09-08-nvx-backend-design.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-09-08-nvx-backend-design.md b/docs/superpowers/specs/2026-09-08-nvx-backend-design.md index 8e0b71725..3f786805d 100644 --- a/docs/superpowers/specs/2026-09-08-nvx-backend-design.md +++ b/docs/superpowers/specs/2026-09-08-nvx-backend-design.md @@ -12,6 +12,8 @@ Approved design for an experimental MXC backend named `nvx`. 4. All requested host paths must be beneath one canonical common host root. 5. Correctness and observability take priority over a fixed startup-latency target. Cold-boot and warm-exec latency are measured but do not initially gate acceptance. 6. The backend remains behind `--experimental` until its limitations and policy behavior are proven. +7. VM sizing is immutable after provision. The public NVX config exposes `memoryMiB` and + `processors`; processors accepts only NVX's supported values `1`, `2`, `4`, or `8`. ## Goal @@ -116,7 +118,7 @@ This is safer and smaller than adding virtio-vsock for the first milestone. It a | Phase | Behavior | |---|---| -| `provision` | Validate runtime compatibility, MXC policy, common-root filesystem mappings, network mode, and resource settings. Persist provision metadata without booting the VM. Mint an `nvx:` sandbox ID. | +| `provision` | Validate runtime compatibility, MXC policy, common-root filesystem mappings, network mode, `memoryMiB`, and `processors`. Persist provision metadata without booting the VM. Mint an `nvx:` sandbox ID. | | `start` | Launch OpenVMM with the provisioned immutable attachments. Wait for the guest agent's launch-bound, version-compatible readiness message. | | `exec` | Run a command repeatedly in the warm VM. Stream separate stdin/stdout/stderr, honor cwd/env/timeout, support cancellation, and return exit metadata. | | `stop` | Request graceful guest shutdown, then force termination after a bounded deadline. Preserve provisioned host-side state for a later `start`. | @@ -305,6 +307,15 @@ The installer or runtime resolver must: - reject incompatible partial installations; - define upgrade, rollback, and stale-daemon behavior. +The public `experimental.nvx` configuration exposes only immutable VM sizing in the first +milestone: + +- `memoryMiB`, with a conservative CPython-capable default; +- `processors`, restricted to `1`, `2`, `4`, or `8`. + +Runtime package resolution remains installation policy rather than a per-request production +setting. Development builds may use an environment-based local artifact override. + ## Error Semantics | Condition | MXC error/outcome | From 7c710dbb5f0816a0d9bcc9fffaabdcd9aa634238 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Wed, 9 Sep 2026 10:55:26 -0700 Subject: [PATCH 03/41] docs: document NVX backend gaps Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 179 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 docs/nvx-backend-gaps.md diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md new file mode 100644 index 000000000..e6aaac677 --- /dev/null +++ b/docs/nvx-backend-gaps.md @@ -0,0 +1,179 @@ +# NVX Daemon Requirements and MXC Integration Gaps + +## Scope + +The NVX team will provide a daemon that owns `openvmm.exe` and presents the +stable runtime interface consumed by MXC. +The intended boundary is: + +```mermaid +flowchart LR + MXC[MXC] --> Daemon[NVX daemon] + Daemon --> OpenVMM[openvmm.exe] + OpenVMM --> Guest[NVX guest] +``` + +## What NVX Already Provides + +NVX already supplies the core VM foundation: + +- hardware-backed Linux microVM execution through OpenVMM and Windows + Hypervisor Platform; +- configurable memory and supported vCPU counts; +- Linux kernel and Alpine-based initramfs artifacts; +- a CPython guest-image build path; +- one host directory exported through virtio-fs; +- optional portable networking with static configuration; +- portb diagnostics and a bidirectional virtio-console device; +- guest-to-host exit reporting; +- capture-and-terminate snapshot and same-backend restore primitives; +- unit tests and microVM lifecycle scenarios across supported hypervisors. + +The primary gap is turning those primitives into a stable daemon-managed +sandbox service. + +## NVX Runtime Artifacts + +The following sizes were observed in the current Windows NVX build and may vary +slightly between revisions: + +| Artifact | Purpose | Observed size | +| --- | --- | ---: | +| `openvmm.exe` | Windows host process that creates and runs the NVX microVM through WHP. | 21.38 MiB | +| `vmlinux` | Linux kernel that runs inside the microVM. | 21.83 MiB | +| `initramfs.cpio.gz` | Compressed Alpine guest filesystem containing startup scripts, tools, and NVX utilities. | 7.12 MiB | +| `vmlinux.config` | Exact Linux kernel configuration used for the build. | 66.80 KiB | +| `initramfs.cpio.gz.packages.json` | Manifest of packages and source inputs included in the initramfs. | 22.97 KiB | + +The five artifacts total approximately **50.35 MiB**. A CPython/agent +initramfs would be an additional runtime artifact and should carry its own +version and package manifest. + +## Recommended Daemon API Semantics + +The exact transport belongs to NVX, but the API should express the following +operations without requiring MXC to know OpenVMM details: + +| Operation | Minimum result | +| --- | --- | +| `GetCapabilities` | Daemon version, protocol range, runtime version, guest-agent version, supported features, host readiness, and limitations | +| `Provision` | Opaque sandbox ID and effective immutable configuration | +| `Start` | Ready state, launch identity, guest identity, and network status | +| `Exec` | Exec ID plus handles or framed channels for stdin, stdout, and stderr | +| `WaitExec` | Exit code or timeout/cancel outcome, signal information, and duration | +| `CancelExec` | Typed acknowledgement followed by confirmed terminal state | +| `Stop` | Confirmed guest/OpenVMM shutdown or an explicit cleanup error | +| `Deprovision` | Confirmed removal of provisioned and disposable state | +| `GetStatus` | Current lifecycle state, active exec, failure information, and diagnostic correlation ID | +| `ShutdownDaemon` | Controlled service shutdown for upgrades and testing | + +Long-running operations should support cancellation and deadlines. Requests +should carry correlation IDs, and retries should be idempotent where the daemon +can prove the prior result. + +## Guest-Agent Requirements Behind the Daemon + +The daemon needs a production guest agent; OpenVMM lifecycle control alone is +not enough. + +The guest agent must provide: + +1. a launch-bound, versioned readiness handshake; +2. immutable session configuration; +3. repeated process creation and supervision; +4. separate stdin, stdout, and stderr; +5. bounded flow control and backpressure; +6. stdin EOF, cancellation, timeout, and exact terminal events; +7. fixed unprivileged workload identity; +8. private process and mount isolation; +9. read-only and read-write bind-mount enforcement; +10. structured network status; +11. health, quiesce, and graceful-shutdown operations; +12. cleanup of child processes after host-channel loss. + +The daemon should hide this guest protocol from MXC. MXC consumes daemon-level +operations and does not need to know whether the internal transport is +virtio-console, vsock, or another NVX mechanism. + +## MXC-Side Work After the Daemon Exists + +With a complete NVX daemon, the remaining MXC integration follows established +backend patterns: + +| MXC area | Required work | +| --- | --- | +| Schema and model | Add the distinct `nvx` containment value and its immutable provision settings. | +| State-aware dispatch | Register `nvx:` sandbox IDs and implement a `StatefulSandboxBackend` adapter over the daemon API. | +| One-shot execution | Compose daemon provision, start, exec, stop, and deprovision using the same adapter. | +| Policy validation | Define the per-phase fail-closed honor matrix and reject unsupported fields before contacting the daemon. | +| Stream adaptation | Convert daemon streams and terminal results into MXC's `SandboxProcess` and `ExecHandle` contracts. | +| Error mapping | Map daemon error kinds to MXC errors without string matching. | +| Capability probe | Surface daemon installation, host readiness, runtime compatibility, and supported features. | +| SDKs and FFI | Add `nvx` to TypeScript, C#, Rust, and C ABI surfaces where applicable. | +| Build and packaging | Discover or install the separately distributed NVX runtime and daemon. | +| Tests and CI | Add one-shot and state-aware lifecycle, policy, failure, stream, and cleanup coverage. | + + +## Summary + +The architectural change removes the largest host-side responsibility from +MXC: **the NVX daemon, not MXC, owns OpenVMM and the guest control plane**. + +The daemon should be viewed as the production successor to the one-workload +`nanvixd` model. It must retain the same fundamental ability to launch a +microVM and propagate workload results, while adding durable lifecycle, +repeated exec, structured streams, policy configuration, guest supervision, +snapshots, diagnostics, compatibility negotiation, and crash-safe cleanup. + +Once this daemon contract exists, the MXC work becomes a relatively conventional +backend adapter using patterns already established by WSLc, Windows Sandbox, +IsolationSession, and the existing `microvm` backend. + +## Appendix A: Daemon Requirements (P0) + +The first experimental MXC backend requires the daemon to provide the following +capabilities. More detailed acceptance requirements appear in Appendix B. + +| Area | P0 requirement | +| --- | --- | +| Runtime ownership | Own `openvmm.exe`, its control endpoints, handles, logs, exit status, and cleanup. MXC must never launch OpenVMM directly or parse its console output. | +| Lifecycle | Provide deterministic provision, start, repeated exec, stop, deprovision, status, cancellation, and timeout behavior over opaque sandbox and exec IDs. | +| Guest control | Use a bounded, versioned guest-agent protocol with an authenticated launch-bound readiness handshake, separate binary-safe process streams, backpressure, EOF, and exact terminal events. | +| Workload isolation | Run workloads as a fixed unprivileged guest identity and isolate their processes and mounts. Clean up all descendants after completion or control-channel loss. | +| Filesystem | Validate immutable provision-time mappings beneath one canonical host root and enforce each declared mapping as read-only or read-write without traversal or symlink escape. | +| Network and proxy | Support no-NIC isolation and NVX portable networking, report structured readiness, and support cooperative URL-form HTTP/HTTPS proxy injection without claiming raw-socket enforcement. | +| Resources and compatibility | Validate memory and supported processor settings and reject incompatible daemon, OpenVMM, VM ABI, kernel, initramfs, guest-agent, or protocol combinations before launch. | +| Snapshots | Capture only an idle, guest-quiesced VM; bind the snapshot to the complete runtime and attachment configuration; revalidate it before restore; and require a fresh guest and network readiness handshake after restore. An untrusted or incompatible optimization snapshot must be invalidated and replaced by a cold boot. | +| Security | Restrict daemon access to authorized local clients, validate every caller-controlled value, use bounded framing and buffering, verify runtime artifacts, avoid shell command construction and unnecessary inherited handles, and run with the minimum required privileges. | +| Recovery and diagnostics | Use exact process identity and ownership evidence for orphan recovery, guarantee bounded cleanup across client, guest, OpenVMM, and daemon failures, never report success when cleanup is uncertain, and keep trusted diagnostics separate from workload output. | + +## Appendix B: Required NVX Daemon Capabilities + +The following capabilities are required for the first experimental MXC backend. + +| Daemon capability | Requirement | +| --- | --- | +| Versioned API | Expose a stable, machine-readable API independent of OpenVMM's command-line interface. Negotiate protocol and runtime versions and reject incompatible clients, guest images, or OpenVMM builds. | +| Lifecycle operations | Provide `provision`, `start`, `exec`, `stop`, and `deprovision`, or equivalent operations with the same semantics. Provision stores immutable configuration without booting; start launches and readies the VM; exec is repeatable; stop preserves provisioned state; deprovision deletes it. | +| Stable sandbox identity | Mint an opaque sandbox ID and reject unknown, stale, malformed, or already-deprovisioned IDs with typed errors. | +| OpenVMM ownership | Be the sole owner of the OpenVMM child process, command line, control endpoints, handles, logs, exit status, and forced termination. Do not require MXC to parse OpenVMM output. | +| Explicit VM state machine | Track states such as provisioned, starting, ready, executing, stopping, stopped, failed, and deprovisioned. Reject invalid or concurrent transitions deterministically. | +| Guest readiness | Complete a versioned handshake with the guest agent and report readiness only after the agent, configured filesystem, and requested network are usable. Boot-marker text is not sufficient. | +| Repeated exec | Run multiple sequential commands in one warm VM without rebooting. The first milestone may allow only one active exec at a time, but it must support repeated exec calls. | +| Process configuration | Accept an argument vector, working directory, environment, timeout, mapping selection, and stdin mode without kernel-command-line quoting or whitespace restrictions. | +| Fixed workload identity | Launch workloads as a fixed unprivileged guest account. The daemon must not report readiness if the guest cannot enforce that identity. | +| Separate process streams | Return distinct binary-safe stdin, stdout, and stderr streams. Support stdin EOF, bounded buffering, backpressure, large output, and deterministic stream closure. | +| Wait and cancellation | Provide wait, graceful cancellation, forced termination, timeout, and exit metadata. A timeout is reported only after the workload is confirmed gone. | +| Guest-control channel | Own a framed, bounded, versioned connection to the guest agent. The channel must support request IDs, stream IDs, cancellation, flow control, terminal events, health, and shutdown. | +| Diagnostic separation | Keep kernel/OpenVMM/daemon diagnostics separate from workload stdout and stderr. Kernel panic or boot output must not corrupt guest-agent protocol traffic. | +| Filesystem configuration | Accept provision-time host mappings, validate them, attach the common virtio-fs root, and configure the guest agent to expose only declared children as read-only or read-write. | +| Filesystem immutability | Reject attempts to change filesystem mappings after provision. Reject path traversal, symlink escape, conflicting overlaps, and mappings outside the configured common root. | +| Network modes | Support an isolated mode with no NIC and an enabled mode using NVX portable networking. Report structured network readiness or a typed setup failure. | +| Exec-time proxy | Accept a URL-form HTTP/HTTPS proxy for an exec and arrange cooperative environment injection. Do not claim that this provides raw-socket enforcement. | +| Resource configuration | Accept immutable provision-time memory and processor settings, validate supported processor counts, and communicate the effective values to the caller. | +| Graceful VM shutdown | Request guest shutdown, wait for acknowledgement and process exit, then force OpenVMM termination after a bounded deadline. | +| Cleanup guarantees | Remove disposable state and close handles after normal completion, timeout, cancellation, client disconnect, guest crash, OpenVMM crash, or daemon shutdown. Never return success when cleanup status is uncertain. | +| Orphan recovery | On daemon restart, identify prior OpenVMM processes using exact process identity and ownership evidence. Reclaim only positively identified instances; never terminate a process based on PID alone. | +| Structured errors | Return stable error kinds for unavailable runtime, incompatible version, capacity, invalid transition, not provisioned, not started, busy, timeout, guest failure, OpenVMM failure, protocol failure, and uncertain cleanup. | +| Capability reporting | Report supported lifecycle operations, stream modes, network modes, filesystem restrictions, snapshot support, processor values, and protocol versions so MXC can probe the backend accurately. | +| Runtime integrity | Validate OpenVMM, kernel, initramfs, guest-agent, manifest, checksums, architecture, and compatibility before VM launch. | From f34f5d34f376c4250f85b9548aa4ea98c0888174 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 14 Sep 2026 11:10:18 -0700 Subject: [PATCH 04/41] docs: update NVX backend policy results Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 256 +++++++++++++-------------------------- 1 file changed, 84 insertions(+), 172 deletions(-) diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index e6aaac677..43539d8a7 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -1,179 +1,91 @@ -# NVX Daemon Requirements and MXC Integration Gaps +# NVX Backend: MXC policy testing with a prototype -## Scope +## Status and scope -The NVX team will provide a daemon that owns `openvmm.exe` and presents the -stable runtime interface consumed by MXC. -The intended boundary is: +The NVX prototype accepted a real MXC `0.9.0-dev` JSON, validated it against the +schema, and adapted supported fields into typed NVX launch plans. +The prototype then exercised those plans on Windows through OpenVMM +and WHP. + +## Current architecture + +The intended MXC integration uses a direct ownership boundary: ```mermaid flowchart LR - MXC[MXC] --> Daemon[NVX daemon] - Daemon --> OpenVMM[openvmm.exe] - OpenVMM --> Guest[NVX guest] + MXC[MXC] --> OpenVMM[OpenVMM / WHP] + OpenVMM --> Agent[NVX PID 1 guest agent] ``` -## What NVX Already Provides - -NVX already supplies the core VM foundation: - -- hardware-backed Linux microVM execution through OpenVMM and Windows - Hypervisor Platform; -- configurable memory and supported vCPU counts; -- Linux kernel and Alpine-based initramfs artifacts; -- a CPython guest-image build path; -- one host directory exported through virtio-fs; -- optional portable networking with static configuration; -- portb diagnostics and a bidirectional virtio-console device; -- guest-to-host exit reporting; -- capture-and-terminate snapshot and same-backend restore primitives; -- unit tests and microVM lifecycle scenarios across supported hypervisors. - -The primary gap is turning those primitives into a stable daemon-managed -sandbox service. - -## NVX Runtime Artifacts - -The following sizes were observed in the current Windows NVX build and may vary -slightly between revisions: - -| Artifact | Purpose | Observed size | -| --- | --- | ---: | -| `openvmm.exe` | Windows host process that creates and runs the NVX microVM through WHP. | 21.38 MiB | -| `vmlinux` | Linux kernel that runs inside the microVM. | 21.83 MiB | -| `initramfs.cpio.gz` | Compressed Alpine guest filesystem containing startup scripts, tools, and NVX utilities. | 7.12 MiB | -| `vmlinux.config` | Exact Linux kernel configuration used for the build. | 66.80 KiB | -| `initramfs.cpio.gz.packages.json` | Manifest of packages and source inputs included in the initramfs. | 22.97 KiB | - -The five artifacts total approximately **50.35 MiB**. A CPython/agent -initramfs would be an additional runtime artifact and should carry its own -version and package manifest. - -## Recommended Daemon API Semantics - -The exact transport belongs to NVX, but the API should express the following -operations without requiring MXC to know OpenVMM details: - -| Operation | Minimum result | -| --- | --- | -| `GetCapabilities` | Daemon version, protocol range, runtime version, guest-agent version, supported features, host readiness, and limitations | -| `Provision` | Opaque sandbox ID and effective immutable configuration | -| `Start` | Ready state, launch identity, guest identity, and network status | -| `Exec` | Exec ID plus handles or framed channels for stdin, stdout, and stderr | -| `WaitExec` | Exit code or timeout/cancel outcome, signal information, and duration | -| `CancelExec` | Typed acknowledgement followed by confirmed terminal state | -| `Stop` | Confirmed guest/OpenVMM shutdown or an explicit cleanup error | -| `Deprovision` | Confirmed removal of provisioned and disposable state | -| `GetStatus` | Current lifecycle state, active exec, failure information, and diagnostic correlation ID | -| `ShutdownDaemon` | Controlled service shutdown for upgrades and testing | - -Long-running operations should support cancellation and deadlines. Requests -should carry correlation IDs, and retries should be idempotent where the daemon -can prove the prior result. - -## Guest-Agent Requirements Behind the Daemon - -The daemon needs a production guest agent; OpenVMM lifecycle control alone is -not enough. - -The guest agent must provide: - -1. a launch-bound, versioned readiness handshake; -2. immutable session configuration; -3. repeated process creation and supervision; -4. separate stdin, stdout, and stderr; -5. bounded flow control and backpressure; -6. stdin EOF, cancellation, timeout, and exact terminal events; -7. fixed unprivileged workload identity; -8. private process and mount isolation; -9. read-only and read-write bind-mount enforcement; -10. structured network status; -11. health, quiesce, and graceful-shutdown operations; -12. cleanup of child processes after host-channel loss. - -The daemon should hide this guest protocol from MXC. MXC consumes daemon-level -operations and does not need to know whether the internal transport is -virtio-console, vsock, or another NVX mechanism. - -## MXC-Side Work After the Daemon Exists - -With a complete NVX daemon, the remaining MXC integration follows established -backend patterns: - -| MXC area | Required work | -| --- | --- | -| Schema and model | Add the distinct `nvx` containment value and its immutable provision settings. | -| State-aware dispatch | Register `nvx:` sandbox IDs and implement a `StatefulSandboxBackend` adapter over the daemon API. | -| One-shot execution | Compose daemon provision, start, exec, stop, and deprovision using the same adapter. | -| Policy validation | Define the per-phase fail-closed honor matrix and reject unsupported fields before contacting the daemon. | -| Stream adaptation | Convert daemon streams and terminal results into MXC's `SandboxProcess` and `ExecHandle` contracts. | -| Error mapping | Map daemon error kinds to MXC errors without string matching. | -| Capability probe | Surface daemon installation, host readiness, runtime compatibility, and supported features. | -| SDKs and FFI | Add `nvx` to TypeScript, C#, Rust, and C ABI surfaces where applicable. | -| Build and packaging | Discover or install the separately distributed NVX runtime and daemon. | -| Tests and CI | Add one-shot and state-aware lifecycle, policy, failure, stream, and cleanup coverage. | - - -## Summary - -The architectural change removes the largest host-side responsibility from -MXC: **the NVX daemon, not MXC, owns OpenVMM and the guest control plane**. - -The daemon should be viewed as the production successor to the one-workload -`nanvixd` model. It must retain the same fundamental ability to launch a -microVM and propagate workload results, while adding durable lifecycle, -repeated exec, structured streams, policy configuration, guest supervision, -snapshots, diagnostics, compatibility negotiation, and crash-safe cleanup. - -Once this daemon contract exists, the MXC work becomes a relatively conventional -backend adapter using patterns already established by WSLc, Windows Sandbox, -IsolationSession, and the existing `microvm` backend. - -## Appendix A: Daemon Requirements (P0) - -The first experimental MXC backend requires the daemon to provide the following -capabilities. More detailed acceptance requirements appear in Appendix B. - -| Area | P0 requirement | -| --- | --- | -| Runtime ownership | Own `openvmm.exe`, its control endpoints, handles, logs, exit status, and cleanup. MXC must never launch OpenVMM directly or parse its console output. | -| Lifecycle | Provide deterministic provision, start, repeated exec, stop, deprovision, status, cancellation, and timeout behavior over opaque sandbox and exec IDs. | -| Guest control | Use a bounded, versioned guest-agent protocol with an authenticated launch-bound readiness handshake, separate binary-safe process streams, backpressure, EOF, and exact terminal events. | -| Workload isolation | Run workloads as a fixed unprivileged guest identity and isolate their processes and mounts. Clean up all descendants after completion or control-channel loss. | -| Filesystem | Validate immutable provision-time mappings beneath one canonical host root and enforce each declared mapping as read-only or read-write without traversal or symlink escape. | -| Network and proxy | Support no-NIC isolation and NVX portable networking, report structured readiness, and support cooperative URL-form HTTP/HTTPS proxy injection without claiming raw-socket enforcement. | -| Resources and compatibility | Validate memory and supported processor settings and reject incompatible daemon, OpenVMM, VM ABI, kernel, initramfs, guest-agent, or protocol combinations before launch. | -| Snapshots | Capture only an idle, guest-quiesced VM; bind the snapshot to the complete runtime and attachment configuration; revalidate it before restore; and require a fresh guest and network readiness handshake after restore. An untrusted or incompatible optimization snapshot must be invalidated and replaced by a cold boot. | -| Security | Restrict daemon access to authorized local clients, validate every caller-controlled value, use bounded framing and buffering, verify runtime artifacts, avoid shell command construction and unnecessary inherited handles, and run with the minimum required privileges. | -| Recovery and diagnostics | Use exact process identity and ownership evidence for orphan recovery, guarantee bounded cleanup across client, guest, OpenVMM, and daemon failures, never report success when cleanup is uncertain, and keep trusted diagnostics separate from workload output. | - -## Appendix B: Required NVX Daemon Capabilities - -The following capabilities are required for the first experimental MXC backend. - -| Daemon capability | Requirement | -| --- | --- | -| Versioned API | Expose a stable, machine-readable API independent of OpenVMM's command-line interface. Negotiate protocol and runtime versions and reject incompatible clients, guest images, or OpenVMM builds. | -| Lifecycle operations | Provide `provision`, `start`, `exec`, `stop`, and `deprovision`, or equivalent operations with the same semantics. Provision stores immutable configuration without booting; start launches and readies the VM; exec is repeatable; stop preserves provisioned state; deprovision deletes it. | -| Stable sandbox identity | Mint an opaque sandbox ID and reject unknown, stale, malformed, or already-deprovisioned IDs with typed errors. | -| OpenVMM ownership | Be the sole owner of the OpenVMM child process, command line, control endpoints, handles, logs, exit status, and forced termination. Do not require MXC to parse OpenVMM output. | -| Explicit VM state machine | Track states such as provisioned, starting, ready, executing, stopping, stopped, failed, and deprovisioned. Reject invalid or concurrent transitions deterministically. | -| Guest readiness | Complete a versioned handshake with the guest agent and report readiness only after the agent, configured filesystem, and requested network are usable. Boot-marker text is not sufficient. | -| Repeated exec | Run multiple sequential commands in one warm VM without rebooting. The first milestone may allow only one active exec at a time, but it must support repeated exec calls. | -| Process configuration | Accept an argument vector, working directory, environment, timeout, mapping selection, and stdin mode without kernel-command-line quoting or whitespace restrictions. | -| Fixed workload identity | Launch workloads as a fixed unprivileged guest account. The daemon must not report readiness if the guest cannot enforce that identity. | -| Separate process streams | Return distinct binary-safe stdin, stdout, and stderr streams. Support stdin EOF, bounded buffering, backpressure, large output, and deterministic stream closure. | -| Wait and cancellation | Provide wait, graceful cancellation, forced termination, timeout, and exit metadata. A timeout is reported only after the workload is confirmed gone. | -| Guest-control channel | Own a framed, bounded, versioned connection to the guest agent. The channel must support request IDs, stream IDs, cancellation, flow control, terminal events, health, and shutdown. | -| Diagnostic separation | Keep kernel/OpenVMM/daemon diagnostics separate from workload stdout and stderr. Kernel panic or boot output must not corrupt guest-agent protocol traffic. | -| Filesystem configuration | Accept provision-time host mappings, validate them, attach the common virtio-fs root, and configure the guest agent to expose only declared children as read-only or read-write. | -| Filesystem immutability | Reject attempts to change filesystem mappings after provision. Reject path traversal, symlink escape, conflicting overlaps, and mappings outside the configured common root. | -| Network modes | Support an isolated mode with no NIC and an enabled mode using NVX portable networking. Report structured network readiness or a typed setup failure. | -| Exec-time proxy | Accept a URL-form HTTP/HTTPS proxy for an exec and arrange cooperative environment injection. Do not claim that this provides raw-socket enforcement. | -| Resource configuration | Accept immutable provision-time memory and processor settings, validate supported processor counts, and communicate the effective values to the caller. | -| Graceful VM shutdown | Request guest shutdown, wait for acknowledgement and process exit, then force OpenVMM termination after a bounded deadline. | -| Cleanup guarantees | Remove disposable state and close handles after normal completion, timeout, cancellation, client disconnect, guest crash, OpenVMM crash, or daemon shutdown. Never return success when cleanup status is uncertain. | -| Orphan recovery | On daemon restart, identify prior OpenVMM processes using exact process identity and ownership evidence. Reclaim only positively identified instances; never terminate a process based on PID alone. | -| Structured errors | Return stable error kinds for unavailable runtime, incompatible version, capacity, invalid transition, not provisioned, not started, busy, timeout, guest failure, OpenVMM failure, protocol failure, and uncertain cleanup. | -| Capability reporting | Report supported lifecycle operations, stream modes, network modes, filesystem restrictions, snapshot support, processor values, and protocol versions so MXC can probe the backend accurately. | -| Runtime integrity | Validate OpenVMM, kernel, initramfs, guest-agent, manifest, checksums, architecture, and compatibility before VM launch. | +MXC owns the OpenVMM process and the host side of the versioned guest-agent +control channel; no additional host service sits between them. In the current +prototype, an NVX-side policy harness stands in for the future MXC adapter and +proves this boundary end to end. + +## MXC policies that worked + +| MXC policy or control | Validated behavior | +|---|---| +| `filesystem.readonlyPaths` | Host paths were exposed to the guest as read-only mappings. Guest writes were denied. | +| `filesystem.readwritePaths` | Host paths were exposed as writable mappings and guest writes were reflected on the host. | +| Mapping validation | Duplicate, overlapping, escaping, symlink, reparse-point, and containment-invalid mappings were rejected before launch. | +| Undeclared-path isolation | The guest could not access undeclared host paths or bypass policy through raw virtio-fs exports. | +| `network.defaultPolicy: allow` | The guest received the supported unrestricted network posture. | +| `network.defaultPolicy: block` | Guest network access was blocked. | +| `process.commandLine` | The command was lowered exactly to `["/bin/sh", "-c", commandLine]`. | +| `process.cwd` | The requested guest working directory was preserved. | +| `process.env` | Non-reserved environment variables were preserved exactly. | +| `process.timeout` | The timeout flowed from real MXC JSON into the OpenVMM process execution plan. | +| Exec-time `runtimeConfig.networkProxy` | The sole supported proxy form was normalized and injected as controlled proxy environment variables during exec. | +| Version, containment, phase, and IDs | Exact schema version, temporary `containment: "vm"`, lifecycle phase, `sandboxId`, and `containerId` constraints were enforced. | + +## Accepted inert metadata + +| Field | Behavior | +|---|---| +| `$schema` | Accepted for schema tooling; it does not change the launch plan. | +| `_comment` | Accepted as descriptive metadata; it does not change runtime behavior. | +| Omitted optional parent sections | Accepted; their absence does not create unsupported policy or change the supported defaults. | + +## MXC policies that did not work + +| MXC policy or control | What the MXC control does | NVX gap | +|---|---|---| +| `filesystem.deniedPaths` | Explicitly hides or denies access to selected filesystem paths. | NVX has no independently enforced deny mapping primitive. | +| `network.allowedHosts` and `network.blockedHosts` | Allows or blocks network traffic by destination host. | Per-host filtering is not implemented. | +| `network.egress` and `network.ingress` | Controls outbound and inbound network traffic independently. | Directional network policy is not implemented. | +| Host-loopback and unsupported enforcement controls | Controls guest access to services on the host and selects how network restrictions are enforced. | The available network modes cannot truthfully enforce these controls. | +| Legacy `network.proxy`, including `network.proxy.url` | Routes sandbox network traffic through a configured proxy. | Only exec-time `runtimeConfig.networkProxy` is supported. | +| Caller-provided proxy environment variables | Lets process environment variables influence proxy routing. | `HTTP_PROXY`, `HTTPS_PROXY`, lowercase variants, `NO_PROXY`, and mixed-case equivalents are reserved and rejected to prevent policy bypass or ambiguity. | +| `ui` | Limits access to desktop, clipboard, window, and other UI capabilities. | NVX does not currently enforce the MXC UI policy. | +| `telemetry` | Controls diagnostic and usage-data collection. | No NVX mapping is defined for this policy section. | +| `lifecycle` policy | Controls sandbox persistence and cleanup behavior. | The tested lifecycle contract uses explicit state-aware phases rather than these policy knobs. | +| `fallback` | Permits selection of another containment backend if the requested backend cannot run. | NVX does not silently fall back to a weaker containment backend. | +| Foreign backend sections | Configure another containment implementation, such as ProcessContainer, LXC, or Seatbelt. | These backend-specific and experimental sections do not apply to NVX. | +| Policy mutation in the wrong phase | Restricts policy fields to the lifecycle phase where they can be safely applied. | Provision-only and exec-only fields are rejected outside their valid lifecycle phase. | +| Wrong version, non-VM containment, or invalid lifecycle IDs | Selects the contract and backend and identifies the state-aware sandbox instance. | Invalid control-plane inputs are rejected before effects. | + +## Remaining MXC integration work + +| Workstream | Estimate | +|---|---:| +| Add `nvx` containment, schema, wire, and policy/model types | 2-3 engineer-days | +| Implement state-aware and one-shot engine dispatch that owns OpenVMM | 7-10 engineer-days | +| Adapt guest streams, terminal outcomes, cancellation, and typed errors | 3-5 engineer-days | +| Expose NVX through applicable Rust, TypeScript, C#, and FFI surfaces | 4-6 engineer-days | +| Add runtime packaging/discovery and capability probing | 3-5 engineer-days | +| Add MXC-native E2E automation and CI coverage | 5-8 engineer-days | +| **Total** | **24-37 engineer-days** | + +### E2E testing plan + +- Verify one-shot launch, command execution, output, exit status, and teardown. +- Verify provision, start, repeated exec, stop, and deprovision, including + invalid lifecycle IDs and phase transitions. +- Run positive and negative checks for every supported filesystem, network, and + process policy. +- Prove every unsupported policy is rejected before VM or host effects. +- Exercise timeout, cancellation, guest/OpenVMM failure, reconnect, stream, and + cleanup behavior. +- Run applicable Rust, TypeScript, C#, and FFI paths on Windows x64 and ARM64 CI + hosts. \ No newline at end of file From 1382462391a1e6e190e7ecf3d57ac091816953a4 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 13:59:39 -0700 Subject: [PATCH 05/41] docs: update NVX backend gaps Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 187 ++++++++++++++++++++++++++++++++------- 1 file changed, 154 insertions(+), 33 deletions(-) diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index 43539d8a7..2b839a957 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -1,11 +1,10 @@ -# NVX Backend: MXC policy testing with a prototype +# NVX Backend: MXC policy compatibility and remaining gaps ## Status and scope -The NVX prototype accepted a real MXC `0.9.0-dev` JSON, validated it against the -schema, and adapted supported fields into typed NVX launch plans. -The prototype then exercised those plans on Windows through OpenVMM -and WHP. +The original NVX prototype accepted a real MXC `0.9.0-dev` JSON, validated it +against the schema, and adapted supported fields into typed NVX launch plans. +The prototype first exercised those plans on Windows through OpenVMM and WHP. ## Current architecture @@ -18,65 +17,126 @@ flowchart LR ``` MXC owns the OpenVMM process and the host side of the versioned guest-agent -control channel; no additional host service sits between them. In the current -prototype, an NVX-side policy harness stands in for the future MXC adapter and -proves this boundary end to end. +control channel; no additional host service sits between them. The NVX policy +harness stands in for the future MXC adapter and proves this boundary end to +end. -## MXC policies that worked +## Implemented and validated capabilities | MXC policy or control | Validated behavior | -|---|---| +| --- | --- | | `filesystem.readonlyPaths` | Host paths were exposed to the guest as read-only mappings. Guest writes were denied. | | `filesystem.readwritePaths` | Host paths were exposed as writable mappings and guest writes were reflected on the host. | +| `filesystem.deniedPaths` | Denied subtrees are hidden by the host filesystem provider, including direct, parent-relative, symlink/junction alias, and second-mount access. Unsafe policies are rejected before boot. | | Mapping validation | Duplicate, overlapping, escaping, symlink, reparse-point, and containment-invalid mappings were rejected before launch. | | Undeclared-path isolation | The guest could not access undeclared host paths or bypass policy through raw virtio-fs exports. | -| `network.defaultPolicy: allow` | The guest received the supported unrestricted network posture. | -| `network.defaultPolicy: block` | Guest network access was blocked. | | `process.commandLine` | The command was lowered exactly to `["/bin/sh", "-c", commandLine]`. | | `process.cwd` | The requested guest working directory was preserved. | | `process.env` | Non-reserved environment variables were preserved exactly. | | `process.timeout` | The timeout flowed from real MXC JSON into the OpenVMM process execution plan. | | Exec-time `runtimeConfig.networkProxy` | The sole supported proxy form was normalized and injected as controlled proxy environment variables during exec. | +| `network.egress.default` | Both `allow` and `deny` are enforced by the portable network profile. | +| `network.ingress.default: deny` | New inbound connections are denied while replies to guest-initiated traffic remain available. Unsupported unrestricted ingress is rejected before launch. | +| `network.egress.allow` and `network.egress.deny` | IPv4/CIDR TCP and UDP destination rules are enforced, with deny precedence and default-deny behavior. | +| `network.ingress.hostLoopback: deny` | General guest-to-host loopback and host-to-guest forwards are denied while the exact TCP runtime-proxy endpoint remains reachable. UDP on the proxy port remains denied. | +| Explicit host-to-guest forwarding | Selected TCP or UDP localhost ports can be deliberately published with NVX-specific forward options. | +| Fixed workload identity | Managed workloads run as the selected fixed non-root UID/GID with capabilities removed and `no_new_privs`. | +| State-aware lifecycle | Provision, start, repeated exec, stop, and deprovision preserve warm guest state and reject invalid transitions. | +| Bounded outcomes | Execution and VM-level reports expose bounded result categories, numeric status, operation IDs, and teardown outcomes without including command arguments, environment values, or credentials. | | Version, containment, phase, and IDs | Exact schema version, temporary `containment: "vm"`, lifecycle phase, `sandboxId`, and `containerId` constraints were enforced. | +## Remaining NVX policy gaps + +| MXC policy or control | Current NVX behavior | Remaining decision or implementation | +| --- | --- | --- | +| `network.ingress.hostLoopback: allow` | The portable socket-NAT profile requires explicit per-port forwarding. Generic `allow` without forwards is rejected before VM resources are opened. | Implement true bidirectional host-loopback connectivity without an NVX-specific port list, or retain the fail-closed rejection as a documented backend limitation. | +| Full schema 0.9 egress-rule vocabulary | NVX supports IPv4/CIDR rules with one TCP or UDP destination port. | Decide whether to implement IPv6, CIDR `except`, port ranges, ICMP, protocol `any`, and omitted destination/port selectors. Every unsupported form must be rejected before launch. | +| `network.ingress.default: allow` | NVX implements fixed ingress denial and rejects unrestricted ingress. | Implement unrestricted inbound support only if it is required for the NVX backend; otherwise preserve and document the rejection. | +| Caller-provided proxy environment variables | NVX rejects `HTTP_PROXY`, `HTTPS_PROXY`, lowercase variants, `NO_PROXY`, and mixed-case equivalents so `runtimeConfig.networkProxy` remains authoritative. | Decide whether this remains a permanent proxy-hygiene invariant or whether caller-provided values should be supported with explicit precedence and enforcement semantics. | +| Detailed telemetry | NVX returns bounded execution and teardown outcomes. | Decide whether MXC needs additional structured lifecycle, startup, performance, and failure facts from NVX. MXC remains responsible for consent, administrative policy, per-request gating, and event emission. | + +## Intentionally unsupported policy + +These fields are not missing NVX mechanisms. The MXC adapter should reject +them for this backend: + +| Policy | Required behavior | +| --- | --- | +| `lifecycle` on the one-shot surface | One-shot execution destroys the VM. Persistence and reuse belong to the explicit state-aware lifecycle. Requests for retained one-shot state must be rejected. | +| Policy mutation in the wrong phase | Provision-only and exec-only fields are rejected outside their valid lifecycle phase. | +| Wrong version, non-VM containment, or invalid lifecycle IDs | Invalid control-plane inputs are rejected before effects. | +| `ui` | A Linux microVM has no equivalent MXC desktop-policy surface. | +| `fallback` | NVX does not silently select a weaker containment backend. | + ## Accepted inert metadata | Field | Behavior | -|---|---| +| --- | --- | | `$schema` | Accepted for schema tooling; it does not change the launch plan. | | `_comment` | Accepted as descriptive metadata; it does not change runtime behavior. | | Omitted optional parent sections | Accepted; their absence does not create unsupported policy or change the supported defaults. | -## MXC policies that did not work - -| MXC policy or control | What the MXC control does | NVX gap | -|---|---|---| -| `filesystem.deniedPaths` | Explicitly hides or denies access to selected filesystem paths. | NVX has no independently enforced deny mapping primitive. | -| `network.allowedHosts` and `network.blockedHosts` | Allows or blocks network traffic by destination host. | Per-host filtering is not implemented. | -| `network.egress` and `network.ingress` | Controls outbound and inbound network traffic independently. | Directional network policy is not implemented. | -| Host-loopback and unsupported enforcement controls | Controls guest access to services on the host and selects how network restrictions are enforced. | The available network modes cannot truthfully enforce these controls. | -| Legacy `network.proxy`, including `network.proxy.url` | Routes sandbox network traffic through a configured proxy. | Only exec-time `runtimeConfig.networkProxy` is supported. | -| Caller-provided proxy environment variables | Lets process environment variables influence proxy routing. | `HTTP_PROXY`, `HTTPS_PROXY`, lowercase variants, `NO_PROXY`, and mixed-case equivalents are reserved and rejected to prevent policy bypass or ambiguity. | -| `ui` | Limits access to desktop, clipboard, window, and other UI capabilities. | NVX does not currently enforce the MXC UI policy. | -| `telemetry` | Controls diagnostic and usage-data collection. | No NVX mapping is defined for this policy section. | -| `lifecycle` policy | Controls sandbox persistence and cleanup behavior. | The tested lifecycle contract uses explicit state-aware phases rather than these policy knobs. | -| `fallback` | Permits selection of another containment backend if the requested backend cannot run. | NVX does not silently fall back to a weaker containment backend. | -| Foreign backend sections | Configure another containment implementation, such as ProcessContainer, LXC, or Seatbelt. | These backend-specific and experimental sections do not apply to NVX. | -| Policy mutation in the wrong phase | Restricts policy fields to the lifecycle phase where they can be safely applied. | Provision-only and exec-only fields are rejected outside their valid lifecycle phase. | -| Wrong version, non-VM containment, or invalid lifecycle IDs | Selects the contract and backend and identifies the state-aware sandbox instance. | Invalid control-plane inputs are rejected before effects. | +## Binary acquisition and packaging + +NVX replaces the existing NanVix microVM runtime and its `--with-microvm` +packaging path. A new `--with-nvx` build option should reuse the existing +NanVix artifact-acquisition implementation rather than introduce a second +download system: + +- pin an exact `microsoft/nvx` release tag, platform asset name, and SHA-256 + checksums in the repository; +- download and verify the release during the build, never when a sandbox is + launched; +- cache verified artifacts under Cargo's build output and avoid downloading + them again when the pinned files are already present; +- support `NVX_BIN` as an offline override containing a pre-fetched and + checksum-verifiable NVX bundle; +- stage `openvmm`, the guest kernel, and the guest initramfs beside the MXC + executor and copy them into the matching Node and .NET SDK runtime package; +- fail the NVX-enabled build if an expected artifact is absent, corrupt, or + does not match the selected platform. + +The existing `nanvix_binaries` and `nanvix_build_common` mechanisms should be +renamed and adapted for NVX, preserving their pinned-version manifest, +checksum verification, offline-prefetch, cache, and SDK-staging behavior. +The obsolete NanVix release definitions and binaries should be removed rather +than retained alongside NVX. + +`--with-nvx` selects the WHP bundle on Windows and the KVM bundle by default on +Linux. Linux MSHV packaging requires an additional explicit build selection; +the build must not infer the desired release bundle from the capabilities of +the machine performing the build. + +The current `microsoft/nvx` prerelease +`v0.1.0-dev.142587bbceae` has the following package sizes: + +| Release bundle | Compressed download | Expanded runtime | +| --- | ---: | ---: | +| Windows WHP | 22.73 MiB | 51.41 MiB | +| Linux KVM | 130.95 MiB | 486.69 MiB | +| Linux MSHV | 125.35 MiB | 489.11 MiB | + +The expanded Windows runtime consists primarily of `openvmm.exe` (22.05 MiB), +`vmlinux` (21.84 MiB), and `initramfs.cpio.gz` (7.42 MiB). The Linux bundles +use the same kernel and initramfs, but their OpenVMM executables are currently +approximately 457-460 MiB before archive compression. ## Remaining MXC integration work +The following work belongs primarily in the MXC repository. It is separate +from the remaining NVX runtime policy gaps above. + | Workstream | Estimate | -|---|---:| +| --- | ---: | | Add `nvx` containment, schema, wire, and policy/model types | 2-3 engineer-days | | Implement state-aware and one-shot engine dispatch that owns OpenVMM | 7-10 engineer-days | | Adapt guest streams, terminal outcomes, cancellation, and typed errors | 3-5 engineer-days | | Expose NVX through applicable Rust, TypeScript, C#, and FFI surfaces | 4-6 engineer-days | -| Add runtime packaging/discovery and capability probing | 3-5 engineer-days | +| Replace the NanVix artifact pipeline with pinned NVX acquisition, packaging, discovery, and capability probing | 3-5 engineer-days | | Add MXC-native E2E automation and CI coverage | 5-8 engineer-days | | **Total** | **24-37 engineer-days** | + ### E2E testing plan - Verify one-shot launch, command execution, output, exit status, and teardown. @@ -88,4 +148,65 @@ proves this boundary end to end. - Exercise timeout, cancellation, guest/OpenVMM failure, reconnect, stream, and cleanup behavior. - Run applicable Rust, TypeScript, C#, and FFI paths on Windows x64 and ARM64 CI - hosts. \ No newline at end of file + hosts. + +## Appendix A: Current OpenVMM and NVX baseline + +This is the current direct OpenVMM/NVX capability baseline after the merged +policy work. An MXC backend adapter still needs to select these controls from +the exact schema 0.9 request and expose their outcomes through MXC APIs. + +| Area | Supported today | Limitation for MXC | +| --- | --- | --- | +| Filesystem | Virtio-fs mappings support `ro`/`rw`, safe common-root planning, undeclared-path isolation, and explicit denied subtrees. | Policies that cannot be represented safely are rejected before boot. | +| Sandbox filesystem | The sandbox workload uses read-only EROFS layers, a writable ext4 scratch layer, private mount/PID/UTS namespaces, a private `/dev`, a fixed unprivileged identity, no Linux capabilities, and `no_new_privs`. | The MXC adapter must package and select the appropriate image and working layers. | +| Lifecycle | The host and guest protocols support provision, start, repeated exec, stop, deprovision, reconnect, operation cancellation, and bounded outcomes. | MXC still needs to register and dispatch NVX on its one-shot and state-aware surfaces. | +| Networking | The portable network profile works across WHP, KVM, and MSHV and enforces egress defaults, IPv4/CIDR TCP/UDP rules, deny ingress, host-loopback denial, exact proxy reachability, and explicit host-to-guest forwards. | Generic host-loopback allow, unrestricted ingress, and the broader schema 0.9 rule vocabulary remain unsupported. | +| HTTP/HTTPS proxy | An exec-time `runtimeConfig.networkProxy` endpoint is injected as controlled proxy environment variables and allowed as one exact TCP endpoint under host-loopback denial. | Caller-provided proxy variables remain rejected; UDP to the same endpoint is not allowed. | +| Outcomes and telemetry | Execution, lifecycle, and teardown return bounded status and failure categories without workload secrets. | Richer MXC telemetry facts are optional future work; MXC owns telemetry policy and emission. | + +See the NVX [run guide](https://github.com/microsoft/nvx/blob/main/doc/run.md) +and [command-line reference](https://github.com/microsoft/nvx/blob/main/doc/usage.md) +for the current direct runtime options. + +## Appendix B: NVX issue tracking + +The historical issues captured the original prototype gaps. Repository +migration means some issue links may no longer resolve; the merged PRs above +are the durable implementation references. + +| Historical issue | Current outcome | +| --- | --- | +| #37 - read-only/read-write mappings and mount isolation | Closed after prototype validation. | +| #38 - directional ingress and egress defaults | Implemented by microsoft/nvx#51 and nanvix/openvmm#71. NVX supports both egress defaults and a deny ingress posture; unrestricted ingress remains rejected. | +| #39 - fixed unprivileged workload identity | Implemented by microsoft/nvx#58 and nanvix/openvmm#75. | +| #40 - sandbox lifecycle | Implemented by microsoft/nvx#58 and nanvix/openvmm#75. | +| #41 - L3/L4 egress-rule filtering | Implemented for IPv4/CIDR TCP and UDP by microsoft/nvx#58 and nanvix/openvmm#75. The broader schema 0.9 vocabulary remains a compatibility decision. | +| #42 - host-loopback network policy | Partially resolved by microsoft/nvx#61 and nanvix/openvmm#76. Deny is enforced, the exact TCP runtime proxy is preserved, UDP leakage is closed, and explicit forwards remain available. Generic `hostLoopback: allow` without forwards remains unsupported. | +| #43 - denied filesystem paths | Implemented by microsoft/nvx#58 and nanvix/openvmm#75. | +| #44 - exec-time proxy configuration | Closed after prototype validation; subsequent work preserved an exact TCP-only proxy exception under host-loopback denial. | +| #45 - fail-closed backend selection and policy admission | Implemented across the policy and lifecycle changes; unsupported policy is rejected rather than weakened. | +| #46 - typed execution/lifecycle outcomes | Implemented as bounded execution, operation, VM-exit, and teardown reports. | +| #47 - detailed telemetry | Bounded outcomes are implemented. Richer telemetry facts remain optional follow-up work rather than a schema 0.9 blocker. | + +## Appendix C: GitHub Copilot CLI schema migration + +The current GitHub Copilot CLI builds MXC policy version `0.7.0-alpha` and +emits the legacy `allowOutbound`, `allowLocalNetwork`, and `network.proxy` +fields. It no longer provides a raw `sandbox.config` passthrough. + +Before the CLI can use the NVX backend, it must migrate its generated policy +to schema 0.8+: + +- map outbound allow/block to `network.egress.default`; +- map local-network intent to `network.ingress.default` and + `network.ingress.hostLoopback`; +- express direct egress exceptions through `network.egress.allow` and + `network.egress.deny`; +- move the supported loopback HTTP/HTTPS proxy endpoint to + `runtimeConfig.networkProxy`; +- update its sandbox settings, policy checks, telemetry, UI, and tests to use + the new directional model. + +This migration belongs to the GitHub Copilot CLI and is a prerequisite for NVX +integration; NVX does not need to implement the legacy network contract. From 23473a9bbce4e64bd297c30e45a44a51853c9635 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 14:22:17 -0700 Subject: [PATCH 06/41] Document NVX schema changes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 59 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index 2b839a957..f2f0b0057 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -76,6 +76,65 @@ them for this backend: | `_comment` | Accepted as descriptive metadata; it does not change runtime behavior. | | Omitted optional parent sections | Accepted; their absence does not create unsupported policy or change the supported defaults. | +## Schema changes + +The only required new public identifier is `containment: "nvx"`. The +`experimental.nvx.provision` section is necessary only if images remain caller-configurable. + +Example state-aware provision request: + +```json +{ + "$schema": "https://aka.ms/mxc/schemas/0.9.0-alpha.json", + "version": "0.9.0-alpha", + "phase": "provision", + "containment": "nvx", + "filesystem": { + "readonlyPaths": [ + "C:\\workspace\\source" + ], + "readwritePaths": [ + "C:\\workspace\\output" + ], + "deniedPaths": [] + }, + "network": { + "egress": { + "default": "deny" + }, + "ingress": { + "default": "deny", + "hostLoopback": "deny" + } + }, + "experimental": { + "nvx": { + "provision": { + "layers": [ + { + "role": "distro", + "path": "C:\\nvx\\images\\distro.erofs", + "uuid": "11111111-1111-1111-1111-111111111111" + }, + { + "role": "runtime", + "path": "C:\\nvx\\images\\runtime.erofs", + "uuid": "22222222-2222-2222-2222-222222222222" + } + ], + "scratchPath": "C:\\nvx\\images\\scratch.ext4" + } + } + } +} +``` + +- `distro` is the read-only base operating-system and userspace layer. +- `runtime` is an optional read-only layer containing the workload runtime and + supporting files. +- `scratchPath` is the writable ext4 image used for changes made while the + sandbox is running. + ## Binary acquisition and packaging NVX replaces the existing NanVix microVM runtime and its `--with-microvm` From e755a2f7eb614e39b96d2f4e56bcb8fda189480f Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 15:09:17 -0700 Subject: [PATCH 07/41] feat: register NVX containment Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../mxc_config_contract/src/dev/one_shot.rs | 2 ++ .../tests/v0_9_0_alpha/enums.rs | 24 +++++++++++++++++-- .../tests/version_boundaries/containment.rs | 1 + .../config_contract_adapters/dev/one_shot.rs | 1 + src/core/wxc_common/src/config_parser.rs | 1 + src/core/wxc_common/src/models.rs | 2 ++ src/core/wxc_common/src/wire.rs | 2 ++ 7 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/core/mxc_config_contract/src/dev/one_shot.rs b/src/core/mxc_config_contract/src/dev/one_shot.rs index 593b54d41..070af5637 100644 --- a/src/core/mxc_config_contract/src/dev/one_shot.rs +++ b/src/core/mxc_config_contract/src/dev/one_shot.rs @@ -33,6 +33,8 @@ string_enum! { WindowsSandbox => ["windows_sandbox"], /// Select the NanVix micro-VM backend. Microvm => ["microvm"], + /// Select the NVX micro-VM backend. + Nvx => ["nvx"], /// Select the Hyperlight micro-VM backend. Hyperlight => ["hyperlight"], /// Select the WSL container backend. diff --git a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs index b4865fa16..73f89fb93 100644 --- a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs +++ b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::common::{assert_invalid, assert_valid}; +use mxc_config_contract::dev::{OneShotContainment, OneShotRequest, Version}; // Enum value tests #[test] @@ -15,6 +16,7 @@ fn accepts_every_containment_value() { "vm", "windows_sandbox", "microvm", + "nvx", "hyperlight", "isolation_session", "wslc", @@ -202,8 +204,6 @@ fn rejects_invalid_capture_denials_mode_value() { ); } -use mxc_config_contract::dev::{OneShotContainment, OneShotRequest}; - #[test] fn appcontainer_containment_value_alias_maps_to_process_container() { let json = r#"{ @@ -239,3 +239,23 @@ fn macos_sandbox_containment_value_alias_maps_to_seatbelt() { Some(OneShotContainment::Seatbelt) )); } + +#[test] +fn nvx_is_an_exact_v09_containment() { + let json = r#"{ + "version": "0.9.0-alpha", + "containment": "nvx", + "process": { + "commandLine": "/bin/true" + } + }"#; + + let request: OneShotRequest = serde_json::from_str(json).unwrap(); + + assert!(matches!(request.version, Version::V0_9_0Alpha)); + assert!(matches!( + request.containment.as_ref(), + Some(OneShotContainment::Nvx) + )); + assert_eq!(request.process.command_line.as_str(), "/bin/true"); +} diff --git a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs index f27dc3dc5..4846c0baf 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs @@ -9,6 +9,7 @@ fn development_containment_values_are_introduced_in_v09() { "vm", "windows_sandbox", "microvm", + "nvx", "hyperlight", "isolation_session", "wslc", diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs index 2b8395231..34eb32a25 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs @@ -17,6 +17,7 @@ fn convert_containment(value: contract::OneShotContainment) -> wire::Containment contract::OneShotContainment::Vm => wire::Containment::Vm, contract::OneShotContainment::WindowsSandbox => wire::Containment::WindowsSandbox, contract::OneShotContainment::Microvm => wire::Containment::Microvm, + contract::OneShotContainment::Nvx => wire::Containment::Nvx, contract::OneShotContainment::Hyperlight => wire::Containment::Hyperlight, contract::OneShotContainment::Wslc => wire::Containment::Wslc, contract::OneShotContainment::IsolationSession => wire::Containment::IsolationSession, diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index ea07e366e..217108805 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -818,6 +818,7 @@ fn requested_sandbox_kind(c: Option<&wire::Containment>) -> &'static str { Some(wire::Containment::WindowsSandbox) => "windows_sandbox", Some(wire::Containment::Lxc) => "lxc", Some(wire::Containment::Microvm) => "microvm", + Some(wire::Containment::Nvx) => "nvx", Some(wire::Containment::Hyperlight) => "hyperlight", Some(wire::Containment::Wslc) => "wslc", Some(wire::Containment::Seatbelt) => "seatbelt", diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index e786c1504..621e00f4a 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -125,6 +125,8 @@ impl From for ContainmentBackend { W::WindowsSandbox => Self::WindowsSandbox, W::Lxc => Self::Lxc, W::Microvm => Self::MicroVm, + // NVX still shares the existing micro-VM runtime path for now. + W::Nvx => Self::MicroVm, W::Hyperlight => Self::Hyperlight, W::Wslc => Self::Wslc, W::Seatbelt => Self::Seatbelt, diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index ea0ffa789..f09efae79 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -141,6 +141,8 @@ pub enum Containment { Lxc, /// NanVix micro-VM (experimental). Microvm, + /// NVX micro-VM (experimental). + Nvx, /// Hyperlight micro-VM (experimental). Hyperlight, /// WSL container (experimental). From 619d4c83f2f925c9cc0b55a9388290f68ca205d4 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 15:17:46 -0700 Subject: [PATCH 08/41] feat: map NVX requests into the domain model Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/core/wxc_common/src/cmdline.rs | 3 +++ src/core/wxc_common/src/config_parser.rs | 9 +++++++++ src/core/wxc_common/src/models.rs | 7 +++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/core/wxc_common/src/cmdline.rs b/src/core/wxc_common/src/cmdline.rs index 4eeb79d76..c79528fea 100644 --- a/src/core/wxc_common/src/cmdline.rs +++ b/src/core/wxc_common/src/cmdline.rs @@ -47,6 +47,9 @@ impl CommandLineContext { | ContainmentBackend::Vm | ContainmentBackend::MicroVm | ContainmentBackend::Hyperlight => Self::WindowsCreateProcess, + ContainmentBackend::Nvx => { + unreachable!("NVX command-line context is not implemented yet") + } } } } diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index 217108805..6219a9add 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -4891,6 +4891,15 @@ mod tests { assert_eq!(req.containment, ContainmentBackend::MicroVm); } + #[test] + fn containment_nvx_accepted() { + let json = r#"{"version":"0.9.0-alpha","process":{"commandLine":"/bin/true"},"containment":"nvx"}"#; + let mut logger = test_logger(); + + let req = load_request_from_json(json, &mut logger).unwrap(); + assert_eq!(req.containment, ContainmentBackend::Nvx); + } + #[test] fn unknown_top_level_field_rejected() { let json = r#"{"process": {"commandLine": "echo hi"}, "bogusField": true}"#; diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index 621e00f4a..9f884ea18 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -28,6 +28,8 @@ pub enum ContainmentBackend { /// MicroVM isolation via Windows Hypervisor Platform (internally powered by NanVix). #[serde(rename = "microvm")] MicroVm, + /// NVX Linux micro-VM hosted by OpenVMM. + Nvx, /// MicroVM isolation via Hyperlight + Unikraft, using an embedded /// warmed-up CPython snapshot. ~100 ms cold start per invocation, /// hermetic via snapshot restore. Experimental — requires @@ -59,6 +61,7 @@ impl ContainmentBackend { ContainmentBackend::Lxc => "lxc", ContainmentBackend::Vm => "vm", ContainmentBackend::MicroVm => "microvm", + ContainmentBackend::Nvx => "nvx", ContainmentBackend::Hyperlight => "hyperlight", ContainmentBackend::WindowsSandbox => "windows_sandbox", ContainmentBackend::IsolationSession => "isolation_session", @@ -81,6 +84,7 @@ impl ContainmentBackend { ContainmentBackend::Bubblewrap | ContainmentBackend::Hyperlight | ContainmentBackend::MicroVm + | ContainmentBackend::Nvx | ContainmentBackend::Vm => None, } } @@ -125,8 +129,7 @@ impl From for ContainmentBackend { W::WindowsSandbox => Self::WindowsSandbox, W::Lxc => Self::Lxc, W::Microvm => Self::MicroVm, - // NVX still shares the existing micro-VM runtime path for now. - W::Nvx => Self::MicroVm, + W::Nvx => Self::Nvx, W::Hyperlight => Self::Hyperlight, W::Wslc => Self::Wslc, W::Seatbelt => Self::Seatbelt, From 2d33304bbd427a6e611a8ade49c6e6b46d317cfd Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 15:23:05 -0700 Subject: [PATCH 09/41] fix: reject unavailable NVX execution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/core/mxc_engine/src/run.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index 7a43c4f94..a55c7682a 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -243,6 +243,9 @@ fn resolve_runner_inner_windows( ContainmentBackend::Vm => Err(MxcError::unsupported_containment( "VM backend not yet implemented", )), + ContainmentBackend::Nvx => Err(MxcError::unsupported_containment( + "NVX backend not yet available", + )), ContainmentBackend::MicroVm => { if !request.experimental_enabled { return Err(MxcError::malformed_request( @@ -341,6 +344,9 @@ fn resolve_runner_inner( )) } } + ContainmentBackend::Nvx => Err(MxcError::unsupported_containment( + "NVX backend not yet available", + )), ContainmentBackend::Bubblewrap => Ok(ResolvedRunner::without_guard(Box::new(Runner::new( bwrap_common::bwrap_runner::BubblewrapScriptRunner::new(), )))), From e9ac4256463cab008bfcb0f77b6656613dae3623 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 15:34:32 -0700 Subject: [PATCH 10/41] test: cover unavailable NVX execution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/core/mxc_engine/src/run.rs | 50 ++++++++++++++++++++++++++++++ src/core/wxc/src/main.rs | 13 ++++++++ src/core/wxc_common/src/cmdline.rs | 12 +++++-- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index a55c7682a..925611186 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -447,6 +447,7 @@ mod tests { use super::*; use wxc_common::logger::Mode; use wxc_common::models::{ExperimentalConfig, WindowsSandboxConfig}; + use wxc_common::mxc_error::MxcErrorCode; fn windows_sandbox_request(config: Option) -> ExecutionRequest { ExecutionRequest { @@ -460,6 +461,13 @@ mod tests { } } + fn nvx_request() -> ExecutionRequest { + ExecutionRequest { + containment: ContainmentBackend::Nvx, + ..Default::default() + } + } + #[test] fn policy_hash_identity_matches_runner_identity_rules() { assert_eq!(policy_hash_identity(""), "CLI"); @@ -497,4 +505,46 @@ mod tests { assert!(warning.contains("daemonPipeName")); assert!(warning.contains("fresh VM")); } + + #[test] + fn nvx_backend_returns_typed_unsupported_containment() { + let request = nvx_request(); + let mut logger = Logger::new(Mode::Buffer); + + let err = match resolve_runner_inner_windows(&request, &mut logger) { + Ok(_) => panic!("expected unsupported_containment"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); + assert_eq!(err.message, "NVX backend not yet available"); + } +} + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::*; + use wxc_common::logger::Mode; + use wxc_common::mxc_error::MxcErrorCode; + + fn nvx_request() -> ExecutionRequest { + ExecutionRequest { + containment: ContainmentBackend::Nvx, + ..Default::default() + } + } + + #[test] + fn nvx_backend_returns_typed_unsupported_containment() { + let request = nvx_request(); + let mut logger = Logger::new(Mode::Buffer); + + let err = match resolve_runner_inner(&request, &mut logger) { + Ok(_) => panic!("expected unsupported_containment"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); + assert_eq!(err.message, "NVX backend not yet available"); + } } diff --git a/src/core/wxc/src/main.rs b/src/core/wxc/src/main.rs index c5da7d58e..b8affa7e1 100644 --- a/src/core/wxc/src/main.rs +++ b/src/core/wxc/src/main.rs @@ -2400,6 +2400,19 @@ mod tests { assert_eq!(command_override, "echo 'safe&whoami'"); } + #[test] + fn nvx_cli_command_uses_posix_shell_quoting() { + let cli = parse_cli(&["wxc-exec", "policy.json", "--", "echo", "safe&whoami"]); + let command_override = command_override_from_cli( + &cli, + CommandLineContext::for_backend(&ContainmentBackend::Nvx), + ) + .unwrap() + .unwrap(); + + assert_eq!(command_override, "echo 'safe&whoami'"); + } + #[test] fn state_aware_command_override_only_applies_to_exec_phase() { let parsed = ParsedStateAwareRequest { diff --git a/src/core/wxc_common/src/cmdline.rs b/src/core/wxc_common/src/cmdline.rs index c79528fea..3b46f8951 100644 --- a/src/core/wxc_common/src/cmdline.rs +++ b/src/core/wxc_common/src/cmdline.rs @@ -47,9 +47,7 @@ impl CommandLineContext { | ContainmentBackend::Vm | ContainmentBackend::MicroVm | ContainmentBackend::Hyperlight => Self::WindowsCreateProcess, - ContainmentBackend::Nvx => { - unreachable!("NVX command-line context is not implemented yet") - } + ContainmentBackend::Nvx => Self::PosixShell, } } } @@ -384,6 +382,14 @@ mod tests { ); } + #[test] + fn nvx_backend_uses_posix_shell_context() { + assert_eq!( + CommandLineContext::for_backend(&ContainmentBackend::Nvx), + CommandLineContext::PosixShell + ); + } + #[test] fn context_renderer_rejects_null_bytes() { let err = cmdline_from_argv_for_context( From 14012ae8680c29fd74d700b848b8f0b4b4ceb4db Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 15:49:49 -0700 Subject: [PATCH 11/41] build: add pinned NVX artifact foundation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Cargo.lock | 26 +++ src/Cargo.toml | 5 + src/backends/nvx/binaries/Cargo.toml | 26 +++ src/backends/nvx/binaries/build.rs | 275 +++++++++++++++++++++++ src/backends/nvx/binaries/checksums.json | 5 + src/backends/nvx/binaries/src/lib.rs | 59 +++++ src/backends/nvx/binaries/versions.json | 6 + src/backends/nvx/build_common/Cargo.toml | 12 + src/backends/nvx/build_common/src/lib.rs | 255 +++++++++++++++++++++ src/backends/nvx/common/Cargo.toml | 10 + src/backends/nvx/common/src/lib.rs | 84 +++++++ 11 files changed, 763 insertions(+) create mode 100644 src/backends/nvx/binaries/Cargo.toml create mode 100644 src/backends/nvx/binaries/build.rs create mode 100644 src/backends/nvx/binaries/checksums.json create mode 100644 src/backends/nvx/binaries/src/lib.rs create mode 100644 src/backends/nvx/binaries/versions.json create mode 100644 src/backends/nvx/build_common/Cargo.toml create mode 100644 src/backends/nvx/build_common/src/lib.rs create mode 100644 src/backends/nvx/common/Cargo.toml create mode 100644 src/backends/nvx/common/src/lib.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 104f07376..e2fc7a2bf 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1681,6 +1681,32 @@ dependencies = [ "syn", ] +[[package]] +name = "nvx_binaries" +version = "0.8.0" +dependencies = [ + "nvx_build_common", + "nvx_common", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "nvx_build_common" +version = "0.8.0" +dependencies = [ + "nvx_common", + "tempfile", +] + +[[package]] +name = "nvx_common" +version = "0.8.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "oci-spec" version = "0.10.0" diff --git a/src/Cargo.toml b/src/Cargo.toml index 4a5ed2640..fbe415d8b 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -11,6 +11,9 @@ members = [ "backends/nanvix/build_common", "backends/nanvix/common", "backends/nanvix/runner", + "backends/nvx/binaries", + "backends/nvx/build_common", + "backends/nvx/common", "backends/seatbelt/common", "backends/windows_sandbox/common", "backends/windows_sandbox/daemon", @@ -88,6 +91,8 @@ mxc-sdk = { path = "core/mxc-sdk" } mxc_telemetry = { path = "mxc_telemetry" } nanvix_runner = { path = "backends/nanvix/runner" } nix = { version = "0.29", features = ["fs", "mount", "sched", "signal", "net", "process", "user", "term"] } +nvx_build_common = { path = "backends/nvx/build_common" } +nvx_common = { path = "backends/nvx/common" } process_security_environment_spec = { path = "core/generated/process_security_environment_specification" } plm = { path = "host/plm" } quick-xml = "0.41" diff --git a/src/backends/nvx/binaries/Cargo.toml b/src/backends/nvx/binaries/Cargo.toml new file mode 100644 index 000000000..378684319 --- /dev/null +++ b/src/backends/nvx/binaries/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "nvx_binaries" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Build-time acquisition of pinned NVX release artifacts" +links = "nvx_binaries" + +[lib] +path = "src/lib.rs" + +[features] +# Expensive download and verification work is disabled for default workspace +# builds and enabled only by future NVX backend consumers. +nvx = [] + +[dependencies] +nvx_common = { workspace = true } + +[build-dependencies] +nvx_build_common = { workspace = true } +nvx_common = { workspace = true } +sha2 = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } diff --git a/src/backends/nvx/binaries/build.rs b/src/backends/nvx/binaries/build.rs new file mode 100644 index 000000000..8824b3d2b --- /dev/null +++ b/src/backends/nvx/binaries/build.rs @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Downloads and verifies pinned NVX artifacts at build time. + +use std::collections::HashMap; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use nvx_common::{ + github_download_url, load_checksums, load_json, ReleaseConfig, WINDOWS_PLATFORM_ARTIFACTS, + WORKLOAD_IMAGE_ARTIFACTS, +}; +use sha2::{Digest, Sha256}; + +fn main() { + println!("cargo:rerun-if-env-changed=CARGO_FEATURE_NVX"); + + if std::env::var_os("CARGO_FEATURE_NVX").is_none() { + emit_disabled_metadata(); + return; + } + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if target_os != "windows" { + emit_disabled_metadata(); + return; + } + + let out_dir = PathBuf::from( + std::env::var_os("OUT_DIR").expect("nvx_binaries: OUT_DIR is not set by Cargo"), + ); + let (bin_dir, prefetched) = + nvx_build_common::resolve_bin_dir(&out_dir).unwrap_or_else(|error| { + panic!("nvx_binaries: failed to resolve artifact directory: {error}") + }); + let release: ReleaseConfig = load_json("versions.json"); + let checksums = load_checksums("checksums.json"); + let workload_images_available = release.workload_image_asset.is_some(); + + if prefetched { + eprintln!( + "nvx_binaries: NVX_BIN set; using pre-fetched artifacts from '{}' (offline)", + bin_dir.display() + ); + } else { + ensure_asset( + &release, + &release.windows_whp_asset, + &WINDOWS_PLATFORM_ARTIFACTS, + &bin_dir, + &checksums, + ); + + if let Some(asset) = release.workload_image_asset.as_deref() { + ensure_asset( + &release, + asset, + &WORKLOAD_IMAGE_ARTIFACTS, + &bin_dir, + &checksums, + ); + } + } + + validate_artifacts( + &bin_dir, + nvx_build_common::available_artifact_rel_paths(workload_images_available), + &checksums, + ) + .unwrap_or_else(|error| panic!("nvx_binaries: {error}")); + + nvx_build_common::emit_rerun_for_artifacts(&bin_dir, workload_images_available); + + emit_metadata(&bin_dir, workload_images_available); +} + +fn emit_disabled_metadata() { + let out_dir = std::env::var("OUT_DIR").expect("nvx_binaries: OUT_DIR is not set by Cargo"); + println!("cargo:rustc-env=NVX_BIN_DIR={out_dir}"); + println!("cargo:rustc-env=NVX_WORKLOAD_IMAGES_AVAILABLE=0"); + println!("cargo:BIN_DIR={out_dir}"); + println!("cargo:WORKLOAD_IMAGES_AVAILABLE=0"); + println!("cargo:rerun-if-changed=build.rs"); +} + +fn emit_metadata(bin_dir: &Path, workload_images_available: bool) { + let available = if workload_images_available { "1" } else { "0" }; + println!("cargo:rustc-env=NVX_BIN_DIR={}", bin_dir.display()); + println!("cargo:rustc-env=NVX_WORKLOAD_IMAGES_AVAILABLE={available}"); + println!("cargo:BIN_DIR={}", bin_dir.display()); + println!("cargo:WORKLOAD_IMAGES_AVAILABLE={available}"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=versions.json"); + println!("cargo:rerun-if-changed=checksums.json"); + println!("cargo:rerun-if-env-changed=NVX_BIN"); +} + +fn ensure_asset( + release: &ReleaseConfig, + asset: &str, + relative_paths: &[&str], + bin_dir: &Path, + checksums: &HashMap, +) { + if validate_artifacts(bin_dir, relative_paths.iter().copied(), checksums).is_ok() { + eprintln!("nvx_binaries: '{asset}' artifacts are cached and verified"); + return; + } + + let url = github_download_url(&release.repository, &release.tag, asset); + let archive = bin_dir.join(asset); + download(&url, &archive).unwrap_or_else(|error| { + let _ = fs::remove_file(&archive); + panic!("nvx_binaries: failed to download '{asset}': {error}"); + }); + + extract_and_stage(&archive, asset, relative_paths, bin_dir, checksums).unwrap_or_else( + |error| { + let _ = fs::remove_file(&archive); + panic!("nvx_binaries: failed to extract '{asset}': {error}"); + }, + ); + let _ = fs::remove_file(archive); +} + +fn download(url: &str, destination: &Path) -> Result<(), String> { + let mut command = Command::new("curl"); + command.args([ + "--silent", + "--show-error", + "--fail", + "--location", + "--retry", + "5", + "--retry-delay", + "5", + "--retry-all-errors", + "--output", + ]); + command.arg(destination); + command.args(["--header", "User-Agent: mxc-nvx-build/0.1"]); + + command.arg(url); + + let output = command + .output() + .map_err(|error| format!("could not start curl: {error}"))?; + if !output.status.success() { + return Err(format!( + "curl exited with {}\nstderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(()) +} + +fn extract_and_stage( + archive: &Path, + asset: &str, + relative_paths: &[&str], + bin_dir: &Path, + checksums: &HashMap, +) -> Result<(), String> { + let archive_root = archive_root(asset)?; + let extraction_dir = bin_dir.join(".nvx-extract"); + if extraction_dir.exists() { + fs::remove_dir_all(&extraction_dir).map_err(|error| { + format!( + "failed to clean extraction directory '{}': {error}", + extraction_dir.display() + ) + })?; + } + fs::create_dir_all(&extraction_dir).map_err(|error| { + format!( + "failed to create extraction directory '{}': {error}", + extraction_dir.display() + ) + })?; + + let archive_paths: Vec = relative_paths + .iter() + .map(|path| format!("{archive_root}/{path}")) + .collect(); + let mut command = Command::new("tar"); + command + .arg("-xf") + .arg(archive) + .arg("-C") + .arg(&extraction_dir); + command.args(&archive_paths); + let output = command + .output() + .map_err(|error| format!("could not start tar: {error}"))?; + if !output.status.success() { + let _ = fs::remove_dir_all(&extraction_dir); + return Err(format!( + "tar exited with {}\nstderr: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + )); + } + + let extracted_root = extraction_dir.join(archive_root); + let result = validate_artifacts(&extracted_root, relative_paths.iter().copied(), checksums) + .and_then(|()| { + nvx_build_common::copy_artifact_paths( + &extracted_root, + bin_dir, + relative_paths.iter().copied(), + ) + .map_err(|error| error.to_string()) + }); + let cleanup_result = fs::remove_dir_all(&extraction_dir); + + result?; + cleanup_result.map_err(|error| { + format!( + "failed to remove extraction directory '{}': {error}", + extraction_dir.display() + ) + }) +} + +fn archive_root(asset: &str) -> Result<&str, String> { + asset + .strip_suffix(".zip") + .or_else(|| asset.strip_suffix(".tar.gz")) + .ok_or_else(|| format!("unsupported NVX archive name '{asset}'")) +} + +fn validate_artifacts<'a>( + root: &Path, + relative_paths: impl IntoIterator, + checksums: &HashMap, +) -> Result<(), String> { + for relative_path in relative_paths { + let artifact = root.join(relative_path); + if !artifact.is_file() { + return Err(format!( + "required artifact '{}' is missing", + artifact.display() + )); + } + let expected = checksums.get(relative_path).ok_or_else(|| { + format!("artifact '{relative_path}' has no checksum entry; refusing unverified input") + })?; + let actual = sha256(&artifact) + .map_err(|error| format!("failed to hash '{}': {error}", artifact.display()))?; + if actual != *expected { + return Err(format!( + "SHA-256 mismatch for '{relative_path}': expected {expected}, actual {actual}" + )); + } + } + Ok(()) +} + +fn sha256(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 64 * 1024]; + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + Ok(format!("{:x}", hasher.finalize())) +} diff --git a/src/backends/nvx/binaries/checksums.json b/src/backends/nvx/binaries/checksums.json new file mode 100644 index 000000000..888b8d4a5 --- /dev/null +++ b/src/backends/nvx/binaries/checksums.json @@ -0,0 +1,5 @@ +{ + "bin/openvmm.exe": "9ded05b56389ebd9fd2c7fc245779ecea203ce1b1e16488fd275cf31bc442913", + "guest/vmlinux": "1678a788ce5329db3e0e97ebc8703c1263da553dd2c682b502d9d5e6fbd0f3bb", + "guest/initramfs.cpio.gz": "0f0324d6a1a959f5156f2e8ab94195b3befe34523892d31933bf6e3d7bf7bae4" +} diff --git a/src/backends/nvx/binaries/src/lib.rs b/src/backends/nvx/binaries/src/lib.rs new file mode 100644 index 000000000..a95df2700 --- /dev/null +++ b/src/backends/nvx/binaries/src/lib.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Build-time location and availability metadata for pinned NVX artifacts. + +/// Directory containing the acquired NVX artifacts. +pub const NVX_BIN_DIR: &str = env!("NVX_BIN_DIR"); + +/// `"1"` when workload images are included in the configured release. +pub const NVX_WORKLOAD_IMAGES_AVAILABLE: &str = env!("NVX_WORKLOAD_IMAGES_AVAILABLE"); + +pub use nvx_common::{WINDOWS_PLATFORM_ARTIFACTS, WORKLOAD_IMAGE_ARTIFACTS}; + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + use nvx_common::ReleaseConfig; + + const VERSIONS_JSON: &str = include_str!("../versions.json"); + const CHECKSUMS_JSON: &str = include_str!("../checksums.json"); + + #[test] + fn pinned_release_manifest_is_complete_for_platform_archive() { + let release: ReleaseConfig = + serde_json::from_str(VERSIONS_JSON).expect("versions.json must be valid"); + + assert_eq!(release.repository, "microsoft/nvx"); + assert_eq!(release.tag, "v0.1.0-dev.5c86da3dff02"); + assert_eq!(release.windows_whp_asset, "nvx-0.1.0-windows-whp.zip"); + assert_eq!(release.workload_image_asset, None); + } + + #[test] + fn checksums_cover_only_published_platform_artifacts() { + let checksums: HashMap = + serde_json::from_str(CHECKSUMS_JSON).expect("checksums.json must be valid"); + + assert_eq!(checksums.len(), WINDOWS_PLATFORM_ARTIFACTS.len()); + for relative_path in WINDOWS_PLATFORM_ARTIFACTS { + let checksum = checksums + .get(relative_path) + .expect("platform artifact must have a checksum"); + assert_eq!(checksum.len(), 64); + assert!(checksum + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))); + } + for relative_path in WORKLOAD_IMAGE_ARTIFACTS { + assert!(!checksums.contains_key(relative_path)); + } + } + + #[test] + fn release_without_workload_asset_reports_images_unavailable() { + assert_eq!(NVX_WORKLOAD_IMAGES_AVAILABLE, "0"); + } +} diff --git a/src/backends/nvx/binaries/versions.json b/src/backends/nvx/binaries/versions.json new file mode 100644 index 000000000..d404445b6 --- /dev/null +++ b/src/backends/nvx/binaries/versions.json @@ -0,0 +1,6 @@ +{ + "repository": "microsoft/nvx", + "tag": "v0.1.0-dev.5c86da3dff02", + "windows_whp_asset": "nvx-0.1.0-windows-whp.zip", + "workload_image_asset": null +} diff --git a/src/backends/nvx/build_common/Cargo.toml b/src/backends/nvx/build_common/Cargo.toml new file mode 100644 index 000000000..b06fe6ebe --- /dev/null +++ b/src/backends/nvx/build_common/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nvx_build_common" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Build-only helpers for staging NVX artifacts" + +[dependencies] +nvx_common = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/src/backends/nvx/build_common/src/lib.rs b/src/backends/nvx/build_common/src/lib.rs new file mode 100644 index 000000000..9ee19fd39 --- /dev/null +++ b/src/backends/nvx/build_common/src/lib.rs @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Build-only helpers for locating and staging NVX artifacts. + +use std::io; +use std::path::{Path, PathBuf}; + +use nvx_common::{WINDOWS_PLATFORM_ARTIFACTS, WORKLOAD_IMAGE_ARTIFACTS}; + +/// Category of an NVX release artifact. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ArtifactKind { + /// OpenVMM and guest boot artifacts. + Platform, + /// Optional workload filesystem images. + WorkloadImage, +} + +/// Returns every relative path in a complete NVX artifact bundle. +pub fn artifact_rel_paths() -> impl Iterator { + WINDOWS_PLATFORM_ARTIFACTS + .into_iter() + .map(|path| (ArtifactKind::Platform, path)) + .chain( + WORKLOAD_IMAGE_ARTIFACTS + .into_iter() + .map(|path| (ArtifactKind::WorkloadImage, path)), + ) +} + +/// Returns the artifacts required by the currently configured release. +pub fn available_artifact_rel_paths( + workload_images_available: bool, +) -> impl Iterator { + artifact_rel_paths().filter_map(move |(kind, path)| { + (kind == ArtifactKind::Platform + || (kind == ArtifactKind::WorkloadImage && workload_images_available)) + .then_some(path) + }) +} + +/// Resolves the artifact cache, honouring the `NVX_BIN` offline override. +/// +/// The returned boolean is `true` when the caller supplied `NVX_BIN`. +pub fn resolve_bin_dir(out_dir: &Path) -> io::Result<(PathBuf, bool)> { + let prefetched = std::env::var_os("NVX_BIN") + .filter(|value| !value.is_empty()) + .map(PathBuf::from); + + if let Some(dir) = prefetched { + if !dir.is_dir() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!( + "NVX_BIN is set to '{}', but that directory does not exist", + dir.display() + ), + )); + } + return std::path::absolute(&dir).map(|absolute| (absolute, true)); + } + + let dir = out_dir.join("nvx-binaries"); + std::fs::create_dir_all(&dir)?; + Ok((dir, false)) +} + +/// Copies selected artifacts while preserving their release-relative paths. +/// +/// Missing artifacts and copy failures are returned as explicit errors. The +/// destination file is removed after a failed copy so a partial artifact is +/// never left staged. +pub fn copy_artifact_paths<'a>( + src_dir: &Path, + target_dir: &Path, + relative_paths: impl IntoIterator, +) -> io::Result<()> { + let relative_paths: Vec<&str> = relative_paths.into_iter().collect(); + for relative_path in &relative_paths { + let source = src_dir.join(relative_path); + if !source.is_file() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("required NVX artifact '{}' is missing", source.display()), + )); + } + } + + for relative_path in relative_paths { + let source = src_dir.join(relative_path); + let destination = target_dir.join(relative_path); + let parent = destination.parent().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "NVX artifact '{}' has no destination parent", + destination.display() + ), + ) + })?; + std::fs::create_dir_all(parent)?; + + if let Err(error) = std::fs::copy(&source, &destination) { + let _ = std::fs::remove_file(&destination); + return Err(io::Error::new( + error.kind(), + format!( + "failed to copy NVX artifact '{}' to '{}': {error}", + source.display(), + destination.display() + ), + )); + } + } + Ok(()) +} + +/// Stages all artifacts available in the configured release. +pub fn copy_artifacts_to_target( + src_dir: &Path, + target_dir: &Path, + workload_images_available: bool, +) -> io::Result<()> { + copy_artifact_paths( + src_dir, + target_dir, + available_artifact_rel_paths(workload_images_available), + ) +} + +/// Emits Cargo change tracking for every artifact available in this release. +pub fn emit_rerun_for_artifacts(src_dir: &Path, workload_images_available: bool) { + for relative_path in available_artifact_rel_paths(workload_images_available) { + println!( + "cargo:rerun-if-changed={}", + src_dir.join(relative_path).display() + ); + } +} + +/// Stages NVX artifacts beside the consuming executable. +pub fn stage_artifacts_next_to_exe(nvx_bin_dir: &Path) -> io::Result<()> { + let workload_images_available = + match std::env::var("DEP_NVX_BINARIES_WORKLOAD_IMAGES_AVAILABLE").as_deref() { + Ok("1") => true, + Ok("0") => false, + Ok(value) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "DEP_NVX_BINARIES_WORKLOAD_IMAGES_AVAILABLE has invalid value '{value}'" + ), + )) + } + Err(std::env::VarError::NotPresent) => false, + Err(error) => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("cannot read DEP_NVX_BINARIES_WORKLOAD_IMAGES_AVAILABLE: {error}"), + )) + } + }; + let out_dir = PathBuf::from( + std::env::var_os("OUT_DIR") + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "OUT_DIR is not set"))?, + ); + let target_dir = out_dir + .parent() + .and_then(Path::parent) + .and_then(Path::parent) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "cannot determine target directory from '{}'", + out_dir.display() + ), + ) + })?; + + copy_artifacts_to_target(nvx_bin_dir, target_dir, workload_images_available)?; + emit_rerun_for_artifacts(nvx_bin_dir, workload_images_available); + println!("cargo:rerun-if-env-changed=DEP_NVX_BINARIES_BIN_DIR"); + println!("cargo:rerun-if-env-changed=DEP_NVX_BINARIES_WORKLOAD_IMAGES_AVAILABLE"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn write_artifact(root: &Path, relative_path: &str, contents: &[u8]) { + let path = root.join(relative_path); + std::fs::create_dir_all(path.parent().expect("artifact must have a parent")) + .expect("failed to create artifact parent"); + std::fs::write(path, contents).expect("failed to write artifact"); + } + + #[test] + fn artifact_paths_cover_complete_bundle_in_release_order() { + assert_eq!( + artifact_rel_paths().collect::>(), + vec![ + (ArtifactKind::Platform, "bin/openvmm.exe"), + (ArtifactKind::Platform, "guest/vmlinux"), + (ArtifactKind::Platform, "guest/initramfs.cpio.gz"), + (ArtifactKind::WorkloadImage, "images/distro.erofs"), + (ArtifactKind::WorkloadImage, "images/runtime.erofs"), + (ArtifactKind::WorkloadImage, "images/scratch.ext4"), + ] + ); + } + + #[test] + fn staging_preserves_release_layout_without_optional_images() { + let source = tempfile::tempdir().expect("failed to create source directory"); + let target = tempfile::tempdir().expect("failed to create target directory"); + for relative_path in WINDOWS_PLATFORM_ARTIFACTS { + write_artifact(source.path(), relative_path, relative_path.as_bytes()); + } + + copy_artifacts_to_target(source.path(), target.path(), false) + .expect("platform-only staging failed"); + + for relative_path in WINDOWS_PLATFORM_ARTIFACTS { + assert_eq!( + std::fs::read(target.path().join(relative_path)) + .expect("staged artifact is missing"), + relative_path.as_bytes() + ); + } + for relative_path in WORKLOAD_IMAGE_ARTIFACTS { + assert!(!target.path().join(relative_path).exists()); + } + } + + #[test] + fn complete_bundle_requires_every_workload_image() { + let source = tempfile::tempdir().expect("failed to create source directory"); + let target = tempfile::tempdir().expect("failed to create target directory"); + for relative_path in WINDOWS_PLATFORM_ARTIFACTS { + write_artifact(source.path(), relative_path, b"platform"); + } + + let error = copy_artifacts_to_target(source.path(), target.path(), true) + .expect_err("missing workload images must fail"); + + assert_eq!(error.kind(), io::ErrorKind::NotFound); + assert!(error.to_string().contains("images/distro.erofs")); + for relative_path in WINDOWS_PLATFORM_ARTIFACTS { + assert!(!target.path().join(relative_path).exists()); + } + } +} diff --git a/src/backends/nvx/common/Cargo.toml b/src/backends/nvx/common/Cargo.toml new file mode 100644 index 000000000..3eebd587f --- /dev/null +++ b/src/backends/nvx/common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nvx_common" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Shared constants and release configuration for NVX artifacts" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/src/backends/nvx/common/src/lib.rs b/src/backends/nvx/common/src/lib.rs new file mode 100644 index 000000000..3c162af7f --- /dev/null +++ b/src/backends/nvx/common/src/lib.rs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Shared artifact paths and release configuration for the NVX backend. + +use std::collections::HashMap; +use std::path::Path; + +use serde::Deserialize; + +/// Platform artifacts required to launch NVX with OpenVMM on Windows/WHP. +pub const WINDOWS_PLATFORM_ARTIFACTS: [&str; 3] = [ + "bin/openvmm.exe", + "guest/vmlinux", + "guest/initramfs.cpio.gz", +]; + +/// Optional workload image artifacts used by a complete NVX bundle. +pub const WORKLOAD_IMAGE_ARTIFACTS: [&str; 3] = [ + "images/distro.erofs", + "images/runtime.erofs", + "images/scratch.ext4", +]; + +/// Pinned upstream release configuration loaded from `versions.json`. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct ReleaseConfig { + /// GitHub repository in `owner/name` form. + pub repository: String, + /// Published release tag. + pub tag: String, + /// Windows/WHP platform archive name. + pub windows_whp_asset: String, + /// Optional workload image archive name. + #[serde(default)] + pub workload_image_asset: Option, +} + +/// Loads and deserializes a JSON file, failing with path-specific context. +pub fn load_json(path: &str) -> T { + let content = std::fs::read_to_string(Path::new(path)) + .unwrap_or_else(|error| panic!("nvx_common: failed to read {path}: {error}")); + serde_json::from_str(&content) + .unwrap_or_else(|error| panic!("nvx_common: failed to parse {path}: {error}")) +} + +/// Loads the artifact checksum map from `checksums.json`. +pub fn load_checksums(path: &str) -> HashMap { + load_json(path) +} + +/// Constructs a deterministic GitHub release asset URL. +pub fn github_download_url(repository: &str, tag: &str, asset: &str) -> String { + format!("https://github.com/{repository}/releases/download/{tag}/{asset}") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn windows_platform_artifacts_match_release_layout() { + assert_eq!( + WINDOWS_PLATFORM_ARTIFACTS, + [ + "bin/openvmm.exe", + "guest/vmlinux", + "guest/initramfs.cpio.gz", + ] + ); + } + + #[test] + fn workload_image_artifacts_match_staged_layout() { + assert_eq!( + WORKLOAD_IMAGE_ARTIFACTS, + [ + "images/distro.erofs", + "images/runtime.erofs", + "images/scratch.ext4", + ] + ); + } +} From 8aecab99afb150a62a004f6ca6ee2988fc57bfbc Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 16:08:57 -0700 Subject: [PATCH 12/41] feat: add NVX backend foundation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Cargo.lock | 9 ++ src/Cargo.toml | 3 + src/backends/nvx/runner/Cargo.toml | 14 +++ src/backends/nvx/runner/src/lib.rs | 48 ++++++++ src/core/mxc-sdk/Cargo.toml | 2 + src/core/mxc_engine/Cargo.toml | 2 + src/core/mxc_engine/src/platform.rs | 23 ++++ src/core/mxc_engine/src/probe.rs | 21 ++++ src/core/mxc_engine/src/run.rs | 170 +++++++++++++++++++++++++--- src/core/wxc/Cargo.toml | 1 + 10 files changed, 278 insertions(+), 15 deletions(-) create mode 100644 src/backends/nvx/runner/Cargo.toml create mode 100644 src/backends/nvx/runner/src/lib.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index e2fc7a2bf..a1d76fb67 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1548,6 +1548,7 @@ dependencies = [ "learning_mode_core", "lxc_common", "nanvix_runner", + "nvx_runner", "plm", "seatbelt_common", "serde", @@ -1707,6 +1708,14 @@ dependencies = [ "serde_json", ] +[[package]] +name = "nvx_runner" +version = "0.8.0" +dependencies = [ + "nvx_binaries", + "wxc_common", +] + [[package]] name = "oci-spec" version = "0.10.0" diff --git a/src/Cargo.toml b/src/Cargo.toml index fbe415d8b..cd3354392 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -14,6 +14,7 @@ members = [ "backends/nvx/binaries", "backends/nvx/build_common", "backends/nvx/common", + "backends/nvx/runner", "backends/seatbelt/common", "backends/windows_sandbox/common", "backends/windows_sandbox/daemon", @@ -91,8 +92,10 @@ mxc-sdk = { path = "core/mxc-sdk" } mxc_telemetry = { path = "mxc_telemetry" } nanvix_runner = { path = "backends/nanvix/runner" } nix = { version = "0.29", features = ["fs", "mount", "sched", "signal", "net", "process", "user", "term"] } +nvx_binaries = { path = "backends/nvx/binaries" } nvx_build_common = { path = "backends/nvx/build_common" } nvx_common = { path = "backends/nvx/common" } +nvx_runner = { path = "backends/nvx/runner" } process_security_environment_spec = { path = "core/generated/process_security_environment_specification" } plm = { path = "host/plm" } quick-xml = "0.41" diff --git a/src/backends/nvx/runner/Cargo.toml b/src/backends/nvx/runner/Cargo.toml new file mode 100644 index 000000000..59339f05e --- /dev/null +++ b/src/backends/nvx/runner/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nvx_runner" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "NVX backend preflight checks for incomplete runtime builds" + +[features] +default = [] +nvx = ["nvx_binaries/nvx"] + +[dependencies] +nvx_binaries = { workspace = true } +wxc_common = { workspace = true } diff --git a/src/backends/nvx/runner/src/lib.rs b/src/backends/nvx/runner/src/lib.rs new file mode 100644 index 000000000..1cad495f6 --- /dev/null +++ b/src/backends/nvx/runner/src/lib.rs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! NVX backend preflight checks. +//! +//! PR1 intentionally does not include an executable NVX runtime runner. This +//! crate currently provides only typed availability checks that the engine can +//! call before dispatch. + +use wxc_common::mxc_error::MxcError; + +/// Typed backend-unavailable message for pinned releases without the NVX +/// workload-image archive. +pub const ERR_WORKLOAD_IMAGE_ASSET_UNAVAILABLE: &str = + "NVX workload image asset is not available in the pinned release"; + +/// Verifies whether the pinned NVX release includes workload images required +/// by the runtime implementation. +/// +/// The current pinned release does not ship those images, so preflight returns +/// a typed backend-unavailable error. +pub fn preflight() -> Result<(), MxcError> { + if nvx_binaries::NVX_WORKLOAD_IMAGES_AVAILABLE != "1" { + return Err(MxcError::backend_unavailable( + ERR_WORKLOAD_IMAGE_ASSET_UNAVAILABLE, + )); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::preflight; + use super::ERR_WORKLOAD_IMAGE_ASSET_UNAVAILABLE; + use wxc_common::mxc_error::MxcErrorCode; + + #[test] + fn pinned_release_marks_workload_images_unavailable() { + assert_eq!(nvx_binaries::NVX_WORKLOAD_IMAGES_AVAILABLE, "0"); + } + + #[test] + fn preflight_returns_typed_backend_unavailable_error() { + let err = preflight().expect_err("preflight must fail in PR1"); + assert_eq!(err.code, MxcErrorCode::BackendUnavailable); + assert_eq!(err.message, ERR_WORKLOAD_IMAGE_ASSET_UNAVAILABLE); + } +} diff --git a/src/core/mxc-sdk/Cargo.toml b/src/core/mxc-sdk/Cargo.toml index b3a4bd747..50dd33776 100644 --- a/src/core/mxc-sdk/Cargo.toml +++ b/src/core/mxc-sdk/Cargo.toml @@ -16,6 +16,8 @@ default = [] wslc = ["mxc_engine/wslc"] # IsolationSession backend (Windows only, experimental). isolation_session = ["mxc_engine/isolation_session"] +# NVX backend foundation (experimental). +nvx = ["mxc_engine/nvx"] [dependencies] wxc_common.workspace = true diff --git a/src/core/mxc_engine/Cargo.toml b/src/core/mxc_engine/Cargo.toml index 3b76341af..0767ab94d 100644 --- a/src/core/mxc_engine/Cargo.toml +++ b/src/core/mxc_engine/Cargo.toml @@ -16,6 +16,7 @@ serde_json = { workspace = true } # MicroVM runner — used by the Windows and Linux run-to-completion bodies under # the `microvm` feature. Never built on hosts/features that don't select it. nanvix_runner = { workspace = true, optional = true } +nvx_runner = { workspace = true, optional = true } [target.'cfg(target_arch = "x86_64")'.dependencies] hyperlight_common = { workspace = true, optional = true } @@ -50,6 +51,7 @@ default = [] # live in one place. The build-time binary-staging that some backends also need # (e.g. NanVix's `nanvix_binaries`) stays with the executor crates. microvm = ["dep:nanvix_runner"] +nvx = ["dep:nvx_runner", "nvx_runner/nvx"] wslc = ["dep:wslc_common", "wslc_common/link-wslcsdk"] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight"] isolation_session = [ diff --git a/src/core/mxc_engine/src/platform.rs b/src/core/mxc_engine/src/platform.rs index 7eeeb81d5..c6c797f55 100644 --- a/src/core/mxc_engine/src/platform.rs +++ b/src/core/mxc_engine/src/platform.rs @@ -211,6 +211,29 @@ mod tests { } } + fn assert_nvx_method_is_omitted() { + let support = platform_support(); + assert!( + support + .available_methods + .iter() + .all(|method| method != "nvx"), + "platform_support must not advertise NVX while runtime is incomplete" + ); + } + + #[cfg(not(feature = "nvx"))] + #[test] + fn nvx_is_not_advertised_without_feature() { + assert_nvx_method_is_omitted(); + } + + #[cfg(feature = "nvx")] + #[test] + fn nvx_is_not_advertised_with_feature_enabled() { + assert_nvx_method_is_omitted(); + } + #[cfg(target_os = "linux")] #[test] fn linux_support_reports_bubblewrap_when_probe_succeeds() { diff --git a/src/core/mxc_engine/src/probe.rs b/src/core/mxc_engine/src/probe.rs index 7d85434d8..a0069ea91 100644 --- a/src/core/mxc_engine/src/probe.rs +++ b/src/core/mxc_engine/src/probe.rs @@ -267,6 +267,27 @@ mod tests { } } + fn assert_nvx_backend_is_omitted() { + assert!( + available_backends() + .iter() + .all(|backend| backend.backend != "nvx"), + "available_backends must not advertise NVX while runtime is incomplete" + ); + } + + #[cfg(not(feature = "nvx"))] + #[test] + fn nvx_is_not_advertised_without_feature() { + assert_nvx_backend_is_omitted(); + } + + #[cfg(feature = "nvx")] + #[test] + fn nvx_is_not_advertised_with_feature_enabled() { + assert_nvx_backend_is_omitted(); + } + /// Every backend the probe can emit, across all platforms/features — derived /// from `ContainmentBackend` (the same source as the `push` calls) so the /// emitted names can't be typo'd, and checked against the `wire::Containment` diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index 925611186..a0565962a 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -35,6 +35,21 @@ use wxc_common::script_runner::ScriptRunner; use crate::error::Error; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +const ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED: &str = + "NVX is an experimental feature. Use --experimental flag."; +#[cfg(all( + not(feature = "nvx"), + any(target_os = "windows", target_os = "linux", target_os = "macos") +))] +const ERR_NVX_FEATURE_REQUIRED: &str = "NVX backend not compiled in (build with --features nvx)"; +#[cfg(all( + feature = "nvx", + any(target_os = "windows", target_os = "linux", target_os = "macos") +))] +const ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING: &str = + "NVX runtime implementation is not present in this build"; + /// A backend runner resolved for an [`ExecutionRequest`], ready to run. /// /// On Windows, `dacl_manager` — when present — is the guard for the @@ -243,9 +258,7 @@ fn resolve_runner_inner_windows( ContainmentBackend::Vm => Err(MxcError::unsupported_containment( "VM backend not yet implemented", )), - ContainmentBackend::Nvx => Err(MxcError::unsupported_containment( - "NVX backend not yet available", - )), + ContainmentBackend::Nvx => resolve_nvx_backend(request), ContainmentBackend::MicroVm => { if !request.experimental_enabled { return Err(MxcError::malformed_request( @@ -344,9 +357,7 @@ fn resolve_runner_inner( )) } } - ContainmentBackend::Nvx => Err(MxcError::unsupported_containment( - "NVX backend not yet available", - )), + ContainmentBackend::Nvx => resolve_nvx_backend(request), ContainmentBackend::Bubblewrap => Ok(ResolvedRunner::without_guard(Box::new(Runner::new( bwrap_common::bwrap_runner::BubblewrapScriptRunner::new(), )))), @@ -384,6 +395,10 @@ fn resolve_runner_inner( ) -> Result { use wxc_common::sandbox_process::Runner; + if request.containment == ContainmentBackend::Nvx { + return resolve_nvx_backend(request); + } + if request.containment != ContainmentBackend::Seatbelt { logger.log_line("Note: Overriding containment backend to Seatbelt on macOS."); } @@ -407,6 +422,39 @@ fn resolve_runner_inner( )) } +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +fn resolve_nvx_backend(request: &ExecutionRequest) -> Result { + if !request.experimental_enabled { + return Err(MxcError::malformed_request( + ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED, + )); + } + + #[cfg(feature = "nvx")] + { + return resolve_nvx_backend_with_preflight(nvx_runner::preflight); + } + + #[cfg(not(feature = "nvx"))] + { + Err(MxcError::unsupported_containment(ERR_NVX_FEATURE_REQUIRED)) + } +} + +#[cfg(all( + feature = "nvx", + any(target_os = "windows", target_os = "linux", target_os = "macos") +))] +fn resolve_nvx_backend_with_preflight(preflight: F) -> Result +where + F: FnOnce() -> Result<(), MxcError>, +{ + preflight()?; + Err(MxcError::backend_unavailable( + ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING, + )) +} + /// Construct the Hyperlight runner, shared by the Windows and Linux bodies. /// Requires x86_64 (Hyperlight needs KVM or WHP) and the `hyperlight` feature. /// On Windows, pre-checks that `winhvplatform.dll` is loadable so a missing @@ -461,9 +509,10 @@ mod tests { } } - fn nvx_request() -> ExecutionRequest { + fn nvx_request(experimental_enabled: bool) -> ExecutionRequest { ExecutionRequest { containment: ContainmentBackend::Nvx, + experimental_enabled, ..Default::default() } } @@ -507,8 +556,23 @@ mod tests { } #[test] - fn nvx_backend_returns_typed_unsupported_containment() { - let request = nvx_request(); + fn nvx_without_experimental_opt_in_is_rejected_as_malformed_request() { + let request = nvx_request(false); + let mut logger = Logger::new(Mode::Buffer); + + let err = match resolve_runner_inner_windows(&request, &mut logger) { + Ok(_) => panic!("expected malformed_request"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::MalformedRequest); + assert_eq!(err.message, ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED); + } + + #[cfg(not(feature = "nvx"))] + #[test] + fn nvx_without_feature_returns_typed_unsupported_containment() { + let request = nvx_request(true); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner_windows(&request, &mut logger) { @@ -517,26 +581,72 @@ mod tests { }; assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); - assert_eq!(err.message, "NVX backend not yet available"); + assert_eq!(err.message, ERR_NVX_FEATURE_REQUIRED); + } + + #[cfg(feature = "nvx")] + #[test] + fn nvx_with_feature_propagates_preflight_backend_unavailable() { + let request = nvx_request(true); + let mut logger = Logger::new(Mode::Buffer); + + let err = match resolve_runner_inner_windows(&request, &mut logger) { + Ok(_) => panic!("expected backend_unavailable"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::BackendUnavailable); + assert_eq!( + err.message, + nvx_runner::ERR_WORKLOAD_IMAGE_ASSET_UNAVAILABLE + ); + } + + #[cfg(feature = "nvx")] + #[test] + fn nvx_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { + let err = match resolve_nvx_backend_with_preflight(|| Ok(())) { + Ok(_) => panic!("runtime stub must not resolve a runner"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::BackendUnavailable); + assert_eq!(err.message, ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING); } } -#[cfg(all(test, target_os = "linux"))] +#[cfg(all(test, any(target_os = "linux", target_os = "macos")))] mod tests { use super::*; use wxc_common::logger::Mode; use wxc_common::mxc_error::MxcErrorCode; - fn nvx_request() -> ExecutionRequest { + fn nvx_request(experimental_enabled: bool) -> ExecutionRequest { ExecutionRequest { containment: ContainmentBackend::Nvx, + experimental_enabled, ..Default::default() } } #[test] - fn nvx_backend_returns_typed_unsupported_containment() { - let request = nvx_request(); + fn nvx_without_experimental_opt_in_is_rejected_as_malformed_request() { + let request = nvx_request(false); + let mut logger = Logger::new(Mode::Buffer); + + let err = match resolve_runner_inner(&request, &mut logger) { + Ok(_) => panic!("expected malformed_request"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::MalformedRequest); + assert_eq!(err.message, ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED); + } + + #[cfg(not(feature = "nvx"))] + #[test] + fn nvx_without_feature_returns_typed_unsupported_containment() { + let request = nvx_request(true); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner(&request, &mut logger) { @@ -545,6 +655,36 @@ mod tests { }; assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); - assert_eq!(err.message, "NVX backend not yet available"); + assert_eq!(err.message, ERR_NVX_FEATURE_REQUIRED); + } + + #[cfg(feature = "nvx")] + #[test] + fn nvx_with_feature_propagates_preflight_backend_unavailable() { + let request = nvx_request(true); + let mut logger = Logger::new(Mode::Buffer); + + let err = match resolve_runner_inner(&request, &mut logger) { + Ok(_) => panic!("expected backend_unavailable"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::BackendUnavailable); + assert_eq!( + err.message, + nvx_runner::ERR_WORKLOAD_IMAGE_ASSET_UNAVAILABLE + ); + } + + #[cfg(feature = "nvx")] + #[test] + fn nvx_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { + let err = match resolve_nvx_backend_with_preflight(|| Ok(())) { + Ok(_) => panic!("runtime stub must not resolve a runner"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::BackendUnavailable); + assert_eq!(err.message, ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING); } } diff --git a/src/core/wxc/Cargo.toml b/src/core/wxc/Cargo.toml index 2c4f1261b..2cea488e9 100644 --- a/src/core/wxc/Cargo.toml +++ b/src/core/wxc/Cargo.toml @@ -39,6 +39,7 @@ nanvix_build_common = { path = "../../backends/nanvix/build_common" } default = [] test-support = ["wxc_common/test-support"] microvm = ["dep:nanvix_binaries", "nanvix_binaries/microvm", "mxc_engine/microvm"] +nvx = ["mxc_engine/nvx"] wslc = ["dep:wslc_common", "wslc_common/link-wslcsdk", "mxc_engine/wslc"] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight", "mxc_engine/hyperlight"] isolation_session = ["mxc_engine/isolation_session"] From ab26c060796d3fd31b7af6795d3082baeaae55ab Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 16:42:04 -0700 Subject: [PATCH 13/41] build: add NVX-enabled Windows packaging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Build.Windows.Job.yml | 8 ++++++++ build.bat | 17 +++++++++++++++++ src/Cargo.lock | 2 ++ src/backends/nvx/build_common/src/lib.rs | 9 +++++++-- src/core/wxc/Cargo.toml | 9 ++++++++- src/core/wxc/build.rs | 17 ++++++++++++++++- 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Build.Windows.Job.yml b/.github/workflows/Build.Windows.Job.yml index f9c228195..a9d488188 100644 --- a/.github/workflows/Build.Windows.Job.yml +++ b/.github/workflows/Build.Windows.Job.yml @@ -66,6 +66,14 @@ jobs: --no-default-features --features "${{ matrix.features }}" + # Keep NVX out of the uploaded release executable while its workload + # image bundle is unavailable, but verify its opt-in build and staging. + - name: Check opt-in NVX packaging + if: matrix.arch == 'x64' + run: cargo check --locked --target ${{ matrix.target }} -p wxc + --no-default-features + --features nvx + - name: Test telemetry consent and policy in release mode run: | cargo test --locked --release --target ${{ matrix.target }} -p wxc_common telemetry::consent diff --git a/build.bat b/build.bat index 24a7935bd..7710cdeb1 100644 --- a/build.bat +++ b/build.bat @@ -6,6 +6,7 @@ set "BUILD_CONFIG=release" set "BUILD_ARCH=" set "BUILD_ALL=0" set "WITH_NANVIX=0" +set "WITH_NVX=0" set "WITH_WSLC=0" set "WITH_ISOLATION_SESSION=0" set "WITH_HYPERLIGHT=0" @@ -19,6 +20,7 @@ if /i "%~1"=="--x64" ( set "BUILD_ARCH=x86_64-pc-windows-msvc" & shift & g if /i "%~1"=="--arm64" ( set "BUILD_ARCH=aarch64-pc-windows-msvc" & shift & goto :parse_args ) if /i "%~1"=="--all" ( set "BUILD_ALL=1" & shift & goto :parse_args ) if /i "%~1"=="--with-microvm" ( set "WITH_NANVIX=1" & shift & goto :parse_args ) +if /i "%~1"=="--with-nvx" ( set "WITH_NVX=1" & shift & goto :parse_args ) if /i "%~1"=="--with-wslc" ( set "WITH_WSLC=1" & shift & goto :parse_args ) if /i "%~1"=="--with-isolation-session" ( set "WITH_ISOLATION_SESSION=1" & shift & goto :parse_args ) if /i "%~1"=="--with-hyperlight" ( set "WITH_HYPERLIGHT=1" & shift & goto :parse_args ) @@ -37,6 +39,17 @@ if "%BUILD_ALL%"=="0" if "%BUILD_ARCH%"=="" ( ) ) +if "%WITH_NVX%"=="1" ( + if "%BUILD_ALL%"=="1" ( + echo ERROR: --with-nvx supports x64 only and cannot be combined with --all. + exit /b 1 + ) + if /i not "%BUILD_ARCH%"=="x86_64-pc-windows-msvc" ( + echo ERROR: --with-nvx supports x64 only. Use --x64 on an ARM64 host. + exit /b 1 + ) +) + :: Build flags set "CARGO_FLAGS=--target" if "%BUILD_CONFIG%"=="release" set "CARGO_FLAGS=--release --target" @@ -45,6 +58,7 @@ if "%BUILD_CONFIG%"=="release" set "CARGO_FLAGS=--release --target" set "PLM_FLAGS=--target" if "%BUILD_CONFIG%"=="release" set "PLM_FLAGS=--release --target" if "%WITH_NANVIX%"=="1" set "CARGO_FLAGS=--features microvm %CARGO_FLAGS%" +if "%WITH_NVX%"=="1" set "CARGO_FLAGS=--features nvx %CARGO_FLAGS%" if "%WITH_WSLC%"=="1" set "CARGO_FLAGS=--features wslc %CARGO_FLAGS%" if "%WITH_ISOLATION_SESSION%"=="1" set "CARGO_FLAGS=--features isolation_session %CARGO_FLAGS%" if "%WITH_HYPERLIGHT%"=="1" set "CARGO_FLAGS=--features hyperlight %CARGO_FLAGS%" @@ -274,6 +288,9 @@ echo --x64 Build for x64 only echo --arm64 Build for ARM64 only echo --all Build for both x64 and ARM64 echo --with-microvm Download and include NanVix micro-VM binaries +echo --with-nvx Add the incomplete NVX foundation and platform artifacts (x64) +echo Runtime preflight remains unavailable until NVX publishes +echo the workload image bundle echo --with-wslc Build with WSL Container (WSLC SDK) support echo --with-isolation-session Build with IsolationSession backend (IsoEnvBroker) echo --with-hyperlight Build with Hyperlight (micro-VM) backend (x86_64 only) diff --git a/src/Cargo.lock b/src/Cargo.lock index a1d76fb67..bc5cf81d4 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -3223,6 +3223,8 @@ dependencies = [ "mxc_engine", "nanvix_binaries", "nanvix_build_common", + "nvx_binaries", + "nvx_build_common", "plm", "serde_json", "tempfile", diff --git a/src/backends/nvx/build_common/src/lib.rs b/src/backends/nvx/build_common/src/lib.rs index 9ee19fd39..031df9738 100644 --- a/src/backends/nvx/build_common/src/lib.rs +++ b/src/backends/nvx/build_common/src/lib.rs @@ -153,7 +153,12 @@ pub fn stage_artifacts_next_to_exe(nvx_bin_dir: &Path) -> io::Result<()> { ), )) } - Err(std::env::VarError::NotPresent) => false, + Err(std::env::VarError::NotPresent) => { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "DEP_NVX_BINARIES_WORKLOAD_IMAGES_AVAILABLE is not set", + )) + } Err(error) => { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -213,7 +218,7 @@ mod tests { } #[test] - fn staging_preserves_release_layout_without_optional_images() { + fn copy_artifacts_preserves_release_layout() { let source = tempfile::tempdir().expect("failed to create source directory"); let target = tempfile::tempdir().expect("failed to create target directory"); for relative_path in WINDOWS_PLATFORM_ARTIFACTS { diff --git a/src/core/wxc/Cargo.toml b/src/core/wxc/Cargo.toml index 2cea488e9..d162c2d69 100644 --- a/src/core/wxc/Cargo.toml +++ b/src/core/wxc/Cargo.toml @@ -22,6 +22,7 @@ hyperlight_common = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] appcontainer_common = { workspace = true } nanvix_binaries = { path = "../../backends/nanvix/binaries", optional = true } +nvx_binaries = { workspace = true, optional = true } wslc_common = { workspace = true, optional = true } # Shared PLM dep: coordination, guarded lifecycle, and restricted WPR control. # wxc-exec never bypasses PLM's public singleton. @@ -34,12 +35,18 @@ mxc_build_common.workspace = true [target.'cfg(windows)'.build-dependencies] nanvix_build_common = { path = "../../backends/nanvix/build_common" } +nvx_build_common = { workspace = true, optional = true } [features] default = [] test-support = ["wxc_common/test-support"] microvm = ["dep:nanvix_binaries", "nanvix_binaries/microvm", "mxc_engine/microvm"] -nvx = ["mxc_engine/nvx"] +nvx = [ + "dep:nvx_binaries", + "nvx_binaries/nvx", + "dep:nvx_build_common", + "mxc_engine/nvx", +] wslc = ["dep:wslc_common", "wslc_common/link-wslcsdk", "mxc_engine/wslc"] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight", "mxc_engine/hyperlight"] isolation_session = ["mxc_engine/isolation_session"] diff --git a/src/core/wxc/build.rs b/src/core/wxc/build.rs index 28cc26f04..fa03059f7 100644 --- a/src/core/wxc/build.rs +++ b/src/core/wxc/build.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Build script for wxc — embeds Windows VersionInfo and copies NanVix binaries. +//! Build script for wxc — embeds Windows VersionInfo and stages backend artifacts. fn main() { mxc_build_common::embed_version_info("MXC sandbox executor", "wxc-exec.exe"); @@ -12,6 +12,9 @@ fn main() { #[cfg(all(windows, feature = "microvm"))] copy_nanvix_binaries(); + #[cfg(all(windows, feature = "nvx"))] + copy_nvx_binaries(); + // Delay-load winhvplatform.dll so WHP-less hosts don't crash before main(). // CARGO_CFG_TARGET_* (not #[cfg]) because build.rs cfg gates are host, not target. #[cfg(feature = "hyperlight")] @@ -96,3 +99,15 @@ fn copy_nanvix_binaries() { // rerun emission) lives in the build-only `nanvix_build_common` crate. nanvix_build_common::stage_artifacts_next_to_exe(Path::new(&nanvix_bin_dir)); } + +#[cfg(all(windows, feature = "nvx"))] +fn copy_nvx_binaries() { + use std::path::Path; + + let nvx_bin_dir = std::env::var("DEP_NVX_BINARIES_BIN_DIR").unwrap_or_else(|error| { + panic!("wxc build.rs: DEP_NVX_BINARIES_BIN_DIR is required for the nvx feature: {error}") + }); + + nvx_build_common::stage_artifacts_next_to_exe(Path::new(&nvx_bin_dir)) + .unwrap_or_else(|error| panic!("wxc build.rs: failed to stage NVX artifacts: {error}")); +} From d4713fcf789cf958bb53f4f677a7abae40e3c6c0 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 16:52:54 -0700 Subject: [PATCH 14/41] fix: satisfy NVX feature lint Co-authored-by: GitHub Copilot --- src/core/mxc_engine/src/run.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index a0565962a..08fd6ef7b 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -432,7 +432,7 @@ fn resolve_nvx_backend(request: &ExecutionRequest) -> Result Date: Thu, 17 Sep 2026 17:02:01 -0700 Subject: [PATCH 15/41] docs: describe the NVX integration foundation Co-authored-by: GitHub Copilot --- README.md | 15 +++ docs/nvx-backend-gaps.md | 92 ++++++++----------- .../dev/mxc-config.schema.0.9.0-alpha.json | 7 ++ schemas/dev/mxc-config.schema.0.9.0-dev.json | 7 ++ sdk/node/src/generated/v0_9_0_alpha/wire.ts | 2 +- sdk/node/src/generated/wire.ts | 2 +- 6 files changed, 67 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 566cd3dec..19452a8ff 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,23 @@ build.bat # Release build for current architecture build.bat --debug # Debug build build.bat --all # Release build for both x64 and ARM64 build.bat --with-microvm # Include NanVix micro-VM binaries +build.bat --with-nvx # Include the incomplete NVX foundation (Windows x64) ``` +`--with-nvx` is additive to the existing `--with-microvm` path. It acquires the +pinned NVX platform archive at build time and verifies every staged file by +SHA-256; sandbox launches never download artifacts. Set `NVX_BIN` to a +pre-fetched, checksum-verifiable bundle directory for an offline build. The +current pin, `v0.1.0-dev.5c86da3dff02`, contains only `openvmm.exe`, the guest +kernel, and the guest initramfs, not the workload-image bundle. + +This foundation adds `containment: "nvx"` to the development schema while +retaining `microvm`. An NVX-enabled binary parses the new value, but execution +returns a typed backend-unavailable error and capability probes do not advertise +NVX. Runtime work remains blocked on NVX-produced distro/runtime EROFS images +and scratch image, a proven combined managed-sandbox/virtio-fs contract, and the +required WHP runner. + #### Linux ```bash diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index f2f0b0057..570e94e0d 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -6,6 +6,11 @@ The original NVX prototype accepted a real MXC `0.9.0-dev` JSON, validated it against the schema, and adapted supported fields into typed NVX launch plans. The prototype first exercised those plans on Windows through OpenVMM and WHP. +The current MXC foundation is additive: `containment: "nvx"` parses while the +existing `microvm` backend remains available. An NVX-enabled build returns a +typed backend-unavailable error for execution, and capability probes do not +advertise NVX while the runtime is incomplete. + ## Current architecture The intended MXC integration uses a direct ownership boundary: @@ -78,8 +83,11 @@ them for this backend: ## Schema changes -The only required new public identifier is `containment: "nvx"`. The -`experimental.nvx.provision` section is necessary only if images remain caller-configurable. +The rolling `0.9.0-dev` and exact `0.9.0-alpha` development contracts now +include `containment: "nvx"` in their generated schemas and TypeScript wire +types. The existing `microvm` identifier remains in both artifact sets. No +stable schema is changed. An `experimental.nvx.provision` section is necessary +only if images remain caller-configurable. Example state-aware provision request: @@ -137,63 +145,35 @@ Example state-aware provision request: ## Binary acquisition and packaging -NVX replaces the existing NanVix microVM runtime and its `--with-microvm` -packaging path. A new `--with-nvx` build option should reuse the existing -NanVix artifact-acquisition implementation rather than introduce a second -download system: - -- pin an exact `microsoft/nvx` release tag, platform asset name, and SHA-256 - checksums in the repository; -- download and verify the release during the build, never when a sandbox is - launched; -- cache verified artifacts under Cargo's build output and avoid downloading - them again when the pinned files are already present; -- support `NVX_BIN` as an offline override containing a pre-fetched and - checksum-verifiable NVX bundle; -- stage `openvmm`, the guest kernel, and the guest initramfs beside the MXC - executor and copy them into the matching Node and .NET SDK runtime package; -- fail the NVX-enabled build if an expected artifact is absent, corrupt, or - does not match the selected platform. - -The existing `nanvix_binaries` and `nanvix_build_common` mechanisms should be -renamed and adapted for NVX, preserving their pinned-version manifest, -checksum verification, offline-prefetch, cache, and SDK-staging behavior. -The obsolete NanVix release definitions and binaries should be removed rather -than retained alongside NVX. - -`--with-nvx` selects the WHP bundle on Windows and the KVM bundle by default on -Linux. Linux MSHV packaging requires an additional explicit build selection; -the build must not infer the desired release bundle from the capabilities of -the machine performing the build. - -The current `microsoft/nvx` prerelease -`v0.1.0-dev.142587bbceae` has the following package sizes: - -| Release bundle | Compressed download | Expanded runtime | -| --- | ---: | ---: | -| Windows WHP | 22.73 MiB | 51.41 MiB | -| Linux KVM | 130.95 MiB | 486.69 MiB | -| Linux MSHV | 125.35 MiB | 489.11 MiB | - -The expanded Windows runtime consists primarily of `openvmm.exe` (22.05 MiB), -`vmlinux` (21.84 MiB), and `initramfs.cpio.gz` (7.42 MiB). The Linux bundles -use the same kernel and initramfs, but their OpenVMM executables are currently -approximately 457-460 MiB before archive compression. +`build.bat --with-nvx` enables the incomplete x64 Windows/WHP foundation. This +path is additive and does not replace the existing `--with-microvm` packaging +path. + +The build pins the exact `microsoft/nvx` release tag, platform asset name, and +per-file SHA-256 checksums in the repository. It downloads and verifies the +platform archive during the build, reuses the verified Cargo-output cache, and +stages the files beside the MXC executor. Sandbox execution never downloads +artifacts. `NVX_BIN` is an offline override for a pre-fetched bundle directory; +its files must pass the same checksum validation. + +The current published pin is `v0.1.0-dev.5c86da3dff02`. Its Windows/WHP asset +contains only the platform files (`openvmm.exe`, `vmlinux`, and +`initramfs.cpio.gz`); it does not contain the distro/runtime EROFS images or +writable scratch image required to run a workload. ## Remaining MXC integration work -The following work belongs primarily in the MXC repository. It is separate -from the remaining NVX runtime policy gaps above. - -| Workstream | Estimate | -| --- | ---: | -| Add `nvx` containment, schema, wire, and policy/model types | 2-3 engineer-days | -| Implement state-aware and one-shot engine dispatch that owns OpenVMM | 7-10 engineer-days | -| Adapt guest streams, terminal outcomes, cancellation, and typed errors | 3-5 engineer-days | -| Expose NVX through applicable Rust, TypeScript, C#, and FFI surfaces | 4-6 engineer-days | -| Replace the NanVix artifact pipeline with pinned NVX acquisition, packaging, discovery, and capability probing | 3-5 engineer-days | -| Add MXC-native E2E automation and CI coverage | 5-8 engineer-days | -| **Total** | **24-37 engineer-days** | +The schema/wire, policy/model, typed-unavailable dispatch, and pinned Windows +artifact-acquisition foundations are now present without replacing NanVix. +PR2/runtime remains blocked on all of the following: + +- NVX-produced distro and runtime EROFS images plus a writable scratch image; +- a proven combined managed-sandbox/virtio-fs contract; and +- the required WHP runner. + +After those inputs are available, MXC still needs the state-aware and one-shot +OpenVMM runtime, guest streams and lifecycle outcomes, applicable SDK/FFI +surfaces, capability advertisement, and MXC-native E2E/CI coverage. ### E2E testing plan diff --git a/schemas/dev/mxc-config.schema.0.9.0-alpha.json b/schemas/dev/mxc-config.schema.0.9.0-alpha.json index 0abca8741..b482ca1d0 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-alpha.json +++ b/schemas/dev/mxc-config.schema.0.9.0-alpha.json @@ -837,6 +837,13 @@ ], "type": "string" }, + { + "description": "Nvx", + "enum": [ + "nvx" + ], + "type": "string" + }, { "description": "Hyperlight", "enum": [ diff --git a/schemas/dev/mxc-config.schema.0.9.0-dev.json b/schemas/dev/mxc-config.schema.0.9.0-dev.json index abd0a78d6..4f42a2b80 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.9.0-dev.json @@ -150,6 +150,13 @@ ], "type": "string" }, + { + "description": "NVX micro-VM (experimental).", + "enum": [ + "nvx" + ], + "type": "string" + }, { "description": "Hyperlight micro-VM (experimental).", "enum": [ diff --git a/sdk/node/src/generated/v0_9_0_alpha/wire.ts b/sdk/node/src/generated/v0_9_0_alpha/wire.ts index 4900947d8..74429184e 100644 --- a/sdk/node/src/generated/v0_9_0_alpha/wire.ts +++ b/sdk/node/src/generated/v0_9_0_alpha/wire.ts @@ -389,7 +389,7 @@ export interface NetworkRule { export type NonEmptyString = string; -export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "vm" | "windows_sandbox" | "microvm" | "hyperlight" | "wslc" | "isolation_session"; +export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "vm" | "windows_sandbox" | "microvm" | "nvx" | "hyperlight" | "wslc" | "isolation_session"; /** * Experimental settings. diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index 9d7c221cd..f75ec3100 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -69,7 +69,7 @@ export type ClipboardPolicy = "none" | "read" | "write" | "all"; /** * Containment backend (abstract intent or concrete backend). */ -export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "microvm" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap"; +export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "microvm" | "nvx" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap"; /** * Experimental features (only honored with `--experimental`). This block is intentionally **permissive** (no `deny_unknown_fields`): experimental backends are in flux, so the schema documents the known shapes for editor help without rejecting in-progress fields. The strict, closed contract is the stable (top-level) surface. From 99c7f248a16671c052fc8baba6e5174f2a28f04f Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Thu, 17 Sep 2026 17:37:47 -0700 Subject: [PATCH 16/41] fix: enforce NVX x64 packaging Co-authored-by: GitHub Copilot --- src/backends/nvx/binaries/build.rs | 16 +++++-- src/backends/nvx/build_common/src/lib.rs | 60 ++++++++++++++++++++++++ src/core/wxc/build.rs | 16 ++++++- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/backends/nvx/binaries/build.rs b/src/backends/nvx/binaries/build.rs index 8824b3d2b..71919ab81 100644 --- a/src/backends/nvx/binaries/build.rs +++ b/src/backends/nvx/binaries/build.rs @@ -23,11 +23,7 @@ fn main() { return; } - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - if target_os != "windows" { - emit_disabled_metadata(); - return; - } + ensure_supported_target(); let out_dir = PathBuf::from( std::env::var_os("OUT_DIR").expect("nvx_binaries: OUT_DIR is not set by Cargo"), @@ -77,6 +73,16 @@ fn main() { emit_metadata(&bin_dir, workload_images_available); } +fn ensure_supported_target() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); + let target = std::env::var("TARGET") + .unwrap_or_else(|_| format!("{target_arch}-pc-{target_os}-{target_env}")); + nvx_build_common::validate_nvx_target(&target, &target_os, &target_arch, &target_env) + .unwrap_or_else(|error| panic!("nvx_binaries: {error}")); +} + fn emit_disabled_metadata() { let out_dir = std::env::var("OUT_DIR").expect("nvx_binaries: OUT_DIR is not set by Cargo"); println!("cargo:rustc-env=NVX_BIN_DIR={out_dir}"); diff --git a/src/backends/nvx/build_common/src/lib.rs b/src/backends/nvx/build_common/src/lib.rs index 031df9738..a7c2dbbc2 100644 --- a/src/backends/nvx/build_common/src/lib.rs +++ b/src/backends/nvx/build_common/src/lib.rs @@ -8,6 +8,9 @@ use std::path::{Path, PathBuf}; use nvx_common::{WINDOWS_PLATFORM_ARTIFACTS, WORKLOAD_IMAGE_ARTIFACTS}; +/// The only supported NVX package target triple. +pub const SUPPORTED_NVX_TARGET_TRIPLE: &str = "x86_64-pc-windows-msvc"; + /// Category of an NVX release artifact. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ArtifactKind { @@ -29,6 +32,38 @@ pub fn artifact_rel_paths() -> impl Iterator bool { + target_os == "windows" && target_arch == "x86_64" && target_env == "msvc" +} + +/// Returns `Ok(())` for the only supported NVX target, or a clear error +/// message for anything else. +pub fn validate_nvx_target( + target: &str, + target_os: &str, + target_arch: &str, + target_env: &str, +) -> Result<(), String> { + if target_supports_nvx(target_os, target_arch, target_env) { + Ok(()) + } else { + Err(unsupported_target_message(target)) + } +} + +/// Formats the explicit error message used when NVX is requested for an +/// unsupported target. +pub fn unsupported_target_message(target: &str) -> String { + format!( + "nvx packaging is only supported for target {SUPPORTED_NVX_TARGET_TRIPLE}; current target is {target}" + ) +} + /// Returns the artifacts required by the currently configured release. pub fn available_artifact_rel_paths( workload_images_available: bool, @@ -257,4 +292,29 @@ mod tests { assert!(!target.path().join(relative_path).exists()); } } + + #[test] + fn target_support_only_allows_windows_x86_64_msvc() { + assert!(target_supports_nvx("windows", "x86_64", "msvc")); + assert!(!target_supports_nvx("windows", "x86_64", "gnu")); + assert!(!target_supports_nvx("windows", "aarch64", "msvc")); + assert!(!target_supports_nvx("linux", "x86_64", "gnu")); + } + + #[test] + fn unsupported_target_message_names_supported_triple() { + let message = unsupported_target_message("aarch64-pc-windows-msvc"); + + assert!(message.contains(SUPPORTED_NVX_TARGET_TRIPLE)); + assert!(message.contains("aarch64-pc-windows-msvc")); + } + + #[test] + fn validate_nvx_target_rejects_unsupported_target() { + let error = validate_nvx_target("aarch64-pc-windows-msvc", "windows", "aarch64", "msvc") + .expect_err("unsupported target must fail closed"); + + assert!(error.contains(SUPPORTED_NVX_TARGET_TRIPLE)); + assert!(error.contains("aarch64-pc-windows-msvc")); + } } diff --git a/src/core/wxc/build.rs b/src/core/wxc/build.rs index fa03059f7..e28c6c765 100644 --- a/src/core/wxc/build.rs +++ b/src/core/wxc/build.rs @@ -13,7 +13,10 @@ fn main() { copy_nanvix_binaries(); #[cfg(all(windows, feature = "nvx"))] - copy_nvx_binaries(); + { + ensure_supported_nvx_target(); + copy_nvx_binaries(); + } // Delay-load winhvplatform.dll so WHP-less hosts don't crash before main(). // CARGO_CFG_TARGET_* (not #[cfg]) because build.rs cfg gates are host, not target. @@ -100,6 +103,17 @@ fn copy_nanvix_binaries() { nanvix_build_common::stage_artifacts_next_to_exe(Path::new(&nanvix_bin_dir)); } +#[cfg(all(windows, feature = "nvx"))] +fn ensure_supported_nvx_target() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); + let target = std::env::var("TARGET") + .unwrap_or_else(|_| format!("{target_arch}-pc-{target_os}-{target_env}")); + nvx_build_common::validate_nvx_target(&target, &target_os, &target_arch, &target_env) + .unwrap_or_else(|error| panic!("wxc build.rs: {error}")); +} + #[cfg(all(windows, feature = "nvx"))] fn copy_nvx_binaries() { use std::path::Path; From a30d1747f09009d770fbd63cefdfb0d0a743e8a7 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 11:13:01 -0700 Subject: [PATCH 17/41] refactor: remove microvm contract Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../mxc_config_contract/src/dev/one_shot.rs | 2 - .../tests/v0_9_0_alpha/enums.rs | 12 +++++- .../tests/version_boundaries/containment.rs | 1 - src/core/wxc_common/src/cmdline.rs | 1 - .../config_contract_adapters/dev/one_shot.rs | 1 - .../dev/one_shot_tests/experimental.rs | 4 -- src/core/wxc_common/src/config_parser.rs | 37 ++++++++++++++++--- src/core/wxc_common/src/models.rs | 6 --- src/core/wxc_common/src/wire.rs | 2 - 9 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/core/mxc_config_contract/src/dev/one_shot.rs b/src/core/mxc_config_contract/src/dev/one_shot.rs index 070af5637..dc8f1e12f 100644 --- a/src/core/mxc_config_contract/src/dev/one_shot.rs +++ b/src/core/mxc_config_contract/src/dev/one_shot.rs @@ -31,8 +31,6 @@ string_enum! { Vm => ["vm"], /// Select the Windows Sandbox backend. WindowsSandbox => ["windows_sandbox"], - /// Select the NanVix micro-VM backend. - Microvm => ["microvm"], /// Select the NVX micro-VM backend. Nvx => ["nvx"], /// Select the Hyperlight micro-VM backend. diff --git a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs index 73f89fb93..7a6e64266 100644 --- a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs +++ b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs @@ -15,7 +15,6 @@ fn accepts_every_containment_value() { "seatbelt", "vm", "windows_sandbox", - "microvm", "nvx", "hyperlight", "isolation_session", @@ -33,6 +32,17 @@ fn accepts_every_containment_value() { } } +#[test] +fn rejects_removed_microvm_containment_value() { + assert_invalid( + r#"{ + "version": "0.9.0-alpha", + "containment": "microvm", + "process": {"commandLine": "echo"} + }"#, + ); +} + #[test] fn rejects_invalid_containment_value() { assert_invalid( diff --git a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs index 4846c0baf..5ba23bb0e 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs @@ -8,7 +8,6 @@ fn development_containment_values_are_introduced_in_v09() { for containment in [ "vm", "windows_sandbox", - "microvm", "nvx", "hyperlight", "isolation_session", diff --git a/src/core/wxc_common/src/cmdline.rs b/src/core/wxc_common/src/cmdline.rs index 3b46f8951..707e7841d 100644 --- a/src/core/wxc_common/src/cmdline.rs +++ b/src/core/wxc_common/src/cmdline.rs @@ -45,7 +45,6 @@ impl CommandLineContext { | ContainmentBackend::Bubblewrap => Self::PosixShell, ContainmentBackend::ProcessContainer | ContainmentBackend::Vm - | ContainmentBackend::MicroVm | ContainmentBackend::Hyperlight => Self::WindowsCreateProcess, ContainmentBackend::Nvx => Self::PosixShell, } diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs index 34eb32a25..0bec35e73 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs @@ -16,7 +16,6 @@ fn convert_containment(value: contract::OneShotContainment) -> wire::Containment contract::OneShotContainment::Seatbelt => wire::Containment::Seatbelt, contract::OneShotContainment::Vm => wire::Containment::Vm, contract::OneShotContainment::WindowsSandbox => wire::Containment::WindowsSandbox, - contract::OneShotContainment::Microvm => wire::Containment::Microvm, contract::OneShotContainment::Nvx => wire::Containment::Nvx, contract::OneShotContainment::Hyperlight => wire::Containment::Hyperlight, contract::OneShotContainment::Wslc => wire::Containment::Wslc, diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs index 55462e641..9ce2eacd3 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs @@ -187,10 +187,6 @@ const DEVELOPMENT_CONTAINMENT_CASES: &[DevelopmentContainmentCase] = &[ input: "windows_sandbox", expected: "windows_sandbox", }, - DevelopmentContainmentCase { - input: "microvm", - expected: "microvm", - }, DevelopmentContainmentCase { input: "hyperlight", expected: "hyperlight", diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index 6219a9add..09bc3f2d9 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -72,6 +72,8 @@ struct RequestDiscriminator<'a> { phase: Option<&'a RawValue>, #[serde(borrow, default, deserialize_with = "deserialize_present_raw")] experimental: Option<&'a RawValue>, + #[serde(borrow, default, deserialize_with = "deserialize_present_raw")] + containment: Option<&'a RawValue>, } fn deserialize_present_raw<'de, D>(deserializer: D) -> Result, D::Error> @@ -81,6 +83,20 @@ where <&RawValue>::deserialize(deserializer).map(Some) } +fn reject_removed_microvm(containment: Option<&RawValue>) -> Result<(), WxcError> { + if containment + .and_then(|raw| serde_json::from_str::(raw.get()).ok()) + .as_deref() + == Some("microvm") + { + return Err(WxcError::ConfigParse( + "containment \"microvm\" was removed; use \"nvx\" for the NVX micro-VM backend" + .to_string(), + )); + } + Ok(()) +} + fn reject_legacy_telemetry_raw(experimental: Option<&str>) -> Result<(), WxcError> { let Some(experimental) = experimental else { return Ok(()); @@ -165,6 +181,7 @@ pub fn load_request_with_options( let json_str = decode_request_input(input, opts.is_base64)?; let discriminator: RequestDiscriminator<'_> = config_deserialize::from_str(&json_str) .map_err(|error| WxcError::ConfigParse(error.to_string()))?; + reject_removed_microvm(discriminator.containment)?; reject_legacy_telemetry_raw(discriminator.experimental.map(|raw| raw.get()))?; let cfg: wire::MxcConfig = config_deserialize::from_str(&json_str) @@ -218,6 +235,7 @@ pub(crate) fn load_request_from_json_with_options( .to_string(), )); } + reject_removed_microvm(discriminator.containment)?; reject_legacy_telemetry_raw(discriminator.experimental.map(|raw| raw.get()))?; let cfg: wire::MxcConfig = config_deserialize::from_str(json_str) @@ -369,6 +387,7 @@ fn parse_mxc_request_json( .map(MxcRequest::StateAware) .map_err(|e| ParseError::StateAware(MxcError::malformed_request(e.to_string()))) } else { + reject_removed_microvm(discriminator.containment).map_err(ParseError::OneShot)?; reject_legacy_telemetry_raw(discriminator.experimental.map(|raw| raw.get())) .map_err(ParseError::OneShot)?; let cfg: wire::MxcConfig = config_deserialize::from_str(json_str).map_err(|error| { @@ -817,7 +836,6 @@ fn requested_sandbox_kind(c: Option<&wire::Containment>) -> &'static str { Some(wire::Containment::Vm) => "vm", Some(wire::Containment::WindowsSandbox) => "windows_sandbox", Some(wire::Containment::Lxc) => "lxc", - Some(wire::Containment::Microvm) => "microvm", Some(wire::Containment::Nvx) => "nvx", Some(wire::Containment::Hyperlight) => "hyperlight", Some(wire::Containment::Wslc) => "wslc", @@ -4882,13 +4900,20 @@ mod tests { } #[test] - fn containment_microvm_accepted() { - let json = r#"{"process": {"commandLine": "echo hi"}, "containment": "microvm"}"#; - let encoded = base64_encode(json.as_bytes()); + fn containment_microvm_rejected_with_nvx_migration_guidance() { + let json = r#"{ + "version": "0.9.0-alpha", + "process": {"commandLine": "echo hi"}, + "containment": "microvm" + }"#; let mut logger = test_logger(); - let req = load_request(&encoded, &mut logger, true).unwrap(); - assert_eq!(req.containment, ContainmentBackend::MicroVm); + let error = load_request_from_json(json, &mut logger) + .expect_err("removed microvm containment must be rejected"); + let message = error.to_string(); + assert!(message.contains("microvm")); + assert!(message.contains("nvx")); + assert!(message.contains("removed")); } #[test] diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index 9f884ea18..35413f90b 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -25,9 +25,6 @@ pub enum ContainmentBackend { Lxc, /// VM-based isolation. Vm, - /// MicroVM isolation via Windows Hypervisor Platform (internally powered by NanVix). - #[serde(rename = "microvm")] - MicroVm, /// NVX Linux micro-VM hosted by OpenVMM. Nvx, /// MicroVM isolation via Hyperlight + Unikraft, using an embedded @@ -60,7 +57,6 @@ impl ContainmentBackend { ContainmentBackend::Wslc => "wslc", ContainmentBackend::Lxc => "lxc", ContainmentBackend::Vm => "vm", - ContainmentBackend::MicroVm => "microvm", ContainmentBackend::Nvx => "nvx", ContainmentBackend::Hyperlight => "hyperlight", ContainmentBackend::WindowsSandbox => "windows_sandbox", @@ -83,7 +79,6 @@ impl ContainmentBackend { ContainmentBackend::IsolationSession => Some("experimental.isolation_session"), ContainmentBackend::Bubblewrap | ContainmentBackend::Hyperlight - | ContainmentBackend::MicroVm | ContainmentBackend::Nvx | ContainmentBackend::Vm => None, } @@ -128,7 +123,6 @@ impl From for ContainmentBackend { } W::WindowsSandbox => Self::WindowsSandbox, W::Lxc => Self::Lxc, - W::Microvm => Self::MicroVm, W::Nvx => Self::Nvx, W::Hyperlight => Self::Hyperlight, W::Wslc => Self::Wslc, diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index f09efae79..153e3806d 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -139,8 +139,6 @@ pub enum Containment { WindowsSandbox, /// Full Linux container. Lxc, - /// NanVix micro-VM (experimental). - Microvm, /// NVX micro-VM (experimental). Nvx, /// Hyperlight micro-VM (experimental). From c83a3edadf6ac6d3446c3fd1d5714510f9cc1953 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 11:16:10 -0700 Subject: [PATCH 18/41] build: remove NanVix runtime and packaging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Build.Windows.Job.yml | 4 +- .github/workflows/CodeQL.yml | 2 +- build.bat | 24 - build.sh | 9 - src/Cargo.lock | 41 - src/Cargo.toml | 5 - src/backends/hyperlight/common/src/lib.rs | 2 +- src/backends/nanvix/binaries/Cargo.toml | 25 - src/backends/nanvix/binaries/build.rs | 837 ---------- src/backends/nanvix/binaries/checksums.json | 14 - src/backends/nanvix/binaries/src/lib.rs | 10 - src/backends/nanvix/binaries/versions.json | 17 - src/backends/nanvix/build_common/Cargo.toml | 9 - src/backends/nanvix/build_common/src/lib.rs | 363 ----- src/backends/nanvix/common/Cargo.toml | 10 - src/backends/nanvix/common/src/lib.rs | 293 ---- src/backends/nanvix/runner/Cargo.toml | 16 - src/backends/nanvix/runner/src/lib.rs | 1576 ------------------- src/core/lxc/Cargo.toml | 4 - src/core/lxc/build.rs | 25 +- src/core/lxc/src/main.rs | 2 +- src/core/mxc_engine/Cargo.toml | 6 +- src/core/mxc_engine/src/dispatch.rs | 2 +- src/core/mxc_engine/src/platform.rs | 2 +- src/core/mxc_engine/src/probe.rs | 2 +- src/core/mxc_engine/src/run.rs | 40 +- src/core/wxc/Cargo.toml | 3 - src/core/wxc/build.rs | 21 - src/core/wxc/src/main.rs | 2 +- src/core/wxc_common/Cargo.toml | 3 - src/core/wxc_common/src/lib.rs | 2 - src/core/wxc_common/src/microvm_staging.rs | 1102 ------------- src/testing/fuzz/Cargo.toml | 2 - src/testing/fuzz/fuzz_targets/validator.rs | 6 - 34 files changed, 12 insertions(+), 4469 deletions(-) delete mode 100644 src/backends/nanvix/binaries/Cargo.toml delete mode 100644 src/backends/nanvix/binaries/build.rs delete mode 100644 src/backends/nanvix/binaries/checksums.json delete mode 100644 src/backends/nanvix/binaries/src/lib.rs delete mode 100644 src/backends/nanvix/binaries/versions.json delete mode 100644 src/backends/nanvix/build_common/Cargo.toml delete mode 100644 src/backends/nanvix/build_common/src/lib.rs delete mode 100644 src/backends/nanvix/common/Cargo.toml delete mode 100644 src/backends/nanvix/common/src/lib.rs delete mode 100644 src/backends/nanvix/runner/Cargo.toml delete mode 100644 src/backends/nanvix/runner/src/lib.rs delete mode 100644 src/core/wxc_common/src/microvm_staging.rs diff --git a/.github/workflows/Build.Windows.Job.yml b/.github/workflows/Build.Windows.Job.yml index a9d488188..59f1d4b46 100644 --- a/.github/workflows/Build.Windows.Job.yml +++ b/.github/workflows/Build.Windows.Job.yml @@ -14,11 +14,11 @@ jobs: runner: windows-2025 target: x86_64-pc-windows-msvc # tier2_bfs must NOT ship on Win 11 25H2 (bfscfg.exe hangs the host). - features: hyperlight isolation_session microvm wslc + features: hyperlight isolation_session wslc - arch: arm64 runner: windows-11-arm target: aarch64-pc-windows-msvc - # Hyperlight and MicroVM runtimes are x64-only. + # Hyperlight is x64-only. features: isolation_session wslc runs-on: ${{ matrix.runner }} defaults: diff --git a/.github/workflows/CodeQL.yml b/.github/workflows/CodeQL.yml index 7855393d7..23c65379d 100644 --- a/.github/workflows/CodeQL.yml +++ b/.github/workflows/CodeQL.yml @@ -55,7 +55,7 @@ jobs: # surface the pinned toolchain and the MxcDependencies feed so that # dependency resolution succeeds (the private feed 401s otherwise). No # `cargo build` is run, so the feature-gated backends' build workarounds - # (hyperlight/microvm/windows_sandbox) are irrelevant here. + # (hyperlight/nvx/windows_sandbox) are irrelevant here. - name: Surface toolchain file at repo root if: matrix.language == 'rust' shell: bash diff --git a/build.bat b/build.bat index 7710cdeb1..0bb5026fb 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,6 @@ setlocal enabledelayedexpansion set "BUILD_CONFIG=release" set "BUILD_ARCH=" set "BUILD_ALL=0" -set "WITH_NANVIX=0" set "WITH_NVX=0" set "WITH_WSLC=0" set "WITH_ISOLATION_SESSION=0" @@ -19,7 +18,6 @@ if /i "%~1"=="--release" ( set "BUILD_CONFIG=release" & shift & goto :parse_arg if /i "%~1"=="--x64" ( set "BUILD_ARCH=x86_64-pc-windows-msvc" & shift & goto :parse_args ) if /i "%~1"=="--arm64" ( set "BUILD_ARCH=aarch64-pc-windows-msvc" & shift & goto :parse_args ) if /i "%~1"=="--all" ( set "BUILD_ALL=1" & shift & goto :parse_args ) -if /i "%~1"=="--with-microvm" ( set "WITH_NANVIX=1" & shift & goto :parse_args ) if /i "%~1"=="--with-nvx" ( set "WITH_NVX=1" & shift & goto :parse_args ) if /i "%~1"=="--with-wslc" ( set "WITH_WSLC=1" & shift & goto :parse_args ) if /i "%~1"=="--with-isolation-session" ( set "WITH_ISOLATION_SESSION=1" & shift & goto :parse_args ) @@ -57,7 +55,6 @@ if "%BUILD_CONFIG%"=="release" set "CARGO_FLAGS=--release --target" :: workspace feature flags above, so it uses its own profile/target-only flags. set "PLM_FLAGS=--target" if "%BUILD_CONFIG%"=="release" set "PLM_FLAGS=--release --target" -if "%WITH_NANVIX%"=="1" set "CARGO_FLAGS=--features microvm %CARGO_FLAGS%" if "%WITH_NVX%"=="1" set "CARGO_FLAGS=--features nvx %CARGO_FLAGS%" if "%WITH_WSLC%"=="1" set "CARGO_FLAGS=--features wslc %CARGO_FLAGS%" if "%WITH_ISOLATION_SESSION%"=="1" set "CARGO_FLAGS=--features isolation_session %CARGO_FLAGS%" @@ -136,26 +133,6 @@ for %%T in (x86_64-pc-windows-msvc aarch64-pc-windows-msvc) do ( copy /Y "!BIN_DIR!\plm.exe" "sdk\node\bin\!SDK_ARCH!\" >nul echo Copied !SDK_ARCH!\plm.exe ) - if "%WITH_NANVIX%"=="1" ( - for %%B in (nanvixd.exe nanvix_rootfs.img python3.initrd) do ( - if exist "!BIN_DIR!\%%B" ( - copy /Y "!BIN_DIR!\%%B" "sdk\node\bin\!SDK_ARCH!\" >nul - echo Copied !SDK_ARCH!\%%B - ) - ) - if exist "!BIN_DIR!\bin\kernel.elf" ( - if not exist "sdk\node\bin\!SDK_ARCH!\bin" mkdir "sdk\node\bin\!SDK_ARCH!\bin" - copy /Y "!BIN_DIR!\bin\kernel.elf" "sdk\node\bin\!SDK_ARCH!\bin\" >nul - echo Copied !SDK_ARCH!\bin\kernel.elf - ) - for %%S in (kernel.vmem kernel.whp.cbor) do ( - if exist "!BIN_DIR!\snapshots\%%S" ( - if not exist "sdk\node\bin\!SDK_ARCH!\snapshots" mkdir "sdk\node\bin\!SDK_ARCH!\snapshots" - copy /Y "!BIN_DIR!\snapshots\%%S" "sdk\node\bin\!SDK_ARCH!\snapshots\" >nul - echo Copied !SDK_ARCH!\snapshots\%%S - ) - ) - ) if "!COPY_WSLC_RUNTIME!"=="1" ( if "%WITH_WSLC%"=="1" ( for %%B in (wxc-wslc-daemon.exe wslcsdk.dll) do ( @@ -287,7 +264,6 @@ echo --release Build release configuration echo --x64 Build for x64 only echo --arm64 Build for ARM64 only echo --all Build for both x64 and ARM64 -echo --with-microvm Download and include NanVix micro-VM binaries echo --with-nvx Add the incomplete NVX foundation and platform artifacts (x64) echo Runtime preflight remains unavailable until NVX publishes echo the workload image bundle diff --git a/build.sh b/build.sh index 16f8f99e7..d7cc6904a 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,6 @@ BUILD_TYPE="release" BUILD_SDK=true WITH_HYPERLIGHT=false -WITH_MICROVM=false while [[ $# -gt 0 ]]; do case $1 in @@ -29,10 +28,6 @@ while [[ $# -gt 0 ]]; do WITH_HYPERLIGHT=true shift ;; - --with-microvm) - WITH_MICROVM=true - shift - ;; --help|-h) echo "Usage: build.sh [OPTIONS]" echo "" @@ -40,7 +35,6 @@ while [[ $# -gt 0 ]]; do echo " --debug Build in debug mode (default: release)" echo " --rust-only Only build Rust binaries, skip SDK" echo " --with-hyperlight Build with Hyperlight (micro-VM) backend (x86_64 only)" - echo " --with-microvm Build with NanVix MicroVM backend (KVM required at runtime)" echo " -h, --help Show this help message" exit 0 ;; @@ -77,9 +71,6 @@ FEATURES_LIST=() if [ "$WITH_HYPERLIGHT" = true ]; then FEATURES_LIST+=(hyperlight) fi -if [ "$WITH_MICROVM" = true ]; then - FEATURES_LIST+=(microvm) -fi if [ ${#FEATURES_LIST[@]} -gt 0 ]; then CARGO_FEATURES=(--features "$(IFS=,; echo "${FEATURES_LIST[*]}")") fi diff --git a/src/Cargo.lock b/src/Cargo.lock index bc5cf81d4..b97b0580a 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -1396,8 +1396,6 @@ dependencies = [ "lxc_common", "mxc_build_common", "mxc_engine", - "nanvix_binaries", - "nanvix_build_common", "wxc_common", ] @@ -1547,7 +1545,6 @@ dependencies = [ "isolation_session_common", "learning_mode_core", "lxc_common", - "nanvix_runner", "nvx_runner", "plm", "seatbelt_common", @@ -1605,40 +1602,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "nanvix_binaries" -version = "0.8.0" -dependencies = [ - "nanvix_build_common", - "nanvix_common", -] - -[[package]] -name = "nanvix_build_common" -version = "0.8.0" -dependencies = [ - "nanvix_common", -] - -[[package]] -name = "nanvix_common" -version = "0.8.0" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "nanvix_runner" -version = "0.8.0" -dependencies = [ - "libc", - "nanvix_common", - "uuid", - "windows", - "wxc_common", -] - [[package]] name = "nix" version = "0.29.0" @@ -3221,8 +3184,6 @@ dependencies = [ "learning_mode_core", "mxc_build_common", "mxc_engine", - "nanvix_binaries", - "nanvix_build_common", "nvx_binaries", "nvx_build_common", "plm", @@ -3245,7 +3206,6 @@ dependencies = [ "mxc_config_contract", "mxc_schema_support", "mxc_telemetry", - "nanvix_common", "schemars", "semver", "serde", @@ -3256,7 +3216,6 @@ dependencies = [ "thiserror", "unicode-general-category", "url", - "uuid", "widestring", "windows", "windows-core", diff --git a/src/Cargo.toml b/src/Cargo.toml index cd3354392..2164ce53d 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -7,10 +7,6 @@ members = [ "backends/isolation_session/common", "backends/learning_mode/windows", "backends/lxc/common", - "backends/nanvix/binaries", - "backends/nanvix/build_common", - "backends/nanvix/common", - "backends/nanvix/runner", "backends/nvx/binaries", "backends/nvx/build_common", "backends/nvx/common", @@ -90,7 +86,6 @@ mxc_pty = { path = "core/mxc_pty" } mxc_schema_support = { path = "core/mxc_schema_support" } mxc-sdk = { path = "core/mxc-sdk" } mxc_telemetry = { path = "mxc_telemetry" } -nanvix_runner = { path = "backends/nanvix/runner" } nix = { version = "0.29", features = ["fs", "mount", "sched", "signal", "net", "process", "user", "term"] } nvx_binaries = { path = "backends/nvx/binaries" } nvx_build_common = { path = "backends/nvx/build_common" } diff --git a/src/backends/hyperlight/common/src/lib.rs b/src/backends/hyperlight/common/src/lib.rs index b313ad088..0b7a43fd4 100644 --- a/src/backends/hyperlight/common/src/lib.rs +++ b/src/backends/hyperlight/common/src/lib.rs @@ -6,7 +6,7 @@ // `hyperlight` cargo feature. On other targets, or when the feature is // disabled, this crate compiles to an empty library so workspace builds // (`cargo build --workspace`) on ARM64 hosts and feature-less builds -// (e.g. `cargo build --features microvm`) succeed without pulling in +// (e.g. `cargo build --features nvx`) succeed without pulling in // hyperlight-host. Consumers gate their use of `HyperlightScriptRunner` // on `target_arch = "x86_64"` and the `hyperlight` cargo feature. #![cfg(all(feature = "hyperlight", target_arch = "x86_64"))] diff --git a/src/backends/nanvix/binaries/Cargo.toml b/src/backends/nanvix/binaries/Cargo.toml deleted file mode 100644 index 8b71c06e3..000000000 --- a/src/backends/nanvix/binaries/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "nanvix_binaries" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Build-time download of NanVix binaries from GitHub releases" -links = "nanvix_binaries" - -[lib] -path = "src/lib.rs" - -[features] -# Gates the build script's expensive work (downloading NanVix release assets -# and verifying their checksums). OFF by default so a plain `cargo build` — -# which still compiles this crate as a workspace member — performs no network -# or hashing work. Enabled transitively via the `microvm` feature of `wxc` and -# `lxc`. -microvm = [] - -[dependencies] -nanvix_common = { path = "../common" } - -[build-dependencies] -nanvix_common = { path = "../common" } -nanvix_build_common = { path = "../build_common" } diff --git a/src/backends/nanvix/binaries/build.rs b/src/backends/nanvix/binaries/build.rs deleted file mode 100644 index c1bb66955..000000000 --- a/src/backends/nanvix/binaries/build.rs +++ /dev/null @@ -1,837 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Build script that downloads NanVix binaries from GitHub releases. -//! -//! Uses system tools (`curl.exe`, `tar.exe`, `certutil`) instead of Rust -//! crates. Zero HTTP/zip/crypto build-dependencies — only `nanvix_common` -//! for shared constants and serde-based config parsing. -//! -//! ## Configuration files -//! -//! - `versions.json` — pinned release tags and exact asset names -//! - `checksums.json` — SHA256 hashes for integrity verification -//! -//! ## Environment variables -//! -//! - `GITHUB_TOKEN` / `GH_TOKEN` — optional; increases API rate limit -//! - `NANVIX_BIN` — optional; path to a directory of pre-fetched NanVix -//! binaries. When set, the build uses that directory directly and performs -//! no network downloads, enabling fully offline builds where all dynamic -//! build inputs are pre-fetched. The directory must already contain the -//! required binaries (flat files plus the `bin/` subdirectory); checksums -//! are still verified against `checksums.json`. -//! -//! ## Caching -//! -//! Binaries are cached in OUT_DIR (or read from `NANVIX_BIN` when set). -//! Checksums are verified whenever this build script runs (triggered by -//! changes to build.rs, versions.json, or checksums.json) to catch corrupted -//! or truncated files. -//! -//! # TODO(security): NanVix binaries are not ESRP-signed. Before shipping in -//! # official MXC releases, either extend ESRP to cover these binaries or -//! # establish an internal mirror with supply-chain controls. - -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; - -use nanvix_common::{github_download_url, load_checksums, load_json, ReleaseConfig, RepoConfig}; - -fn main() { - // The build script's output (`NANVIX_BIN_DIR` and whether the download / - // verify path runs) depends on the `microvm` feature, surfaced here as the - // `CARGO_FEATURE_MICROVM` env var. Declare it as a rerun trigger so toggling - // the feature between builds re-runs this script instead of reusing stale - // output. - println!("cargo:rerun-if-env-changed=CARGO_FEATURE_MICROVM"); - - // The expensive work in this build script — downloading NanVix release - // assets and verifying their checksums via `certutil` — is only needed - // when the micro-VM backend is actually being built. Gate it behind this - // crate's `microvm` feature so that a default `cargo build` (which still - // compiles this crate as a workspace member) performs no network or hashing - // work. `wxc` and `lxc` enable `nanvix_binaries/microvm` through their own - // `microvm` features. - // - // `NANVIX_BIN_DIR` must still be emitted in every configuration because - // `lib.rs` references it via `env!`. - if std::env::var_os("CARGO_FEATURE_MICROVM").is_none() { - let out_dir = std::env::var("OUT_DIR").unwrap(); - println!("cargo:rustc-env=NANVIX_BIN_DIR={}", out_dir); - println!("cargo:rerun-if-changed=build.rs"); - return; - } - - // Check the TARGET platform (not host). NanVix binaries are only needed when - // the output binary will run on Windows or Linux with KVM. - let target = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); - if target != "windows" && target != "linux" { - let out_dir = std::env::var("OUT_DIR").unwrap(); - println!("cargo:rustc-env=NANVIX_BIN_DIR={}", out_dir); - println!("cargo:rerun-if-changed=build.rs"); - return; - } - - let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); - - // When `NANVIX_BIN` is set, use the caller-provided directory of - // pre-fetched binaries and skip all network downloads (offline builds); - // otherwise download into a subdirectory of OUT_DIR as before. The - // resolution lives in the build-only `nanvix_build_common` crate. - let (bin_dir, use_prefetched_binaries) = nanvix_build_common::resolve_bin_dir(&out_dir); - - let versions: ReleaseConfig = load_json("versions.json"); - let checksums: HashMap = load_checksums("checksums.json", &target); - - if target == "linux" { - // Linux: download tar.gz and extract Linux binaries. - let asset = versions - .nanvix_python - .asset_linux - .as_deref() - .unwrap_or("microvm-standalone-256mb.tar.gz"); - let binaries: Vec<&str> = versions - .nanvix_python - .binaries_linux - .as_ref() - .map(|v| v.iter().map(|s| s.as_str()).collect()) - .unwrap_or_else(|| nanvix_common::REQUIRED_BINARIES.to_vec()); - - let needs_download = - !use_prefetched_binaries && needs_download_linux(&binaries, &bin_dir, &checksums); - if needs_download { - eprintln!( - "nanvix_binaries: downloading nanvix/nanvix-python {} (Linux)...", - versions.nanvix_python.tag - ); - download_and_extract_linux(&versions.nanvix_python.tag, asset, &binaries, &bin_dir); - } else if use_prefetched_binaries { - eprintln!( - "nanvix_binaries: offline — verifying pre-fetched Linux binaries in '{}'", - bin_dir.display() - ); - } else { - eprintln!("nanvix_binaries: all Linux binaries cached and verified"); - } - - verify_checksums_linux(&binaries, &bin_dir, &checksums); - verify_bin_subdir_checksums_linux(&bin_dir, &checksums); - - if use_prefetched_binaries { - nanvix_build_common::emit_rerun_for_copied_artifacts(&bin_dir); - } - } else { - // Windows: original logic - let all_binaries: Vec<&str> = versions - .nanvix_python - .binaries - .iter() - .map(|s| s.as_str()) - .collect(); - - let needs_nanvix_python = !use_prefetched_binaries - && needs_download(&versions.nanvix_python, &bin_dir, &checksums); - - if needs_nanvix_python { - eprintln!( - "nanvix_binaries: downloading nanvix/nanvix-python {}...", - versions.nanvix_python.tag - ); - download_and_extract(&versions.nanvix_python, "nanvix/nanvix-python", &bin_dir); - } else if use_prefetched_binaries { - eprintln!( - "nanvix_binaries: offline — verifying pre-fetched binaries in '{}'", - bin_dir.display() - ); - } else { - eprintln!("nanvix_binaries: all binaries cached and verified"); - } - - verify_checksums(&all_binaries, &bin_dir, &checksums); - verify_bin_subdir_checksums(&bin_dir, &checksums); - - if use_prefetched_binaries { - nanvix_build_common::emit_rerun_for_copied_artifacts(&bin_dir); - } - - // Generate host-local WHP snapshots at build time so even the first - // runtime execution uses warm start. The runtime fallback in - // nanvix_runner.rs handles the case where snapshots are missing. - // - // Skip on non-x86_64 hosts: `nanvixd.exe` is an x86_64 Windows binary - // and launching it on (e.g.) ARM64 Windows fails with - // STATUS_INVALID_IMAGE_FORMAT (0xc000007b). Snapshot pre-generation is - // a warm-start cache only — the runtime fallback covers cold boot on - // hosts where this build step is skipped. - let host = std::env::var("HOST").unwrap_or_default(); - let host_is_x86_64 = host.starts_with("x86_64-"); - if !host_is_x86_64 { - eprintln!( - "nanvix_binaries: skipping host-local snapshot generation \ - (host '{}' is not x86_64; nanvixd.exe cannot run here). \ - Runtime will cold-boot on first use.", - host - ); - } else { - let snapshots_dir = bin_dir.join(nanvix_common::SNAPSHOTS_SUBDIR); - let snapshots_present = nanvix_common::SNAPSHOT_FILES - .iter() - .all(|name| snapshots_dir.join(name).exists()); - if snapshots_present { - eprintln!("nanvix_binaries: host-local snapshots already present"); - } else if use_prefetched_binaries { - // In offline mode the NANVIX_BIN directory is treated as an - // immutable, pre-fetched input — it may be a read-only or - // shared cache. Do not run nanvixd.exe to generate snapshots - // into it. The runtime fallback in nanvix_runner.rs cold-boots - // when snapshots are absent. - eprintln!( - "nanvix_binaries: offline — snapshots absent in NANVIX_BIN; \ - skipping generation (runtime will cold-boot on first use)." - ); - } else { - fs::create_dir_all(&snapshots_dir).expect("failed to create snapshots dir"); - eprintln!("nanvix_binaries: generating host-local snapshots (cold boot)..."); - generate_snapshots_locally(&bin_dir); - } - } - } - - println!("cargo:rustc-env=NANVIX_BIN_DIR={}", bin_dir.display()); - println!("cargo:BIN_DIR={}", bin_dir.display()); - // Propagate whether these binaries came from an externally supplied - // (prefetched) directory. Consumers read this as - // `DEP_NANVIX_BINARIES_PREFETCHED` and must not trust prefetched WHP - // snapshots (they are not covered by checksums.json). - println!( - "cargo:PREFETCHED={}", - if use_prefetched_binaries { "1" } else { "0" } - ); - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=versions.json"); - println!("cargo:rerun-if-changed=checksums.json"); - println!("cargo:rerun-if-env-changed=GITHUB_TOKEN"); - println!("cargo:rerun-if-env-changed=GH_TOKEN"); - println!("cargo:rerun-if-env-changed=NANVIX_BIN"); -} - -// -- Download logic ---------------------------------------------------------- - -fn needs_download( - config: &RepoConfig, - bin_dir: &Path, - checksums: &HashMap, -) -> bool { - // Check flat binaries. - let flat_missing = config.binaries.iter().any(|name| { - let path = bin_dir.join(name); - if !path.exists() { - return true; - } - if let Some(expected) = checksums.get(name.as_str()) { - certutil_sha256(&path) != *expected - } else { - false - } - }); - if flat_missing { - return true; - } - - // Check bin/ subdir files. - let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); - for name in nanvix_common::BIN_SUBDIR_FILES { - let path = bin_subdir.join(name); - if !path.exists() { - return true; - } - if let Some(expected) = checksums.get(*name) { - if certutil_sha256(&path) != *expected { - return true; - } - } - } - - false -} - -fn download_and_extract(config: &RepoConfig, repo: &str, bin_dir: &Path) { - let url = github_download_url(repo, &config.tag, &config.asset); - let zip_path = bin_dir.join(&config.asset); - - // Cleanup helper: remove zip on failure. - let cleanup = |zip: &Path| { - let _ = fs::remove_file(zip); - }; - - eprintln!(" downloading {}...", config.asset); - if let Err(msg) = try_curl_download(&url, &zip_path) { - cleanup(&zip_path); - panic!("nanvix_binaries: {}", msg); - } - - let size = zip_path.metadata().map(|m| m.len()).unwrap_or(0); - eprintln!(" downloaded {} bytes, extracting...", size); - - let binaries: Vec<&str> = config.binaries.iter().map(|s| s.as_str()).collect(); - - // Extract flat binaries (nanvixd.exe from bin/, rootfs + initrd from root). - if let Err(msg) = try_tar_extract(&zip_path, bin_dir, &binaries) { - cleanup(&zip_path); - panic!("nanvix_binaries: {}", msg); - } - - // Extract bin/ subdir files (kernel.elf stays in bin/ as nanvixd expects). - let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); - fs::create_dir_all(&bin_subdir).expect("failed to create bin subdir"); - if let Err(msg) = try_tar_extract_bin_subdir(&zip_path, &bin_subdir) { - cleanup(&zip_path); - panic!("nanvix_binaries: {}", msg); - } - - let _ = fs::remove_file(&zip_path); -} - -// -- Snapshot generation ----------------------------------------------------- - -fn generate_snapshots_locally(bin_dir: &Path) { - let nanvixd = bin_dir.join("nanvixd.exe"); - let ramfs = bin_dir.join("nanvix_rootfs.img"); - let initrd = bin_dir.join("python3.initrd"); - let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); - - if !nanvixd.exists() || !ramfs.exists() || !initrd.exists() { - panic!( - "nanvix_binaries: cannot generate snapshots — required binaries missing:\n\ - \x20 nanvixd.exe: {}\n\ - \x20 nanvix_rootfs.img: {}\n\ - \x20 python3.initrd: {}", - nanvixd.exists(), - ramfs.exists(), - initrd.exists() - ); - } - - nanvix_common::generate_snapshot(bin_dir, &nanvixd, &bin_subdir, &ramfs, &initrd) - .unwrap_or_else(|e| panic!("nanvix_binaries: {}", e)); - - // Log generated file sizes. - let snapshots_dir = bin_dir.join(nanvix_common::SNAPSHOTS_SUBDIR); - for name in nanvix_common::SNAPSHOT_FILES { - let path = snapshots_dir.join(name); - let size = path.metadata().map(|m| m.len()).unwrap_or(0); - eprintln!(" snapshots/{} -- generated ({} bytes)", name, size); - } -} - -// -- curl.exe ---------------------------------------------------------------- - -fn try_curl_download(url: &str, dest: &Path) -> Result<(), String> { - let mut cmd = Command::new("curl"); - cmd.args([ - "--silent", - "--show-error", - "--fail", - "--location", - "--retry", - "5", - "--retry-delay", - "5", - "--retry-all-errors", - "--output", - ]); - cmd.arg(dest); - cmd.args(["--header", "User-Agent: mxc-nanvix-build/0.1"]); - - if let Some(token) = github_token() { - cmd.arg("--header"); - cmd.arg(format!("Authorization: Bearer {}", token)); - } - - cmd.arg(url); - - let output = cmd.output().map_err(|e| { - format!( - "curl not found: {}\n\ - Ensure curl is in PATH.", - e - ) - })?; - - if !output.status.success() { - return Err(format!( - "curl failed for {}\n exit code: {}\n stderr: {}", - url, - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - - Ok(()) -} - -// -- tar.exe ----------------------------------------------------------------- - -fn try_tar_extract(zip_path: &Path, dest_dir: &Path, files: &[&str]) -> Result<(), String> { - // The nanvix-python zip has a top-level directory with two sub-layouts: - // bin/nanvixd.exe → strip 2 components - // nanvix_rootfs.img, python3.initrd → strip 1 component - - const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb"; - const BIN_DIR_FILES: &[&str] = &["nanvixd.exe"]; - - let (bin_files, root_files): (Vec<&&str>, Vec<&&str>) = - files.iter().partition(|f| BIN_DIR_FILES.contains(f)); - - // Pass 1: files under /bin/ — strip 2 path components. - if !bin_files.is_empty() { - let mut cmd = Command::new("tar"); - cmd.arg("-xf").arg(zip_path).arg("-C").arg(dest_dir); - cmd.args(["--strip-components", "2"]); - for f in &bin_files { - cmd.arg(format!("{}/bin/{}", ARCHIVE_PREFIX, f)); - } - let output = cmd.output().map_err(|e| { - format!( - "tar.exe not found: {}\n\ - tar.exe ships with Windows 10 1803+. Ensure it's in PATH.", - e - ) - })?; - if !output.status.success() { - return Err(format!( - "tar extraction failed (bin files)\n exit code: {}\n stderr: {}", - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - } - - // Pass 2: files at / root — strip 1 path component. - if !root_files.is_empty() { - let mut cmd = Command::new("tar"); - cmd.arg("-xf").arg(zip_path).arg("-C").arg(dest_dir); - cmd.args(["--strip-components", "1"]); - for f in &root_files { - cmd.arg(format!("{}/{}", ARCHIVE_PREFIX, f)); - } - let output = cmd.output().map_err(|e| { - format!( - "tar.exe not found: {}\n\ - tar.exe ships with Windows 10 1803+. Ensure it's in PATH.", - e - ) - })?; - if !output.status.success() { - return Err(format!( - "tar extraction failed (root files)\n exit code: {}\n stderr: {}", - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - } - - for f in files { - let path = dest_dir.join(f); - if path.exists() { - let size = path.metadata().map(|m| m.len()).unwrap_or(0); - eprintln!(" {} -- extracted ({} bytes)", f, size); - } else { - return Err(format!("'{}' not found in zip after extraction", f)); - } - } - - Ok(()) -} - -fn try_tar_extract_bin_subdir(zip_path: &Path, dest_dir: &Path) -> Result<(), String> { - const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb"; - - for name in nanvix_common::BIN_SUBDIR_FILES { - let mut cmd = Command::new("tar"); - cmd.arg("-xf").arg(zip_path).arg("-C").arg(dest_dir); - cmd.args(["--strip-components", "2"]); - cmd.arg(format!("{}/bin/{}", ARCHIVE_PREFIX, name)); - let output = cmd - .output() - .map_err(|e| format!("tar.exe not found: {}", e))?; - if !output.status.success() { - return Err(format!( - "tar extraction failed (bin/{})\n exit code: {}\n stderr: {}", - name, - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - let path = dest_dir.join(name); - if path.exists() { - let size = path.metadata().map(|m| m.len()).unwrap_or(0); - eprintln!(" bin/{} -- extracted ({} bytes)", name, size); - } else { - return Err(format!("'bin/{}' not found in zip after extraction", name)); - } - } - - Ok(()) -} - -// -- Helpers ----------------------------------------------------------------- - -fn github_token() -> Option { - std::env::var("GITHUB_TOKEN") - .or_else(|_| std::env::var("GH_TOKEN")) - .ok() -} - -// -- certutil SHA256 --------------------------------------------------------- - -fn certutil_sha256(path: &Path) -> String { - let output = Command::new("certutil") - .args(["-hashfile"]) - .arg(path) - .arg("SHA256") - .output() - .unwrap_or_else(|e| { - panic!("nanvix_binaries: failed to run certutil: {}", e); - }); - - if !output.status.success() { - panic!( - "nanvix_binaries: certutil -hashfile failed for {}: {}", - path.display(), - String::from_utf8_lossy(&output.stderr) - ); - } - - // certutil output format: - // SHA256 hash of : - // - // CertUtil: -hashfile command completed successfully. - // - // Use a lossy conversion because the localized header/footer lines are - // emitted in the console's OEM code page (e.g. CP850 on French Windows), - // not UTF-8 -- a strict `from_utf8` would panic on those bytes. The hash - // line itself is pure ASCII hex, so it survives the lossy conversion. We - // locate the hash by scanning for a 64-character hex line rather than - // relying on a fixed line index, which keeps this locale-independent. - let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .lines() - .map(|line| line.trim().replace(' ', "").to_lowercase()) - .find(|line| line.len() == 64 && line.bytes().all(|b| b.is_ascii_hexdigit())) - .unwrap_or_else(|| panic!("nanvix_binaries: unexpected certutil output: {}", stdout)) -} - -fn verify_checksums(binaries: &[&str], bin_dir: &Path, checksums: &HashMap) { - for name in binaries { - let path = bin_dir.join(name); - if !path.exists() { - panic!("nanvix_binaries: {} not found after download/extract", name); - } - - if let Some(expected) = checksums.get(*name) { - let actual = certutil_sha256(&path); - if actual != *expected { - panic!( - "nanvix_binaries: SHA256 mismatch for '{}'!\n\ - \x20 expected: {}\n\ - \x20 actual: {}\n\ - This may indicate a corrupted download or a NanVix version update.\n\ - Update checksums.json with the new hashes.", - name, expected, actual - ); - } - eprintln!(" {} -- checksum OK", name); - } else { - panic!( - "nanvix_binaries: '{}' has no entry in checksums.json — \ - every binary must be hash-verified", - name - ); - } - } -} - -fn verify_bin_subdir_checksums(bin_dir: &Path, checksums: &HashMap) { - let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); - for name in nanvix_common::BIN_SUBDIR_FILES { - let path = bin_subdir.join(name); - if !path.exists() { - panic!( - "nanvix_binaries: bin/{} not found after download/extract", - name - ); - } - - if let Some(expected) = checksums.get(*name) { - let actual = certutil_sha256(&path); - if actual != *expected { - panic!( - "nanvix_binaries: SHA256 mismatch for 'bin/{}'!\n\ - \x20 expected: {}\n\ - \x20 actual: {}\n\ - Update checksums.json with the new hashes.", - name, expected, actual - ); - } - eprintln!(" bin/{} -- checksum OK", name); - } else { - panic!( - "nanvix_binaries: 'bin/{}' has no entry in checksums.json — \ - every binary must be hash-verified", - name - ); - } - } -} - -// -- Linux-specific functions ------------------------------------------------ - -fn needs_download_linux( - binaries: &[&str], - bin_dir: &Path, - checksums: &HashMap, -) -> bool { - // Check flat binaries. - for name in binaries { - let path = bin_dir.join(name); - if !path.exists() { - return true; - } - if let Some(expected) = checksums.get(*name) { - if sha256sum(&path) != *expected { - return true; - } - } - } - - // Check bin/ subdir files. - let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); - for name in nanvix_common::BIN_SUBDIR_FILES { - let path = bin_subdir.join(name); - if !path.exists() { - return true; - } - if let Some(expected) = checksums.get(*name) { - if sha256sum(&path) != *expected { - return true; - } - } - } - - false -} - -fn download_and_extract_linux(tag: &str, asset: &str, binaries: &[&str], bin_dir: &Path) { - let url = github_download_url("nanvix/nanvix-python", tag, asset); - let tar_path = bin_dir.join(asset); - - let cleanup = |p: &Path| { - let _ = fs::remove_file(p); - }; - - eprintln!(" downloading {}...", asset); - if let Err(msg) = try_curl_download(&url, &tar_path) { - cleanup(&tar_path); - panic!("nanvix_binaries: {}", msg); - } - - let size = tar_path.metadata().map(|m| m.len()).unwrap_or(0); - eprintln!(" downloaded {} bytes, extracting...", size); - - // Extract flat binaries from / using tar on Linux. - if let Err(msg) = try_tar_extract_linux(&tar_path, bin_dir, binaries) { - cleanup(&tar_path); - panic!("nanvix_binaries: {}", msg); - } - - // Extract bin/ subdir files (kernel.elf, nanvixd.elf). - let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); - fs::create_dir_all(&bin_subdir).expect("failed to create bin subdir"); - if let Err(msg) = try_tar_extract_bin_subdir_linux(&tar_path, &bin_subdir) { - cleanup(&tar_path); - panic!("nanvix_binaries: {}", msg); - } - - let _ = fs::remove_file(&tar_path); -} - -fn try_tar_extract_linux(tar_path: &Path, dest_dir: &Path, files: &[&str]) -> Result<(), String> { - const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb"; - - // nanvixd.elf lives under /bin/, other flat binaries under /. - let bin_dir_files: &[&str] = &["nanvixd.elf"]; - let (bin_files, root_files): (Vec<&&str>, Vec<&&str>) = - files.iter().partition(|f| bin_dir_files.contains(f)); - - // Pass 1: files under /bin/ — strip 2 path components. - if !bin_files.is_empty() { - let mut cmd = Command::new("tar"); - cmd.arg("-xzf").arg(tar_path).arg("-C").arg(dest_dir); - cmd.args(["--strip-components", "2"]); - for f in &bin_files { - cmd.arg(format!("{}/bin/{}", ARCHIVE_PREFIX, f)); - } - let output = cmd.output().map_err(|e| format!("tar not found: {}", e))?; - if !output.status.success() { - return Err(format!( - "tar extraction failed (bin files)\n exit code: {}\n stderr: {}", - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - } - - // Pass 2: files at / root — strip 1 path component. - if !root_files.is_empty() { - let mut cmd = Command::new("tar"); - cmd.arg("-xzf").arg(tar_path).arg("-C").arg(dest_dir); - cmd.args(["--strip-components", "1"]); - for f in &root_files { - cmd.arg(format!("{}/{}", ARCHIVE_PREFIX, f)); - } - let output = cmd.output().map_err(|e| format!("tar not found: {}", e))?; - if !output.status.success() { - return Err(format!( - "tar extraction failed (root files)\n exit code: {}\n stderr: {}", - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - } - - for f in files { - let path = dest_dir.join(f); - if path.exists() { - let size = path.metadata().map(|m| m.len()).unwrap_or(0); - eprintln!(" {} -- extracted ({} bytes)", f, size); - } else { - return Err(format!("'{}' not found in archive after extraction", f)); - } - } - - Ok(()) -} - -fn try_tar_extract_bin_subdir_linux(tar_path: &Path, dest_dir: &Path) -> Result<(), String> { - const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb"; - - for name in nanvix_common::BIN_SUBDIR_FILES { - let mut cmd = Command::new("tar"); - cmd.arg("-xzf").arg(tar_path).arg("-C").arg(dest_dir); - cmd.args(["--strip-components", "2"]); - cmd.arg(format!("{}/bin/{}", ARCHIVE_PREFIX, name)); - let output = cmd.output().map_err(|e| format!("tar not found: {}", e))?; - if !output.status.success() { - return Err(format!( - "tar extraction failed (bin/{})\n exit code: {}\n stderr: {}", - name, - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - let path = dest_dir.join(name); - if path.exists() { - let size = path.metadata().map(|m| m.len()).unwrap_or(0); - eprintln!(" bin/{} -- extracted ({} bytes)", name, size); - } else { - return Err(format!( - "'bin/{}' not found in archive after extraction", - name - )); - } - } - - Ok(()) -} - -/// Compute SHA256 hash using the `sha256sum` command (Linux). -fn sha256sum(path: &Path) -> String { - let output = Command::new("sha256sum") - .arg(path) - .output() - .unwrap_or_else(|e| { - panic!("nanvix_binaries: failed to run sha256sum: {}", e); - }); - - if !output.status.success() { - panic!( - "nanvix_binaries: sha256sum failed for {}: {}", - path.display(), - String::from_utf8_lossy(&output.stderr) - ); - } - - let stdout = String::from_utf8(output.stdout).expect("sha256sum output not UTF-8"); - stdout - .split_whitespace() - .next() - .unwrap_or_else(|| panic!("nanvix_binaries: unexpected sha256sum output: {}", stdout)) - .to_lowercase() -} - -fn verify_checksums_linux(binaries: &[&str], bin_dir: &Path, checksums: &HashMap) { - for name in binaries { - let path = bin_dir.join(name); - if !path.exists() { - panic!("nanvix_binaries: {} not found after download/extract", name); - } - - if let Some(expected) = checksums.get(*name) { - let actual = sha256sum(&path); - if actual != *expected { - panic!( - "nanvix_binaries: SHA256 mismatch for '{}'!\n\ - \x20 expected: {}\n\ - \x20 actual: {}\n\ - This may indicate a corrupted download or a NanVix version update.\n\ - Update checksums.json with the new hashes.", - name, expected, actual - ); - } - eprintln!(" {} -- checksum OK", name); - } else { - panic!( - "nanvix_binaries: '{}' has no entry in checksums.json — \ - every binary must be hash-verified", - name - ); - } - } -} - -fn verify_bin_subdir_checksums_linux(bin_dir: &Path, checksums: &HashMap) { - let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR); - for name in nanvix_common::BIN_SUBDIR_FILES { - let path = bin_subdir.join(name); - if !path.exists() { - panic!( - "nanvix_binaries: bin/{} not found after download/extract", - name - ); - } - - if let Some(expected) = checksums.get(*name) { - let actual = sha256sum(&path); - if actual != *expected { - panic!( - "nanvix_binaries: SHA256 mismatch for 'bin/{}'!\n\ - \x20 expected: {}\n\ - \x20 actual: {}\n\ - Update checksums.json with the new hashes.", - name, expected, actual - ); - } - eprintln!(" bin/{} -- checksum OK", name); - } else { - panic!( - "nanvix_binaries: 'bin/{}' has no entry in checksums.json — \ - every binary must be hash-verified", - name - ); - } - } -} diff --git a/src/backends/nanvix/binaries/checksums.json b/src/backends/nanvix/binaries/checksums.json deleted file mode 100644 index 68d5fb082..000000000 --- a/src/backends/nanvix/binaries/checksums.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "windows": { - "nanvixd.exe": "1c6f985da5f7bd914e737a7bff01d3589bdf23912258a5d27284c2ccf36bbe89", - "nanvix_rootfs.img": "a1e34f4658c4740351fca0147d6061d75ff67e29a586be1accf3bde96753b036", - "python3.initrd": "b5f545510e3a5f94b39632f38493e20fa938a7a649a8d9d18b62eabc16386686", - "kernel.elf": "49c9a1a184b1fec4a437488b3e8145ed1247fa5692799c8eb8499531d6f98c7a" - }, - "linux": { - "nanvixd.elf": "49de37a6002fd88acf890eaef1b7a5fc8259ff1469a635261948e386b4012a8e", - "nanvix_rootfs.img": "a1e34f4658c4740351fca0147d6061d75ff67e29a586be1accf3bde96753b036", - "python3.initrd": "b5f545510e3a5f94b39632f38493e20fa938a7a649a8d9d18b62eabc16386686", - "kernel.elf": "dff28c4668083e7b0148d8d66b8e2a72ba6d9d2515562931bb062f0a74c32882" - } -} diff --git a/src/backends/nanvix/binaries/src/lib.rs b/src/backends/nanvix/binaries/src/lib.rs deleted file mode 100644 index 50b725f6b..000000000 --- a/src/backends/nanvix/binaries/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Path to the directory containing downloaded NanVix binaries. -/// -/// Set by build.rs via `cargo:rustc-env`. This points to the build-time -/// OUT_DIR and is used by wxc/build.rs to copy binaries next to the final -/// executable. At runtime, the NanVix runner discovers binaries via -/// `std::env::current_exe()` — it does NOT use this constant. -pub const NANVIX_BIN_DIR: &str = env!("NANVIX_BIN_DIR"); - -// Re-export shared constants from nanvix_common. -pub use nanvix_common::REQUIRED_BINARIES; diff --git a/src/backends/nanvix/binaries/versions.json b/src/backends/nanvix/binaries/versions.json deleted file mode 100644 index bead419bd..000000000 --- a/src/backends/nanvix/binaries/versions.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "nanvix_python": { - "tag": "3.12.3-nanvix-0.16.65-fce3620", - "asset": "microvm-standalone-256mb.zip", - "asset_linux": "microvm-standalone-256mb.tar.gz", - "binaries": [ - "nanvixd.exe", - "nanvix_rootfs.img", - "python3.initrd" - ], - "binaries_linux": [ - "nanvixd.elf", - "nanvix_rootfs.img", - "python3.initrd" - ] - } -} diff --git a/src/backends/nanvix/build_common/Cargo.toml b/src/backends/nanvix/build_common/Cargo.toml deleted file mode 100644 index 30f31a2bd..000000000 --- a/src/backends/nanvix/build_common/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "nanvix_build_common" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Build-time helpers for staging NanVix micro-VM binaries (build-only; never linked into runtime)" - -[dependencies] -nanvix_common = { path = "../common" } diff --git a/src/backends/nanvix/build_common/src/lib.rs b/src/backends/nanvix/build_common/src/lib.rs deleted file mode 100644 index 52f190657..000000000 --- a/src/backends/nanvix/build_common/src/lib.rs +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Build-time helpers for staging NanVix micro-VM binaries next to the -//! consuming executable. -//! -//! This crate is **build-only**: it is consumed exclusively as a -//! `[build-dependencies]` entry by the `nanvix_binaries`, `wxc`, and `lxc` -//! build scripts and is never linked into the shipping runtime binary. The -//! file-staging logic lives here (rather than in the runtime `nanvix_common` -//! crate) so it adds no weight to mainline code. - -use std::io; -use std::path::{Path, PathBuf}; - -use nanvix_common::{ - BIN_SUBDIR, BIN_SUBDIR_FILES, REQUIRED_BINARIES, SNAPSHOTS_SUBDIR, SNAPSHOT_FILES, -}; - -/// Category of a staged NanVix artifact. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ArtifactKind { - /// Flat binary next to the executable (e.g. `nanvixd.exe`). - Binary, - /// File under the `bin/` subdirectory (e.g. `bin/kernel.elf`). - BinFile, - /// WHP warm-start snapshot under `snapshots/` (used by the Windows runner). - Snapshot, -} - -/// Relative paths (from the nanvix binary directory) of every artifact the -/// backend stages, paired with its [`ArtifactKind`]. -/// -/// This is the single source of truth that drives both copying -/// ([`copy_artifacts_to_target`]) and `cargo:rerun-if-changed` emission -/// ([`emit_rerun_for_copied_artifacts`]) so the two can never drift apart. -pub fn artifact_rel_paths() -> Vec<(ArtifactKind, PathBuf)> { - let mut paths = Vec::new(); - for name in REQUIRED_BINARIES { - paths.push((ArtifactKind::Binary, PathBuf::from(name))); - } - for name in BIN_SUBDIR_FILES { - paths.push((ArtifactKind::BinFile, Path::new(BIN_SUBDIR).join(name))); - } - for name in SNAPSHOT_FILES { - paths.push(( - ArtifactKind::Snapshot, - Path::new(SNAPSHOTS_SUBDIR).join(name), - )); - } - paths -} - -/// Resolve the directory of NanVix binaries to use for a build, and report -/// whether it came from an externally supplied (prefetched) location. -/// -/// When `NANVIX_BIN` is set to a non-empty value, that directory is used -/// directly (no network downloads) and the returned boolean is `true`. An -/// empty `NANVIX_BIN` is treated as unset, matching the online/cached default. -/// Otherwise a `nanvix-binaries` subdirectory of `out_dir` is created and -/// returned with `false`. -/// -/// The prefetched directory is made absolute via [`std::path::absolute`] (not -/// `fs::canonicalize`) so it is stable regardless of the build script's working -/// directory while still letting an atomic symlink swap of the cache be noticed -/// by Cargo. -pub fn resolve_bin_dir(out_dir: &Path) -> (PathBuf, bool) { - let prefetched = std::env::var_os("NANVIX_BIN") - .filter(|v| !v.is_empty()) - .map(PathBuf::from); - - match prefetched { - Some(dir) => { - if !dir.is_dir() { - panic!( - "nanvix_binaries: NANVIX_BIN is set to '{}', but that directory \ - does not exist. Point NANVIX_BIN at a directory containing the \ - pre-fetched NanVix binaries.", - dir.display() - ); - } - let dir = std::path::absolute(&dir).unwrap_or_else(|e| { - panic!( - "nanvix_binaries: failed to resolve NANVIX_BIN '{}' to an \ - absolute path: {}", - dir.display(), - e - ) - }); - eprintln!( - "nanvix_binaries: NANVIX_BIN set — using pre-fetched binaries from '{}' (offline)", - dir.display() - ); - (dir, true) - } - None => { - let dir = out_dir.join("nanvix-binaries"); - std::fs::create_dir_all(&dir).expect("failed to create nanvix-binaries dir"); - (dir, false) - } - } -} - -/// Copy NanVix artifacts from the build cache (`src_dir`) to the target -/// directory next to the output executable. -/// -/// Binaries and `bin/` files are copied whenever the source exists (the build -/// script only re-runs when a tracked input changed, so this is not a -/// per-build cost — and a modification-time comparison would silently skip a -/// legitimate override or rollback to an older cache). A failure to copy a -/// binary is logged as a warning and does not abort the build (preserving the -/// historical behavior). -/// -/// `trust_snapshots` governs WHP warm-start snapshots, which are **not** -/// covered by `checksums.json` (they are normally host-generated, not pinned -/// release artifacts): -/// - `true` (normal online build, snapshots produced locally): snapshots are -/// mirrored — present ones are copied and target snapshots absent from the -/// source are purged, so a stale warm-start image is never used against -/// mismatched binaries. -/// - `false` (source is an externally supplied `NANVIX_BIN` prefetch dir): -/// snapshots are never copied and any stale target snapshot is removed, so -/// the runtime falls back to a verified cold boot instead of warm-booting an -/// unverified VM memory image. -/// -/// Returns `Err` on any snapshot integrity failure (a snapshot could not be -/// copied or a stale snapshot could not be removed) so the caller can decide -/// how to react. Build scripts should treat such an error as fatal — leaving a -/// mismatched/unverified snapshot next to the executable is unsafe. -pub fn copy_artifacts_to_target( - src_dir: &Path, - target_dir: &Path, - trust_snapshots: bool, -) -> io::Result<()> { - use std::fs; - - for (kind, rel) in artifact_rel_paths() { - let src = src_dir.join(&rel); - let dst = target_dir.join(&rel); - - // Snapshots from an untrusted (prefetched) source are never copied; we - // must also guarantee no stale snapshot is left behind so the runtime - // does not warm-boot an unverified image. - if kind == ArtifactKind::Snapshot && !trust_snapshots { - remove_stale_snapshot(&dst)?; - continue; - } - - if src.exists() { - if let Some(parent) = dst.parent() { - let _ = fs::create_dir_all(parent); - } - eprintln!("nanvix: copying {} -> {}", src.display(), dst.display()); - if let Err(e) = fs::copy(&src, &dst) { - // Never leave a partial/stale file behind. - let _ = fs::remove_file(&dst); - if kind == ArtifactKind::Snapshot { - return Err(io::Error::new( - e.kind(), - format!("nanvix: failed to copy {}: {}", rel.display(), e), - )); - } - eprintln!("nanvix: WARNING: failed to copy {}: {}", rel.display(), e); - } - } else if kind == ArtifactKind::Snapshot { - // Trusted source is missing this snapshot — purge any stale target - // copy so an incomplete set forces a clean cold boot. - remove_stale_snapshot(&dst)?; - } - } - Ok(()) -} - -/// Remove a target snapshot file if present, returning an error if it cannot be -/// removed. A leftover stale snapshot would be warm-booted by the runner -/// (which trusts a complete exe-side snapshot set on presence alone) against -/// mismatched binaries, so a failure here must be surfaced rather than ignored. -fn remove_stale_snapshot(dst: &Path) -> io::Result<()> { - if dst.exists() { - eprintln!( - "nanvix: removing stale {} (absent or untrusted in source)", - dst.display() - ); - std::fs::remove_file(dst).map_err(|e| { - io::Error::new( - e.kind(), - format!( - "nanvix: failed to remove stale snapshot {}: {} — refusing to \ - leave an unverified warm-start image that would be booted \ - against mismatched binaries", - dst.display(), - e - ), - ) - })?; - } - Ok(()) -} - -/// Emit `cargo:rerun-if-changed` for every artifact that -/// [`copy_artifacts_to_target`] reads from `src_dir`. Call this from a -/// consuming crate's build script so the copy reruns when the source contents -/// change in place — for example when an offline `NANVIX_BIN` prefetch -/// directory is updated at the same path. Without it, the consumer only reruns -/// when the source *path* changes, leaving stale artifacts next to the exe. -pub fn emit_rerun_for_copied_artifacts(src_dir: &Path) { - for (_, rel) in artifact_rel_paths() { - println!("cargo:rerun-if-changed={}", src_dir.join(rel).display()); - } -} - -/// Stage NanVix artifacts from `nanvix_bin_dir` next to the executable being -/// built and emit the appropriate `cargo:rerun-*` triggers. -/// -/// Intended to be called from a consumer (`wxc` / `lxc`) build script. The -/// target directory is derived from `OUT_DIR` (the binary lands in -/// `target//`), and snapshot trust is read from the -/// `DEP_NANVIX_BINARIES_PREFETCHED` link var the `nanvix_binaries` build script -/// exports (defaulting to trusted when absent). Panics on a snapshot integrity -/// failure — acceptable in the build path, where leaving an unverified -/// warm-start image next to the executable must abort the build. -pub fn stage_artifacts_next_to_exe(nanvix_bin_dir: &Path) { - // Cargo puts the output binary in OUT_DIR/../../.. (target//). - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set"); - let target_dir = Path::new(&out_dir) - .parent() - .and_then(|p| p.parent()) - .and_then(|p| p.parent()) - .expect("could not determine target dir from OUT_DIR"); - - // WHP snapshots from a prefetched (externally supplied) directory are not - // covered by checksums.json, so they must not be trusted/copied. Default to - // trusting (online build) when the flag is absent. - let trust_snapshots = std::env::var("DEP_NANVIX_BINARIES_PREFETCHED") - .map(|v| v != "1") - .unwrap_or(true); - - copy_artifacts_to_target(nanvix_bin_dir, target_dir, trust_snapshots) - .expect("nanvix: failed to stage artifacts next to the executable"); - - // Re-run when the source path changes (detected via nanvix_binaries - // rebuild) and when the source artifacts themselves change in place (e.g. - // an offline NANVIX_BIN prefetch dir updated at the same path). - emit_rerun_for_copied_artifacts(nanvix_bin_dir); - println!("cargo:rerun-if-env-changed=DEP_NANVIX_BINARIES_BIN_DIR"); - println!("cargo:rerun-if-env-changed=DEP_NANVIX_BINARIES_PREFETCHED"); -} - -#[cfg(test)] -mod tests { - use super::*; - use nanvix_common::{SNAPSHOT_CBOR, SNAPSHOT_VMEM}; - use std::fs; - use std::time::{SystemTime, UNIX_EPOCH}; - - /// Create a unique, empty scratch directory under the OS temp dir. - fn scratch(tag: &str) -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let dir = std::env::temp_dir().join(format!( - "nanvix_copy_test_{}_{}_{}", - tag, - std::process::id(), - nanos - )); - fs::create_dir_all(&dir).unwrap(); - dir - } - - fn write_snapshot(root: &Path, name: &str, contents: &[u8]) { - let dir = root.join(SNAPSHOTS_SUBDIR); - fs::create_dir_all(&dir).unwrap(); - fs::write(dir.join(name), contents).unwrap(); - } - - fn snapshot_path(root: &Path, name: &str) -> PathBuf { - root.join(SNAPSHOTS_SUBDIR).join(name) - } - - #[test] - fn trusted_source_lacking_snapshot_purges_stale_target() { - let src = scratch("purge_src"); - let target = scratch("purge_tgt"); - // Target has both snapshots; source has none. - write_snapshot(&target, SNAPSHOT_VMEM, b"stale-vmem"); - write_snapshot(&target, SNAPSHOT_CBOR, b"stale-cbor"); - - copy_artifacts_to_target(&src, &target, /* trust_snapshots = */ true).unwrap(); - - assert!(!snapshot_path(&target, SNAPSHOT_VMEM).exists()); - assert!(!snapshot_path(&target, SNAPSHOT_CBOR).exists()); - - fs::remove_dir_all(&src).ok(); - fs::remove_dir_all(&target).ok(); - } - - #[test] - fn trusted_source_with_snapshots_copies_into_target() { - let src = scratch("copy_src"); - let target = scratch("copy_tgt"); - write_snapshot(&src, SNAPSHOT_VMEM, b"new-vmem"); - write_snapshot(&src, SNAPSHOT_CBOR, b"new-cbor"); - - copy_artifacts_to_target(&src, &target, true).unwrap(); - - assert_eq!( - fs::read(snapshot_path(&target, SNAPSHOT_VMEM)).unwrap(), - b"new-vmem" - ); - assert_eq!( - fs::read(snapshot_path(&target, SNAPSHOT_CBOR)).unwrap(), - b"new-cbor" - ); - - fs::remove_dir_all(&src).ok(); - fs::remove_dir_all(&target).ok(); - } - - #[test] - fn trusted_partial_source_copies_present_and_purges_absent() { - let src = scratch("partial_src"); - let target = scratch("partial_tgt"); - // Source has only vmem; target starts with both. - write_snapshot(&src, SNAPSHOT_VMEM, b"fresh-vmem"); - write_snapshot(&target, SNAPSHOT_VMEM, b"old-vmem"); - write_snapshot(&target, SNAPSHOT_CBOR, b"old-cbor"); - - copy_artifacts_to_target(&src, &target, true).unwrap(); - - // Present-in-source file is overwritten; absent-in-source file purged. - assert_eq!( - fs::read(snapshot_path(&target, SNAPSHOT_VMEM)).unwrap(), - b"fresh-vmem" - ); - assert!(!snapshot_path(&target, SNAPSHOT_CBOR).exists()); - - fs::remove_dir_all(&src).ok(); - fs::remove_dir_all(&target).ok(); - } - - #[test] - fn untrusted_source_never_copies_snapshots_and_purges_target() { - let src = scratch("untrusted_src"); - let target = scratch("untrusted_tgt"); - // Source ships snapshots, but they are untrusted (prefetched). - write_snapshot(&src, SNAPSHOT_VMEM, b"attacker-vmem"); - write_snapshot(&src, SNAPSHOT_CBOR, b"attacker-cbor"); - // Target has stale snapshots from a prior build. - write_snapshot(&target, SNAPSHOT_VMEM, b"stale-vmem"); - write_snapshot(&target, SNAPSHOT_CBOR, b"stale-cbor"); - - copy_artifacts_to_target(&src, &target, /* trust_snapshots = */ false).unwrap(); - - // Untrusted snapshots are neither copied nor left behind. - assert!(!snapshot_path(&target, SNAPSHOT_VMEM).exists()); - assert!(!snapshot_path(&target, SNAPSHOT_CBOR).exists()); - - fs::remove_dir_all(&src).ok(); - fs::remove_dir_all(&target).ok(); - } -} diff --git a/src/backends/nanvix/common/Cargo.toml b/src/backends/nanvix/common/Cargo.toml deleted file mode 100644 index 264a1c287..000000000 --- a/src/backends/nanvix/common/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "nanvix_common" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Shared constants and configuration types for NanVix micro-VM binaries" - -[dependencies] -serde = { workspace = true } -serde_json = { workspace = true } diff --git a/src/backends/nanvix/common/src/lib.rs b/src/backends/nanvix/common/src/lib.rs deleted file mode 100644 index 67c4ea2ce..000000000 --- a/src/backends/nanvix/common/src/lib.rs +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Shared constants and configuration types for NanVix micro-VM binaries. -//! -//! This crate is the single source of truth for binary filenames, release -//! configuration, and checksum data. It is consumed as a `[build-dependency]` -//! by `nanvix_binaries` (download) and `wxc` (copy to output dir). - -use std::collections::HashMap; -use std::path::Path; - -use serde::Deserialize; - -/// All required NanVix binary filenames (flat, next to wxc-exec) — Windows. -#[cfg(target_os = "windows")] -pub const REQUIRED_BINARIES: &[&str] = &["nanvixd.exe", "nanvix_rootfs.img", "python3.initrd"]; - -/// All required NanVix binary filenames (flat, next to lxc-exec) — Linux. -#[cfg(target_os = "linux")] -pub const REQUIRED_BINARIES: &[&str] = &["nanvixd.elf", "nanvix_rootfs.img", "python3.initrd"]; - -/// NanVix daemon binary name (platform-conditional). -#[cfg(target_os = "windows")] -pub const NANVIXD_BINARY: &str = "nanvixd.exe"; - -/// NanVix daemon binary name (platform-conditional). -#[cfg(target_os = "linux")] -pub const NANVIXD_BINARY: &str = "nanvixd.elf"; - -/// Multi-binary initrd (daemons + CPython) loaded by NanVix at warm start. -pub const INITRD_BINARY: &str = "python3.initrd"; - -/// Combined rootfs image (NanVix kernel userspace + CPython stdlib). -pub const RAMFS_IMAGE: &str = "nanvix_rootfs.img"; - -/// Pre-built VM state snapshot (CBOR) for warm start (Windows/WHP only). -pub const SNAPSHOT_CBOR: &str = "kernel.whp.cbor"; - -/// Pre-built VM memory snapshot for warm start (Windows/WHP only). -pub const SNAPSHOT_VMEM: &str = "kernel.vmem"; - -/// Subdirectory holding kernel binary (nanvixd expects `./bin/kernel.elf`). -pub const BIN_SUBDIR: &str = "bin"; - -/// Subdirectory holding WHP snapshot files. -pub const SNAPSHOTS_SUBDIR: &str = "snapshots"; - -/// Files that live in a `bin/` subdirectory (nanvixd expects ./bin/kernel.elf). -pub const BIN_SUBDIR_FILES: &[&str] = &["kernel.elf"]; - -/// Snapshot files that live in a `snapshots/` subdirectory next to the exe. -pub const SNAPSHOT_FILES: &[&str] = &[SNAPSHOT_VMEM, SNAPSHOT_CBOR]; - -/// Binaries sourced from the `nanvix/nanvix-python` GitHub release. -pub const NANVIX_PYTHON_REPO_BINARIES: &[&str] = REQUIRED_BINARIES; - -/// Number of bytes to retain from the end of nanvixd stderr when capturing -/// it for diagnostics. Bounds host memory growth in the face of an -/// untrusted/verbose child (availability / DoS hardening). -pub const STDERR_TAIL_BYTES: usize = 8 * 1024; - -/// Render a bounded tail of nanvixd stderr bytes as a UTF-8 string, -/// prefixed with `...(truncated)` when truncation occurred. -/// -/// `bytes` may be the full stderr buffer (post-hoc trim) or a buffer that -/// the caller already capped while streaming. The `truncated` flag tells -/// us that streaming-time bytes were dropped even when the resulting -/// buffer length is at or below [`STDERR_TAIL_BYTES`]. -pub fn format_stderr_tail(bytes: &[u8], truncated: bool) -> String { - if bytes.len() > STDERR_TAIL_BYTES { - // Trim at the byte level *before* UTF-8 decoding. Slicing into the - // `String` produced by `from_utf8_lossy` would panic if the byte - // offset fell inside a multi-byte codepoint; `from_utf8_lossy` on - // a raw byte slice tolerates a partial leading codepoint by - // emitting U+FFFD replacement characters instead. - let start = bytes.len() - STDERR_TAIL_BYTES; - let tail = String::from_utf8_lossy(&bytes[start..]); - format!("...(truncated){}", tail) - } else if truncated { - let text = String::from_utf8_lossy(bytes); - format!("...(truncated){}", text) - } else { - String::from_utf8_lossy(bytes).into_owned() - } -} - -/// Drain `reader` to completion, retaining only the last -/// [`STDERR_TAIL_BYTES`] bytes. Returns `(tail, truncated)` where -/// `truncated` is set when any earlier bytes were dropped. -/// -/// Used to bound host memory growth when capturing nanvixd stderr from a -/// potentially untrusted / verbose child (availability / DoS hardening). -/// Read errors terminate the drain and return whatever was captured so -/// far; this mirrors `read_to_end` failure semantics for our use case -/// where stderr is best-effort diagnostic data. -pub fn drain_stderr_tail(mut reader: R) -> (Vec, bool) { - let mut tail: Vec = Vec::new(); - let mut chunk = [0u8; 8 * 1024]; - let mut truncated = false; - loop { - match reader.read(&mut chunk) { - Ok(0) => break, - Ok(n) => { - tail.extend_from_slice(&chunk[..n]); - if tail.len() > STDERR_TAIL_BYTES { - let drop = tail.len() - STDERR_TAIL_BYTES; - tail.drain(..drop); - truncated = true; - } - } - Err(_) => break, - } - } - (tail, truncated) -} - -/// Release configuration loaded from `versions.json`. -#[derive(Debug, Deserialize)] -pub struct ReleaseConfig { - /// Configuration for the `nanvix/nanvix-python` GitHub repo. - pub nanvix_python: RepoConfig, -} - -/// Configuration for a single upstream GitHub repo release. -#[derive(Debug, Deserialize)] -pub struct RepoConfig { - /// Git tag of the pinned release (e.g., "v0.12.291"). - pub tag: String, - /// Exact filename of the zip asset in the GitHub release (Windows). - pub asset: String, - /// Exact filename of the tar.gz asset in the GitHub release (Linux). - #[serde(default)] - pub asset_linux: Option, - /// List of binary filenames to extract from the zip (Windows). - pub binaries: Vec, - /// List of binary filenames to extract from the tar.gz (Linux). - #[serde(default)] - pub binaries_linux: Option>, -} - -/// Load and deserialize a JSON file. -pub fn load_json(path: &str) -> T { - let content = std::fs::read_to_string(Path::new(path)) - .unwrap_or_else(|e| panic!("nanvix_common: failed to read {}: {}", path, e)); - serde_json::from_str(&content) - .unwrap_or_else(|e| panic!("nanvix_common: failed to parse {}: {}", path, e)) -} - -/// Load checksums from `checksums.json`. -/// -/// The file is a platform-keyed map of the form -/// `{ "windows": { "name": "hash", ... }, "linux": { ... } }`; `platform` -/// selects which sub-map to return. -pub fn load_checksums(path: &str, platform: &str) -> HashMap { - let mut value: HashMap> = load_json(path); - value.remove(platform).unwrap_or_else(|| { - panic!( - "nanvix_common: {} does not contain a '{}' section", - path, platform - ) - }) -} - -/// Construct a deterministic GitHub release download URL. -/// -/// Format: `https://github.com/{repo}/releases/download/{tag}/{asset}` -pub fn github_download_url(repo: &str, tag: &str, asset: &str) -> String { - format!( - "https://github.com/{}/releases/download/{}/{}", - repo, tag, asset - ) -} - -/// Generate WHP snapshots by cold-booting nanvixd. -/// -/// `snapshot_home` is used as the process working directory. nanvixd writes -/// snapshot files to `/snapshots/`, so the resulting files end up at -/// `/snapshots/kernel.vmem` and `kernel.whp.cbor`. -/// -/// `bin_dir` is the directory containing `kernel.elf` (passed as `-bin-dir`). -/// -/// Returns `Ok(())` on success. On failure, returns a human-readable error -/// message suitable for both build scripts (which panic) and runtime callers -/// (which wrap in their own error type). -pub fn generate_snapshot( - snapshot_home: &Path, - nanvixd: &Path, - bin_dir: &Path, - ramfs: &Path, - initrd: &Path, -) -> Result<(), String> { - use std::process::{Command, Stdio}; - - let output = Command::new(nanvixd) - .current_dir(snapshot_home) - .arg("-bin-dir") - .arg(bin_dir) - .arg("-ramfs") - .arg(ramfs) - .arg("-kernel-args") - .arg("snapshot") - .arg("--") - .arg(initrd) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .output() - .map_err(|e| format!("failed to run nanvixd for snapshot generation: {}", e))?; - - if !output.status.success() { - // Include a bounded tail of stderr so callers (build script panic - // or runtime preflight error) can surface actionable diagnostics - // without growing host memory unboundedly. - let tail = format_stderr_tail(&output.stderr, false); - let trimmed = tail.trim_end(); - if trimmed.is_empty() { - return Err(format!( - "snapshot generation failed (exit code: {})", - output.status - )); - } - return Err(format!( - "snapshot generation failed (exit code: {})\nnanvixd stderr:\n{}", - output.status, trimmed - )); - } - - let snap_dir = snapshot_home.join(SNAPSHOTS_SUBDIR); - for name in SNAPSHOT_FILES { - if !snap_dir.join(name).exists() { - return Err(format!( - "snapshot generation completed but '{}' not found in {:?}", - name, snap_dir - )); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn format_stderr_tail_short_buffer_no_prefix() { - let out = format_stderr_tail(b"hello", false); - assert_eq!(out, "hello"); - } - - #[test] - fn format_stderr_tail_short_buffer_with_truncated_flag() { - let out = format_stderr_tail(b"hello", true); - assert_eq!(out, "...(truncated)hello"); - } - - #[test] - fn format_stderr_tail_trims_oversized_ascii_buffer() { - let big = vec![b'A'; STDERR_TAIL_BYTES + 16]; - let out = format_stderr_tail(&big, false); - assert!(out.starts_with("...(truncated)")); - // 14 chars for the prefix + STDERR_TAIL_BYTES trailing ASCII bytes. - assert_eq!(out.len(), "...(truncated)".len() + STDERR_TAIL_BYTES); - } - - /// Regression test for PR review comment r3283559877: an oversized - /// buffer containing multi-byte UTF-8 must not panic when the - /// truncation byte offset falls inside a codepoint. - #[test] - fn format_stderr_tail_oversized_multibyte_does_not_panic() { - // 4-byte UTF-8 emoji repeated until well past the cap. - let unit = "🦀"; // 4 bytes - let repeats = (STDERR_TAIL_BYTES / unit.len()) + 64; - let big = unit.repeat(repeats).into_bytes(); - assert!(big.len() > STDERR_TAIL_BYTES); - let out = format_stderr_tail(&big, false); - assert!(out.starts_with("...(truncated)")); - // Ensure the result is still valid UTF-8 (String guarantees this); - // partial leading codepoints become U+FFFD via `from_utf8_lossy`. - assert!(out.len() >= "...(truncated)".len()); - } - - #[test] - fn format_stderr_tail_oversized_with_truncated_flag_uses_byte_trim() { - // When the buffer is oversized, the explicit `truncated` flag - // should not change behavior — byte-level trim still applies. - let big = vec![b'B'; STDERR_TAIL_BYTES + 1]; - let out = format_stderr_tail(&big, true); - assert!(out.starts_with("...(truncated)")); - assert_eq!(out.len(), "...(truncated)".len() + STDERR_TAIL_BYTES); - } -} diff --git a/src/backends/nanvix/runner/Cargo.toml b/src/backends/nanvix/runner/Cargo.toml deleted file mode 100644 index 081942e21..000000000 --- a/src/backends/nanvix/runner/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "nanvix_runner" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -wxc_common = { workspace = true, features = ["microvm"] } -nanvix_common = { path = "../common" } -uuid = { workspace = true } - -[target.'cfg(target_os = "windows")'.dependencies] -windows = { workspace = true } - -[target.'cfg(target_os = "linux")'.dependencies] -libc = { workspace = true } diff --git a/src/backends/nanvix/runner/src/lib.rs b/src/backends/nanvix/runner/src/lib.rs deleted file mode 100644 index 99e21cf1d..000000000 --- a/src/backends/nanvix/runner/src/lib.rs +++ /dev/null @@ -1,1576 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! `NanVixScriptRunner` -- executes code inside a NanVix micro-VM. -//! -//! The initial runtime is CPython 3.12 with a trimmed FAT32 stdlib filesystem. -//! The architecture supports additional runtimes (QuickJS, C, C++, Rust binaries). -//! -//! ## I/O model -//! -//! - **stdin**: set to `Stdio::null()` (NanVix guest does not read host stdin) -//! - **stdout**: inherited from parent via `Stdio::inherit()` (not captured) -//! - **stderr**: inherited from parent by default (kernel traces stream -//! straight to the parent terminal). When the `MXC_NANVIX_TRACE` env var -//! is truthy, stderr is piped and captured so it can be embedded in the -//! wxc-exec log on non-zero exit. -//! -//! **Note for SDK consumers:** Use `usePty: false` (non-PTY mode) for the MicroVM -//! backend. PTY mode is not supported. Because stdout/stderr are inherited, -//! `ScriptResponse.standard_out` and `standard_err` are always empty strings. -//! Output is streamed directly to the parent's pipes. -//! -//! ## Diagnostics -//! -//! By default the runner sets `RUST_LOG=off` in nanvixd's environment, which -//! suppresses the per-run `%LOCALAPPDATA%\nanvix\logs\nanvixd_*.log` trace -//! file and noticeably reduces warm-start latency. Set `MXC_NANVIX_TRACE=1` -//! (or `true`/`yes`, case-insensitive) before invoking wxc-exec to let -//! nanvixd use its own `RUST_LOG` default and to capture nanvixd's stderr -//! for inclusion in the wxc-exec log. -//! -//! ## Exit codes -//! -//! `nanvixd` propagates the guest process exit code directly. -//! -//! ## Networking -//! -//! Host networking is **off by default** and is enabled per-run by passing -//! `-allow-host-networking` to `nanvixd`. The runner adds that flag when the -//! request sets `network.defaultPolicy = "allow"`, or when `allowedHosts` / -//! `blockedHosts` is present (forwarded as `-allow-host` / `-block-host`). -//! Network proxies are not supported and are rejected at validation time. -//! -//! Auto-discovery -//! -//! All required binaries (`nanvixd.exe`, `python3.initrd`, `nanvix_rootfs.img`) -//! are discovered next to the running executable. No configuration is needed. - -use std::fmt::Write; -use std::net::ToSocketAddrs; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Condvar, Mutex}; -use std::thread; -use std::thread::JoinHandle; -use std::time::{Duration, Instant}; - -use wxc_common::logger::Logger; -use wxc_common::models::{ExecutionRequest, NetworkPolicy, ScriptResponse}; -use wxc_common::script_runner::ScriptRunner; -use wxc_common::validator::{validate_network_policy_support, NetworkPolicySupport}; - -/// Multi-binary initrd (daemons + CPython) loaded by NanVix at warm start. -const INITRD_BINARY: &str = nanvix_common::INITRD_BINARY; -/// NanVix daemon binary launched by the host runner (platform-conditional). -const NANVIXD_BINARY: &str = nanvix_common::NANVIXD_BINARY; -/// Combined rootfs image (NanVix kernel userspace + CPython stdlib). -const RAMFS_IMAGE: &str = nanvix_common::RAMFS_IMAGE; -/// Pre-built VM state snapshot (CBOR) for warm start (Windows/WHP only). -#[cfg(target_os = "windows")] -const SNAPSHOT_CBOR: &str = nanvix_common::SNAPSHOT_CBOR; -/// Subdirectory holding snapshot files next to the exe (Windows/WHP only). -#[cfg(target_os = "windows")] -const SNAPSHOTS_DIR: &str = nanvix_common::SNAPSHOTS_SUBDIR; -/// Subdirectory holding kernel binary. -const BIN_DIR: &str = nanvix_common::BIN_SUBDIR; -/// Env var override for the NanVix snapshot home directory. Set this to -/// force a specific location; otherwise the runner uses a standard -/// OS-local data path or falls back to `/snapshots/`. -#[cfg(target_os = "windows")] -const NANVIX_HOME_ENV: &str = "NANVIX_HOME"; -/// Env var that opts in to nanvixd's verbose tracing (and captured stderr). -/// When unset (the default), the runner forces `RUST_LOG=off` for nanvixd -/// and inherits stderr, which saves ~25–30 ms per warm execution by -/// avoiding nanvixd's per-run log file and the host-side stderr drain. -const NANVIX_TRACE_ENV: &str = "MXC_NANVIX_TRACE"; -/// Final component of the default OS-local data path (Windows only). -#[cfg(target_os = "windows")] -const DEFAULT_HOME_LEAF: &str = "nanvix"; -/// Boot grace period that is always enforced. -const BOOT_TIMEOUT_MS: u64 = 60_000; -/// Generic error exit code returned to host callers. -const ERROR_EXIT_CODE: i32 = -1; -/// Maximum age of orphaned staging dirs before cleanup (1 hour). -const ORPHAN_SWEEP_MAX_AGE_SECS: u64 = 3600; -const ERR_DENIED_PATHS: &str = concat!( - "denied_paths is not meaningful for the microvm backend ", - "-- the guest has no host filesystem visibility. ", - "Only readwrite_paths and readonly_paths are supported", -); -const ERR_NETWORK_HOSTS: &str = concat!( - "allowedHosts and blockedHosts are mutually exclusive for the NanVix backend -- ", - "the guest egress filter is allow-XOR-block. Specify an allowlist (allowedHosts) ", - "or a blocklist (blockedHosts), not both", -); -const ERR_HOSTS_UNRESOLVED: &str = concat!( - "none of the specified allowedHosts/blockedHosts resolved to an IPv4 address -- ", - "the NanVix guest filter is IPv4-only; use IPv4 literals/CIDR or hosts with A records", -); -const ERR_BLOCKED_HOST_UNRESOLVED: &str = concat!( - "a blockedHosts entry did not resolve to any IPv4 address -- ", - "the NanVix guest egress filter is static (resolved once at preflight) and IPv4-only, ", - "so an unresolvable blocked host cannot be enforced. Silently dropping it would let ", - "traffic the policy explicitly blocks flow freely, so the run is rejected (fail-closed). ", - "Use an IPv4 literal/CIDR or a host with A records", -); -const ERR_PROXY_POLICY: &str = "network proxy is not supported by the NanVix backend"; -const ERR_WORKDIR: &str = "workingDirectory is not supported by the NanVix backend -- guest has its own filesystem namespace"; - -/// Outcome of resolving a request's egress host lists. -/// -/// `allow`/`block` are the IPv4/CIDR literals handed to nanvixd; at most one is -/// non-empty. `warnings` carries human-readable notices for allowlist entries -/// that were dropped during resolution (blocklist drops are a hard error and -/// never reach here). -#[derive(Debug)] -struct ResolvedHostLists { - allow: Vec, - block: Vec, - warnings: Vec, -} - -/// Maps a finished child's [`ExitStatus`] to a host-visible exit code. -/// -/// On Unix, processes terminated by a signal have no exit code (`status.code()` -/// returns `None`); we surface them as the negated signal number (e.g. SIGKILL -/// → `-9`) so callers can distinguish them from normal exits and from the -/// generic [`ERROR_EXIT_CODE`] sentinel. On Windows, `status.code()` always -/// returns `Some(_)`. -fn exit_code_from_status(status: &std::process::ExitStatus) -> i32 { - if let Some(code) = status.code() { - return code; - } - #[cfg(unix)] - { - use std::os::unix::process::ExitStatusExt; - if let Some(signal) = status.signal() { - return -signal; - } - } - ERROR_EXIT_CODE -} - -// -- NanVix error classification --------------------------------------------- - -/// Classifies NanVix runner errors for structured error handling. -#[derive(Debug)] -enum NanVixError { - /// Pre-spawn validation failures (missing artifacts, invalid config, unsupported policy). - Preflight(String), - /// OS/platform failures while spawning/managing the NanVix process (WHP/spawn/handles). - Platform(String), - /// Stdin broken pipe, VM crash. - Runtime(String), - /// Watchdog killed the process. - Timeout { - script_timeout_ms: u32, - total_ms: u64, - }, -} - -impl std::fmt::Display for NanVixError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - NanVixError::Preflight(msg) => write!(f, "NanVix preflight error: {}", msg), - NanVixError::Platform(msg) => write!(f, "NanVix platform error: {}", msg), - NanVixError::Runtime(msg) => write!(f, "NanVix runtime error: {}", msg), - NanVixError::Timeout { - script_timeout_ms, - total_ms, - } => write!( - f, - "NanVix execution timed out after {}ms \ - (boot_timeout={}ms, script_timeout={}ms)", - total_ms, BOOT_TIMEOUT_MS, script_timeout_ms - ), - } - } -} - -impl NanVixError { - fn to_response(&self) -> ScriptResponse { - ScriptResponse { - exit_code: ERROR_EXIT_CODE, - error_message: self.to_string(), - ..Default::default() - } - } -} - -/// Returns the directory containing the current executable. -/// -/// Inlined (rather than reusing `wxc_common::process_util::exe_dir`) because -/// `process_util` is gated to `target_os = "windows"`. -fn exe_dir() -> Result { - std::env::current_exe() - .map_err(|e| NanVixError::Preflight(format!("cannot determine exe path: {}", e))) - .and_then(|exe| { - exe.parent() - .map(|p| p.to_path_buf()) - .ok_or_else(|| NanVixError::Preflight("exe has no parent directory".to_string())) - }) -} - -/// Returns `true` when [`NANVIX_TRACE_ENV`] is set to a truthy value -/// (`"1"`, `"true"`, or `"yes"`, case-insensitive). Any other value -/// (including unset, empty, or `"0"`/`"false"`/`"no"`) means trace is off. -fn nanvix_trace_enabled() -> bool { - match std::env::var(NANVIX_TRACE_ENV) { - Ok(v) => matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes"), - Err(_) => false, - } -} - -/// Watchdog thread: waits for timeout or cancellation, then terminates the process. -/// -/// On Windows, `process_id_or_handle` is a duplicated process HANDLE (as usize). -/// On Linux, `process_id_or_handle` is the child PID (as usize). -fn watchdog_thread_fn( - process_id_or_handle: usize, - duration: Duration, - cancel_pair: Arc<(Mutex, Condvar)>, - timed_out: Arc, -) { - let (lock, cvar) = &*cancel_pair; - let mut cancelled = lock.lock().unwrap_or_else(|e| e.into_inner()); - let start = Instant::now(); - let mut remaining = duration; - loop { - let result = cvar - .wait_timeout(cancelled, remaining) - .unwrap_or_else(|e| e.into_inner()); - cancelled = result.0; - if *cancelled || result.1.timed_out() { - break; - } - let elapsed = start.elapsed(); - if elapsed >= duration { - break; - } - remaining = duration.saturating_sub(elapsed); - } - - #[cfg(target_os = "windows")] - { - // Always close the duplicated handle to avoid leaks. - let close_handle = |handle_raw: usize| { - use windows::Win32::Foundation::{CloseHandle, HANDLE}; - let handle = HANDLE(handle_raw as *mut std::ffi::c_void); - // SAFETY: `handle` was returned by `DuplicateHandle` in this - // process and is closed exactly once by this watchdog thread. - let _ = unsafe { CloseHandle(handle) }; - }; - - if *cancelled { - close_handle(process_id_or_handle); - return; - } - - timed_out.store(true, Ordering::SeqCst); - - use windows::Win32::Foundation::HANDLE; - use windows::Win32::System::Threading::TerminateProcess; - - let handle = HANDLE(process_id_or_handle as *mut std::ffi::c_void); - // SAFETY: `handle` is a valid duplicated process handle owned by - // this thread, and passing exit code 1 is valid for termination. - let _ = unsafe { TerminateProcess(handle, 1) }; - close_handle(process_id_or_handle); - } - - #[cfg(target_os = "linux")] - { - if *cancelled { - return; - } - - timed_out.store(true, Ordering::SeqCst); - - // Kill the child process by PID using SIGKILL. - let pid = process_id_or_handle as i32; - // SAFETY: sending SIGKILL to a known child PID is always valid. - // If the process already exited, `kill()` returns ESRCH which we ignore. - unsafe { - libc::kill(pid, libc::SIGKILL); - } - } -} - -/// Components returned by [`NanVixScriptRunner::setup_watchdog`]: the watchdog -/// thread handle (if a finite timeout was requested), the cancellation pair -/// used to signal early completion, and the `timed_out` flag. -type WatchdogState = ( - Option>, - Arc<(Mutex, Condvar)>, - Arc, -); - -// -- NanVixScriptRunner ------------------------------------------------------ - -/// Script runner that executes Python code inside a NanVix microkernel VM. -/// -/// All binaries are auto-discovered next to the running executable. -pub struct NanVixScriptRunner { - _private: (), -} - -impl Default for NanVixScriptRunner { - fn default() -> Self { - Self::new() - } -} - -/// Resolved paths for NanVix invocation. -#[derive(Debug)] -struct ResolvedPaths { - nanvixd: PathBuf, - ramfs: PathBuf, - initrd: PathBuf, - /// Directory holding the `bin/` subdir next to the exe. - exe_dir: PathBuf, - /// Snapshot home directory — used as cwd for nanvixd so it can locate - /// `snapshots/kernel.vmem` relative to cwd (nanvixd constraint). - snapshot_home: PathBuf, -} - -impl NanVixScriptRunner { - pub fn new() -> Self { - Self { _private: () } - } - - /// Resolve and validate all required paths next to the running executable. - fn resolve_paths(&self) -> Result { - let dir = exe_dir()?; - - let nanvixd = dir.join(NANVIXD_BINARY); - if !nanvixd.exists() { - return Err(NanVixError::Preflight(format!( - "{} not found in {:?}", - NANVIXD_BINARY, dir - ))); - } - - let ramfs = dir.join(RAMFS_IMAGE); - if !ramfs.exists() { - return Err(NanVixError::Preflight(format!( - "{} not found in {:?}", - RAMFS_IMAGE, dir - ))); - } - - let initrd = dir.join(INITRD_BINARY); - if !initrd.exists() { - return Err(NanVixError::Preflight(format!( - "{} not found in {:?}", - INITRD_BINARY, dir - ))); - } - - // Preflight-check bin/ subdir contents (nanvixd loads `./bin/kernel.elf` - // via `-bin-dir`; missing the file here yields a clearer error than - // letting nanvixd fail at boot time). - let bin_subdir = dir.join(BIN_DIR); - for name in nanvix_common::BIN_SUBDIR_FILES { - let path = bin_subdir.join(name); - if !path.exists() { - return Err(NanVixError::Preflight(format!( - "{}/{} not found in {:?}", - BIN_DIR, name, dir - ))); - } - } - - // Snapshot resolution — Windows only (WHP snapshots for warm start). - // Linux uses cold boot via KVM every time. - #[cfg(target_os = "windows")] - let snapshot_home = { - let home = Self::resolve_snapshot_home(&dir)?; - // Warm start requires *all* snapshot files (kernel.vmem + kernel.whp.cbor); - // a partial/corrupt set must trigger regeneration instead of a late failure - // inside nanvixd. - let snapshots_present = nanvix_common::SNAPSHOT_FILES - .iter() - .all(|name| home.join(SNAPSHOTS_DIR).join(name).exists()); - if !snapshots_present { - // No (complete) snapshot yet — generate one via cold boot - // (one-time cost, ~400–500 ms). Subsequent runs restore directly. - Self::generate_snapshot(&dir, &home, &nanvixd, &ramfs, &initrd)?; - } - home - }; - - #[cfg(target_os = "linux")] - let snapshot_home = dir.clone(); - - Ok(ResolvedPaths { - nanvixd, - ramfs, - initrd, - exe_dir: dir, - snapshot_home, - }) - } - - /// Resolve the snapshot home directory (Windows only — WHP snapshots). - /// - /// Discovery chain (first match wins): - /// 1. `$NANVIX_HOME` env var (if set and non-empty) - /// 2. `` directory itself, when a complete set of pre-generated - /// snapshots already lives in `/snapshots/` (build-time output - /// or shipped artifacts) — using it avoids a redundant cold boot. - /// 3. OS-local data path (`%LOCALAPPDATA%\nanvix` on Windows) - /// 4. `` directory itself as a last-resort fallback (dev builds — - /// nanvixd will write snapshots into `/snapshots/`). - #[cfg(target_os = "windows")] - fn resolve_snapshot_home(exe_dir: &Path) -> Result { - // 1. Env var override. - if let Some(val) = std::env::var_os(NANVIX_HOME_ENV) { - let p = PathBuf::from(val); - if !p.as_os_str().is_empty() { - std::fs::create_dir_all(&p).map_err(|e| { - NanVixError::Preflight(format!( - "cannot create ${} directory {:?}: {}", - NANVIX_HOME_ENV, p, e - )) - })?; - return Ok(p); - } - } - - // 2. Prefer exe-side snapshots when they're already complete, so - // build-time-generated artifacts shipped next to wxc-exec are - // actually used instead of triggering a fresh cold boot in - // %LOCALAPPDATA%. - let exe_snapshots = exe_dir.join(SNAPSHOTS_DIR); - let exe_snapshots_complete = nanvix_common::SNAPSHOT_FILES - .iter() - .all(|name| exe_snapshots.join(name).exists()); - if exe_snapshots_complete { - return Ok(exe_dir.to_path_buf()); - } - - // 3. OS-local data path. - if let Some(home) = Self::default_home() { - if home.exists() || std::fs::create_dir_all(&home).is_ok() { - return Ok(home); - } - } - - // 4. Fallback: exe directory itself (nanvixd writes to /snapshots/). - Ok(exe_dir.to_path_buf()) - } - - /// Default OS-local snapshot home path. - #[cfg(target_os = "windows")] - fn default_home() -> Option { - std::env::var_os("LOCALAPPDATA").map(|d| PathBuf::from(d).join(DEFAULT_HOME_LEAF)) - } - - /// Generate a WHP snapshot via cold boot (one-time cost, Windows only). - /// - /// Delegates to `nanvix_common::generate_snapshot` which runs nanvixd with - /// `-kernel-args snapshot` and cwd set to `snapshot_home`. nanvixd writes - /// snapshot files to `/snapshots/` directly. Subsequent runs - /// restore from the snapshot (~20 ms vs ~430 ms cold boot). - #[cfg(target_os = "windows")] - fn generate_snapshot( - exe_dir: &Path, - snapshot_home: &Path, - nanvixd: &Path, - ramfs: &Path, - initrd: &Path, - ) -> Result<(), NanVixError> { - std::fs::create_dir_all(snapshot_home).map_err(|e| { - NanVixError::Preflight(format!("failed to create snapshot home: {}", e)) - })?; - - eprintln!("nanvix: no snapshot found — generating via cold boot (one-time cost)..."); - - let start = Instant::now(); - nanvix_common::generate_snapshot( - snapshot_home, - nanvixd, - &exe_dir.join(BIN_DIR), - ramfs, - initrd, - ) - .map_err(NanVixError::Preflight)?; - - eprintln!( - "nanvix: snapshot generated in {:.0?} — subsequent runs will use warm start", - start.elapsed() - ); - Ok(()) - } - - /// Compute total timeout: boot grace + staging overhead + script timeout. - /// When `script_timeout == 0` the caller intends "no limit", so the watchdog - /// is disabled entirely (returns `u64::MAX`). Boot and staging time are - /// unbounded in this case — this is by design for interactive/exploratory use. - fn total_timeout_ms(script_timeout: u32, staging_overhead_ms: u64) -> u64 { - if script_timeout == 0 { - u64::MAX - } else { - BOOT_TIMEOUT_MS - .saturating_add(staging_overhead_ms) - .saturating_add(script_timeout as u64) - } - } - - /// Returns whether the request opts in to host networking. - /// - /// Host networking is enabled when `network.defaultPolicy = "allow"` OR when - /// a per-host allow/block list is present (a list always implies networking, - /// regardless of `defaultPolicy`). When enabled, the runner passes - /// `-allow-host-networking` to nanvixd; per-host lists are additionally - /// forwarded as `-allow-host`/`-block-host` (see [`Self::spawn_nanvixd`]). - fn host_networking_enabled(request: &ExecutionRequest) -> bool { - request.policy.default_network_policy == NetworkPolicy::Allow - || !request.policy.allowed_hosts.is_empty() - || !request.policy.blocked_hosts.is_empty() - } - - /// Resolves a host entry list into IPv4/CIDR literals for nanvixd's - /// `-allow-host`/`-block-host` flags, alongside the entries that resolved - /// to nothing. - /// - /// - `a.b.c.d` and `a.b.c.d/n` literals pass through unchanged. - /// - Hostnames resolve to their IPv4 (A-record) addresses; AAAA results are - /// dropped because the guest filter is IPv4-only. - /// - Entries that fail to parse or resolve to any IPv4 address contribute - /// nothing to the resolved list and are collected into the second return - /// value so callers can warn (allowlist) or reject (blocklist). - /// - /// Mirrors `lxc::network_iptables::resolve_host` for the hostname-to-IPv4 - /// mapping; unlike that helper this also preserves IPv4/CIDR literals. - /// - /// Returns `(resolved_ips, unresolved_entries)`. Empty/whitespace entries - /// are ignored entirely and appear in neither list. - fn resolve_hosts_detailed(hosts: &[String]) -> (Vec, Vec) { - let mut out: Vec = Vec::new(); - let mut unresolved: Vec = Vec::new(); - for host in hosts { - let entry = host.trim(); - if entry.is_empty() { - continue; - } - let before = out.len(); - // CIDR literal: pass through only when the address is IPv4 and the - // prefix is in range. nanvixd parses CIDR directly. - if let Some((addr, prefix)) = entry.split_once('/') { - let addr_ok = addr.trim().parse::().is_ok(); - let prefix_ok = prefix - .trim() - .parse::() - .map(|p| p <= 32) - .unwrap_or(false); - if addr_ok && prefix_ok { - out.push(entry.to_string()); - } - } else if let Ok(addr) = entry.parse::() { - // Bare IP literal: keep IPv4, drop IPv6. - if addr.is_ipv4() { - out.push(entry.to_string()); - } - } else if let Ok(addrs) = format!("{}:0", entry).to_socket_addrs() { - // Hostname: resolve to IPv4 A records. - for ip in addrs.map(|a| a.ip()).filter(|ip| ip.is_ipv4()) { - out.push(ip.to_string()); - } - } - if out.len() == before { - unresolved.push(entry.to_string()); - } - } - (out, unresolved) - } - - /// Resolves the request's allow/block host lists, failing closed. - /// - /// Returns the resolved allow/block IPv4 lists plus human-readable - /// warnings for any allowlist entries that were dropped. At most one list - /// is non-empty (the mutual-exclusion check in [`Self::validate_policies`] - /// runs first). - /// - /// Fail-closed semantics differ by list direction: - /// - **Allowlist** (deny-by-default): a fully unresolvable allowlist is an - /// error, because emitting `-allow-host-networking` with no filter would - /// fail open (nanvixd treats no list as allow-all). Partially dropped - /// entries only narrow access, so they are reported as warnings and the - /// run continues. - /// - **Blocklist** (allow-by-default): *any* unresolvable entry is an - /// error. Silently dropping a blocked host would let traffic the policy - /// explicitly blocks flow freely (fail-open), and the static preflight - /// filter cannot enforce a name that does not resolve. - fn resolve_host_lists(request: &ExecutionRequest) -> Result { - let (allow, allow_unresolved) = Self::resolve_hosts_detailed(&request.policy.allowed_hosts); - if !request.policy.allowed_hosts.is_empty() && allow.is_empty() { - return Err(NanVixError::Preflight(ERR_HOSTS_UNRESOLVED.to_string())); - } - - let (block, block_unresolved) = Self::resolve_hosts_detailed(&request.policy.blocked_hosts); - if let Some(first) = block_unresolved.first() { - return Err(NanVixError::Preflight(format!( - "{} (entry: '{}')", - ERR_BLOCKED_HOST_UNRESOLVED, first - ))); - } - - let warnings = allow_unresolved - .iter() - .map(|h| { - format!( - "Warning: could not resolve allowedHosts entry '{}' to an IPv4 address; skipping", - h - ) - }) - .collect(); - - Ok(ResolvedHostLists { - allow, - block, - warnings, - }) - } - - fn validate_policies(request: &ExecutionRequest) -> Result<(), NanVixError> { - // denied_paths is explicitly rejected — microvm has no host visibility. - if !request.policy.denied_paths.is_empty() { - return Err(NanVixError::Preflight(ERR_DENIED_PATHS.to_string())); - } - // Per-host filtering is supported (forwarded to nanvixd as - // -allow-host/-block-host). The guest egress filter is allow-XOR-block, - // so the two lists are mutually exclusive; defaultPolicy is ignored when - // either list is present. - if !request.policy.allowed_hosts.is_empty() && !request.policy.blocked_hosts.is_empty() { - return Err(NanVixError::Preflight(ERR_NETWORK_HOSTS.to_string())); - } - if request.policy.network_proxy.is_enabled() { - return Err(NanVixError::Preflight(ERR_PROXY_POLICY.to_string())); - } - if !request.working_directory.is_empty() { - return Err(NanVixError::Preflight(ERR_WORKDIR.to_string())); - } - - Ok(()) - } - - fn spawn_nanvixd( - paths: &ResolvedPaths, - staging_dir: &Path, - host_networking: bool, - allow_hosts: &[String], - block_hosts: &[String], - ) -> Result { - let trace = nanvix_trace_enabled(); - // Default: silence nanvixd and inherit stderr so kernel traces (if - // any) stream straight to the parent terminal without a per-run - // host-side drain. Diagnostic mode pipes stderr so the runner can - // attach it to the wxc-exec log on failure. - let stderr = if trace { - Stdio::piped() - } else { - Stdio::inherit() - }; - - let mut cmd = Command::new(&paths.nanvixd); - - // Host networking is opt-in. When enabled, attach the host network - // backend; nanvixd parses this flag regardless of argument order, so - // it is added up front for both the Windows (snapshot) and Linux - // (cold-boot) invocations below. Verified to work on warm-start - // snapshot restore as well as cold boot. - if host_networking { - cmd.arg("-allow-host-networking"); - } - - // Per-host egress filtering. The two lists are mutually exclusive - // (validated upstream), so at most one of these loops emits flags. - // nanvixd requires `-allow-host-networking` for these to take effect, - // which is guaranteed because a non-empty list forces host_networking - // on (see `host_networking_enabled`). The guest daemon auto-exempts the - // DNS port in allowlist mode, so no resolver IPs are added here. - for host in allow_hosts { - cmd.arg("-allow-host").arg(host); - } - for host in block_hosts { - cmd.arg("-block-host").arg(host); - } - - #[cfg(target_os = "windows")] - { - // nanvixd loads kernel.vmem from /snapshots/ so cwd must be - // the snapshot home. All other paths are passed as absolute. - // nanvixd.exe [-allow-host-networking] -snapshot snapshots/kernel.whp.cbor - // -bin-dir /bin -ramfs -mount -- python3.initrd - let snapshot_rel = Path::new(SNAPSHOTS_DIR).join(SNAPSHOT_CBOR); - cmd.current_dir(&paths.snapshot_home) - .arg("-snapshot") - .arg(&snapshot_rel) - .arg("-bin-dir") - .arg(paths.exe_dir.join(BIN_DIR)) - .arg("-ramfs") - .arg(&paths.ramfs) - .arg("-mount") - .arg(staging_dir) - .arg("--") - .arg(&paths.initrd); - } - - #[cfg(target_os = "linux")] - { - // Linux invocation (cold boot via KVM): - // nanvixd.elf [-allow-host-networking] -ramfs -mount -- python3.initrd - cmd.current_dir(&paths.exe_dir) - .arg("-ramfs") - .arg(&paths.ramfs) - .arg("-mount") - .arg(staging_dir) - .arg("--") - .arg(&paths.initrd); - } - - cmd.stdin(Stdio::null()) - .stdout(Stdio::inherit()) - .stderr(stderr); - if !trace { - // Suppress nanvixd's env_logger output and per-run log file. - cmd.env("RUST_LOG", "off"); - } - cmd.spawn().map_err(|e| { - NanVixError::Platform(format!("failed to spawn {}: {}", NANVIXD_BINARY, e)) - }) - } - - fn start_watchdog( - child: &std::process::Child, - timeout_ms: u64, - cancel_pair: Arc<(Mutex, Condvar)>, - timed_out: Arc, - ) -> Option> { - if timeout_ms == u64::MAX { - return None; - } - - let duration = Duration::from_millis(timeout_ms); - - #[cfg(target_os = "windows")] - { - // Duplicate the process handle at spawn time (safe against PID reuse). - use std::os::windows::io::AsRawHandle; - use windows::Win32::Foundation::{DuplicateHandle, DUPLICATE_SAME_ACCESS, HANDLE}; - use windows::Win32::System::Threading::GetCurrentProcess; - - let raw = child.as_raw_handle(); - let mut dup_handle = HANDLE::default(); - let dup_ok = unsafe { - // SAFETY: `raw` is the live process HANDLE from `std::process::Child`. - // We duplicate it into the current process with same access rights so - // the watchdog thread can safely terminate/close it independently. - DuplicateHandle( - GetCurrentProcess(), - HANDLE(raw), - GetCurrentProcess(), - &mut dup_handle, - 0, - false, - DUPLICATE_SAME_ACCESS, - ) - }; - if dup_ok.is_err() { - return None; - } - let process_id_or_handle = dup_handle.0 as usize; - - Some(thread::spawn(move || { - watchdog_thread_fn(process_id_or_handle, duration, cancel_pair, timed_out); - })) - } - - #[cfg(target_os = "linux")] - { - // On Linux, use the child PID directly for kill-based termination. - let pid = child.id() as usize; - - Some(thread::spawn(move || { - watchdog_thread_fn(pid, duration, cancel_pair, timed_out); - })) - } - } - - fn setup_watchdog( - child: &mut std::process::Child, - timeout_ms: u64, - logger: &mut Logger, - ) -> Result { - let timed_out = Arc::new(AtomicBool::new(false)); - let cancel_pair = Arc::new((Mutex::new(false), Condvar::new())); - - let watchdog = if timeout_ms < u64::MAX { - match Self::start_watchdog( - child, - timeout_ms, - Arc::clone(&cancel_pair), - Arc::clone(&timed_out), - ) { - Some(handle) => Some(handle), - None => { - let err = NanVixError::Platform(format!( - "failed to duplicate {} process handle", - NANVIXD_BINARY - )); - let _ = writeln!(logger, "{}", err); - if let Err(e) = child.kill() { - let _ = writeln!( - logger, - "NanVix: failed to kill child after handle dup failure: {}", - e - ); - } - if let Err(e) = child.wait() { - let _ = writeln!( - logger, - "NanVix: failed to wait for child after handle dup failure: {}", - e - ); - } - return Err(err.to_response()); - } - } - } else { - None - }; - - Ok((watchdog, cancel_pair, timed_out)) - } - - fn log_resolved_paths(logger: &mut Logger, paths: &ResolvedPaths) { - let _ = writeln!(logger, "NanVix: nanvixd={:?}", paths.nanvixd); - let _ = writeln!(logger, "NanVix: ramfs={:?}", paths.ramfs); - let _ = writeln!(logger, "NanVix: initrd={:?}", paths.initrd); - let _ = writeln!(logger, "NanVix: snapshot_home={:?}", paths.snapshot_home); - } - - fn wait_and_respond( - child: &mut Child, - watchdog: Option>, - cancel_pair: &Arc<(Mutex, Condvar)>, - timed_out: &AtomicBool, - timeout_ms: u64, - script_timeout: u32, - logger: &mut Logger, - ) -> ScriptResponse { - // Drain stderr concurrently with `wait()` so a verbose child cannot - // block on a full pipe buffer. We retain only the last - // [`nanvix_common::STDERR_TAIL_BYTES`] bytes so an untrusted guest - // emitting unbounded stderr cannot cause host memory growth - // (availability / DoS hardening). In the default (non-trace) mode - // stderr is inherited and `child.stderr` is `None`, so the join - // returns the empty string immediately. - let stderr_handle = child - .stderr - .take() - .map(|s| thread::spawn(move || nanvix_common::drain_stderr_tail(s))); - - let exit_status = child.wait(); - - let stderr_output = stderr_handle - .and_then(|h| h.join().ok()) - .map(|(bytes, truncated)| nanvix_common::format_stderr_tail(&bytes, truncated)) - .unwrap_or_default(); - - { - let (lock, cvar) = &**cancel_pair; - let mut cancelled = lock.lock().unwrap_or_else(|e| e.into_inner()); - *cancelled = true; - cvar.notify_one(); - } - - if let Some(handle) = watchdog { - let _ = handle.join(); - } - - if timed_out.load(Ordering::SeqCst) { - let _ = child.kill(); - let err = NanVixError::Timeout { - script_timeout_ms: script_timeout, - total_ms: timeout_ms, - }; - let _ = writeln!(logger, "{}", err); - return err.to_response(); - } - - match exit_status { - Ok(status) => { - let exit_code = exit_code_from_status(&status); - let _ = writeln!(logger, "NanVix: process exited with code {}", exit_code); - if exit_code != 0 && !stderr_output.is_empty() { - let _ = writeln!(logger, "NanVix stderr:\n{}", stderr_output); - } - ScriptResponse { - exit_code, - ..Default::default() - } - } - Err(e) => { - if !stderr_output.is_empty() { - let _ = writeln!(logger, "NanVix stderr:\n{}", stderr_output); - } - let err = - NanVixError::Runtime(format!("failed to wait for {}: {}", NANVIXD_BINARY, e)); - let _ = writeln!(logger, "{}", err); - err.to_response() - } - } - } - /// Returns `true` when filesystem copyback should run. - /// Copyback runs on any normal process exit (including non-zero exit codes). - /// It is skipped for preflight, spawn, runtime, and timeout errors, and for - /// OS crashes (negative exit codes from NTSTATUS values). - fn should_copy_back(response: &ScriptResponse) -> bool { - response.error_message.is_empty() && response.exit_code >= 0 - } -} - -impl ScriptRunner for NanVixScriptRunner { - fn validate_runner(&self, request: &ExecutionRequest) -> Result<(), ScriptResponse> { - Self::validate_policies(request).map_err(|e| e.to_response())?; - validate_network_policy_support(request, NetworkPolicySupport::LEGACY)?; - Ok(()) - } - - fn execute(&mut self, request: &ExecutionRequest, logger: &mut Logger) -> ScriptResponse { - let paths = match self.resolve_paths() { - Ok(p) => p, - Err(e) => return e.to_response(), - }; - - // Build staging directory with script and filesystem policy paths. - let staging_root = std::env::temp_dir().join("mxc-microvm"); - // Sweep orphaned staging dirs from previous crashed runs (older than 1 hour). - wxc_common::microvm_staging::sweep_orphaned_staging_dirs( - &staging_root, - std::time::Duration::from_secs(ORPHAN_SWEEP_MAX_AGE_SECS), - ); - let mut staging = match wxc_common::microvm_staging::StagingDir::new( - staging_root, - &request.script_code, - &request.policy.readwrite_paths, - &request.policy.readonly_paths, - ) { - Ok(s) => s, - Err(e) => { - let err = NanVixError::Preflight(e.to_string()); - let _ = writeln!(logger, "{}", err); - return err.to_response(); - } - }; - - Self::log_resolved_paths(logger, &paths); - let _ = writeln!(logger, "NanVix: staging_dir={:?}", staging.path()); - - let host_networking = Self::host_networking_enabled(request); - if host_networking { - let _ = writeln!(logger, "NanVix: host networking enabled"); - } - let (allow_hosts, block_hosts) = match Self::resolve_host_lists(request) { - Ok(resolved) => { - for warning in &resolved.warnings { - let _ = writeln!(logger, "NanVix: {}", warning); - } - (resolved.allow, resolved.block) - } - Err(e) => { - let _ = writeln!(logger, "{}", e); - return e.to_response(); - } - }; - if !allow_hosts.is_empty() { - let _ = writeln!(logger, "NanVix: egress allowlist={:?}", allow_hosts); - } - if !block_hosts.is_empty() { - let _ = writeln!(logger, "NanVix: egress blocklist={:?}", block_hosts); - } - let mut child = match Self::spawn_nanvixd( - &paths, - staging.path(), - host_networking, - &allow_hosts, - &block_hosts, - ) { - Ok(c) => c, - Err(e) => { - let _ = writeln!(logger, "{}", e); - return e.to_response(); - } - }; - - let staging_overhead = staging.staging_overhead_ms(); - let timeout_ms = Self::total_timeout_ms(request.script_timeout, staging_overhead); - let (watchdog, cancel_pair, timed_out) = - match Self::setup_watchdog(&mut child, timeout_ms, logger) { - Ok(v) => v, - Err(resp) => return resp, - }; - - let response = Self::wait_and_respond( - &mut child, - watchdog, - &cancel_pair, - timed_out.as_ref(), - timeout_ms, - request.script_timeout, - logger, - ); - - // Copy back RW filesystem changes on normal process exit. - if Self::should_copy_back(&response) { - if let Err(e) = staging.copy_back_to_host() { - let preserved = staging - .preserved_path() - .map(|p| p.display().to_string()) - .unwrap_or_default(); - let err = NanVixError::Runtime(format!( - "failed to copy back microvm filesystem changes: {}. \ - Staged files preserved at: {}", - e, preserved - )); - let _ = writeln!(logger, "{}", err); - return err.to_response(); - } - } - - response - // staging is dropped here → cleanup - } -} - -#[cfg(test)] -mod tests { - use super::*; - use wxc_common::logger::{Logger, Mode}; - use wxc_common::models::{ContainerPolicy, NetworkPolicy}; - - #[test] - fn total_timeout_adds_boot_staging_and_script() { - // script_timeout=0 => infinite script timeout sentinel. - assert_eq!(NanVixScriptRunner::total_timeout_ms(0, 0), u64::MAX); - // script_timeout=30000, staging_overhead=500 -> 30s + 500ms + 60s boot = 90.5s - assert_eq!(NanVixScriptRunner::total_timeout_ms(30_000, 500), 90_500); - // script_timeout=30000, no staging -> 30s + 60s boot = 90s - assert_eq!(NanVixScriptRunner::total_timeout_ms(30_000, 0), 90_000); - } - - #[test] - fn resolve_paths_fails_when_exe_dir_has_no_binaries() { - let runner = NanVixScriptRunner::new(); - let err = runner.resolve_paths().unwrap_err(); - assert!(err.to_string().contains("not found"), "got: {}", err); - } - - // -- Policy validation tests ------------------------------------------------- - - #[test] - fn policy_accepts_readwrite_paths() { - // Validation passes; the runner fails later on path resolution. - let request = ExecutionRequest { - script_code: "echo test".to_string(), - policy: ContainerPolicy { - readwrite_paths: vec!["/tmp".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let result = NanVixScriptRunner::validate_policies(&request); - assert!(result.is_ok(), "readwrite_paths accepted"); - } - - #[test] - fn policy_accepts_readonly_paths() { - let request = ExecutionRequest { - script_code: "echo test".to_string(), - policy: ContainerPolicy { - readonly_paths: vec!["/data".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let result = NanVixScriptRunner::validate_policies(&request); - assert!(result.is_ok(), "readonly_paths accepted"); - } - - #[test] - fn policy_rejects_denied_paths() { - let request = ExecutionRequest { - policy: ContainerPolicy { - denied_paths: vec!["/secret".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let result = NanVixScriptRunner::validate_policies(&request); - assert!(result.is_err(), "denied_paths should be rejected"); - let err = result.unwrap_err().to_string(); - assert!( - err.contains(ERR_DENIED_PATHS), - "expected denied_paths error, got: {}", - err - ); - } - - #[test] - fn policy_accepts_allowlist_only() { - // A bare allowlist is now supported (forwarded as -allow-host). - let request = ExecutionRequest { - script_code: "echo test".to_string(), - policy: ContainerPolicy { - allowed_hosts: vec!["93.184.216.34".to_string()], - ..Default::default() - }, - ..Default::default() - }; - assert!( - NanVixScriptRunner::validate_policies(&request).is_ok(), - "a bare allowlist should pass validation" - ); - // A list implies host networking regardless of defaultPolicy (Block). - assert!(NanVixScriptRunner::host_networking_enabled(&request)); - } - - #[test] - fn policy_accepts_blocklist_only() { - // A bare blocklist is now supported (forwarded as -block-host). - let request = ExecutionRequest { - script_code: "echo test".to_string(), - policy: ContainerPolicy { - blocked_hosts: vec!["93.184.216.34".to_string()], - ..Default::default() - }, - ..Default::default() - }; - assert!( - NanVixScriptRunner::validate_policies(&request).is_ok(), - "a bare blocklist should pass validation" - ); - assert!(NanVixScriptRunner::host_networking_enabled(&request)); - } - - #[test] - fn policy_rejects_both_host_lists() { - // allow + block are mutually exclusive (the guest filter is allow-XOR-block). - let request = ExecutionRequest { - script_code: "echo test".to_string(), - policy: ContainerPolicy { - allowed_hosts: vec!["10.0.0.1".to_string()], - blocked_hosts: vec!["10.0.0.2".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let err = NanVixScriptRunner::validate_policies(&request).unwrap_err(); - assert!( - err.to_string().contains(ERR_NETWORK_HOSTS), - "both lists should be rejected, got: {}", - err - ); - } - - // -- Host resolution / decision-matrix tests -------------------------------- - - #[test] - fn resolve_hosts_passes_ipv4_and_cidr_literals() { - let hosts = vec![ - "1.2.3.4".to_string(), - "10.0.0.0/8".to_string(), - "192.168.1.1/32".to_string(), - ]; - let (resolved, unresolved) = NanVixScriptRunner::resolve_hosts_detailed(&hosts); - assert_eq!(resolved, vec!["1.2.3.4", "10.0.0.0/8", "192.168.1.1/32"]); - assert!(unresolved.is_empty()); - } - - #[test] - fn resolve_hosts_drops_ipv6_and_bad_entries() { - let hosts = vec![ - "::1".to_string(), // IPv6 literal -> dropped - "2001:db8::/32".to_string(), // IPv6 CIDR -> dropped (addr not IPv4) - "1.2.3.4/33".to_string(), // out-of-range prefix -> dropped - " ".to_string(), // blank -> skipped - "5.6.7.8".to_string(), // valid -> kept - ]; - let (resolved, _) = NanVixScriptRunner::resolve_hosts_detailed(&hosts); - assert_eq!(resolved, vec!["5.6.7.8"]); - } - - #[test] - fn resolve_host_lists_returns_resolved_allow() { - let request = ExecutionRequest { - policy: ContainerPolicy { - allowed_hosts: vec!["1.1.1.1".to_string(), "8.8.8.8/32".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let resolved = NanVixScriptRunner::resolve_host_lists(&request).unwrap(); - assert_eq!(resolved.allow, vec!["1.1.1.1", "8.8.8.8/32"]); - assert!(resolved.block.is_empty()); - assert!(resolved.warnings.is_empty()); - } - - #[test] - fn resolve_host_lists_fails_closed_when_allowlist_unresolvable() { - // A non-empty allowlist that resolves to nothing must error rather than - // silently fall through to allow-all. - let request = ExecutionRequest { - policy: ContainerPolicy { - // IPv6-only literal resolves to no IPv4 entry. - allowed_hosts: vec!["::1".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let err = NanVixScriptRunner::resolve_host_lists(&request).unwrap_err(); - assert!( - err.to_string().contains(ERR_HOSTS_UNRESOLVED), - "unresolvable allowlist should fail closed, got: {}", - err - ); - } - - #[test] - fn resolve_hosts_detailed_reports_unresolved_entries() { - // IPv4/CIDR pass through; IPv6 + malformed entries are reported as - // unresolved; blanks are ignored entirely. - let hosts = vec![ - "5.6.7.8".to_string(), // valid -> kept - "::1".to_string(), // IPv6 literal -> unresolved - "2001:db8::/32".to_string(), // IPv6 CIDR -> unresolved - "1.2.3.4/33".to_string(), // out-of-range prefix -> unresolved - "not_a_host.invalid".to_string(), // no A record -> unresolved - " ".to_string(), // blank -> ignored (neither list) - ]; - let (resolved, unresolved) = NanVixScriptRunner::resolve_hosts_detailed(&hosts); - assert_eq!(resolved, vec!["5.6.7.8"]); - assert_eq!( - unresolved, - vec!["::1", "2001:db8::/32", "1.2.3.4/33", "not_a_host.invalid"] - ); - } - - #[test] - fn resolve_host_lists_warns_on_dropped_allowlist_entries() { - // A partially-resolvable allowlist narrows access (fail-safe): the run - // continues and each dropped entry produces a warning. - let request = ExecutionRequest { - policy: ContainerPolicy { - allowed_hosts: vec![ - "9.9.9.9".to_string(), - "::1".to_string(), - "dropme.invalid".to_string(), - ], - ..Default::default() - }, - ..Default::default() - }; - let resolved = NanVixScriptRunner::resolve_host_lists(&request).unwrap(); - assert_eq!(resolved.allow, vec!["9.9.9.9"]); - assert!(resolved.block.is_empty()); - assert_eq!(resolved.warnings.len(), 2, "two entries were dropped"); - assert!(resolved.warnings.iter().any(|w| w.contains("::1"))); - assert!(resolved - .warnings - .iter() - .any(|w| w.contains("dropme.invalid"))); - } - - #[test] - fn resolve_host_lists_fails_closed_on_unresolvable_blocklist_entry() { - // A blocklist (allow-by-default) must fail closed if ANY entry cannot - // be resolved -- silently dropping it would let blocked traffic flow. - let request = ExecutionRequest { - policy: ContainerPolicy { - blocked_hosts: vec!["10.0.0.1".to_string(), "::1".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let err = NanVixScriptRunner::resolve_host_lists(&request).unwrap_err(); - assert!( - err.to_string().contains(ERR_BLOCKED_HOST_UNRESOLVED), - "unresolvable blocklist entry should fail closed, got: {}", - err - ); - assert!( - err.to_string().contains("::1"), - "error should name the offending entry, got: {}", - err - ); - } - - #[test] - fn resolve_host_lists_accepts_fully_resolved_blocklist() { - let request = ExecutionRequest { - policy: ContainerPolicy { - blocked_hosts: vec!["10.0.0.1".to_string(), "192.168.0.0/16".to_string()], - ..Default::default() - }, - ..Default::default() - }; - let resolved = NanVixScriptRunner::resolve_host_lists(&request).unwrap(); - assert!(resolved.allow.is_empty()); - assert_eq!(resolved.block, vec!["10.0.0.1", "192.168.0.0/16"]); - assert!(resolved.warnings.is_empty()); - } - - #[test] - fn default_block_no_lists_disables_host_networking() { - // The default posture (block, no lists) keeps networking off. - let request = ExecutionRequest::default(); - assert!(!NanVixScriptRunner::host_networking_enabled(&request)); - let resolved = NanVixScriptRunner::resolve_host_lists(&request).unwrap(); - assert!(resolved.allow.is_empty() && resolved.block.is_empty()); - assert!(resolved.warnings.is_empty()); - } - - #[test] - fn allow_policy_enables_host_networking() { - // `network.defaultPolicy = "allow"` maps to host networking and must - // pass validation (the run later fails on missing nanvixd binaries, - // not on policy). Per-host filtering is absent, so it is accepted. - let request = ExecutionRequest { - script_code: "echo test".to_string(), - policy: ContainerPolicy { - default_network_policy: NetworkPolicy::Allow, - ..Default::default() - }, - ..Default::default() - }; - assert!(NanVixScriptRunner::host_networking_enabled(&request)); - assert!( - NanVixScriptRunner::validate_policies(&request).is_ok(), - "allow posture without per-host filtering should pass validation" - ); - - let mut runner = NanVixScriptRunner::new(); - let mut logger = Logger::new(Mode::Buffer); - let resp = runner.run(&request, &mut logger); - assert_eq!(resp.exit_code, ERROR_EXIT_CODE); - assert!( - !resp.error_message.contains(ERR_NETWORK_HOSTS), - "allow posture must not trigger a network policy rejection, got: {}", - resp.error_message - ); - } - - #[test] - fn policy_rejects_working_directory() { - let mut runner = NanVixScriptRunner::new(); - let request = ExecutionRequest { - script_code: "echo test".to_string(), - working_directory: "/home/user".to_string(), - ..Default::default() - }; - let mut logger = Logger::new(Mode::Buffer); - let resp = runner.run(&request, &mut logger); - assert_eq!(resp.exit_code, ERROR_EXIT_CODE); - assert!(resp.error_message.contains(ERR_WORKDIR)); - } - - #[test] - fn policy_allows_defaults() { - // NanVix accepts a default (deny-by-default) policy. With no host - // networking requested, the run later fails on missing nanvixd - // binaries, not on policy. - let mut runner = NanVixScriptRunner::new(); - let request = ExecutionRequest { - script_code: "echo test".to_string(), - ..Default::default() - }; - assert!(!NanVixScriptRunner::host_networking_enabled(&request)); - let mut logger = Logger::new(Mode::Buffer); - let resp = runner.run(&request, &mut logger); - assert_eq!(resp.exit_code, ERROR_EXIT_CODE); - assert!( - !resp.error_message.contains(ERR_NETWORK_HOSTS), - "default request should not trigger network policy rejection" - ); - assert!( - !resp.error_message.contains(ERR_WORKDIR), - "default request should not trigger workingDirectory rejection" - ); - } - - #[test] - fn policy_rejects_network_proxy() { - let mut runner = NanVixScriptRunner::new(); - let request = ExecutionRequest { - script_code: "echo test".to_string(), - policy: ContainerPolicy { - network_proxy: wxc_common::models::ProxyConfig { - address: Some(wxc_common::models::ProxyAddress::new( - "127.0.0.1".to_string(), - 8080, - )), - builtin_test_server: false, - }, - ..Default::default() - }, - ..Default::default() - }; - let mut logger = Logger::new(Mode::Buffer); - let resp = runner.run(&request, &mut logger); - assert_eq!(resp.exit_code, ERROR_EXIT_CODE); - assert!(resp.error_message.contains(ERR_PROXY_POLICY)); - } - - #[test] - fn resolve_paths_checks_for_snapshot() { - let runner = NanVixScriptRunner::new(); - let err = runner.resolve_paths().unwrap_err(); - // Should fail on missing binaries (not on snapshot specifically, - // since nanvixd.exe is checked first). - assert!(err.to_string().contains("not found"), "got: {}", err); - } - - // -- Copyback decision tests ------------------------------------------------ - - #[test] - fn copyback_allowed_for_zero_exit() { - let response = ScriptResponse { - exit_code: 0, - ..Default::default() - }; - assert!(NanVixScriptRunner::should_copy_back(&response)); - } - - #[test] - fn copyback_allowed_for_nonzero_normal_exit() { - let response = ScriptResponse { - exit_code: 42, - ..Default::default() - }; - assert!(NanVixScriptRunner::should_copy_back(&response)); - } - - #[test] - fn copyback_skipped_for_runner_error() { - let response = ScriptResponse { - exit_code: ERROR_EXIT_CODE, - error_message: "NanVix execution timed out after 90000ms".to_string(), - ..Default::default() - }; - assert!(!NanVixScriptRunner::should_copy_back(&response)); - } - - #[test] - fn copyback_skipped_for_os_crash() { - // STATUS_ACCESS_VIOLATION = 0xC0000005 → interpreted as i32 = -1073741819. - // This is a nanvixd OS crash — copyback must be suppressed. - let response = ScriptResponse { - exit_code: -1073741819_i32, - error_message: String::new(), - ..Default::default() - }; - assert!( - !NanVixScriptRunner::should_copy_back(&response), - "copyback must be skipped for NTSTATUS crash exit codes" - ); - } - - #[test] - fn copyback_skipped_for_signal_killed() { - // On Linux, SIGKILL results in exit code -9 (negative signal number). - let response = ScriptResponse { - exit_code: -9, - error_message: String::new(), - ..Default::default() - }; - assert!( - !NanVixScriptRunner::should_copy_back(&response), - "copyback must be skipped for signal-killed processes" - ); - } - - // -- Platform-specific constant tests --------------------------------------- - - #[test] - fn nanvixd_binary_matches_platform() { - #[cfg(target_os = "linux")] - assert_eq!(NANVIXD_BINARY, "nanvixd.elf"); - #[cfg(target_os = "windows")] - assert_eq!(NANVIXD_BINARY, "nanvixd.exe"); - } - - #[test] - fn total_timeout_infinite_when_zero() { - assert_eq!(NanVixScriptRunner::total_timeout_ms(0, 0), u64::MAX); - assert_eq!(NanVixScriptRunner::total_timeout_ms(0, 500), u64::MAX); - } - - #[test] - fn total_timeout_saturates_on_overflow() { - // With values that would cause u64 overflow, should saturate at u64::MAX. - let result = NanVixScriptRunner::total_timeout_ms(u32::MAX, u64::MAX - 1); - assert_eq!(result, u64::MAX); - } - - // -- Watchdog timeout state tests ------------------------------------------ - - #[test] - fn watchdog_state_no_thread_when_infinite_timeout() { - // When timeout is u64::MAX, start_watchdog should return None. - // We can't test this directly without a real child process, but we can - // verify the total_timeout_ms sentinel logic. - let timeout = NanVixScriptRunner::total_timeout_ms(0, 0); - assert_eq!( - timeout, - u64::MAX, - "zero script_timeout should yield infinite" - ); - } - - // -- NanVixError display tests --------------------------------------------- - - #[test] - fn error_display_preflight() { - let err = NanVixError::Preflight("missing binary".to_string()); - assert!(err.to_string().contains("preflight")); - assert!(err.to_string().contains("missing binary")); - } - - #[test] - fn error_display_platform() { - let err = NanVixError::Platform("spawn failed".to_string()); - assert!(err.to_string().contains("platform")); - assert!(err.to_string().contains("spawn failed")); - } - - #[test] - fn error_display_runtime() { - let err = NanVixError::Runtime("VM crashed".to_string()); - assert!(err.to_string().contains("runtime")); - assert!(err.to_string().contains("VM crashed")); - } - - #[test] - fn error_display_timeout() { - let err = NanVixError::Timeout { - script_timeout_ms: 5000, - total_ms: 65000, - }; - let msg = err.to_string(); - assert!(msg.contains("timed out")); - assert!(msg.contains("65000")); - assert!(msg.contains("5000")); - } - - #[test] - fn error_to_response_has_error_exit_code() { - let err = NanVixError::Preflight("test".to_string()); - let resp = err.to_response(); - assert_eq!(resp.exit_code, ERROR_EXIT_CODE); - assert!(!resp.error_message.is_empty()); - } -} diff --git a/src/core/lxc/Cargo.toml b/src/core/lxc/Cargo.toml index 6b0b692d7..05bcb33ae 100644 --- a/src/core/lxc/Cargo.toml +++ b/src/core/lxc/Cargo.toml @@ -10,11 +10,9 @@ path = "src/main.rs" [features] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight", "mxc_engine/hyperlight"] -microvm = ["dep:nanvix_binaries", "nanvix_binaries/microvm", "mxc_engine/microvm"] [build-dependencies] mxc_build_common.workspace = true -nanvix_build_common = { path = "../../backends/nanvix/build_common" } [dependencies] wxc_common = { workspace = true } @@ -22,7 +20,5 @@ mxc_engine = { workspace = true } lxc_common = { workspace = true } anyhow = { workspace = true } clap = { workspace = true } -nanvix_binaries = { path = "../../backends/nanvix/binaries", optional = true } - [target.'cfg(target_arch = "x86_64")'.dependencies] hyperlight_common = { workspace = true, optional = true } diff --git a/src/core/lxc/build.rs b/src/core/lxc/build.rs index 1d5d5da99..d0f33bcf2 100644 --- a/src/core/lxc/build.rs +++ b/src/core/lxc/build.rs @@ -1,16 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Build script for lxc — embeds Windows VersionInfo (no-op on non-Windows) -//! and copies NanVix binaries next to the output executable when the -//! `microvm` feature is enabled. +//! Build script for lxc — embeds Windows VersionInfo (no-op on non-Windows). fn main() { mxc_build_common::embed_version_info("LXC container executor (Linux stub)", "lxc-exec.exe"); - #[cfg(all(target_os = "linux", feature = "microvm"))] - copy_nanvix_binaries(); - // Delay-load winhvplatform.dll so WHP-less hosts don't crash before main(). // CARGO_CFG_TARGET_* (not #[cfg]) because build.rs cfg gates are host, not target. #[cfg(feature = "hyperlight")] @@ -25,21 +20,3 @@ fn main() { println!("cargo:rerun-if-changed=build.rs"); } - -#[cfg(all(target_os = "linux", feature = "microvm"))] -fn copy_nanvix_binaries() { - use std::path::Path; - - let nanvix_bin_dir = match std::env::var("DEP_NANVIX_BINARIES_BIN_DIR") { - Ok(dir) => dir, - Err(_) => { - eprintln!("lxc build.rs: DEP_NANVIX_BINARIES_BIN_DIR not set, skipping copy"); - return; - } - }; - - // Stage the artifacts next to the executable and emit rerun triggers. All - // of the staging logic (target-dir derivation, snapshot trust, copy/purge, - // rerun emission) lives in the build-only `nanvix_build_common` crate. - nanvix_build_common::stage_artifacts_next_to_exe(Path::new(&nanvix_bin_dir)); -} diff --git a/src/core/lxc/src/main.rs b/src/core/lxc/src/main.rs index ea26499e9..09912f327 100644 --- a/src/core/lxc/src/main.rs +++ b/src/core/lxc/src/main.rs @@ -254,7 +254,7 @@ fn main() { // Dispatch by containment backend. Backend selection and runner // construction — Bubblewrap (the Linux default for abstract intents), LXC // (explicit `containment: "lxc"`, plus the catch-all for anything else such - // as `processcontainer`), and the experimental Hyperlight / MicroVM + // as `processcontainer`), and the experimental Hyperlight // backends — live in `mxc_engine::run`, the single home for one-shot backend // dispatch. It runs the selected backend to completion and returns the // response; experimental backends that require `--experimental` (or that diff --git a/src/core/mxc_engine/Cargo.toml b/src/core/mxc_engine/Cargo.toml index 0767ab94d..9da8c11ad 100644 --- a/src/core/mxc_engine/Cargo.toml +++ b/src/core/mxc_engine/Cargo.toml @@ -13,9 +13,6 @@ path = "src/lib.rs" wxc_common.workspace = true serde = { workspace = true } serde_json = { workspace = true } -# MicroVM runner — used by the Windows and Linux run-to-completion bodies under -# the `microvm` feature. Never built on hosts/features that don't select it. -nanvix_runner = { workspace = true, optional = true } nvx_runner = { workspace = true, optional = true } [target.'cfg(target_arch = "x86_64")'.dependencies] @@ -49,8 +46,7 @@ default = [] # Enables constructing the run-to-completion runner for each experimental # backend. Mirrors the executor binaries' feature set so backend selection can # live in one place. The build-time binary-staging that some backends also need -# (e.g. NanVix's `nanvix_binaries`) stays with the executor crates. -microvm = ["dep:nanvix_runner"] +# stays with the executor crates. nvx = ["dep:nvx_runner", "nvx_runner/nvx"] wslc = ["dep:wslc_common", "wslc_common/link-wslcsdk"] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight"] diff --git a/src/core/mxc_engine/src/dispatch.rs b/src/core/mxc_engine/src/dispatch.rs index 2e65f3114..208508adb 100644 --- a/src/core/mxc_engine/src/dispatch.rs +++ b/src/core/mxc_engine/src/dispatch.rs @@ -15,7 +15,7 @@ //! run-to-completion path via `appcontainer_common::dispatcher`), Bubblewrap //! (Linux), Seatbelt (macOS), and WSLC (Windows, experimental, behind the //! `wslc` feature). Every other backend — including the remaining experimental -//! ones (Windows Sandbox, IsolationSession, MicroVM, Hyperlight) and LXC (no +//! ones (Windows Sandbox, IsolationSession, NVX, Hyperlight) and LXC (no //! streaming path suitable for the library) — returns //! [`MxcError::unsupported_containment`]; callers that need those must drive the //! standalone executor binaries (whose run-to-completion path will, in a later diff --git a/src/core/mxc_engine/src/platform.rs b/src/core/mxc_engine/src/platform.rs index c6c797f55..44cf1808f 100644 --- a/src/core/mxc_engine/src/platform.rs +++ b/src/core/mxc_engine/src/platform.rs @@ -172,7 +172,7 @@ mod tests { Containment::Vm, Containment::WindowsSandbox, Containment::Lxc, - Containment::Microvm, + Containment::Nvx, Containment::Hyperlight, Containment::Wslc, Containment::Seatbelt, diff --git a/src/core/mxc_engine/src/probe.rs b/src/core/mxc_engine/src/probe.rs index a0069ea91..7fac3a7fa 100644 --- a/src/core/mxc_engine/src/probe.rs +++ b/src/core/mxc_engine/src/probe.rs @@ -206,7 +206,7 @@ mod tests { Containment::Vm, Containment::WindowsSandbox, Containment::Lxc, - Containment::Microvm, + Containment::Nvx, Containment::Hyperlight, Containment::Wslc, Containment::Seatbelt, diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index 08fd6ef7b..1657d6dc4 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -259,25 +259,6 @@ fn resolve_runner_inner_windows( "VM backend not yet implemented", )), ContainmentBackend::Nvx => resolve_nvx_backend(request), - ContainmentBackend::MicroVm => { - if !request.experimental_enabled { - return Err(MxcError::malformed_request( - "MicroVM is an experimental feature. Use --experimental flag.", - )); - } - #[cfg(feature = "microvm")] - { - Ok(ResolvedRunner::without_guard(Box::new( - nanvix_runner::NanVixScriptRunner::new(), - ))) - } - #[cfg(not(feature = "microvm"))] - { - Err(MxcError::unsupported_containment( - "MicroVM backend not compiled in (build with --features microvm)", - )) - } - } ContainmentBackend::Hyperlight => resolve_hyperlight(request), ContainmentBackend::WindowsSandbox => { if !request.experimental_enabled { @@ -326,7 +307,7 @@ fn resolve_runner_inner_windows( // --------------------------------------------------------------------------- // Linux: mirrors `lxc-exec` — Bubblewrap (default), LXC, and the experimental -// Hyperlight / MicroVM backends. Any other containment falls back to LXC. +// Hyperlight backend. Any other containment falls back to LXC. // --------------------------------------------------------------------------- #[cfg(target_os = "linux")] @@ -338,25 +319,6 @@ fn resolve_runner_inner( match request.containment { ContainmentBackend::Hyperlight => resolve_hyperlight(request), - ContainmentBackend::MicroVm => { - if !request.experimental_enabled { - return Err(MxcError::malformed_request( - "MicroVM is an experimental feature. Use --experimental flag.", - )); - } - #[cfg(feature = "microvm")] - { - Ok(ResolvedRunner::without_guard(Box::new( - nanvix_runner::NanVixScriptRunner::new(), - ))) - } - #[cfg(not(feature = "microvm"))] - { - Err(MxcError::unsupported_containment( - "MicroVM backend not compiled in (build with --features microvm)", - )) - } - } ContainmentBackend::Nvx => resolve_nvx_backend(request), ContainmentBackend::Bubblewrap => Ok(ResolvedRunner::without_guard(Box::new(Runner::new( bwrap_common::bwrap_runner::BubblewrapScriptRunner::new(), diff --git a/src/core/wxc/Cargo.toml b/src/core/wxc/Cargo.toml index d162c2d69..38572e5ff 100644 --- a/src/core/wxc/Cargo.toml +++ b/src/core/wxc/Cargo.toml @@ -21,7 +21,6 @@ hyperlight_common = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] appcontainer_common = { workspace = true } -nanvix_binaries = { path = "../../backends/nanvix/binaries", optional = true } nvx_binaries = { workspace = true, optional = true } wslc_common = { workspace = true, optional = true } # Shared PLM dep: coordination, guarded lifecycle, and restricted WPR control. @@ -34,13 +33,11 @@ tempfile.workspace = true mxc_build_common.workspace = true [target.'cfg(windows)'.build-dependencies] -nanvix_build_common = { path = "../../backends/nanvix/build_common" } nvx_build_common = { workspace = true, optional = true } [features] default = [] test-support = ["wxc_common/test-support"] -microvm = ["dep:nanvix_binaries", "nanvix_binaries/microvm", "mxc_engine/microvm"] nvx = [ "dep:nvx_binaries", "nvx_binaries/nvx", diff --git a/src/core/wxc/build.rs b/src/core/wxc/build.rs index e28c6c765..142278f6d 100644 --- a/src/core/wxc/build.rs +++ b/src/core/wxc/build.rs @@ -9,9 +9,6 @@ fn main() { #[cfg(windows)] check_test_prerequisites(); - #[cfg(all(windows, feature = "microvm"))] - copy_nanvix_binaries(); - #[cfg(all(windows, feature = "nvx"))] { ensure_supported_nvx_target(); @@ -85,24 +82,6 @@ fn check_test_prerequisites() { } } -#[cfg(all(windows, feature = "microvm"))] -fn copy_nanvix_binaries() { - use std::path::Path; - - let nanvix_bin_dir = match std::env::var("DEP_NANVIX_BINARIES_BIN_DIR") { - Ok(dir) => dir, - Err(_) => { - eprintln!("wxc build.rs: DEP_NANVIX_BINARIES_BIN_DIR not set, skipping copy"); - return; - } - }; - - // Stage the artifacts next to the executable and emit rerun triggers. All - // of the staging logic (target-dir derivation, snapshot trust, copy/purge, - // rerun emission) lives in the build-only `nanvix_build_common` crate. - nanvix_build_common::stage_artifacts_next_to_exe(Path::new(&nanvix_bin_dir)); -} - #[cfg(all(windows, feature = "nvx"))] fn ensure_supported_nvx_target() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); diff --git a/src/core/wxc/src/main.rs b/src/core/wxc/src/main.rs index b8affa7e1..3c0b60689 100644 --- a/src/core/wxc/src/main.rs +++ b/src/core/wxc/src/main.rs @@ -1951,7 +1951,7 @@ mod tests { ContainmentBackend::WindowsSandbox, ContainmentBackend::Wslc, ContainmentBackend::IsolationSession, - ContainmentBackend::MicroVm, + ContainmentBackend::Nvx, ] { let containment_name = containment.wire_name(); request.containment = containment; diff --git a/src/core/wxc_common/Cargo.toml b/src/core/wxc_common/Cargo.toml index c056fbc44..842d61463 100644 --- a/src/core/wxc_common/Cargo.toml +++ b/src/core/wxc_common/Cargo.toml @@ -5,7 +5,6 @@ edition.workspace = true license.workspace = true [features] -microvm = ["dep:nanvix_common", "dep:uuid"] # Enables JSON Schema generation from the dedicated wire model. Off by default # so normal builds don't carry schemars. schema-gen = ["dep:mxc_schema_support", "dep:schemars"] @@ -25,8 +24,6 @@ url = { workspace = true } getrandom = { workspace = true } semver = "1" schemars = { workspace = true, optional = true } -nanvix_common = { path = "../../backends/nanvix/common", optional = true } -uuid = { workspace = true, optional = true } mxc_config_contract = { workspace = true } mxc_schema_support = { workspace = true, optional = true } mxc_telemetry = { workspace = true } diff --git a/src/core/wxc_common/src/lib.rs b/src/core/wxc_common/src/lib.rs index aac9ff919..42b6dea4b 100644 --- a/src/core/wxc_common/src/lib.rs +++ b/src/core/wxc_common/src/lib.rs @@ -17,8 +17,6 @@ pub mod filesystem_resolve; pub mod id; pub mod log_symbols; pub mod logger; -#[cfg(all(feature = "microvm", any(target_os = "windows", target_os = "linux")))] -pub mod microvm_staging; pub mod models; pub mod mxc_error; mod network_parser; diff --git a/src/core/wxc_common/src/microvm_staging.rs b/src/core/wxc_common/src/microvm_staging.rs deleted file mode 100644 index 48618fc30..000000000 --- a/src/core/wxc_common/src/microvm_staging.rs +++ /dev/null @@ -1,1102 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! MicroVM staging directory builder for mount-based script delivery. - -use std::fs; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; - -use thiserror::Error; -use uuid::Uuid; - -/// Entry point filename (warm-start protocol executes this automatically). -pub const BOOTSTRAP_FILENAME: &str = "bootstrap.py"; -/// Subdirectory for read-write staged host paths. -pub const RW_DIR: &str = "rw"; -/// Subdirectory for read-only staged host paths. -pub const RO_DIR: &str = "ro"; -/// Guest mount root inside the NanVix VM. -const GUEST_MOUNT_ROOT: &str = "/mnt"; - -/// Builds the guest-visible path for a staged host directory. -fn build_guest_path(category: &str, name: &str) -> String { - format!("{}/{}/{}", GUEST_MOUNT_ROOT, category, name) -} - -/// Preamble prepended to the user script in bootstrap.py. -/// -/// Composed from [`GUEST_MOUNT_ROOT`] and [`BOOTSTRAP_FILENAME`] so that -/// any future change to either constant flows through automatically. -fn bootstrap_preamble() -> String { - format!( - "import sys\nsys.argv = ['{}/{}']\n", - GUEST_MOUNT_ROOT, BOOTSTRAP_FILENAME - ) -} - -/// Errors produced while creating or validating a staging directory. -#[derive(Debug, Error)] -pub enum StagingError { - /// A requested host path does not exist. - #[error("host path does not exist: {0}")] - PathNotFound(String), - /// A symlink was found in a source path. - #[error("symlink found in source path: {0}")] - SymlinkFound(String), - /// I/O failure. - #[error("staging I/O error: {0}")] - Io(#[from] std::io::Error), -} - -/// Intermediate result from the staging build closure. -struct StagingBuildOutput { - /// Read-write directory mappings for copyback. - rw_mappings: Vec, - /// Total bytes written to the staging directory. - size_bytes: u64, -} - -/// Return type of the staging build closure. -type StagingBuildResult = Result; - -/// Whether the original host path was a file or directory. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum HostPathKind { - File, - Directory, -} - -/// Tracks the relationship between a host path, its staged copy, and its type. -#[derive(Debug)] -struct RwMapping { - host_path: PathBuf, - staged_path: PathBuf, - kind: HostPathKind, -} - -/// RAII wrapper over a temporary staging directory. -#[derive(Debug)] -pub struct StagingDir { - path: PathBuf, - rw_mappings: Vec, - size_bytes: u64, - /// When true, `Drop` skips cleanup so the staging dir can be recovered. - preserve: bool, -} - -impl StagingDir { - /// Creates and populates a staging directory under `root`. - pub fn new( - root: PathBuf, - script: &str, - readwrite_paths: &[String], - readonly_paths: &[String], - ) -> Result { - // Validate source paths up front (existence, no symlinks/reparse points, - // no `..` components). Validation is done once here; per-entry - // symlink/reparse rejection in `copy_dir_recursive` closes the TOCTOU - // window between validation and copy, so the per-source loops below - // don't need to re-validate. - for source in readwrite_paths.iter().chain(readonly_paths.iter()) { - validate_source_path(Path::new(source), source)?; - } - - fs::create_dir_all(&root)?; - let path = root.join(format!("mxc-staging-{}", Uuid::new_v4().simple())); - fs::create_dir_all(&path)?; - - let build_result = || -> StagingBuildResult { - // Collect host→guest path mappings for script rewriting. - let mut rewrite_map: Vec<(String, String)> = Vec::new(); - let mut rw_mappings: Vec = Vec::new(); - // Track staged bytes incrementally to avoid a full directory walk - // at the end (which scaled with total staged content size). - let mut size_bytes: u64 = 0; - - if !readwrite_paths.is_empty() { - fs::create_dir_all(path.join(RW_DIR))?; - } - for source in readwrite_paths { - let host_path = PathBuf::from(source); - - let relative = host_path_to_guest_relative(&host_path); - let slot_dir = path.join(RW_DIR).join(&relative); - let (kind, bytes) = stage_host_path(&host_path, &slot_dir)?; - size_bytes = size_bytes.saturating_add(bytes); - let guest_path = build_guest_path(RW_DIR, &relative); - rewrite_map.push((source.clone(), guest_path)); - rw_mappings.push(RwMapping { - host_path, - staged_path: slot_dir, - kind, - }); - } - - if !readonly_paths.is_empty() { - fs::create_dir_all(path.join(RO_DIR))?; - } - for source in readonly_paths { - let host_path = PathBuf::from(source); - - let relative = host_path_to_guest_relative(&host_path); - let slot_dir = path.join(RO_DIR).join(&relative); - let bytes = if host_path.is_dir() { - copy_dir_recursive(&host_path, &slot_dir)? - } else { - fs::create_dir_all(&slot_dir)?; - let file_name = host_path - .file_name() - .ok_or_else(|| StagingError::PathNotFound(source.clone()))?; - fs::copy(&host_path, slot_dir.join(file_name))? - }; - size_bytes = size_bytes.saturating_add(bytes); - set_readonly_recursive(&slot_dir)?; - let guest_path = build_guest_path(RO_DIR, &relative); - rewrite_map.push((source.clone(), guest_path)); - } - - // Rewrite host paths in the user script so callers don't need to - // know about the guest mount layout. Both backslash and forward-slash - // variants of each host path are replaced. - let rewritten_script = rewrite_paths_in_script(script, &rewrite_map); - let bootstrap_content = format!("{}{}", bootstrap_preamble(), rewritten_script); - fs::write(path.join(BOOTSTRAP_FILENAME), &bootstrap_content)?; - size_bytes = size_bytes.saturating_add(bootstrap_content.len() as u64); - - Ok(StagingBuildOutput { - rw_mappings, - size_bytes, - }) - }(); - - let StagingBuildOutput { - rw_mappings, - size_bytes, - } = match build_result { - Ok(result) => result, - Err(err) => { - let _ = remove_dir_all_force(&path); - return Err(err); - } - }; - - Ok(Self { - path, - rw_mappings, - size_bytes, - preserve: false, - }) - } - - /// Returns the host path to the staging directory. - pub fn path(&self) -> &Path { - &self.path - } - - /// Copies all read-write staged paths back to their original host locations. - /// Attempts all mappings even if one fails; returns the first error encountered. - /// On any failure, marks the staging dir as preserved so `Drop` won't delete it. - pub fn copy_back_to_host(&mut self) -> Result<(), StagingError> { - let mut first_err: Option = None; - // Preserve before starting — if any copy fails, staging must survive Drop. - self.preserve = true; - for mapping in &self.rw_mappings { - if let Err(e) = copy_back_mapping(mapping) { - if first_err.is_none() { - first_err = Some(e); - } - } - } - if first_err.is_none() { - // All copies succeeded — allow normal cleanup on Drop. - self.preserve = false; - } - first_err.map_or(Ok(()), Err) - } - - /// Returns the staging directory path (useful for recovery messages). - pub fn preserved_path(&self) -> Option<&Path> { - if self.preserve { - Some(&self.path) - } else { - None - } - } - - /// Returns additional staging overhead in milliseconds. - pub fn staging_overhead_ms(&self) -> u64 { - let ms = ((self.size_bytes as f64 / (1024.0 * 1024.0)) * 100.0) as u64; - ms.min(30_000) - } -} - -impl Drop for StagingDir { - fn drop(&mut self) { - if self.preserve { - return; - } - let _ = remove_dir_all_force(&self.path); - } -} - -/// Converts a host path to a guest-relative path by stripping the drive letter prefix -/// and normalizing separators. E.g. `C:\Users\me\work` → `c/Users/me/work`. -fn host_path_to_guest_relative(host_path: &Path) -> String { - let s = host_path.to_string_lossy(); - // Strip drive letter prefix (e.g. "C:\") and normalize to forward slashes. - let stripped = if s.len() >= 3 - && s.as_bytes()[1] == b':' - && (s.as_bytes()[2] == b'\\' || s.as_bytes()[2] == b'/') - { - let drive = s.as_bytes()[0].to_ascii_lowercase() as char; - format!("{}/{}", drive, s[3..].replace('\\', "/")) - } else { - // UNC or relative path — just normalize slashes. - s.replace('\\', "/") - }; - // Trim leading and trailing slashes to ensure the result is always relative. - // On Linux, absolute paths like `/tmp/xyz` become `tmp/xyz`. - stripped - .trim_start_matches('/') - .trim_end_matches('/') - .to_string() -} - -/// Replaces host paths in the script source with their guest mount equivalents. -/// Both backslash (`C:\Users\work`) and forward-slash (`C:/Users/work`) variants -/// are replaced. Longer paths are replaced first to avoid partial prefix matches. -fn rewrite_paths_in_script(script: &str, mappings: &[(String, String)]) -> String { - let mut result = script.to_string(); - // Sort by host path length descending so longer prefixes match first. - let mut sorted: Vec<_> = mappings.to_vec(); - sorted.sort_by_key(|b| std::cmp::Reverse(b.0.len())); - for (host_path, guest_path) in &sorted { - // Replace escaped backslash variant first (Python string literals: C:\\Users\\work). - let escaped = host_path.replace('\\', "\\\\"); - if escaped != *host_path { - result = result.replace(&escaped, guest_path); - } - // Replace native backslash variant (C:\Users\work). - result = result.replace(host_path, guest_path); - // Replace forward-slash variant (C:/Users/work). - let forward = host_path.replace('\\', "/"); - if forward != *host_path { - result = result.replace(&forward, guest_path); - } - } - result -} - -/// Stages a single host path in a target slot directory using a private copy. -/// Returns the host-path kind and the total number of bytes copied. -fn stage_host_path(host_path: &Path, slot_dir: &Path) -> Result<(HostPathKind, u64), StagingError> { - if host_path.is_dir() { - let bytes = copy_dir_recursive(host_path, slot_dir)?; - return Ok((HostPathKind::Directory, bytes)); - } - - fs::create_dir_all(slot_dir)?; - let file_name = host_path - .file_name() - .ok_or_else(|| StagingError::PathNotFound(host_path.display().to_string()))?; - let bytes = fs::copy(host_path, slot_dir.join(file_name))?; - Ok((HostPathKind::File, bytes)) -} - -/// Copies staged RW content back to the original host path. -fn copy_back_mapping(mapping: &RwMapping) -> Result<(), StagingError> { - match mapping.kind { - HostPathKind::Directory => mirror_directory(&mapping.staged_path, &mapping.host_path), - HostPathKind::File => { - let file_name = mapping.host_path.file_name().ok_or_else(|| { - StagingError::PathNotFound(mapping.host_path.display().to_string()) - })?; - let staged_file = mapping.staged_path.join(file_name); - fs::copy(staged_file, &mapping.host_path)?; - Ok(()) - } - } -} - -/// Replaces the destination directory with the source directory contents. -/// Uses a rename-based backup to avoid permanent data loss if the copy fails mid-way. -fn mirror_directory(src: &Path, dst: &Path) -> Result<(), StagingError> { - // Build a sibling backup path on the same volume as dst — rename is atomic. - // Include the PID to avoid collision with stale backups from prior interrupted runs. - let backup = dst.with_extension(format!("__mxc_bak_{}", std::process::id())); - // Clean up any pre-existing backup with the same name (e.g. from a previous run - // of this process that was interrupted between the rename and cleanup). - if backup.exists() { - let _ = remove_dir_all_force(&backup); - } - if dst.exists() { - fs::rename(dst, &backup)?; - } - match copy_dir_recursive(src, dst) { - Ok(_) => { - // Copy succeeded — best-effort removal of the backup. - if backup.exists() { - let _ = remove_dir_all_force(&backup); - } - Ok(()) - } - Err(e) => { - // Copy failed — attempt to restore the backup to the original path. - if backup.exists() { - if dst.exists() { - let _ = remove_dir_all_force(dst); - } - let _ = fs::rename(&backup, dst); - } - Err(e) - } - } -} - -/// Copies a directory recursively. Returns the total number of bytes copied so -/// callers can track staging size without an extra full-tree walk afterward. -/// Uses `symlink_metadata` per entry to reject symlinks/reparse points during -/// traversal, closing the TOCTOU window between upfront validation and the actual copy. -fn copy_dir_recursive(src: &Path, dst: &Path) -> Result { - fs::create_dir_all(dst)?; - let mut total: u64 = 0; - for entry in fs::read_dir(src)? { - let entry = entry?; - let metadata = fs::symlink_metadata(entry.path())?; - if metadata.file_type().is_symlink() || is_reparse_point(&metadata) { - return Err(StagingError::SymlinkFound( - entry.path().display().to_string(), - )); - } - let target = dst.join(entry.file_name()); - if metadata.file_type().is_dir() { - total = total.saturating_add(copy_dir_recursive(&entry.path(), &target)?); - } else { - total = total.saturating_add(fs::copy(entry.path(), target)?); - } - } - Ok(total) -} - -/// Sets the read-only attribute recursively for all files in `dir`. -fn set_readonly_recursive(dir: &Path) -> Result<(), StagingError> { - if dir.is_file() { - let mut perms = fs::metadata(dir)?.permissions(); - perms.set_readonly(true); - fs::set_permissions(dir, perms)?; - return Ok(()); - } - - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - set_readonly_recursive(&path)?; - } else { - let mut perms = fs::metadata(&path)?.permissions(); - perms.set_readonly(true); - fs::set_permissions(&path, perms)?; - } - } - Ok(()) -} - -/// Validates that a source host path exists, has a filename, and contains no reparse points. -fn validate_source_path(path: &Path, original: &str) -> Result<(), StagingError> { - if !path.exists() { - return Err(StagingError::PathNotFound(original.to_string())); - } - if path.file_name().is_none() { - return Err(StagingError::PathNotFound(format!( - "root paths are not supported for microvm filesystem staging: {}", - original - ))); - } - // Reject paths with `..` components to prevent path-traversal attacks that - // could write outside the staging directory (e.g., `C:\a\..\b`). - for component in path.components() { - if matches!(component, std::path::Component::ParentDir) { - return Err(StagingError::PathNotFound(format!( - "paths with '..' components are not supported: {}", - original - ))); - } - } - check_no_reparse_points(path) -} - -/// Ensures no symlink or Windows reparse point is present in `path` or descendants. -fn check_no_reparse_points(path: &Path) -> Result<(), StagingError> { - let metadata = fs::symlink_metadata(path)?; - if metadata.file_type().is_symlink() || is_reparse_point(&metadata) { - return Err(StagingError::SymlinkFound(path.display().to_string())); - } - - if metadata.is_dir() { - for entry in fs::read_dir(path)? { - check_no_reparse_points(&entry?.path())?; - } - } - - Ok(()) -} - -#[cfg(target_os = "windows")] -fn is_reparse_point(metadata: &fs::Metadata) -> bool { - use std::os::windows::fs::MetadataExt; - use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT; - metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 != 0 -} - -#[cfg(not(target_os = "windows"))] -fn is_reparse_point(_metadata: &fs::Metadata) -> bool { - false -} - -/// Removes a directory tree, clearing read-only attributes first. -#[allow(clippy::permissions_set_readonly_false)] -fn remove_dir_all_force(path: &Path) -> Result<(), StagingError> { - if !path.exists() { - return Ok(()); - } - clear_readonly_recursive(path)?; - fs::remove_dir_all(path)?; - Ok(()) -} - -#[allow(clippy::permissions_set_readonly_false)] -fn clear_readonly_recursive(path: &Path) -> Result<(), StagingError> { - if path.is_file() { - let mut perms = fs::metadata(path)?.permissions(); - if perms.readonly() { - perms.set_readonly(false); - fs::set_permissions(path, perms)?; - } - return Ok(()); - } - - for entry in fs::read_dir(path)? { - clear_readonly_recursive(&entry?.path())?; - } - - let mut perms = fs::metadata(path)?.permissions(); - if perms.readonly() { - perms.set_readonly(false); - fs::set_permissions(path, perms)?; - } - Ok(()) -} - -/// Removes orphaned `mxc-staging-*` directories under `root` that are older than `max_age`. -/// Called at the start of each run to prevent temp dir accumulation on process crash. -pub fn sweep_orphaned_staging_dirs(root: &Path, max_age: Duration) { - let Ok(entries) = fs::read_dir(root) else { - return; - }; - let now = SystemTime::now(); - for entry in entries.flatten() { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - if !name_str.starts_with("mxc-staging-") || !entry.path().is_dir() { - continue; - } - let is_old = entry - .metadata() - .ok() - .and_then(|m| m.modified().ok()) - .and_then(|modified| now.duration_since(modified).ok()) - .map(|age| age >= max_age) - .unwrap_or(false); - if is_old { - let _ = remove_dir_all_force(&entry.path()); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - fn write_file(path: &Path, content: &str) { - fs::write(path, content).unwrap(); - } - - /// Helper: compute the staged directory path for a host path. - fn staged_rw(staging: &StagingDir, host_path: &Path) -> PathBuf { - staging - .path() - .join(RW_DIR) - .join(host_path_to_guest_relative(host_path)) - } - - #[test] - fn staging_creates_bootstrap() { - let root = tempdir().unwrap(); - let script = "print('hello')"; - let staging = StagingDir::new(root.path().to_path_buf(), script, &[], &[]).unwrap(); - - let bootstrap = staging.path().join(BOOTSTRAP_FILENAME); - assert!(bootstrap.exists()); - let content = fs::read_to_string(bootstrap).unwrap(); - assert!(content.starts_with(&bootstrap_preamble())); - assert!(content.contains(script)); - } - - #[test] - fn staging_empty_policy() { - let root = tempdir().unwrap(); - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - assert!(!staging.path().join(RW_DIR).exists()); - assert!(!staging.path().join(RO_DIR).exists()); - } - - #[test] - fn staging_single_rw_path() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("sample"); - fs::create_dir_all(&source).unwrap(); - write_file(&source.join("data.txt"), "abc"); - - let host_path = source.display().to_string(); - let script = format!("open('{}')", host_path); - let rw = vec![host_path.clone()]; - let staging = StagingDir::new(root.path().to_path_buf(), &script, &rw, &[]).unwrap(); - let guest_rel = host_path_to_guest_relative(&PathBuf::from(&host_path)); - assert!(staging.path().join(RW_DIR).join(&guest_rel).exists()); - // Verify the script was rewritten with the guest path. - let rewritten = fs::read_to_string(staging.path().join(BOOTSTRAP_FILENAME)).unwrap(); - let expected_guest = build_guest_path(RW_DIR, &guest_rel); - assert!( - rewritten.contains(&expected_guest), - "expected guest path in rewritten script, got: {}", - rewritten - ); - } - - #[test] - fn staging_ro_path_has_readonly_attribute() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("readonly"); - fs::create_dir_all(&source).unwrap(); - write_file(&source.join("data.txt"), "abc"); - - let ro = vec![source.display().to_string()]; - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &ro).unwrap(); - let guest_rel = host_path_to_guest_relative(&source); - let staged_file = staging - .path() - .join(RO_DIR) - .join(&guest_rel) - .join("data.txt"); - let metadata = fs::metadata(staged_file).unwrap(); - assert!(metadata.permissions().readonly()); - } - - #[test] - fn staging_two_rw_paths_get_distinct_guest_dirs() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let first = source_root.path().join("input"); - let second_parent = source_root.path().join("other"); - let second = second_parent.join("input"); - fs::create_dir_all(&first).unwrap(); - fs::create_dir_all(&second).unwrap(); - - let rw = vec![first.display().to_string(), second.display().to_string()]; - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap(); - // Full path mirroring means both exist at distinct paths. - let rel1 = host_path_to_guest_relative(&first); - let rel2 = host_path_to_guest_relative(&second); - assert!(staging.path().join(RW_DIR).join(&rel1).exists()); - assert!(staging.path().join(RW_DIR).join(&rel2).exists()); - assert_ne!(rel1, rel2); - } - - #[test] - fn staging_script_rewrite_replaces_host_paths() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("sample"); - fs::create_dir_all(&source).unwrap(); - - let host_path = source.display().to_string(); - let forward_path = host_path.replace('\\', "/"); - let script = format!("a = '{}'\nb = '{}'", host_path, forward_path); - let rw = vec![host_path.clone()]; - let staging = StagingDir::new(root.path().to_path_buf(), &script, &rw, &[]).unwrap(); - - let rewritten = fs::read_to_string(staging.path().join(BOOTSTRAP_FILENAME)).unwrap(); - let guest_rel = host_path_to_guest_relative(&PathBuf::from(&host_path)); - let expected_guest = build_guest_path(RW_DIR, &guest_rel); - assert!( - rewritten.contains(&expected_guest), - "expected guest path in rewritten script, got: {}", - rewritten - ); - // On Windows, verify the forward-slash variant was replaced too. - // On Linux, host_path == forward_path and the guest path contains - // the original as a substring (/mnt/rw/tmp/xyz contains /tmp/xyz), - // so we verify replacement by counting occurrences of the guest path. - #[cfg(target_os = "windows")] - assert!( - !rewritten.contains(&forward_path), - "forward-slash host path should have been replaced" - ); - #[cfg(target_os = "linux")] - { - // Both occurrences in the script (a='...' and b='...') should - // have been replaced with the guest path. - let count = rewritten.matches(&expected_guest).count(); - assert!( - count >= 2, - "expected at least 2 occurrences of guest path, got {}: {}", - count, - rewritten - ); - } - } - - #[test] - fn staging_bootstrap_is_stable() { - let root = tempdir().unwrap(); - let a = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - let b = StagingDir::new(root.path().to_path_buf(), "print(2)", &[], &[]).unwrap(); - - let left = fs::read_to_string(a.path().join(BOOTSTRAP_FILENAME)).unwrap(); - let right = fs::read_to_string(b.path().join(BOOTSTRAP_FILENAME)).unwrap(); - // The preamble (loader boilerplate) must be identical regardless of script content. - let preamble = bootstrap_preamble(); - assert!(left.starts_with(&preamble)); - assert!(right.starts_with(&preamble)); - let left_preamble = &left[..preamble.len()]; - let right_preamble = &right[..preamble.len()]; - assert_eq!(left_preamble, right_preamble); - } - - #[test] - fn staging_cleanup_on_drop() { - let root = tempdir().unwrap(); - let path_to_check = { - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - let p = staging.path().to_path_buf(); - assert!(p.exists()); - p - }; - assert!(!path_to_check.exists()); - } - - #[test] - fn staging_missing_path_returns_error() { - let root = tempdir().unwrap(); - let rw = vec![root.path().join("missing").display().to_string()]; - let err = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap_err(); - assert!(matches!(err, StagingError::PathNotFound(_))); - } - - #[test] - fn staging_single_file_rw_wrapped_in_slot() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source_file = source_root.path().join("payload"); - write_file(&source_file, "data"); - - let rw = vec![source_file.display().to_string()]; - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap(); - let staged_file = staged_rw(&staging, &source_file).join("payload"); - assert!(staged_file.exists()); - } - - #[test] - fn host_path_to_guest_relative_strips_drive() { - let p = PathBuf::from(r"C:\Users\me\work"); - assert_eq!(host_path_to_guest_relative(&p), "c/Users/me/work"); - } - - #[test] - fn host_path_to_guest_relative_normalizes_slashes() { - let p = PathBuf::from(r"D:\data\ref-data"); - assert_eq!(host_path_to_guest_relative(&p), "d/data/ref-data"); - } - - #[test] - fn staging_rw_directory_is_private_until_copyback() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("work"); - fs::create_dir_all(&source).unwrap(); - write_file(&source.join("data.txt"), "before"); - - let rw = vec![source.display().to_string()]; - let mut staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap(); - let staged_file = staged_rw(&staging, &source).join("data.txt"); - - // Mutate the staged copy — original must remain unchanged. - fs::write(&staged_file, "after").unwrap(); - assert_eq!( - fs::read_to_string(source.join("data.txt")).unwrap(), - "before" - ); - - // After explicit copyback, original should reflect the staged changes. - staging.copy_back_to_host().unwrap(); - assert_eq!( - fs::read_to_string(source.join("data.txt")).unwrap(), - "after" - ); - } - - #[test] - fn staging_rw_file_copyback_updates_original_file() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source_file = source_root.path().join("payload.txt"); - write_file(&source_file, "before"); - - let rw = vec![source_file.display().to_string()]; - let mut staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap(); - let staged_file = staged_rw(&staging, &source_file).join("payload.txt"); - - fs::write(&staged_file, "after").unwrap(); - assert_eq!(fs::read_to_string(&source_file).unwrap(), "before"); - - staging.copy_back_to_host().unwrap(); - assert_eq!(fs::read_to_string(&source_file).unwrap(), "after"); - } - - #[test] - fn staging_rw_directory_copyback_mirrors_deletions() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("work"); - fs::create_dir_all(&source).unwrap(); - write_file(&source.join("kept.txt"), "before"); - write_file(&source.join("deleted.txt"), "remove me"); - - let rw = vec![source.display().to_string()]; - let mut staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap(); - let staged_dir = staged_rw(&staging, &source); - - fs::remove_file(staged_dir.join("deleted.txt")).unwrap(); - fs::write(staged_dir.join("kept.txt"), "after").unwrap(); - fs::write(staged_dir.join("created.txt"), "new").unwrap(); - - staging.copy_back_to_host().unwrap(); - - assert_eq!( - fs::read_to_string(source.join("kept.txt")).unwrap(), - "after" - ); - assert_eq!( - fs::read_to_string(source.join("created.txt")).unwrap(), - "new" - ); - assert!(!source.join("deleted.txt").exists()); - } - - #[test] - fn staging_preserve_on_copyback_failure() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("work"); - fs::create_dir_all(&source).unwrap(); - write_file(&source.join("data.txt"), "original"); - - let rw = vec![source.display().to_string()]; - let mut staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap(); - let staging_path = staging.path().to_path_buf(); - - // Delete the staged directory to make copyback fail. - fs::remove_dir_all(staging.path().join(RW_DIR)).unwrap(); - - let result = staging.copy_back_to_host(); - assert!(result.is_err(), "expected copyback error"); - - // The staging dir must still exist (preserve=true) so the user can inspect it. - assert!( - staging_path.exists(), - "staging dir must be preserved on copyback failure" - ); - - // The original host directory is unchanged. - assert_eq!( - fs::read_to_string(source.join("data.txt")).unwrap(), - "original" - ); - } - - #[test] - fn staging_allows_large_content() { - // Verify that staging succeeds for large source content (~64 MB). - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("big"); - fs::create_dir_all(&source).unwrap(); - // Sparse file ~64 MB. Sparse so we don't actually consume the disk - // space on test runners. - let big_file = source.join("large.bin"); - let big_size: u64 = 64 * 1024 * 1024; - { - let f = fs::File::create(&big_file).unwrap(); - f.set_len(big_size).unwrap(); - } - - let rw = vec![source.display().to_string()]; - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]) - .expect("staging should succeed for large content"); - - // The large file was staged into the RW slot. - let rel = host_path_to_guest_relative(&source); - let staged = staging.path().join(RW_DIR).join(&rel).join("large.bin"); - assert!(staged.exists(), "expected staged file at {staged:?}"); - assert_eq!(fs::metadata(&staged).unwrap().len(), big_size); - } - - #[test] - #[allow(clippy::permissions_set_readonly_false)] - fn staging_readonly_paths_not_copied_back() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("reference"); - fs::create_dir_all(&source).unwrap(); - write_file(&source.join("data.txt"), "original"); - - let ro = vec![source.display().to_string()]; - let mut staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &ro).unwrap(); - - // Mutate the staged read-only copy. - let guest_rel = host_path_to_guest_relative(&source); - let staged_file = staging - .path() - .join(RO_DIR) - .join(&guest_rel) - .join("data.txt"); - // Clear read-only flag so we can write to the staged copy. - let mut perms = fs::metadata(&staged_file).unwrap().permissions(); - perms.set_readonly(false); - fs::set_permissions(&staged_file, perms).unwrap(); - fs::write(&staged_file, "mutated").unwrap(); - - // copy_back_to_host only copies RW mappings — RO should NOT be copied back. - staging.copy_back_to_host().unwrap(); - assert_eq!( - fs::read_to_string(source.join("data.txt")).unwrap(), - "original", - "read-only paths must not be copied back to host" - ); - } - - #[test] - fn sweep_removes_old_staging_dirs() { - let root = tempdir().unwrap(); - let old_dir = root.path().join("mxc-staging-aabbccdd"); - let fresh_dir = root.path().join("mxc-staging-11223344"); - let unrelated = root.path().join("other-dir"); - fs::create_dir_all(&old_dir).unwrap(); - fs::create_dir_all(&fresh_dir).unwrap(); - fs::create_dir_all(&unrelated).unwrap(); - - // Backdate the old_dir modification time via a workaround: zero-age threshold - // sweeps everything older than 0 seconds (all of them qualify in CI). - sweep_orphaned_staging_dirs(root.path(), Duration::from_secs(0)); - - // Both staging dirs should be removed; unrelated must stay. - assert!(!old_dir.exists(), "old staging dir should be swept"); - assert!(!fresh_dir.exists(), "staging dir should be swept at age 0"); - assert!(unrelated.exists(), "unrelated dir must not be swept"); - } - - #[test] - fn staging_rejects_path_with_parent_dir_component() { - let root = tempdir().unwrap(); - let source = root.path().join("legit"); - fs::create_dir_all(&source).unwrap(); - // Construct a path with `..` to attempt traversal. - let traversal = source.join("..").join("legit"); - let rw = vec![traversal.display().to_string()]; - let err = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap_err(); - assert!( - matches!(err, StagingError::PathNotFound(ref msg) if msg.contains("..")), - "expected PathNotFound with '..' mention, got: {err}" - ); - } - - #[test] - fn staging_mixed_rw_and_ro_paths() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let rw_dir = source_root.path().join("editable"); - let ro_dir = source_root.path().join("reference"); - fs::create_dir_all(&rw_dir).unwrap(); - fs::create_dir_all(&ro_dir).unwrap(); - write_file(&rw_dir.join("a.txt"), "rw-content"); - write_file(&ro_dir.join("b.txt"), "ro-content"); - - let rw = vec![rw_dir.display().to_string()]; - let ro = vec![ro_dir.display().to_string()]; - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &ro).unwrap(); - - // Both subdirectories must exist. - assert!(staging.path().join(RW_DIR).exists()); - assert!(staging.path().join(RO_DIR).exists()); - // Verify file content was staged. - let rw_rel = host_path_to_guest_relative(&rw_dir); - let ro_rel = host_path_to_guest_relative(&ro_dir); - let staged_rw_file = staging.path().join(RW_DIR).join(&rw_rel).join("a.txt"); - let staged_ro_file = staging.path().join(RO_DIR).join(&ro_rel).join("b.txt"); - assert_eq!(fs::read_to_string(staged_rw_file).unwrap(), "rw-content"); - assert_eq!(fs::read_to_string(staged_ro_file).unwrap(), "ro-content"); - } - - #[test] - fn staging_overhead_ms_scales_with_size() { - let root = tempdir().unwrap(); - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - // A minimal staging dir should have near-zero overhead. - assert!(staging.staging_overhead_ms() < 5); - } - - #[test] - fn staging_overhead_ms_capped_at_30s() { - let root = tempdir().unwrap(); - let mut staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - // Simulate a huge staging size (won't actually allocate). - staging.size_bytes = 500 * 1024 * 1024; // 500 MB - assert_eq!(staging.staging_overhead_ms(), 30_000); - } - - #[test] - fn preserved_path_none_when_not_preserved() { - let root = tempdir().unwrap(); - let staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - assert!(staging.preserved_path().is_none()); - } - - #[test] - fn host_path_to_guest_relative_handles_trailing_slash() { - let p = PathBuf::from(r"C:\Users\me\work\"); - assert_eq!(host_path_to_guest_relative(&p), "c/Users/me/work"); - } - - #[test] - fn host_path_to_guest_relative_lowercase_drive() { - let p = PathBuf::from(r"E:\Projects\src"); - let result = host_path_to_guest_relative(&p); - assert!(result.starts_with('e'), "drive letter should be lowercase"); - assert_eq!(result, "e/Projects/src"); - } - - #[test] - fn host_path_to_guest_relative_strips_leading_slash() { - // On Linux, absolute paths like /tmp/xyz should become relative (tmp/xyz). - let p = PathBuf::from("/tmp/my-dir/payload"); - let result = host_path_to_guest_relative(&p); - assert_eq!(result, "tmp/my-dir/payload"); - assert!( - !result.starts_with('/'), - "result must be relative: {}", - result - ); - } - - #[test] - fn host_path_to_guest_relative_linux_root_path() { - // A path like /home/user/project → home/user/project. - let p = PathBuf::from("/home/user/project"); - assert_eq!(host_path_to_guest_relative(&p), "home/user/project"); - } - - #[test] - fn rewrite_paths_handles_escaped_backslashes() { - let host = r"C:\Users\me\work".to_string(); - let guest = "/mnt/rw/c/Users/me/work".to_string(); - let script = r#"path = "C:\\Users\\me\\work""#; - let result = rewrite_paths_in_script(script, &[(host, guest.clone())]); - assert!( - result.contains(&guest), - "escaped backslashes not rewritten: {result}" - ); - } - - #[test] - fn rewrite_paths_longer_prefix_first() { - let short_host = r"C:\data".to_string(); - let short_guest = "/mnt/rw/c/data".to_string(); - let long_host = r"C:\data\subdir".to_string(); - let long_guest = "/mnt/rw/c/data/subdir".to_string(); - let script = r"C:\data\subdir\file.txt"; - let mappings = vec![ - (short_host, short_guest.clone()), - (long_host, long_guest.clone()), - ]; - let result = rewrite_paths_in_script(script, &mappings); - // The longer path must match first so we don't get a partial replacement. - assert!( - result.contains("/mnt/rw/c/data/subdir"), - "longer prefix should match first: {result}" - ); - } - - #[test] - fn build_guest_path_format() { - assert_eq!(build_guest_path("rw", "c/Users/me"), "/mnt/rw/c/Users/me"); - assert_eq!(build_guest_path("ro", "d/ref"), "/mnt/ro/d/ref"); - } - - #[test] - fn staging_empty_script() { - let root = tempdir().unwrap(); - let staging = StagingDir::new(root.path().to_path_buf(), "", &[], &[]).unwrap(); - let content = fs::read_to_string(staging.path().join(BOOTSTRAP_FILENAME)).unwrap(); - // Should only contain the preamble. - assert_eq!(content, bootstrap_preamble()); - } - - #[test] - fn staging_nested_directory_rw_copyback() { - let root = tempdir().unwrap(); - let source_root = tempdir().unwrap(); - let source = source_root.path().join("nested"); - let sub = source.join("sub").join("deep"); - fs::create_dir_all(&sub).unwrap(); - write_file(&sub.join("deep.txt"), "original"); - write_file(&source.join("top.txt"), "top-original"); - - let rw = vec![source.display().to_string()]; - let mut staging = StagingDir::new(root.path().to_path_buf(), "print(1)", &rw, &[]).unwrap(); - let staged_dir = staged_rw(&staging, &source); - - // Modify deep file and add a new file. - fs::write( - staged_dir.join("sub").join("deep").join("deep.txt"), - "modified", - ) - .unwrap(); - fs::write(staged_dir.join("new.txt"), "added").unwrap(); - - staging.copy_back_to_host().unwrap(); - assert_eq!( - fs::read_to_string(sub.join("deep.txt")).unwrap(), - "modified" - ); - assert_eq!(fs::read_to_string(source.join("new.txt")).unwrap(), "added"); - } - - #[test] - fn sweep_ignores_nonexistent_root() { - let nonexistent = PathBuf::from(r"C:\nonexistent_mxc_test_dir_12345"); - // Should not panic or error. - sweep_orphaned_staging_dirs(&nonexistent, Duration::from_secs(0)); - } - - #[test] - fn staging_dir_has_unique_names() { - let root = tempdir().unwrap(); - let a = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - let b = StagingDir::new(root.path().to_path_buf(), "print(1)", &[], &[]).unwrap(); - assert_ne!(a.path(), b.path()); - } -} diff --git a/src/testing/fuzz/Cargo.toml b/src/testing/fuzz/Cargo.toml index bcdda08b9..4689ccc93 100644 --- a/src/testing/fuzz/Cargo.toml +++ b/src/testing/fuzz/Cargo.toml @@ -18,12 +18,10 @@ cargo-fuzz = true # These pull in heavier deps so they're opt-in for local dev. hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight"] isolation_session = ["dep:isolation_session_common"] -microvm = ["dep:nanvix_runner", "wxc_common/microvm"] [dependencies] libfuzzer-sys = "0.4" wxc_common = { path = "../../core/wxc_common" } -nanvix_runner = { path = "../../backends/nanvix/runner", optional = true } hyperlight_common = { path = "../../backends/hyperlight/common", optional = true } isolation_session_common = { path = "../../backends/isolation_session/common", optional = true } diff --git a/src/testing/fuzz/fuzz_targets/validator.rs b/src/testing/fuzz/fuzz_targets/validator.rs index c8ec1872b..4818d665e 100644 --- a/src/testing/fuzz/fuzz_targets/validator.rs +++ b/src/testing/fuzz/fuzz_targets/validator.rs @@ -7,7 +7,6 @@ // path that `--dry-run` takes through the binary. // // Runner-specific validation coverage: -// - NanVix (MicroVm): always (no extra features) // - Hyperlight: requires `--features hyperlight` // - IsolationSession: requires `--features isolation_session` // - Seatbelt: macOS-only, not available in Windows fuzz builds @@ -34,11 +33,6 @@ fuzz_target!(|data: &[u8]| { // Dispatch to runner-specific validation based on backend. #[cfg(target_os = "windows")] match req.containment { - #[cfg(feature = "microvm")] - ContainmentBackend::MicroVm => { - let runner = nanvix_runner::NanVixScriptRunner::new(); - let _ = runner.validate_runner(&req); - } #[cfg(feature = "hyperlight")] ContainmentBackend::Hyperlight => { let runner = hyperlight_common::HyperlightScriptRunner::new(); From 23484305a2ea1e7cc12effef8530ada64a13c268 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 11:30:52 -0700 Subject: [PATCH 19/41] refactor: expose nvx in the node sdk Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../dev/mxc-config.schema.0.9.0-alpha.json | 7 - schemas/dev/mxc-config.schema.0.9.0-dev.json | 9 +- sdk/node/README.md | 16 +- sdk/node/src/generated/v0_9_0_alpha/wire.ts | 2 +- sdk/node/src/generated/wire.ts | 4 +- sdk/node/src/helper.ts | 20 +- sdk/node/src/sandbox.ts | 36 - sdk/node/src/types.ts | 1230 +++++++++-------- .../integration/microvm-filesystem.test.ts | 468 ------- sdk/node/tests/unit/sandbox.test.ts | 148 +- src/core/wxc_common/src/wire.rs | 2 +- 11 files changed, 674 insertions(+), 1268 deletions(-) delete mode 100644 sdk/node/tests/integration/microvm-filesystem.test.ts diff --git a/schemas/dev/mxc-config.schema.0.9.0-alpha.json b/schemas/dev/mxc-config.schema.0.9.0-alpha.json index b482ca1d0..bc76f6453 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-alpha.json +++ b/schemas/dev/mxc-config.schema.0.9.0-alpha.json @@ -830,13 +830,6 @@ ], "type": "string" }, - { - "description": "Microvm", - "enum": [ - "microvm" - ], - "type": "string" - }, { "description": "Nvx", "enum": [ diff --git a/schemas/dev/mxc-config.schema.0.9.0-dev.json b/schemas/dev/mxc-config.schema.0.9.0-dev.json index 4f42a2b80..ac677a089 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.9.0-dev.json @@ -143,13 +143,6 @@ ], "type": "string" }, - { - "description": "NanVix micro-VM (experimental).", - "enum": [ - "microvm" - ], - "type": "string" - }, { "description": "NVX micro-VM (experimental).", "enum": [ @@ -750,7 +743,7 @@ ] }, "cwd": { - "description": "Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NanVix and Hyperlight reject a working directory outright. See `docs/schema.md` (\"Working Directory\").", + "description": "Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NVX and Hyperlight reject a working directory outright. See `docs/schema.md` (\"Working Directory\").", "type": [ "string", "null" diff --git a/sdk/node/README.md b/sdk/node/README.md index d01f9cae9..18a388c50 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -64,7 +64,7 @@ child.on('close', (code) => console.log('exit:', code)); Pick `0.8.0-alpha` for new code on any supported platform. -> **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `wslc`, `microvm`, `hyperlight`, `isolation_session`), the `experimental.*` block, and state-aware lifecycle live in `0.9.0-dev`. The parser still accepts them when paired with `--experimental` regardless of which schema your config validates against — schema choice affects editor validation, not runtime behavior. +> **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `wslc`, `nvx`, `hyperlight`, `isolation_session`), the `experimental.*` block, and state-aware lifecycle live in `0.9.0-dev`. The parser still accepts them when paired with `--experimental` regardless of which schema your config validates against — schema choice affects editor validation, not runtime behavior. > **Network host allow/block lists are not implemented on Windows.** `network.allowedHosts` / `network.blockedHosts` have no enforcement on this platform — use `network.defaultPolicy` (`allow` / `block`) or `network.proxy` to constrain network access. @@ -138,11 +138,11 @@ for all three connectivity modes and backend-specific support. | Platform | Default backend | Other backends | Minimum build | | --- | --- | --- | --- | -| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `microvm`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | +| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `nvx`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | | Linux x64 / ARM64 | `bubblewrap` | `lxc` | — | | macOS ARM64 (schema `0.7.0-alpha`+) | `seatbelt` | — | — | -The default `processcontainer`, `bubblewrap`, `lxc`, and `seatbelt` backends work out of the box. **Experimental backends** (`windows_sandbox`, `wslc`, `microvm`, `isolation_session`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend). +The default `processcontainer`, `bubblewrap`, `lxc`, and `seatbelt` backends work out of the box. **Experimental backends** (`windows_sandbox`, `wslc`, `nvx`, `isolation_session`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend). > **Hyperlight** is an opt-in build flavor (Linux x64 and Windows x64) gated by the `--with-hyperlight` cargo feature. Default shipped binaries do not include it; build from source with `build.bat --with-hyperlight` (Windows) or the equivalent cargo invocation on Linux. @@ -177,7 +177,7 @@ const config = createConfigFromPolicy( network: { allowOutbound: true }, timeoutMs: 30_000, }, - 'process', // intent: "process" | "vm" | "microvm" + 'process', // intent: "process" | "vm" ); // Add the script and any backend-specific runtime settings on the returned config. @@ -256,7 +256,7 @@ console.log(result.stdout);
Table of all backends and links to per-backend guides — click to expand. -`SandboxPolicy` is cross-platform. The backend is selected by the second argument to `createConfigFromPolicy(policy, containment)`. Pass an **abstract intent** (`"process"`, `"vm"`, `"microvm"`) whenever possible — the SDK and native binary resolve it to the right concrete backend for the host. Pass a **concrete backend name** when you need a specific runner. +`SandboxPolicy` is cross-platform. The backend is selected by the second argument to `createConfigFromPolicy(policy, containment)`. Pass an **abstract intent** (`"process"`, `"vm"`) whenever possible — the SDK and native binary resolve it to the right concrete backend for the host. Pass a **concrete backend name** when you need a specific runner. | Backend | Intent | Platforms | Stable? | Guide | | --- | --- | --- | --- | --- | @@ -265,7 +265,7 @@ console.log(result.stdout); | `lxc` | (concrete only) | Linux | ✅ | [`docs/lxc-support/lxc-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/lxc-support/lxc-backend.md) | | `seatbelt` | `process` | macOS | ✅ (schema `0.7.0-alpha`+) | [`docs/seatbelt/seatbelt-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/seatbelt/seatbelt-backend.md) | | `windows_sandbox` | `vm` | Windows | Experimental | [`docs/windows-sandbox/windows-sandbox.md`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.md) | -| `microvm` | `microvm` | Windows | Experimental | [`docs/nanvix-microvm/nanvix.md`](https://github.com/microsoft/mxc/blob/main/docs/nanvix-microvm/nanvix.md) — MicroVM via NanVix on Windows Hypervisor Platform | +| `nvx` | (concrete only) | Windows x64 | Experimental | [`docs/nvx-backend-gaps.md`](https://github.com/microsoft/mxc/blob/main/docs/nvx-backend-gaps.md) — structural foundation only; runtime unavailable in Phase 1 | | `wslc` | (concrete only) | Windows | Experimental | [`docs/wsl/wsl-container-getting-started.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wsl-container-getting-started.md) | | `isolation_session` | (concrete only) | Windows | Experimental | [`docs/isolation-session/oneshot.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) | @@ -436,8 +436,8 @@ Setting `cwd` (or the `workingDirectory` argument) does **not** add that path to | --- | --- | --- | | `MXC is not supported on this platform` | `getPlatformSupport()` returned `isSupported: false`. On Linux, neither LXC nor a usable Bubblewrap 0.5.0+ installation is available. On macOS, the Seatbelt platform probe could not find `/usr/bin/sandbox-exec`. | Inspect `support.reason`. On Linux, also inspect `support.unavailableReasons` and install LXC or Bubblewrap 0.5.0+. On macOS, verify that `/usr/bin/sandbox-exec` exists; its absence indicates an incomplete or unsupported macOS installation. | | `wxc-exec.exe not found` / `lxc-exec not found` | The SDK couldn't locate the native binary. | Set `MXC_BIN_DIR=` so `//wxc-exec.exe` (or `lxc-exec`) exists, or pass `options.executablePath` explicitly. | -| `Invalid containment value ''` | `containment` field doesn't match the parser's accepted values. | Use one of the abstract intents (`process`, `vm`, `microvm`) or a concrete backend listed in [Choosing a Backend](#choosing-a-backend). | -| `'' containment requires experimental mode` | A `windows_sandbox` / `wslc` / `microvm` / `isolation_session` / `hyperlight` backend was selected without the flag. | Pass `{ experimental: true }` in `SandboxSpawnOptions`. | +| `Invalid containment value ''` | `containment` field doesn't match the parser's accepted values. | Use one of the abstract intents (`process`, `vm`) or a concrete backend listed in [Choosing a Backend](#choosing-a-backend). | +| `'' containment requires experimental mode` | A `windows_sandbox` / `wslc` / `nvx` / `isolation_session` / `hyperlight` backend was selected without the flag. | Pass `{ experimental: true }` in `SandboxSpawnOptions`. | | `process.commandLine starts with an unquoted Windows path containing a space` | `wxc-exec` rejects unquoted paths with spaces at parse time. | Quote the executable: `'"C:\\Program Files\\…\\foo.exe" args'`. | | `Experimental_CreateProcessInSandbox failed: WIN32_ERROR(...)` | Native sandbox API returned an OS-level error, e.g. `448` = device feature not supported (Windows build / WIP feature not enabled). Note `120` (call not implemented / BaseContainer disabled) is now handled automatically — the default `process` backend falls back to AppContainer+DACL, so it no longer surfaces here. | Check the Windows build / WIP requirements for the backend you selected. | | Process exits `-1` / `4294967295` with no stdout | Native binary terminated abnormally. | Re-run with `options.debug: true` (or `options.logDir: ''`) to capture diagnostic logs. | diff --git a/sdk/node/src/generated/v0_9_0_alpha/wire.ts b/sdk/node/src/generated/v0_9_0_alpha/wire.ts index 74429184e..89f3e3c6d 100644 --- a/sdk/node/src/generated/v0_9_0_alpha/wire.ts +++ b/sdk/node/src/generated/v0_9_0_alpha/wire.ts @@ -389,7 +389,7 @@ export interface NetworkRule { export type NonEmptyString = string; -export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "vm" | "windows_sandbox" | "microvm" | "nvx" | "hyperlight" | "wslc" | "isolation_session"; +export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "vm" | "windows_sandbox" | "nvx" | "hyperlight" | "wslc" | "isolation_session"; /** * Experimental settings. diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index f75ec3100..cd6d76f9d 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -69,7 +69,7 @@ export type ClipboardPolicy = "none" | "read" | "write" | "all"; /** * Containment backend (abstract intent or concrete backend). */ -export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "microvm" | "nvx" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap"; +export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "nvx" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap"; /** * Experimental features (only honored with `--experimental`). This block is intentionally **permissive** (no `deny_unknown_fields`): experimental backends are in flux, so the schema documents the known shapes for editor help without rejecting in-progress fields. The strict, closed contract is the stable (top-level) surface. @@ -354,7 +354,7 @@ export interface Process { */ commandLine?: string | null; /** - * Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NanVix and Hyperlight reject a working directory outright. See `docs/schema.md` ("Working Directory"). + * Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NVX and Hyperlight reject a working directory outright. See `docs/schema.md` ("Working Directory"). */ cwd?: string | null; /** diff --git a/sdk/node/src/helper.ts b/sdk/node/src/helper.ts index f5d2f3281..721ccfbb1 100644 --- a/sdk/node/src/helper.ts +++ b/sdk/node/src/helper.ts @@ -220,7 +220,12 @@ export function resolveExecutableAndArgs( // bypass the experimental-mode gate (because they are not in // ExperimentalBackends under their alias) and produce a confusing // "not available on this platform" error instead. - const rawContainment = config.containment; + const rawContainment: string | undefined = config.containment; + if (rawContainment === 'microvm') { + throw new Error( + 'containment "microvm" was removed; use "nvx" for the NVX micro-VM backend' + ); + } const effectiveContainment = rawContainment ? (LegacyContainmentAliases[rawContainment] ?? rawContainment) : undefined; @@ -230,7 +235,9 @@ export function resolveExecutableAndArgs( // Check experimental mode before anything else so the caller gets a clear // message about the missing flag rather than a platform/binary error. - if (effectiveContainment && ExperimentalBackends.includes(effectiveContainment) && !options.experimental) { + if (effectiveContainment && + (ExperimentalBackends as readonly string[]).includes(effectiveContainment) && + !options.experimental) { throw new Error( `'${rawContainment}' containment requires experimental mode. Set 'experimental: true' in SandboxSpawnOptions.` ); @@ -243,16 +250,17 @@ export function resolveExecutableAndArgs( throw new Error(`MXC is not supported on this platform: ${platformSupport.reason}`); } - // Hard platform requirement: microvm needs WHP/Hyper-V on Windows. This guard + // Hard platform requirement: NVX needs OpenVMM/WHP on Windows x64. This guard // runs even when `skipPlatformCheck` is set because it's not a build-version // check — the backend literally cannot run on non-Windows hosts. - if (effectiveContainment === 'microvm' && os.platform() !== 'win32') { - throw new Error('The microvm backend is only supported on Windows (requires WHP/Hyper-V).'); + if (effectiveContainment === 'nvx' && + (os.platform() !== 'win32' || os.arch() !== 'x64')) { + throw new Error('The nvx backend is only supported on Windows x64.'); } // Validate containment against platform if (effectiveContainment && !options.skipPlatformCheck) { - // Abstract intents (process, microvm) are resolved by the native binary + // Abstract intents are resolved by the native binary // at run time, so the SDK accepts them without checking against the // host's concrete backend list. const isIntent = (ContainmentTypes as readonly string[]).includes(effectiveContainment); diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index b71577791..ad7866a24 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -219,36 +219,6 @@ function buildProcessBaseContainerConfig( return config; } -/** - * Builds the MicroVM (NanVix) portion of a ContainerConfig. - * MicroVM is Windows-only and does not support network or UI policies. - */ -function buildMicroVmConfig( - config: ContainerConfig, - policy: SandboxPolicy, -): ContainerConfig { - if (os.platform() !== 'win32') { - throw new Error('The microvm backend is only supported on Windows (requires WHP/Hyper-V).'); - } - if (policy.network || usesDirectionalNetwork(policy)) { - throw new Error( - 'The microvm backend does not support network configuration. ' + - 'Remove network, runtimeConfig, and processContainer.network or use a different backend.' - ); - } - if (policy.filesystem?.readwritePaths?.length || - policy.filesystem?.readonlyPaths?.length || - policy.filesystem?.deniedPaths?.length) { - config.filesystem = { - readwritePaths: policy.filesystem?.readwritePaths, - readonlyPaths: policy.filesystem?.readonlyPaths, - deniedPaths: policy.filesystem?.deniedPaths, - }; - } - config.containment = 'microvm'; - return config; -} - /** * Creates a ContainerConfig from a SandboxPolicy and optional containment type. * @@ -303,12 +273,6 @@ export function createConfigFromPolicy( }, }; - // Microvm: delegate to dedicated builder - if (containment === 'microvm') { - diagLog(`createConfigFromPolicy: containment=microvm, id=${containerId}`); - return buildMicroVmConfig(config, policy); - } - config.filesystem = { readwritePaths: [...(policy.filesystem?.readwritePaths ?? [])], readonlyPaths: [...(policy.filesystem?.readonlyPaths ?? [])], diff --git a/sdk/node/src/types.ts b/sdk/node/src/types.ts index e2b5d3d8d..7b2cae92a 100644 --- a/sdk/node/src/types.ts +++ b/sdk/node/src/types.ts @@ -1,614 +1,616 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * MXC SDK Types - * These types match the wxc-exec JSON configuration schema - */ - - -/** - * Process execution settings - */ -export interface ProcessConfig { - /** Complete command line to execute (e.g., "python -c \"print('hello')\"") */ - commandLine: string; - /** Working directory for the process */ - cwd?: string; - /** Environment variables as KEY=VALUE strings */ - env?: string[]; - /** Execution timeout in milliseconds (default: 0 = no timeout) */ - timeout?: number; -} - -/** - * Container lifecycle settings shared across all backends - */ -export interface LifecycleConfig { - /** Destroy the container after execution completes (default: true) */ - destroyOnExit?: boolean; - /** Retain filesystem and network policies after execution (default: false) */ - preservePolicy?: boolean; -} - -/** - * Abstract containment intent. Names the *kind* of isolation the caller - * wants; the native binary resolves it to a concrete - * {@link ContainmentBackend} per host capability. - * - * Today's intents: - * - "process": OS-native process-level isolation. Resolves to - * `processcontainer` (Windows), `bubblewrap` (Linux), or `seatbelt` - * (macOS). On Linux, `lxc` remains available as an explicit concrete - * backend but is no longer the default for the abstract `"process"` - * intent. - * - "vm": full hardware-virtualised VM isolation. Resolves to - * `windows_sandbox` on Windows; no concrete VM backend exists on other - * platforms today. - * - "microvm": lightweight-VM isolation. Resolves to the current MicroVM - * runner (Windows only, experimental); intended to expand as additional - * microvm backends (e.g. NanVix) are added. - * - * Concrete-only backends (such as `"wslc"`) live on - * {@link ContainmentBackend} until there is a meaningful abstraction over - * multiple implementations of the same kind. - */ -export type ContainmentType = "process" | "vm" | "microvm"; - -/** - * Runtime list of {@link ContainmentType} values. Kept in sync with the - * `ContainmentType` union via the type annotation. Use this to recognise - * abstract intents at run time (the union itself only exists at compile - * time). - */ -export const ContainmentTypes: readonly ContainmentType[] = ['process', 'vm', 'microvm']; - -/** - * Deprecated containment wire values, mapped to their canonical - * {@link ContainmentBackend} replacement. The native binary (wxc-exec) accepts - * the deprecated form via serde aliases; this map mirrors that behavior in - * the SDK validator so legacy configs are not rejected before reaching the - * binary. - * - * The map is intentionally partial: only deprecated keys appear. Use a - * presence check (e.g. `LegacyContainmentAliases[value] ?? value`) rather - * than indexing blindly. - * - * The wire payload is forwarded to wxc-exec unchanged — the Rust parser - * performs the final mapping at runtime. Resolution here is purely so the - * SDK's experimental-mode, platform-support, and availability checks see - * the canonical backend. - * - * Internal to the SDK; not part of the public API. Subject to removal in a - * future minor release once the deprecation window closes. - */ -export const LegacyContainmentAliases: Readonly>> = { - appcontainer: 'processcontainer', - macos_sandbox: 'seatbelt', -}; - -/** - * Concrete containment backend. Each value names a specific runner - * implementation in the native binary. Prefer a {@link ContainmentType} - * value unless you specifically need to force a particular backend. - */ -export type ContainmentBackend = - | 'processcontainer' - | 'windows_sandbox' - | 'wslc' - | 'lxc' - | 'microvm' - | 'hyperlight' - | 'seatbelt' - | 'isolation_session' - | 'bubblewrap'; - -/** - * Containment values (abstract intent or concrete backend) that require - * the `--experimental` flag. - */ -export const ExperimentalBackends: readonly (ContainmentType | ContainmentBackend)[] = ['microvm', 'windows_sandbox', 'hyperlight', 'wslc', 'isolation_session']; - -/** - * Clipboard access policy levels - */ -export type ClipboardPolicy = "none" | "read" | "write" | "all"; - -/** - * Cross-platform UI configuration in ContainerConfig. - * Mapped from SandboxPolicy.ui by createConfigFromPolicy. - */ -export interface UiConfig { - /** Whether UI is disabled (no visible windows). Maps from !policy.ui.allowWindows. */ - disable: boolean; - /** Clipboard access level */ - clipboard: ClipboardPolicy; - /** Whether input injection is allowed */ - injection: boolean; -} - -/** - * BaseProcess-specific UI configuration (Windows only). - * Lives under processContainer.ui in ContainerConfig. - */ -export interface BaseProcessUiConfig { - /** UI isolation level for the desktop */ - isolation: "desktop" | "handles" | "atoms" | "container"; - /** Whether desktop system control is allowed */ - desktopSystemControl: boolean; - /** System settings access level */ - systemSettings: string; - /** Whether IME (Input Method Editor) is allowed */ - ime: boolean; -} - -/** - * ProcessContainer configuration for the Windows process-level backend. - * - * `processcontainer` is the abstraction layer; the runner picks between - * the legacy AppContainer implementation (which honors `capabilities`, - * `leastPrivilege`) and the newer BaseContainer implementation (which - * honors `ui`) at run time based on the host OS and the `--experimental` - * flag. - */ -export interface ProcessContainerConfig { - /** AppContainer profile name (default: "CLI"). Deprecated: use containerId instead. */ - name?: string; - /** Use least privilege mode with PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT (default: false) */ - leastPrivilege?: boolean; - /** - * Enable deny-and-record learning mode. Failed access checks are logged while - * accesses remain denied and containment remains enforced. - */ - learningMode?: boolean; - /** - * Additional AppContainer capabilities (e.g., "registryRead", "internetClient"). - * Each entry must contain one capability name and must not contain a comma. - * The reserved learning-mode capability names must not be supplied directly. - */ - capabilities?: string[]; - /** BaseProcess-specific UI settings (Windows only) */ - ui?: BaseProcessUiConfig; - /** ProcessContainer-specific networking settings. */ - network?: { - /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ - allowedProxyPeer?: string; - }; -} - -/** - * Filesystem access configuration - */ -export interface FilesystemConfig { - /** Paths the script can read and write */ - readwritePaths?: string[]; - /** Paths the script can read but not write */ - readonlyPaths?: string[]; - /** Paths the script cannot access */ - deniedPaths?: string[]; - /** Automatically remove file access policy after execution (default: true) */ - clearPolicyOnExit?: boolean; -} - -/** - * Network access configuration - */ -export interface NetworkConfig { - /** - * Network enforcement mode: - * - "capabilities": Use AppContainer capabilities only (no admin required) - * - "firewall": Use Windows Firewall rules (requires admin) - * - "both": Use both capabilities and firewall rules (requires admin) - * (default: "both") - */ - enforcementMode?: 'capabilities' | 'firewall' | 'both'; - /** Default network policy: "allow" or "block" (default: "block") */ - defaultPolicy?: 'allow' | 'block'; - /** - * Whether to allow inbound connections to local IP listeners (i.e. the - * sandboxed process may call `bind()` + `listen()` and accept incoming - * TCP/UDP). Independent of `defaultPolicy`. (default: false) - */ - allowLocalNetwork?: boolean; - /** Hostnames or IP addresses/CIDR blocks to allow (firewall mode only) */ - allowedHosts?: string[]; - /** Hostnames or IP addresses to block (firewall mode only) */ - blockedHosts?: string[]; - /** Proxy configuration (supported on Windows ProcessContainer, Linux Bubblewrap, - * macOS Seatbelt, and WSLC). On Bubblewrap/Seatbelt/WSLC it is a cooperative - * env-var proxy (HTTP_PROXY/HTTPS_PROXY): well-behaved HTTP clients honor it, - * raw-socket clients can bypass it. `builtinTestServer` activates a bundled, - * testing-only proxy; the SDK rejects it unless `allowTestingFeatures: true` is - * set in SandboxSpawnOptions (which maps to the native `--allow-testing-features` - * flag). - * - * WSLC imposes additional parse-time constraints (a violating config is - * rejected before it runs): - * - Only the `{ url }` form is accepted — its containers run in their own - * network namespace, so the `localhost` / `builtinTestServer` loopback - * forms are unreachable and rejected. - * - The `url` scheme must be `http` or `https`. - * - `defaultPolicy` must be `"allow"` and both `allowedHosts` and - * `blockedHosts` must be empty/unset — WSLC has no in-kernel iptables, so - * it cannot enforce host lists, and the container needs outbound - * networking to reach the proxy at all. - * Enforcement is cooperative (no in-kernel iptables). */ - proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; - /** Automatically remove firewall rules after execution (default: true). Deprecated: use lifecycle.preservePolicy. */ - removeRulesOnExit?: boolean; - /** Outbound network policy. */ - egress?: NetworkEgressConfig; - /** Inbound and host-loopback network policy. */ - ingress?: NetworkIngressConfig; -} - -/** Allow or deny network action. */ -export type NetworkAction = 'allow' | 'deny'; - -/** Transport protocol selector. */ -export type NetworkProtocol = 'tcp' | 'udp' | 'icmp' | 'any'; - -/** CIDR network peer. */ -export interface NetworkPeerConfig { - /** IPv4 or IPv6 CIDR. */ - cidr: string; - /** CIDRs excluded from this peer. */ - except?: string[]; -} - -/** Protocol and destination-port selector. */ -export interface NetworkPortConfig { - /** Transport protocol. Defaults to "any". */ - protocol?: NetworkProtocol; - /** Destination port. Omission matches every port. */ - port?: number; - /** Inclusive end of a destination-port range. Requires `port`. */ - endPort?: number; -} - -/** Outbound network rule. */ -export interface NetworkRuleConfig { - /** Destination CIDRs. Omission matches both IP families; an explicit array must be non-empty. */ - to?: NetworkPeerConfig[]; - /** Destination protocols and ports. Omission matches all; an explicit array must be non-empty. */ - ports?: NetworkPortConfig[]; -} - -/** Outbound network policy. */ -export interface NetworkEgressConfig { - /** Action used when no explicit rule matches. Defaults to "deny". */ - default?: NetworkAction; - /** Explicit allow rules. */ - allow?: NetworkRuleConfig[]; - /** Explicit deny rules. Deny rules take precedence. */ - deny?: NetworkRuleConfig[]; -} - -/** Inbound and host-loopback network policy. */ -export interface NetworkIngressConfig { - /** Default action for LAN/private-network inbound traffic. */ - default?: NetworkAction; - /** Bidirectional host-loopback connectivity action. */ - hostLoopback?: NetworkAction; -} - -/** Runtime values supplied separately from sandbox policy. */ -export interface RuntimeConfig { - /** HTTP/S loopback proxy URL. */ - networkProxy?: string; -} - -/** - * WSLC SDK configuration for Linux containers from Windows - */ -export interface WslcConfig { - /** OCI container image name (default: "alpine:latest") */ - image?: string; - /** Storage path for WSLC session image store */ - storagePath?: string; - /** Target OS for the container (default: "linux") */ - targetOs?: string; - /** Number of CPUs allocated to the WSLC session */ - cpuCount?: number; - /** Memory in MB allocated to the WSLC session */ - memoryMb?: number; - /** Enable GPU passthrough to the container (default: false) */ - gpu?: boolean; - /** Path to a local tar file to import as the container image */ - imageTarPath?: string; - /** - * Host↔container port mappings. - * - * Only TCP is currently supported by the WSLC SDK runtime. UDP is declared - * in the SDK header but the shipped runtime returns `E_NOTIMPL` when UDP is - * actually requested, so the parser hard-rejects `"udp"` with a clear - * message at spawn time. The `protocol` field defaults to `"tcp"` when - * omitted. - */ - portMappings?: PortMapping[]; -} - -/** - * Port mapping for host↔container port forwarding. - */ -export interface PortMapping { - /** Port on the Windows host */ - windowsPort: number; - /** Port inside the Linux container */ - containerPort: number; - /** - * Transport protocol. Only `"tcp"` is currently supported; `"udp"` is - * rejected by the parser because the WSLC SDK runtime returns `E_NOTIMPL` - * for UDP even though the header declares it. Defaults to `"tcp"` when - * omitted. - */ - protocol?: 'tcp'; -} - -/** Telemetry configuration for TraceLogging ETW support. */ -export interface TelemetryConfig { - /** - * Explicit telemetry opt-in. `true` requests telemetry subject to user - * consent and administrative policy; `false` or `undefined` keeps it off. - */ - enabled?: boolean; -} - -/** - * Main WXC configuration - */ -export interface ContainerConfig { - /** MXC config schema version. Required. */ - version: string; - /** Externally assigned container identifier */ - containerId?: string; - /** Containment intent (preferred) or concrete backend (override). */ - containment?: ContainmentType | ContainmentBackend; - /** Container lifecycle settings */ - lifecycle?: LifecycleConfig; - /** Process execution settings (required) */ - process?: ProcessConfig; - /** ProcessContainer configuration */ - processContainer?: ProcessContainerConfig; - /** - * Legacy alias of {@link processContainer}. Retained so callers - * migrating from pre-0.6 SDK versions can keep their existing code - * compiling; the native binary parses both names into the same slot - * via a serde alias. - * - * @deprecated Use {@link processContainer} instead. This alias may be - * removed in a future minor release. - */ - appContainer?: ProcessContainerConfig; - /** LXC container configuration (Linux only) */ - lxc?: LxcConfig; - /** Filesystem access configuration */ - filesystem?: FilesystemConfig; - /** Network access configuration */ - network?: NetworkConfig; - /** Runtime values supplied separately from sandbox policy. */ - runtimeConfig?: RuntimeConfig; - /** Telemetry configuration */ - telemetry?: TelemetryConfig; - /** Experimental features (only applied when --experimental flag is set) */ - experimental?: { - /** WSLC SDK configuration for Linux containers from Windows */ - wslc?: WslcConfig; - }; - /** macOS Seatbelt sandbox configuration (macOS only) */ - seatbelt?: SeatbeltConfig; - /** Cross-platform UI configuration */ - ui?: UiConfig; -} - -/** - * The main sandbox policy configuration interface for external consumers - * to define sandboxed execution environments. - * - * Policy describes *what* the caller wants restricted. Cross-platform. - * No OS-specific content. Omitted fields = most restrictive (default-deny). - */ -export type SandboxPolicy = { - /** Policy version (semver). Must match a supported schema version. */ - version: string; - /** Filesystem access restrictions */ - filesystem?: { - /** Paths that are granted read and write access */ - readwritePaths?: string[]; - /** Paths that are granted read-only access */ - readonlyPaths?: string[]; - /** Paths that are explicitly denied all access */ - deniedPaths?: string[]; - /** Whether to clear the filesystem policy when the shell exits. (default: true) */ - clearPolicyOnExit?: boolean; - }; - /** Network access restrictions. All flags default to false (no network access). */ - network?: { - /** Whether to allow outbound connections to the Internet. (default: false) Legacy network field. */ - allowOutbound?: boolean; - /** Whether to allow connections to local networks. (default: false) Legacy network field. */ - allowLocalNetwork?: boolean; - /** When set, ONLY these outbound hosts are reachable. Requires allowOutbound. Legacy network field. */ - allowedHosts?: string[]; - /** Hosts to block even when outbound is allowed. Requires allowOutbound. Legacy network field. */ - blockedHosts?: string[]; - /** - * Proxy configuration. Routes cooperating HTTP traffic through this proxy. - * Supported on Windows ProcessContainer, Linux Bubblewrap, and macOS - * Seatbelt. On Bubblewrap/Seatbelt it is a cooperative env-var proxy - * (HTTP_PROXY/HTTPS_PROXY) — raw-socket clients can bypass it. Native - * validation enforces backend-specific combination rules. - * `builtinTestServer` selects a bundled, testing-only proxy; the SDK - * rejects it unless `allowTestingFeatures: true` is set in - * SandboxSpawnOptions (which maps to the native - * `--allow-testing-features` flag). - * Legacy network field. - */ - proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; - /** Schema 0.8 outbound network policy. Cannot be combined with legacy network fields. */ - egress?: NetworkEgressConfig; - /** Schema 0.8 inbound and host-loopback policy. Cannot be combined with legacy network fields. */ - ingress?: NetworkIngressConfig; - }; - /** Schema 0.8 runtime values supplied separately from sandbox policy. */ - runtimeConfig?: RuntimeConfig; - /** Schema 0.8 ProcessContainer-specific policy. */ - processContainer?: { - /** ProcessContainer-specific networking settings. */ - network?: { - /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ - allowedProxyPeer?: string; - }; - }; - /** UI access restrictions. All flags default to denied. */ - ui?: { - /** Whether the sandbox may create visible windows. (default: false) */ - allowWindows?: boolean; - /** Clipboard access level. (default: "none") */ - clipboard?: ClipboardPolicy; - /** Whether the sandbox may inject keyboard/mouse input. (default: false) */ - allowInputInjection?: boolean; - }; - /** Execution timeout in milliseconds. Omitted = no timeout. */ - timeoutMs?: number; -} - -/** - * LXC container configuration for Linux sandbox - */ -export interface LxcConfig { - /** Container name (default: auto-generated) */ - containerName?: string; - /** Linux distribution for container rootfs (default: "alpine") */ - distribution?: string; - /** Distribution release version (default: "3.19") */ - release?: string; - /** Whether to destroy the container after execution (default: true) */ - destroyOnExit?: boolean; -} - -/** - * macOS Seatbelt sandbox configuration. Used under the top-level - * `seatbelt` key when `containment == "seatbelt"`. - */ -export interface SeatbeltConfig { - /** - * Optional override of the generated TinyScheme sandbox profile. - */ - profileOverride?: string; - /** - * Allow the inner process to allocate its own pseudo-terminals via - * `posix_openpt` (needed by tests, `git`, `gh`, REPLs, and any tool - * that wraps commands in a pty). Adds `(allow pseudo-tty)` and - * read/write/ioctl on `/dev/ptmx` to the generated profile. Defaults - * to `true`; set to `false` for the tightest possible sandbox when - * the inner command does not need to allocate new ttys. - */ - nestedPty?: boolean; - /** - * Allow the inner process to use the macOS Keychain (e.g. via - * `keytar` or `Security.framework`) end-to-end. Adds Mach lookup for - * `securityd`, `trustd`, `ocspd`, `cfprefsd`, `xpcd`, and the - * `com.apple.lsd.*` family; read access to `/private/var/db/mds` and - * `/private/var/protected/trustd`; and read+write access to - * `~/Library/Keychains` and `/private/var/folders` (XPC cache). - * Defaults to `false`; opt in only when the inner workload genuinely - * needs Keychain access. - */ - keychainAccess?: boolean; - /** - * Additional Mach service global-names to allow `mach-lookup` for. - * Escape hatch for callers that need a specific system service the - * baseline doesn't cover (e.g. opt-in agent integrations). Each entry - * is rendered as `(global-name "...")` inside a single - * `(allow mach-lookup ...)` form. - */ - extraMachLookups?: string[]; -} - -/** - * Sandboxing methods available on the platform - * - * @deprecated Prefer {@link ContainmentBackend} (concrete) or - * {@link ContainmentType} (abstract). This alias is retained for - * backward compatibility and may be removed in a future minor release. - */ -export type SandboxingMethod = ContainmentType | ContainmentBackend; - -/** - * Isolation tier selected by the runtime fallback detector. - * - * - `base-container`: full BaseContainer (Experimental_CreateProcessInSandbox) - * - `appcontainer-bfs`: AppContainer + BFS filesystem isolation - * - `appcontainer-dacl`: AppContainer + host DACL augmentation (last-resort fallback) - */ -export type IsolationTier = - | 'base-container' - | 'appcontainer-bfs' - | 'appcontainer-dacl'; - -/** - * Host support for enforcing sandbox UI restrictions. - * - * The fields describe platform-agnostic restriction intents, not the - * OS-specific primitive used to enforce them. For example, Windows derives - * these values from `JOB_OBJECT_UILIMIT_*` support. The SDK currently - * receives this object only from the Windows native probe; other platforms - * omit `PlatformSupport.uiCapabilities` until they expose equivalent probe - * data. - */ -export interface UiCapabilitySupport { - /** Whether the host can block reads from the clipboard. */ - canBlockClipboardRead: boolean; - /** Whether the host can block writes to the clipboard. */ - canBlockClipboardWrite: boolean; - /** Whether the host can block synthetic keyboard/mouse input. */ - canBlockInputInjection: boolean; - /** Whether the host can block input method / IME changes. */ - canBlockInputMethodChanges: boolean; - /** Whether the host can block access to external UI object handles. */ - canBlockExternalUiObjects: boolean; - /** Whether the host can block access to global UI namespaces. */ - canBlockGlobalUiNamespace: boolean; - /** Whether the host can block desktop switching. */ - canBlockDesktopSwitching: boolean; - /** Whether the host can block logoff or shutdown requests. */ - canBlockLogoffOrShutdown: boolean; - /** Whether the host can block system parameter changes. */ - canBlockSystemParameterChanges: boolean; - /** Whether the host can block display settings changes. */ - canBlockDisplaySettingsChanges: boolean; -} - -/** - * Platform support information - */ -export interface PlatformSupport { - /** Whether WXC is supported on the current platform */ - isSupported: boolean; - /** Reason why the platform is not supported (if applicable) */ - reason?: string; - /** Available sandboxing methods on this platform */ - availableMethods: ContainmentBackend[]; - /** - * Why individual Linux backends are unavailable, whether or not another - * backend keeps Linux supported. Omitted on other platforms and when no - * Linux backend failures were observed. - */ - unavailableReasons?: Partial>; - /** - * Tier that would be selected for an empty policy on this system. - * Omitted on non-Windows platforms or when the probe fails. - */ - isolationTier?: IsolationTier; - /** - * Tier degradation warnings (one per fall-through during selection). - * Omitted on non-Windows platforms or when the probe fails. - */ - isolationWarnings?: string[]; - /** - * Host UI-restriction capabilities. Omitted when the backend probe cannot - * determine them, including on Linux and macOS today. - */ - uiCapabilities?: UiCapabilitySupport; -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * MXC SDK Types + * These types match the wxc-exec JSON configuration schema + */ + + +/** + * Process execution settings + */ +export interface ProcessConfig { + /** Complete command line to execute (e.g., "python -c \"print('hello')\"") */ + commandLine: string; + /** Working directory for the process */ + cwd?: string; + /** Environment variables as KEY=VALUE strings */ + env?: string[]; + /** Execution timeout in milliseconds (default: 0 = no timeout) */ + timeout?: number; +} + +/** + * Container lifecycle settings shared across all backends + */ +export interface LifecycleConfig { + /** Destroy the container after execution completes (default: true) */ + destroyOnExit?: boolean; + /** Retain filesystem and network policies after execution (default: false) */ + preservePolicy?: boolean; +} + +/** + * Abstract containment intent. Names the *kind* of isolation the caller + * wants; the native binary resolves it to a concrete + * {@link ContainmentBackend} per host capability. + * + * Today's intents: + * - "process": OS-native process-level isolation. Resolves to + * `processcontainer` (Windows), `bubblewrap` (Linux), or `seatbelt` + * (macOS). On Linux, `lxc` remains available as an explicit concrete + * backend but is no longer the default for the abstract `"process"` + * intent. + * - "vm": full hardware-virtualised VM isolation. Resolves to + * `windows_sandbox` on Windows; no concrete VM backend exists on other + * platforms today. + * Concrete-only backends (such as `"wslc"`) live on + * {@link ContainmentBackend} until there is a meaningful abstraction over + * multiple implementations of the same kind. + */ +export type ContainmentType = 'process' | 'vm'; + +/** + * Runtime list of {@link ContainmentType} values. Kept in sync with the + * `ContainmentType` union via the type annotation. Use this to recognise + * abstract intents at run time (the union itself only exists at compile + * time). + */ +export const ContainmentTypes: readonly ContainmentType[] = ['process', 'vm']; + +/** + * Deprecated containment wire values, mapped to their canonical + * {@link ContainmentBackend} replacement. The native binary (wxc-exec) accepts + * the deprecated form via serde aliases; this map mirrors that behavior in + * the SDK validator so legacy configs are not rejected before reaching the + * binary. + * + * The map is intentionally partial: only deprecated keys appear. Use a + * presence check (e.g. `LegacyContainmentAliases[value] ?? value`) rather + * than indexing blindly. + * + * The wire payload is forwarded to wxc-exec unchanged — the Rust parser + * performs the final mapping at runtime. Resolution here is purely so the + * SDK's experimental-mode, platform-support, and availability checks see + * the canonical backend. + * + * Internal to the SDK; not part of the public API. Subject to removal in a + * future minor release once the deprecation window closes. + */ +export const LegacyContainmentAliases: Readonly>> = { + appcontainer: 'processcontainer', + macos_sandbox: 'seatbelt', +}; + +/** + * Concrete containment backend. Each value names a specific runner + * implementation in the native binary. Prefer a {@link ContainmentType} + * value unless you specifically need to force a particular backend. + */ +export type ContainmentBackend = + | 'processcontainer' + | 'windows_sandbox' + | 'wslc' + | 'lxc' + | 'nvx' + | 'hyperlight' + | 'seatbelt' + | 'isolation_session' + | 'bubblewrap'; + +/** + * Containment values (abstract intent or concrete backend) that require + * the `--experimental` flag. + */ +export const ExperimentalBackends: readonly (ContainmentType | ContainmentBackend)[] = [ + 'nvx', + 'windows_sandbox', + 'hyperlight', + 'wslc', + 'isolation_session', +]; + +/** + * Clipboard access policy levels + */ +export type ClipboardPolicy = "none" | "read" | "write" | "all"; + +/** + * Cross-platform UI configuration in ContainerConfig. + * Mapped from SandboxPolicy.ui by createConfigFromPolicy. + */ +export interface UiConfig { + /** Whether UI is disabled (no visible windows). Maps from !policy.ui.allowWindows. */ + disable: boolean; + /** Clipboard access level */ + clipboard: ClipboardPolicy; + /** Whether input injection is allowed */ + injection: boolean; +} + +/** + * BaseProcess-specific UI configuration (Windows only). + * Lives under processContainer.ui in ContainerConfig. + */ +export interface BaseProcessUiConfig { + /** UI isolation level for the desktop */ + isolation: "desktop" | "handles" | "atoms" | "container"; + /** Whether desktop system control is allowed */ + desktopSystemControl: boolean; + /** System settings access level */ + systemSettings: string; + /** Whether IME (Input Method Editor) is allowed */ + ime: boolean; +} + +/** + * ProcessContainer configuration for the Windows process-level backend. + * + * `processcontainer` is the abstraction layer; the runner picks between + * the legacy AppContainer implementation (which honors `capabilities`, + * `leastPrivilege`) and the newer BaseContainer implementation (which + * honors `ui`) at run time based on the host OS and the `--experimental` + * flag. + */ +export interface ProcessContainerConfig { + /** AppContainer profile name (default: "CLI"). Deprecated: use containerId instead. */ + name?: string; + /** Use least privilege mode with PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT (default: false) */ + leastPrivilege?: boolean; + /** + * Enable deny-and-record learning mode. Failed access checks are logged while + * accesses remain denied and containment remains enforced. + */ + learningMode?: boolean; + /** + * Additional AppContainer capabilities (e.g., "registryRead", "internetClient"). + * Each entry must contain one capability name and must not contain a comma. + * The reserved learning-mode capability names must not be supplied directly. + */ + capabilities?: string[]; + /** BaseProcess-specific UI settings (Windows only) */ + ui?: BaseProcessUiConfig; + /** ProcessContainer-specific networking settings. */ + network?: { + /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ + allowedProxyPeer?: string; + }; +} + +/** + * Filesystem access configuration + */ +export interface FilesystemConfig { + /** Paths the script can read and write */ + readwritePaths?: string[]; + /** Paths the script can read but not write */ + readonlyPaths?: string[]; + /** Paths the script cannot access */ + deniedPaths?: string[]; + /** Automatically remove file access policy after execution (default: true) */ + clearPolicyOnExit?: boolean; +} + +/** + * Network access configuration + */ +export interface NetworkConfig { + /** + * Network enforcement mode: + * - "capabilities": Use AppContainer capabilities only (no admin required) + * - "firewall": Use Windows Firewall rules (requires admin) + * - "both": Use both capabilities and firewall rules (requires admin) + * (default: "both") + */ + enforcementMode?: 'capabilities' | 'firewall' | 'both'; + /** Default network policy: "allow" or "block" (default: "block") */ + defaultPolicy?: 'allow' | 'block'; + /** + * Whether to allow inbound connections to local IP listeners (i.e. the + * sandboxed process may call `bind()` + `listen()` and accept incoming + * TCP/UDP). Independent of `defaultPolicy`. (default: false) + */ + allowLocalNetwork?: boolean; + /** Hostnames or IP addresses/CIDR blocks to allow (firewall mode only) */ + allowedHosts?: string[]; + /** Hostnames or IP addresses to block (firewall mode only) */ + blockedHosts?: string[]; + /** Proxy configuration (supported on Windows ProcessContainer, Linux Bubblewrap, + * macOS Seatbelt, and WSLC). On Bubblewrap/Seatbelt/WSLC it is a cooperative + * env-var proxy (HTTP_PROXY/HTTPS_PROXY): well-behaved HTTP clients honor it, + * raw-socket clients can bypass it. `builtinTestServer` activates a bundled, + * testing-only proxy; the SDK rejects it unless `allowTestingFeatures: true` is + * set in SandboxSpawnOptions (which maps to the native `--allow-testing-features` + * flag). + * + * WSLC imposes additional parse-time constraints (a violating config is + * rejected before it runs): + * - Only the `{ url }` form is accepted — its containers run in their own + * network namespace, so the `localhost` / `builtinTestServer` loopback + * forms are unreachable and rejected. + * - The `url` scheme must be `http` or `https`. + * - `defaultPolicy` must be `"allow"` and both `allowedHosts` and + * `blockedHosts` must be empty/unset — WSLC has no in-kernel iptables, so + * it cannot enforce host lists, and the container needs outbound + * networking to reach the proxy at all. + * Enforcement is cooperative (no in-kernel iptables). */ + proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; + /** Automatically remove firewall rules after execution (default: true). Deprecated: use lifecycle.preservePolicy. */ + removeRulesOnExit?: boolean; + /** Outbound network policy. */ + egress?: NetworkEgressConfig; + /** Inbound and host-loopback network policy. */ + ingress?: NetworkIngressConfig; +} + +/** Allow or deny network action. */ +export type NetworkAction = 'allow' | 'deny'; + +/** Transport protocol selector. */ +export type NetworkProtocol = 'tcp' | 'udp' | 'icmp' | 'any'; + +/** CIDR network peer. */ +export interface NetworkPeerConfig { + /** IPv4 or IPv6 CIDR. */ + cidr: string; + /** CIDRs excluded from this peer. */ + except?: string[]; +} + +/** Protocol and destination-port selector. */ +export interface NetworkPortConfig { + /** Transport protocol. Defaults to "any". */ + protocol?: NetworkProtocol; + /** Destination port. Omission matches every port. */ + port?: number; + /** Inclusive end of a destination-port range. Requires `port`. */ + endPort?: number; +} + +/** Outbound network rule. */ +export interface NetworkRuleConfig { + /** Destination CIDRs. Omission matches both IP families; an explicit array must be non-empty. */ + to?: NetworkPeerConfig[]; + /** Destination protocols and ports. Omission matches all; an explicit array must be non-empty. */ + ports?: NetworkPortConfig[]; +} + +/** Outbound network policy. */ +export interface NetworkEgressConfig { + /** Action used when no explicit rule matches. Defaults to "deny". */ + default?: NetworkAction; + /** Explicit allow rules. */ + allow?: NetworkRuleConfig[]; + /** Explicit deny rules. Deny rules take precedence. */ + deny?: NetworkRuleConfig[]; +} + +/** Inbound and host-loopback network policy. */ +export interface NetworkIngressConfig { + /** Default action for LAN/private-network inbound traffic. */ + default?: NetworkAction; + /** Bidirectional host-loopback connectivity action. */ + hostLoopback?: NetworkAction; +} + +/** Runtime values supplied separately from sandbox policy. */ +export interface RuntimeConfig { + /** HTTP/S loopback proxy URL. */ + networkProxy?: string; +} + +/** + * WSLC SDK configuration for Linux containers from Windows + */ +export interface WslcConfig { + /** OCI container image name (default: "alpine:latest") */ + image?: string; + /** Storage path for WSLC session image store */ + storagePath?: string; + /** Target OS for the container (default: "linux") */ + targetOs?: string; + /** Number of CPUs allocated to the WSLC session */ + cpuCount?: number; + /** Memory in MB allocated to the WSLC session */ + memoryMb?: number; + /** Enable GPU passthrough to the container (default: false) */ + gpu?: boolean; + /** Path to a local tar file to import as the container image */ + imageTarPath?: string; + /** + * Host↔container port mappings. + * + * Only TCP is currently supported by the WSLC SDK runtime. UDP is declared + * in the SDK header but the shipped runtime returns `E_NOTIMPL` when UDP is + * actually requested, so the parser hard-rejects `"udp"` with a clear + * message at spawn time. The `protocol` field defaults to `"tcp"` when + * omitted. + */ + portMappings?: PortMapping[]; +} + +/** + * Port mapping for host↔container port forwarding. + */ +export interface PortMapping { + /** Port on the Windows host */ + windowsPort: number; + /** Port inside the Linux container */ + containerPort: number; + /** + * Transport protocol. Only `"tcp"` is currently supported; `"udp"` is + * rejected by the parser because the WSLC SDK runtime returns `E_NOTIMPL` + * for UDP even though the header declares it. Defaults to `"tcp"` when + * omitted. + */ + protocol?: 'tcp'; +} + +/** Telemetry configuration for TraceLogging ETW support. */ +export interface TelemetryConfig { + /** + * Explicit telemetry opt-in. `true` requests telemetry subject to user + * consent and administrative policy; `false` or `undefined` keeps it off. + */ + enabled?: boolean; +} + +/** + * Main WXC configuration + */ +export interface ContainerConfig { + /** MXC config schema version. Required. */ + version: string; + /** Externally assigned container identifier */ + containerId?: string; + /** Containment intent (preferred) or concrete backend (override). */ + containment?: ContainmentType | ContainmentBackend; + /** Container lifecycle settings */ + lifecycle?: LifecycleConfig; + /** Process execution settings (required) */ + process?: ProcessConfig; + /** ProcessContainer configuration */ + processContainer?: ProcessContainerConfig; + /** + * Legacy alias of {@link processContainer}. Retained so callers + * migrating from pre-0.6 SDK versions can keep their existing code + * compiling; the native binary parses both names into the same slot + * via a serde alias. + * + * @deprecated Use {@link processContainer} instead. This alias may be + * removed in a future minor release. + */ + appContainer?: ProcessContainerConfig; + /** LXC container configuration (Linux only) */ + lxc?: LxcConfig; + /** Filesystem access configuration */ + filesystem?: FilesystemConfig; + /** Network access configuration */ + network?: NetworkConfig; + /** Runtime values supplied separately from sandbox policy. */ + runtimeConfig?: RuntimeConfig; + /** Telemetry configuration */ + telemetry?: TelemetryConfig; + /** Experimental features (only applied when --experimental flag is set) */ + experimental?: { + /** WSLC SDK configuration for Linux containers from Windows */ + wslc?: WslcConfig; + }; + /** macOS Seatbelt sandbox configuration (macOS only) */ + seatbelt?: SeatbeltConfig; + /** Cross-platform UI configuration */ + ui?: UiConfig; +} + +/** + * The main sandbox policy configuration interface for external consumers + * to define sandboxed execution environments. + * + * Policy describes *what* the caller wants restricted. Cross-platform. + * No OS-specific content. Omitted fields = most restrictive (default-deny). + */ +export type SandboxPolicy = { + /** Policy version (semver). Must match a supported schema version. */ + version: string; + /** Filesystem access restrictions */ + filesystem?: { + /** Paths that are granted read and write access */ + readwritePaths?: string[]; + /** Paths that are granted read-only access */ + readonlyPaths?: string[]; + /** Paths that are explicitly denied all access */ + deniedPaths?: string[]; + /** Whether to clear the filesystem policy when the shell exits. (default: true) */ + clearPolicyOnExit?: boolean; + }; + /** Network access restrictions. All flags default to false (no network access). */ + network?: { + /** Whether to allow outbound connections to the Internet. (default: false) Legacy network field. */ + allowOutbound?: boolean; + /** Whether to allow connections to local networks. (default: false) Legacy network field. */ + allowLocalNetwork?: boolean; + /** When set, ONLY these outbound hosts are reachable. Requires allowOutbound. Legacy network field. */ + allowedHosts?: string[]; + /** Hosts to block even when outbound is allowed. Requires allowOutbound. Legacy network field. */ + blockedHosts?: string[]; + /** + * Proxy configuration. Routes cooperating HTTP traffic through this proxy. + * Supported on Windows ProcessContainer, Linux Bubblewrap, and macOS + * Seatbelt. On Bubblewrap/Seatbelt it is a cooperative env-var proxy + * (HTTP_PROXY/HTTPS_PROXY) — raw-socket clients can bypass it. Native + * validation enforces backend-specific combination rules. + * `builtinTestServer` selects a bundled, testing-only proxy; the SDK + * rejects it unless `allowTestingFeatures: true` is set in + * SandboxSpawnOptions (which maps to the native + * `--allow-testing-features` flag). + * Legacy network field. + */ + proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; + /** Schema 0.8 outbound network policy. Cannot be combined with legacy network fields. */ + egress?: NetworkEgressConfig; + /** Schema 0.8 inbound and host-loopback policy. Cannot be combined with legacy network fields. */ + ingress?: NetworkIngressConfig; + }; + /** Schema 0.8 runtime values supplied separately from sandbox policy. */ + runtimeConfig?: RuntimeConfig; + /** Schema 0.8 ProcessContainer-specific policy. */ + processContainer?: { + /** ProcessContainer-specific networking settings. */ + network?: { + /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ + allowedProxyPeer?: string; + }; + }; + /** UI access restrictions. All flags default to denied. */ + ui?: { + /** Whether the sandbox may create visible windows. (default: false) */ + allowWindows?: boolean; + /** Clipboard access level. (default: "none") */ + clipboard?: ClipboardPolicy; + /** Whether the sandbox may inject keyboard/mouse input. (default: false) */ + allowInputInjection?: boolean; + }; + /** Execution timeout in milliseconds. Omitted = no timeout. */ + timeoutMs?: number; +} + +/** + * LXC container configuration for Linux sandbox + */ +export interface LxcConfig { + /** Container name (default: auto-generated) */ + containerName?: string; + /** Linux distribution for container rootfs (default: "alpine") */ + distribution?: string; + /** Distribution release version (default: "3.19") */ + release?: string; + /** Whether to destroy the container after execution (default: true) */ + destroyOnExit?: boolean; +} + +/** + * macOS Seatbelt sandbox configuration. Used under the top-level + * `seatbelt` key when `containment == "seatbelt"`. + */ +export interface SeatbeltConfig { + /** + * Optional override of the generated TinyScheme sandbox profile. + */ + profileOverride?: string; + /** + * Allow the inner process to allocate its own pseudo-terminals via + * `posix_openpt` (needed by tests, `git`, `gh`, REPLs, and any tool + * that wraps commands in a pty). Adds `(allow pseudo-tty)` and + * read/write/ioctl on `/dev/ptmx` to the generated profile. Defaults + * to `true`; set to `false` for the tightest possible sandbox when + * the inner command does not need to allocate new ttys. + */ + nestedPty?: boolean; + /** + * Allow the inner process to use the macOS Keychain (e.g. via + * `keytar` or `Security.framework`) end-to-end. Adds Mach lookup for + * `securityd`, `trustd`, `ocspd`, `cfprefsd`, `xpcd`, and the + * `com.apple.lsd.*` family; read access to `/private/var/db/mds` and + * `/private/var/protected/trustd`; and read+write access to + * `~/Library/Keychains` and `/private/var/folders` (XPC cache). + * Defaults to `false`; opt in only when the inner workload genuinely + * needs Keychain access. + */ + keychainAccess?: boolean; + /** + * Additional Mach service global-names to allow `mach-lookup` for. + * Escape hatch for callers that need a specific system service the + * baseline doesn't cover (e.g. opt-in agent integrations). Each entry + * is rendered as `(global-name "...")` inside a single + * `(allow mach-lookup ...)` form. + */ + extraMachLookups?: string[]; +} + +/** + * Sandboxing methods available on the platform + * + * @deprecated Prefer {@link ContainmentBackend} (concrete) or + * {@link ContainmentType} (abstract). This alias is retained for + * backward compatibility and may be removed in a future minor release. + */ +export type SandboxingMethod = ContainmentType | ContainmentBackend; + +/** + * Isolation tier selected by the runtime fallback detector. + * + * - `base-container`: full BaseContainer (Experimental_CreateProcessInSandbox) + * - `appcontainer-bfs`: AppContainer + BFS filesystem isolation + * - `appcontainer-dacl`: AppContainer + host DACL augmentation (last-resort fallback) + */ +export type IsolationTier = + | 'base-container' + | 'appcontainer-bfs' + | 'appcontainer-dacl'; + +/** + * Host support for enforcing sandbox UI restrictions. + * + * The fields describe platform-agnostic restriction intents, not the + * OS-specific primitive used to enforce them. For example, Windows derives + * these values from `JOB_OBJECT_UILIMIT_*` support. The SDK currently + * receives this object only from the Windows native probe; other platforms + * omit `PlatformSupport.uiCapabilities` until they expose equivalent probe + * data. + */ +export interface UiCapabilitySupport { + /** Whether the host can block reads from the clipboard. */ + canBlockClipboardRead: boolean; + /** Whether the host can block writes to the clipboard. */ + canBlockClipboardWrite: boolean; + /** Whether the host can block synthetic keyboard/mouse input. */ + canBlockInputInjection: boolean; + /** Whether the host can block input method / IME changes. */ + canBlockInputMethodChanges: boolean; + /** Whether the host can block access to external UI object handles. */ + canBlockExternalUiObjects: boolean; + /** Whether the host can block access to global UI namespaces. */ + canBlockGlobalUiNamespace: boolean; + /** Whether the host can block desktop switching. */ + canBlockDesktopSwitching: boolean; + /** Whether the host can block logoff or shutdown requests. */ + canBlockLogoffOrShutdown: boolean; + /** Whether the host can block system parameter changes. */ + canBlockSystemParameterChanges: boolean; + /** Whether the host can block display settings changes. */ + canBlockDisplaySettingsChanges: boolean; +} + +/** + * Platform support information + */ +export interface PlatformSupport { + /** Whether WXC is supported on the current platform */ + isSupported: boolean; + /** Reason why the platform is not supported (if applicable) */ + reason?: string; + /** Available sandboxing methods on this platform */ + availableMethods: ContainmentBackend[]; + /** + * Why individual Linux backends are unavailable, whether or not another + * backend keeps Linux supported. Omitted on other platforms and when no + * Linux backend failures were observed. + */ + unavailableReasons?: Partial>; + /** + * Tier that would be selected for an empty policy on this system. + * Omitted on non-Windows platforms or when the probe fails. + */ + isolationTier?: IsolationTier; + /** + * Tier degradation warnings (one per fall-through during selection). + * Omitted on non-Windows platforms or when the probe fails. + */ + isolationWarnings?: string[]; + /** + * Host UI-restriction capabilities. Omitted when the backend probe cannot + * determine them, including on Linux and macOS today. + */ + uiCapabilities?: UiCapabilitySupport; +} diff --git a/sdk/node/tests/integration/microvm-filesystem.test.ts b/sdk/node/tests/integration/microvm-filesystem.test.ts deleted file mode 100644 index 57bbd636c..000000000 --- a/sdk/node/tests/integration/microvm-filesystem.test.ts +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// MicroVM SDK end-to-end tests — these tests spawn NanVix VMs via wxc-exec.exe. -// -// Requirements: -// - Windows with WHP enabled (bcdedit /set hypervisorlaunchtype auto) -// - wxc-exec.exe built (in src/target/debug/ or src/target/x86_64-pc-windows-msvc/debug/) -// - NanVix binaries next to wxc-exec.exe: nanvixd.exe, kernel.elf, python3.12, nanvix_rootfs.img -// -// Run: cd sdk/tests/integration && npx tsc -p tsconfig.json && node --test dist/microvm-filesystem.test.js -// -// All tests use spawnSandboxFromConfig with usePty:false (non-PTY mode). -// PTY mode is not supported for the MicroVM backend. - -import { describe, it } from 'node:test'; -import assert from 'node:assert'; -import fs from 'node:fs'; -import path from 'node:path'; -import os from 'os'; -import { execSync } from 'child_process'; -import { ChildProcess } from 'child_process'; -import { sdk } from './test-helpers.js'; -import type { ContainerConfig } from '@microsoft/mxc-sdk'; - -function isWhpAvailable(): boolean { - if (os.platform() !== 'win32') return false; - // CI sets this when wxc-exec/nanvix binaries aren't available. - if (process.env.MXC_SKIP_OS_BUILD_DEPENDENT_TESTS === '1') return false; - try { - const result = execSync( - 'powershell -NoProfile -Command "(Get-CimInstance Win32_ComputerSystem).HypervisorPresent"', - { encoding: 'utf8', timeout: 5000 } - ).trim(); - return result === 'True'; - } catch { - return false; - } -} - -const isMicrovmAvailable = isWhpAvailable(); - -/** Escape backslashes for embedding a Windows path in a Python string literal. */ -function pyEscape(p: string): string { - return p.replace(/\\/g, '\\\\'); -} - -/** - * Spawn a microvm sandbox using spawnSandboxFromConfig with usePty:false. - * Returns stdout, stderr, and exit code. - */ -function runMicrovm( - config: ContainerConfig, - options: { timeoutMs?: number } = {}, -): Promise<{ stdout: string; stderr: string; exitCode: number }> { - return new Promise((resolve, reject) => { - const timeout = options.timeoutMs ?? 120_000; - - try { - const child: ChildProcess = sdk.spawnSandboxFromConfig(config, { - experimental: true, - debug: true, - usePty: false, - }); - - let stdout = ''; - let stderr = ''; - - child.stdout?.on('data', (data: Buffer) => { stdout += data.toString(); }); - child.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); }); - - const timer = setTimeout(() => { - child.kill(); - reject(new Error(`MicroVM test timed out after ${timeout}ms.\nstdout: ${stdout}\nstderr: ${stderr}`)); - }, timeout); - - child.on('error', (error: Error) => { - clearTimeout(timer); - reject(new Error(`Failed to spawn wxc-exec: ${error.message}`)); - }); - - child.on('close', (code: number | null) => { - clearTimeout(timer); - resolve({ stdout, stderr, exitCode: code ?? -1 }); - }); - } catch (error) { - reject(error); - } - }); -} - -describe('MicroVM SDK E2E — spawnSandboxFromConfig with containment: microvm', { - skip: !isMicrovmAvailable ? 'MicroVM tests require Windows with WHP' : undefined, -}, () => { - - it('should run a simple Python script and capture output', async () => { - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: "print('Hello from MicroVM SDK E2E!')", - timeout: 30000, - }, - }; - - const { stdout, stderr, exitCode } = await runMicrovm(config); - const combined = stdout + stderr; - assert.strictEqual(exitCode, 0, `Expected exit code 0, got ${exitCode}.\nstdout: ${stdout}\nstderr: ${stderr}`); - assert.ok(combined.includes('Hello from MicroVM SDK E2E!'), `Expected greeting in output:\n${combined}`); - }); - - it('should propagate non-zero exit codes', async () => { - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: "import sys; sys.exit(42)", - timeout: 30000, - }, - }; - - const { exitCode } = await runMicrovm(config); - assert.strictEqual(exitCode, 42, `Expected exit code 42, got ${exitCode}`); - }); - - it('should run multiline scripts with imports', async () => { - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: [ - "import sys", - "import json", - "result = {'python': f'{sys.version_info.major}.{sys.version_info.minor}', 'platform': sys.platform}", - "print(json.dumps(result))", - ].join('\n'), - timeout: 30000, - }, - }; - - const { stdout, stderr, exitCode } = await runMicrovm(config); - const combined = stdout + stderr; - assert.strictEqual(exitCode, 0, `Expected exit code 0.\nstdout: ${stdout}\nstderr: ${stderr}`); - assert.ok(combined.includes('"platform": "nanvix"'), `Expected nanvix platform in output:\n${combined}`); - }); - - it('should support readwritePaths with transparent path translation', async () => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-microvm-e2e-')); - const rwDir = path.join(testDir, 'work'); - fs.mkdirSync(rwDir); - fs.writeFileSync(path.join(rwDir, 'input.txt'), 'data from host'); - - try { - // Use the host path directly in the script — the staging layer rewrites - // it to the guest mount path before the script reaches the VM. - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: [ - "import os", - `path = '${pyEscape(rwDir)}'`, - "print(f'Guest path: {path}')", - "with open(os.path.join(path, 'input.txt')) as f:", - " print(f'Read: {f.read().strip()}')", - ].join('\n'), - timeout: 30000, - }, - filesystem: { - readwritePaths: [rwDir], - }, - }; - - const { stdout, stderr, exitCode } = await runMicrovm(config); - const combined = stdout + stderr; - assert.strictEqual(exitCode, 0, `Expected exit code 0.\nstdout: ${stdout}\nstderr: ${stderr}`); - assert.ok(combined.includes('Guest path: /mnt/rw/'), `Expected guest path starting with /mnt/rw/ in output:\n${combined}`); - assert.ok(combined.includes('Read: data from host'), `Expected host data in output:\n${combined}`); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - }); - - it('should reject denied_paths with an error', async () => { - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: "print('should not run')", - timeout: 30000, - }, - filesystem: { - deniedPaths: ['/secret'], - }, - }; - - const { stdout, stderr, exitCode } = await runMicrovm(config); - const combined = stdout + stderr; - assert.notStrictEqual(exitCode, 0, `Expected non-zero exit code for denied_paths`); - assert.ok(combined.includes('denied_paths'), `Expected denied_paths error in output:\n${combined}`); - }); - - it('should copy readwritePaths changes back to the host on clean exit', async () => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-microvm-copyback-')); - const rwDir = path.join(testDir, 'work'); - fs.mkdirSync(rwDir); - fs.writeFileSync(path.join(rwDir, 'input.txt'), 'before'); - - try { - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: [ - "import os", - `path = '${pyEscape(rwDir)}'`, - "with open(os.path.join(path, 'input.txt'), 'w') as f:", - " f.write('after')", - "with open(os.path.join(path, 'created.txt'), 'w') as f:", - " f.write('created by guest')", - ].join('\n'), - timeout: 30000, - }, - filesystem: { - readwritePaths: [rwDir], - }, - }; - - const { stdout, stderr, exitCode } = await runMicrovm(config); - assert.strictEqual(exitCode, 0, `Expected exit code 0.\nstdout: ${stdout}\nstderr: ${stderr}`); - assert.strictEqual(fs.readFileSync(path.join(rwDir, 'input.txt'), 'utf8'), 'after'); - assert.strictEqual(fs.readFileSync(path.join(rwDir, 'created.txt'), 'utf8'), 'created by guest'); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - }); - - it('should copy readwritePaths changes back after a normal non-zero guest exit', async () => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-microvm-copyback-nonzero-')); - const rwDir = path.join(testDir, 'work'); - fs.mkdirSync(rwDir); - - try { - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: [ - "import os, sys", - `path = '${pyEscape(rwDir)}'`, - "with open(os.path.join(path, 'nonzero.txt'), 'w') as f:", - " f.write('persisted before non-zero exit')", - "sys.exit(7)", - ].join('\n'), - timeout: 30000, - }, - filesystem: { - readwritePaths: [rwDir], - }, - }; - - const { exitCode } = await runMicrovm(config); - assert.strictEqual(exitCode, 7, `Expected exit code 7, got ${exitCode}`); - assert.strictEqual( - fs.readFileSync(path.join(rwDir, 'nonzero.txt'), 'utf8'), - 'persisted before non-zero exit' - ); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - }); - - it('should generate a PPTX file in a readwritePath and copy it back to the host', async () => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-microvm-pptx-')); - const rwDir = path.join(testDir, 'output'); - fs.mkdirSync(rwDir); - - try { - const rwDirPy = pyEscape(rwDir); - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - // Generate a minimal valid PPTX using only stdlib (zipfile + xml). - // A PPTX is an Office Open XML package — a zip with specific XML parts. - commandLine: [ - "import zipfile, os", - `outdir = '${rwDirPy}'`, - "pptx_path = os.path.join(outdir, 'test.pptx')", - "ct = ''", - "rels = ''", - "pres = ''", - "with zipfile.ZipFile(pptx_path, 'w', zipfile.ZIP_DEFLATED) as z:", - " z.writestr('[Content_Types].xml', ct)", - " z.writestr('_rels/.rels', rels)", - " z.writestr('ppt/presentation.xml', pres)", - "print(f'PPTX size: {os.path.getsize(pptx_path)} bytes')", - ].join('\n'), - timeout: 30000, - }, - filesystem: { - readwritePaths: [rwDir], - }, - }; - - const { stdout, stderr, exitCode } = await runMicrovm(config); - const combined = stdout + stderr; - assert.strictEqual(exitCode, 0, `Expected exit code 0.\nstdout: ${stdout}\nstderr: ${stderr}`); - assert.ok(combined.includes('PPTX size:'), `Expected PPTX size in output:\n${combined}`); - - // Verify the PPTX was copied back to the host. - const pptxPath = path.join(rwDir, 'test.pptx'); - assert.ok(fs.existsSync(pptxPath), `Expected test.pptx at ${pptxPath}`); - const size = fs.statSync(pptxPath).size; - assert.ok(size > 0, `Expected non-empty PPTX, got ${size} bytes`); - - // Verify it's a valid zip (PPTX is zip-based). - const header = Buffer.alloc(4); - const fd = fs.openSync(pptxPath, 'r'); - fs.readSync(fd, header, 0, 4, 0); - fs.closeSync(fd); - assert.strictEqual(header[0], 0x50, 'Expected PK zip header byte 1'); - assert.strictEqual(header[1], 0x4B, 'Expected PK zip header byte 2'); - } finally { - fs.rmSync(testDir, { recursive: true, force: true }); - } - }); - - it('should generate a dark-themed 3-slide PPTX about MXC and NanVix using python-pptx', async () => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-microvm-pptx-dark-')); - const rwDir = path.join(testDir, 'output'); - fs.mkdirSync(rwDir); - - try { - const rwDirPy = pyEscape(rwDir); - const config = { - version: '0.6.0-alpha', - containment: 'microvm' as const, - process: { - commandLine: [ - // site-packages isn't on sys.path by default in this NanVix build - "import sys; sys.path.insert(0, '/sysroot/lib/python3.12/site-packages')", - "import os", - "from pptx import Presentation", - "from pptx.util import Inches, Pt, Emu", - "from pptx.dml.color import RGBColor", - "from pptx.enum.text import PP_ALIGN", - "", - "prs = Presentation()", - "prs.slide_width = Emu(12192000)", - "prs.slide_height = Emu(6858000)", - "", - "BG = RGBColor(0x1B, 0x1B, 0x2F)", - "WHITE = RGBColor(0xFF, 0xFF, 0xFF)", - "BLUE = RGBColor(0x56, 0x9C, 0xD6)", - "TEAL = RGBColor(0x4E, 0xC9, 0xB0)", - "GRAY = RGBColor(0xD4, 0xD4, 0xD4)", - "", - "def set_bg(slide):", - " bg = slide.background", - " fill = bg.fill", - " fill.solid()", - " fill.fore_color.rgb = BG", - "", - "def add_text(slide, left, top, width, height, text, font_size, color, bold=False, alignment=PP_ALIGN.LEFT):", - " txBox = slide.shapes.add_textbox(left, top, width, height)", - " tf = txBox.text_frame", - " tf.word_wrap = True", - " p = tf.paragraphs[0]", - " p.text = text", - " p.font.size = Pt(font_size)", - " p.font.color.rgb = color", - " p.font.bold = bold", - " p.alignment = alignment", - " return tf", - "", - "def add_para(tf, text, font_size, color, bold=False):", - " p = tf.add_paragraph()", - " p.text = text", - " p.font.size = Pt(font_size)", - " p.font.color.rgb = color", - " p.font.bold = bold", - " return tf", - "", - "# --- Slide 1: Title ---", - "blank = prs.slide_layouts[6]", - "s1 = prs.slides.add_slide(blank)", - "set_bg(s1)", - "add_text(s1, Inches(0.8), Inches(1.8), Inches(10), Inches(1.2),", - " '\\U0001f680 MXC \\u00d7 NanVix', 44, WHITE, bold=True, alignment=PP_ALIGN.CENTER)", - "tf1 = add_text(s1, Inches(0.8), Inches(3.2), Inches(10), Inches(1.5),", - " 'Sandboxed Code Execution with Micro-VM Isolation', 24, BLUE, alignment=PP_ALIGN.CENTER)", - "add_para(tf1, '', 12, WHITE)", - "add_para(tf1, '\\U0001f512 Secure \\u2022 \\u26a1 Fast \\u2022 \\U0001f30d Cross-Platform', 20, TEAL)", - "tf1.paragraphs[-1].alignment = PP_ALIGN.CENTER", - "", - "# --- Slide 2: Architecture ---", - "s2 = prs.slides.add_slide(blank)", - "set_bg(s2)", - "add_text(s2, Inches(0.8), Inches(0.4), Inches(10), Inches(0.8),", - " '\\U0001f527 How It Works', 32, WHITE, bold=True)", - "tf2 = add_text(s2, Inches(0.8), Inches(1.4), Inches(10), Inches(5),", - " '\\U0001f4e6 MXC (Microsoft eXecution Container)', 20, BLUE, bold=True)", - "add_para(tf2, 'Orchestrates sandboxed execution across backends', 16, GRAY)", - "add_para(tf2, '', 12, GRAY)", - "add_para(tf2, '\\U0001f5a5 NanVix Micro-VM Backend', 20, BLUE, bold=True)", - "add_para(tf2, 'Lightweight hypervisor isolation via WHP', 16, GRAY)", - "add_para(tf2, 'CPython 3.12 inside a minimal microkernel', 16, GRAY)", - "add_para(tf2, '', 12, GRAY)", - "add_para(tf2, '\\U0001f504 The Flow', 20, BLUE, bold=True)", - "add_para(tf2, 'SDK \\u2192 wxc-exec \\u2192 nanvixd \\u2192 kernel.elf \\u2192 Python \\U0001f40d', 16, TEAL)", - "", - "# --- Slide 3: What's Next ---", - "s3 = prs.slides.add_slide(blank)", - "set_bg(s3)", - "add_text(s3, Inches(0.8), Inches(0.4), Inches(10), Inches(0.8),", - " '\\u2728 What\\'s Next', 32, WHITE, bold=True)", - "tf3 = add_text(s3, Inches(0.8), Inches(1.4), Inches(10), Inches(5),", - " '\\u2705 Filesystem sharing via readwritePaths', 18, GRAY)", - "add_para(tf3, '\\u2705 Stdout/stderr streaming back to host', 18, GRAY)", - "add_para(tf3, '\\u2705 Exit code propagation', 18, GRAY)", - "add_para(tf3, '\\U0001f6a7 Network isolation & proxy support', 18, GRAY)", - "add_para(tf3, '\\U0001f6a7 Multi-language guest support', 18, GRAY)", - "add_para(tf3, '\\U0001f6a7 GPU passthrough for AI workloads', 18, GRAY)", - "add_para(tf3, '', 12, GRAY)", - "add_para(tf3, '\\U0001f4ac \"Run untrusted code safely, at VM speed\"', 20, TEAL, bold=True)", - "", - `pptx_path = os.path.join('${rwDirPy}', 'mxc-nanvix.pptx')`, - "prs.save(pptx_path)", - "size = os.path.getsize(pptx_path)", - "print(f'PPTX created: {size} bytes, 3 slides')", - "print(f'Output: {pptx_path}')", - ].join('\n'), - timeout: 60000, - }, - filesystem: { - readwritePaths: [rwDir], - }, - }; - - const { stdout, stderr, exitCode } = await runMicrovm(config); - const combined = stdout + stderr; - - assert.strictEqual(exitCode, 0, `Expected exit code 0.\nstdout: ${stdout}\nstderr: ${stderr}`); - assert.ok(combined.includes('PPTX created:'), `Expected creation message in output:\n${combined}`); - assert.ok(combined.includes('3 slides'), `Expected 3 slides in output:\n${combined}`); - - // Verify the PPTX was copied back to the host. - const pptxPath = path.join(rwDir, 'mxc-nanvix.pptx'); - assert.ok(fs.existsSync(pptxPath), `Expected mxc-nanvix.pptx at ${pptxPath}`); - const size = fs.statSync(pptxPath).size; - assert.ok(size > 10000, `Expected substantial PPTX from python-pptx, got ${size} bytes`); - - // Verify valid zip with PK header. - const header = Buffer.alloc(4); - const fd = fs.openSync(pptxPath, 'r'); - fs.readSync(fd, header, 0, 4, 0); - fs.closeSync(fd); - assert.strictEqual(header[0], 0x50, 'Expected PK zip header byte 1'); - assert.strictEqual(header[1], 0x4B, 'Expected PK zip header byte 2'); - - console.log(`Dark PPTX output: ${pptxPath} (${size} bytes)`); - } finally { - // Keep output for manual inspection — open in PowerPoint to verify. - console.log(`Dark PPTX test dir persisted at: ${testDir}`); - } - }); -}); diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 526cda2c0..14ae4f577 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -10,9 +10,25 @@ import { _setBwrapVersionRunner, _setLxcAvailabilityProbe, } from '../../src/platform.js'; -import { ContainerConfig, SandboxPolicy, SandboxingMethod } from '../../src/types.js'; +import { + ContainerConfig, + ContainmentTypes, + ExperimentalBackends, + SandboxPolicy, + SandboxingMethod, +} from '../../src/types.js'; import { platformSkip } from './test-helpers.js'; +describe('containment exports', () => { + it('exposes nvx and no longer exposes microvm', () => { + assert.deepStrictEqual(ContainmentTypes, ['process', 'vm']); + assert.deepStrictEqual( + ExperimentalBackends, + ['nvx', 'windows_sandbox', 'hyperlight', 'wslc', 'isolation_session'], + ); + }); +}); + describe('buildSandboxPayload', () => { const defaultPolicy: SandboxPolicy = { version: '0.6.0-alpha' }; @@ -317,50 +333,6 @@ describe('buildSandboxPayload', () => { } }; - it('should return minimal config for microvm without filesystem', () => { - mockWindows(); - try { - const payload = buildSandboxPayload('print(42)', defaultPolicy, undefined, undefined, 'microvm'); - assert.strictEqual(payload.containment, 'microvm'); - assert.strictEqual(payload.filesystem, undefined); - assert.strictEqual(payload.processContainer, undefined); - } finally { - restore(); - } - }); - - it('should map clearPolicyOnExit to lifecycle.preservePolicy for microvm when policy has paths', () => { - mockWindows(); - try { - const policy: SandboxPolicy = { - version: '0.6.0-alpha', - filesystem: { readwritePaths: ['/tmp'] }, - }; - const payload = buildSandboxPayload('print(42)', policy, undefined, undefined, 'microvm'); - assert.strictEqual(payload.containment, 'microvm'); - assert.deepStrictEqual(payload.filesystem!.readwritePaths, ['/tmp']); - // clearPolicyOnExit is not a wire `filesystem` field; the intent is - // carried canonically by lifecycle.preservePolicy (default clear => not preserved). - assert.strictEqual(payload.lifecycle!.preservePolicy, false); - } finally { - restore(); - } - }); - - it('should honor clearPolicyOnExit false for microvm (via lifecycle.preservePolicy)', () => { - mockWindows(); - try { - const policy: SandboxPolicy = { - version: '0.6.0-alpha', - filesystem: { readwritePaths: ['/tmp'], clearPolicyOnExit: false }, - }; - const payload = buildSandboxPayload('print(42)', policy, undefined, undefined, 'microvm'); - assert.strictEqual(payload.lifecycle!.preservePolicy, true); - } finally { - restore(); - } - }); - it('should build processcontainer config on Windows with default process containment', () => { mockWindows(); try { @@ -376,74 +348,6 @@ describe('buildSandboxPayload', () => { } }); - it('should reject network policies for microvm', () => { - mockWindows(); - try { - const policy: SandboxPolicy = { - version: '0.6.0-alpha', - network: { allowOutbound: true }, - }; - assert.throws( - () => buildSandboxPayload('print(42)', policy, undefined, undefined, 'microvm'), - { message: /does not support network configuration/ }, - ); - assert.throws( - () => buildSandboxPayload( - 'print(42)', - { - version: '0.8.0-alpha', - runtimeConfig: { networkProxy: 'http://127.0.0.1:8080' }, - }, - undefined, - undefined, - 'microvm', - ), - { message: /does not support network configuration/ }, - ); - } finally { - restore(); - } - }); - - it('should reject microvm on non-Windows platforms', () => { - const orig = Object.getOwnPropertyDescriptor(process, 'platform'); - Object.defineProperty(process, 'platform', { value: 'linux' }); - try { - assert.throws( - () => buildSandboxPayload('print(42)', defaultPolicy, undefined, undefined, 'microvm'), - { message: /only supported on Windows/ }, - ); - } finally { - if (orig) Object.defineProperty(process, 'platform', orig); - } - }); - - it('should preserve lifecycle config for microvm', () => { - mockWindows(); - try { - const policy: SandboxPolicy = { - version: '0.6.0-alpha', - filesystem: { clearPolicyOnExit: false }, - }; - const payload = buildSandboxPayload('print(42)', policy, undefined, undefined, 'microvm'); - assert.strictEqual(payload.lifecycle!.destroyOnExit, true); - assert.strictEqual(payload.lifecycle!.preservePolicy, true); - } finally { - restore(); - } - }); - - it('should set process commandLine and containerId for microvm', () => { - mockWindows(); - try { - const payload = buildSandboxPayload('print(42)', defaultPolicy, undefined, 'my-container', 'microvm'); - assert.strictEqual(payload.process!.commandLine, 'print(42)'); - assert.strictEqual(payload.containerId, 'my-container'); - } finally { - restore(); - } - }); - }); describe('WSLC', () => { @@ -1724,13 +1628,23 @@ describe('resolveExecutableAndArgs (containment validation)', { skip: platformSk ); }); - it('should accept the abstract intent "microvm" with experimental flag (Windows only)', function (this: { skip: (reason?: string) => void }) { - if (process.platform !== 'win32') { - this.skip('microvm is Windows-only'); + it('should reject removed microvm containment with NVX migration guidance', () => { + assert.throws( + () => resolveExecutableAndArgs(makeConfig('microvm'), { + executablePath: fakeExe, + experimental: true, + }), + { message: /microvm.*removed.*nvx/i }, + ); + }); + + it('should accept nvx with experimental mode on Windows x64', function (this: { skip: (reason?: string) => void }) { + if (process.platform !== 'win32' || process.arch !== 'x64') { + this.skip('nvx is Windows x64-only'); return; } assert.doesNotThrow(() => - resolveExecutableAndArgs(makeConfig('microvm'), { + resolveExecutableAndArgs(makeConfig('nvx'), { executablePath: fakeExe, experimental: true, }), diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index 153e3806d..1a8644aff 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -166,7 +166,7 @@ pub struct Process { /// Windows ProcessContainer picks the first `readwritePaths` entry that is /// an existing directory, else the first such `readonlyPaths` entry, else /// the system drive root; Seatbelt applies the same precedence with a `/` - /// fallback; LXC/WSL use the container root; NanVix and Hyperlight reject a + /// fallback; LXC/WSL use the container root; NVX and Hyperlight reject a /// working directory outright. See `docs/schema.md` ("Working Directory"). pub cwd: Option, /// Environment variables as `"KEY=VALUE"` strings. From 46d9b391a261f065ce529572936e443666e6e12d Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 11:30:58 -0700 Subject: [PATCH 20/41] test: remove NanVix validation coverage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/templates/Fuzz.Build.Job.yml | 2 +- .azure-pipelines/templates/Rust.Build.Job.yml | 2 +- .github/workflows/microvm-e2e.yml | 110 ------ .gitignore | 1 - scripts/ci/prepare-linux-host.sh | 7 +- scripts/ci/prepare-windows-host.ps1 | 23 -- scripts/ci/print-perf-summary.ps1 | 19 - scripts/ci/resolve-validation-test-matrix.mjs | 2 +- scripts/ci/run_backend_validation_tests.ps1 | 6 - scripts/ci/run_backend_validation_tests.sh | 8 +- scripts/ci/validation-test-matrix.json | 7 - .../base64_decode/microvm_hello.b64.txt | 1 - src/testing/wxc_e2e_tests/src/lib.rs | 53 +-- .../wxc_e2e_tests/tests/e2e_linux_microvm.rs | 318 --------------- .../wxc_e2e_tests/tests/e2e_windows.rs | 371 +----------------- tests/configs/microvm_error.json | 7 - tests/configs/microvm_error_linux.json | 7 - tests/configs/microvm_exit_code.json | 7 - tests/configs/microvm_exit_code_linux.json | 7 - tests/configs/microvm_hello.json | 7 - tests/configs/microvm_hello_linux.json | 7 - tests/configs/microvm_large_output.json | 7 - tests/configs/microvm_large_output_linux.json | 7 - tests/configs/microvm_multiline.json | 7 - tests/configs/microvm_multiline_linux.json | 7 - tests/configs/microvm_network.json | 10 - tests/configs/microvm_network_linux.json | 10 - tests/configs/microvm_stdlib.json | 7 - tests/configs/microvm_stdlib_linux.json | 7 - tests/configs/microvm_timeout.json | 8 - tests/configs/microvm_timeout_linux.json | 8 - tests/playground/package.json | 24 -- tests/playground/src/main/main.ts | 36 +- tests/playground/src/renderer/app.ts | 161 +------- tests/playground/src/renderer/index.html | 3 +- tests/scripts/README.md | 25 +- tests/scripts/run_microvm_basic_test.ps1 | 44 --- tests/scripts/run_microvm_tests.ps1 | 235 ----------- 38 files changed, 32 insertions(+), 1546 deletions(-) delete mode 100644 .github/workflows/microvm-e2e.yml delete mode 100644 scripts/ci/print-perf-summary.ps1 delete mode 100644 src/testing/fuzz/corpus/base64_decode/microvm_hello.b64.txt delete mode 100644 src/testing/wxc_e2e_tests/tests/e2e_linux_microvm.rs delete mode 100644 tests/configs/microvm_error.json delete mode 100644 tests/configs/microvm_error_linux.json delete mode 100644 tests/configs/microvm_exit_code.json delete mode 100644 tests/configs/microvm_exit_code_linux.json delete mode 100644 tests/configs/microvm_hello.json delete mode 100644 tests/configs/microvm_hello_linux.json delete mode 100644 tests/configs/microvm_large_output.json delete mode 100644 tests/configs/microvm_large_output_linux.json delete mode 100644 tests/configs/microvm_multiline.json delete mode 100644 tests/configs/microvm_multiline_linux.json delete mode 100644 tests/configs/microvm_network.json delete mode 100644 tests/configs/microvm_network_linux.json delete mode 100644 tests/configs/microvm_stdlib.json delete mode 100644 tests/configs/microvm_stdlib_linux.json delete mode 100644 tests/configs/microvm_timeout.json delete mode 100644 tests/configs/microvm_timeout_linux.json delete mode 100644 tests/scripts/run_microvm_basic_test.ps1 delete mode 100644 tests/scripts/run_microvm_tests.ps1 diff --git a/.azure-pipelines/templates/Fuzz.Build.Job.yml b/.azure-pipelines/templates/Fuzz.Build.Job.yml index 397bdde93..560b7ddd2 100644 --- a/.azure-pipelines/templates/Fuzz.Build.Job.yml +++ b/.azure-pipelines/templates/Fuzz.Build.Job.yml @@ -76,7 +76,7 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "fuzz build failed for $t" } } Write-Host "===== building validator =====" - cargo +$(rustNightly) fuzz build --fuzz-dir . validator --release --features hyperlight,isolation_session,microvm + cargo +$(rustNightly) fuzz build --fuzz-dir . validator --release --features hyperlight,isolation_session if ($LASTEXITCODE -ne 0) { throw "fuzz build failed for validator" } displayName: Build fuzz targets (ASAN) diff --git a/.azure-pipelines/templates/Rust.Build.Job.yml b/.azure-pipelines/templates/Rust.Build.Job.yml index 09bddfc0b..a32e17893 100644 --- a/.azure-pipelines/templates/Rust.Build.Job.yml +++ b/.azure-pipelines/templates/Rust.Build.Job.yml @@ -46,7 +46,7 @@ jobs: workingDirectory: $(Build.SourcesDirectory)/src # Explicit feature set (no --all-features): tier2_bfs must NOT ship # on Win 11 25H2 (bfscfg.exe risks an OS hang). - cargoFeatures: hyperlight isolation_session microvm wslc + cargoFeatures: hyperlight isolation_session wslc signPattern: | wxc-exec.exe plm.exe diff --git a/.github/workflows/microvm-e2e.yml b/.github/workflows/microvm-e2e.yml deleted file mode 100644 index 3f602116c..000000000 --- a/.github/workflows/microvm-e2e.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Integration Tests - -# Retained until the unified test matrix has equivalent MicroVM coverage. - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - actions: write - contents: read - -jobs: - microvm-e2e: - name: WXC-Exec MicroVM - runs-on: windows-latest - timeout-minutes: 30 - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Setup Rust toolchain - run: | - rustup update stable - rustup target add x86_64-pc-windows-msvc - - - name: Point cargo at the MxcDependencies feed - uses: ./.github/actions/setup-cargo-feed - - - name: Cache Rust build artifacts - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - with: - workspaces: src - - - name: Build with MicroVM support - working-directory: src - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: cargo build --features microvm --target x86_64-pc-windows-msvc - - - name: Exclude build output from Windows Defender - shell: pwsh - run: | - $binDir = Join-Path $env:GITHUB_WORKSPACE "src\target\x86_64-pc-windows-msvc\debug" - Add-MpPreference -ExclusionPath $binDir - Write-Host "Added Defender exclusion for $binDir" - - - name: Verify MicroVM binaries - shell: pwsh - run: | - $binDir = Join-Path $env:GITHUB_WORKSPACE "src\target\x86_64-pc-windows-msvc\debug" - $required = @( - "wxc-exec.exe", - "nanvixd.exe", - "nanvix_rootfs.img", - "python3.initrd", - "bin\kernel.elf", - "snapshots\kernel.vmem", - "snapshots\kernel.whp.cbor" - ) - $missing = $required | Where-Object { -not (Test-Path (Join-Path $binDir $_)) } - if ($missing) { - Write-Host "::error::Missing binaries: $($missing -join ', ')" - exit 1 - } - $leaves = $required | ForEach-Object { Split-Path $_ -Leaf } - Get-ChildItem $binDir -Include $leaves -Recurse | Format-Table FullName, Length - - - name: Diagnose hypervisor environment - shell: pwsh - run: ./scripts/ci/diagnose-whp.ps1 - - - name: Check Windows Hypervisor Platform - id: whp-check - shell: pwsh - run: ./scripts/ci/check-whp.ps1 - - - name: Run MicroVM E2E Tests - if: steps.whp-check.outputs.whp_available == 'true' - shell: pwsh - working-directory: src - run: | - cargo test -p wxc_e2e_tests --target x86_64-pc-windows-msvc test_microvm_suite -- --nocapture - - - name: Upload logs on failure - if: failure() || cancelled() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: microvm-e2e-logs-${{ github.event.pull_request.number || github.run_number }} - retention-days: 7 - path: | - logs/ - **/*.log - - - name: Print performance summary - if: steps.whp-check.outputs.whp_available == 'true' && !cancelled() - shell: pwsh - run: ./scripts/ci/print-perf-summary.ps1 -JsonPath (Join-Path $env:GITHUB_WORKSPACE "microvm-perf-results.json") - - - name: Upload performance results - if: steps.whp-check.outputs.whp_available == 'true' && !cancelled() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: microvm-perf-results-${{ github.event.pull_request.number || github.run_number }} - retention-days: 30 - path: microvm-perf-results.json diff --git a/.gitignore b/.gitignore index 2feaea311..6f63c4527 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ bld/ # Test artifacts *.etl *.log -microvm-perf-results*.json # npm pack artifacts *.tgz diff --git a/scripts/ci/prepare-linux-host.sh b/scripts/ci/prepare-linux-host.sh index 2f91bd80f..c57f956ce 100644 --- a/scripts/ci/prepare-linux-host.sh +++ b/scripts/ci/prepare-linux-host.sh @@ -6,7 +6,7 @@ set -euo pipefail # matrix entry works on Ubuntu, Debian, and RHEL images. usage() { - echo "Usage: $0 " >&2 + echo "Usage: $0 " >&2 } if [[ $# -ne 2 ]]; then @@ -336,11 +336,6 @@ case "$backend" in enable_bridge_netfilter ensure_bridge_nat ;; - microvm) - for file in nanvixd.elf nanvix_rootfs.img python3.initrd bin/kernel.elf; do - test -f "$binary_directory/$file" - done - ;; hyperlight) echo "Hyperlight has no artifact-only Linux test prerequisites yet." ;; diff --git a/scripts/ci/prepare-windows-host.ps1 b/scripts/ci/prepare-windows-host.ps1 index 491e861f4..2c8a2d5f1 100644 --- a/scripts/ci/prepare-windows-host.ps1 +++ b/scripts/ci/prepare-windows-host.ps1 @@ -23,7 +23,6 @@ param( 'isolation-session', 'wslc', 'windows-sandbox', - 'microvm', 'hyperlight' )] [string]$Backend, @@ -419,27 +418,6 @@ function Install-PackagedTooling { $global:LASTEXITCODE = 0 } -function Initialize-MicroVmHost { - # Staged next to wxc-exec.exe by the --features microvm build, so their - # absence means a broken artifact rather than a host problem. Snapshots are - # excluded: they are a warm-start cache the runner regenerates on demand. - Assert-RequiredFile @( - 'wxc-exec.exe', - 'nanvixd.exe', - 'nanvix_rootfs.img', - 'python3.initrd', - 'bin\kernel.elf' - ) - - # NanVix boots a VM from these images on every invocation; Defender scanning - # them can push boot past its timeout. - Add-MpPreference -ExclusionPath $BinaryDirectory - Write-Host "Added Defender exclusion for $BinaryDirectory" - - Write-HypervisorDiagnostic - Assert-HypervisorPlatform -} - # The optional features must be baked into the pool image (enabling one needs a # reboot this job cannot take), but the WSL runtime package is installed here if # missing. Container images are pulled by the suite itself @@ -595,7 +573,6 @@ Assert-WorkloadInterpreters switch ($Backend) { 'process-t3' { Initialize-ProcessContainerHost } - 'microvm' { Initialize-MicroVmHost } 'wslc' { Initialize-WslcHost } default { Write-Host "$Backend has no artifact-only Windows test prerequisites yet." } } diff --git a/scripts/ci/print-perf-summary.ps1 b/scripts/ci/print-perf-summary.ps1 deleted file mode 100644 index eb5ec52c6..000000000 --- a/scripts/ci/print-perf-summary.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string]$JsonPath -) - -if (Test-Path $JsonPath) { - $data = Get-Content $JsonPath -Raw | ConvertFrom-Json - Write-Host "`n=== MicroVM Performance Summary ===" - Write-Host "Commit: $($data.commit)" - Write-Host "Timestamp: $($data.timestamp)" - Write-Host "" - Write-Host ("{0,-35} {1,10} {2,8}" -f "Test", "Time (ms)", "Status") - Write-Host ("{0,-35} {1,10} {2,8}" -f "----", "---------", "------") - foreach ($r in $data.results) { - Write-Host ("{0,-35} {1,10} {2,8}" -f $r.description, $r.wall_time_ms, $r.status) - } -} else { - Write-Host "::warning::No performance results found — perf JSON was not generated." -} diff --git a/scripts/ci/resolve-validation-test-matrix.mjs b/scripts/ci/resolve-validation-test-matrix.mjs index 565fcec1d..0b16b0ca8 100644 --- a/scripts/ci/resolve-validation-test-matrix.mjs +++ b/scripts/ci/resolve-validation-test-matrix.mjs @@ -10,7 +10,7 @@ import process from 'node:process'; import { fileURLToPath } from 'node:url'; const FAMILIES = ['windows', 'linux', 'macos']; -const ARM64_UNSUPPORTED_BACKENDS = new Set(['hyperlight', 'microvm']); +const ARM64_UNSUPPORTED_BACKENDS = new Set(['hyperlight']); function assertNonEmptyString(value, label) { if (typeof value !== 'string' || value.trim() === '') { diff --git a/scripts/ci/run_backend_validation_tests.ps1 b/scripts/ci/run_backend_validation_tests.ps1 index c3e33b980..d542f758f 100644 --- a/scripts/ci/run_backend_validation_tests.ps1 +++ b/scripts/ci/run_backend_validation_tests.ps1 @@ -15,7 +15,6 @@ param( 'isolation-session', 'windows-sandbox', 'wslc', - 'microvm', 'hyperlight' )] [string]$Backend, @@ -188,11 +187,6 @@ switch ($Backend) { WxcExecPath = $wxc } } - 'microvm' { - Invoke-TestScript -Path (Join-Path $testScriptRoot 'run_microvm_tests.ps1') -Arguments @{ - BinDir = $binaryDirectoryPath - } - } 'hyperlight' { # Keep unwired backends explicit so accidental activation fails loudly. throw 'The Hyperlight CI backend is not wired to an existing test entry point yet.' diff --git a/scripts/ci/run_backend_validation_tests.sh b/scripts/ci/run_backend_validation_tests.sh index a82734198..d7a394164 100644 --- a/scripts/ci/run_backend_validation_tests.sh +++ b/scripts/ci/run_backend_validation_tests.sh @@ -6,7 +6,7 @@ set -euo pipefail # explicitly rather than reporting a false-success placeholder job. usage() { - echo "Usage: $0 " >&2 + echo "Usage: $0 " >&2 } if [[ $# -ne 2 ]]; then @@ -22,12 +22,6 @@ test_script_root="$repo_root/tests/scripts" release_directory="$repo_root/src/target/release" case "$backend" in - microvm) - # Keep unwired commands explicit so accidental activation fails loudly. - # Future test script: run_microvm_tests.sh - echo "The MicroVM CI backend is not wired to an artifact-only Linux test entry point yet." >&2 - exit 2 - ;; hyperlight) # Keep unwired commands explicit so accidental activation fails loudly. # Future test script: run_hyperlight_tests.sh diff --git a/scripts/ci/validation-test-matrix.json b/scripts/ci/validation-test-matrix.json index d2f00a454..953fa0bb3 100644 --- a/scripts/ci/validation-test-matrix.json +++ b/scripts/ci/validation-test-matrix.json @@ -17,7 +17,6 @@ "isolation-session", "wslc", "windows-sandbox", - "microvm", "hyperlight" ] }, @@ -51,7 +50,6 @@ "isolation-session", "wslc", "windows-sandbox", - "microvm", "hyperlight" ] }, @@ -82,7 +80,6 @@ "process-t3", "wslc", "windows-sandbox", - "microvm", "hyperlight" ] }, @@ -111,7 +108,6 @@ "process-t3", "wslc", "windows-sandbox", - "microvm", "hyperlight" ] }, @@ -140,7 +136,6 @@ "process-t3", "wslc", "windows-sandbox", - "microvm", "hyperlight" ] }, @@ -169,7 +164,6 @@ "process-t3", "wslc", "windows-sandbox", - "microvm", "hyperlight" ] }, @@ -222,7 +216,6 @@ "pool": "1es-mxc-e2e-ubuntu-24.04-x64", "backends": [ "bubblewrap", - "microvm", "hyperlight", "lxc" ] diff --git a/src/testing/fuzz/corpus/base64_decode/microvm_hello.b64.txt b/src/testing/fuzz/corpus/base64_decode/microvm_hello.b64.txt deleted file mode 100644 index ec4a40528..000000000 --- a/src/testing/fuzz/corpus/base64_decode/microvm_hello.b64.txt +++ /dev/null @@ -1 +0,0 @@ -ew0KICAgICJwcm9jZXNzIjogew0KICAgICAgICAiY29tbWFuZExpbmUiOiAieCA9IDQyXG55ID0gNThcbnByaW50KCdIZWxsbyBmcm9tIE1pY3JvVk0hIHN1bT0lZCcgJSAoeCArIHkpKSIsDQogICAgICAgICJ0aW1lb3V0IjogMzAwMDANCiAgICB9LA0KICAgICJjb250YWlubWVudCI6ICJtaWNyb3ZtIg0KfQ0K \ No newline at end of file diff --git a/src/testing/wxc_e2e_tests/src/lib.rs b/src/testing/wxc_e2e_tests/src/lib.rs index cea6f4fb3..3bc197a28 100644 --- a/src/testing/wxc_e2e_tests/src/lib.rs +++ b/src/testing/wxc_e2e_tests/src/lib.rs @@ -158,27 +158,6 @@ pub fn has_daemon() -> bool { } } -/// Return whether the NanVix runtime binaries are available next to wxc-exec. -pub fn has_nanvix_binaries() -> bool { - let Some(exe) = find_binary("wxc-exec.exe") else { - return false; - }; - let exe_dir = exe.parent().unwrap_or(Path::new(".")); - // Flat binaries staged next to wxc-exec.exe by `nanvix_binaries`. - let flat_present = ["nanvixd.exe", "nanvix_rootfs.img", "python3.initrd"] - .iter() - .all(|name| exe_dir.join(name).exists()); - // Kernel binary now lives under `bin/` (nanvixd locates it via -bin-dir). - let bin_present = ["kernel.elf"] - .iter() - .all(|name| exe_dir.join("bin").join(name).exists()); - let present = flat_present && bin_present; - if !present { - println!("SKIPPED: NanVix binaries not found next to wxc-exec.exe"); - } - present -} - /// Return whether `lxc-exec` is available for direct E2E execution. pub fn has_lxc_exe() -> bool { match find_binary("lxc-exec") { @@ -187,42 +166,12 @@ pub fn has_lxc_exe() -> bool { true } None => { - println!("SKIPPED: lxc-exec not found — build with `cargo build -p lxc --features microvm` first"); + println!("SKIPPED: lxc-exec not found — build with `cargo build -p lxc` first"); false } } } -/// Return whether the NanVix runtime binaries are available next to lxc-exec (Linux). -pub fn has_lxc_nanvix_binaries() -> bool { - let Some(exe) = find_binary("lxc-exec") else { - return false; - }; - let exe_dir = exe.parent().unwrap_or(Path::new(".")); - // Flat binaries staged next to lxc-exec by `nanvix_binaries`. - let flat_present = ["nanvixd.elf", "nanvix_rootfs.img", "python3.initrd"] - .iter() - .all(|name| exe_dir.join(name).exists()); - // Kernel binary under `bin/` (nanvixd locates it relative to cwd). - let bin_present = ["kernel.elf"] - .iter() - .all(|name| exe_dir.join("bin").join(name).exists()); - let present = flat_present && bin_present; - if !present { - println!("SKIPPED: NanVix binaries not found next to lxc-exec — build with `cargo build -p lxc --features microvm`"); - } - present -} - -/// Return whether `/dev/kvm` is available for KVM-based execution. -pub fn has_kvm() -> bool { - let available = Path::new("/dev/kvm").exists(); - if !available { - println!("SKIPPED: /dev/kvm not available — KVM required for NanVix on Linux"); - } - available -} - /// Run `lxc-exec` with the supplied config file and extra arguments. pub fn run_lxc_config(config_file: &str, extra_args: &[&str]) -> CommandResult { let exe = find_binary("lxc-exec").expect("lxc-exec should be available"); diff --git a/src/testing/wxc_e2e_tests/tests/e2e_linux_microvm.rs b/src/testing/wxc_e2e_tests/tests/e2e_linux_microvm.rs deleted file mode 100644 index ac67c9aa3..000000000 --- a/src/testing/wxc_e2e_tests/tests/e2e_linux_microvm.rs +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Linux MicroVM (NanVix/KVM) E2E integration tests. -//! -//! These tests mirror the Windows MicroVM E2E suite in `e2e_windows.rs` and -//! invoke `lxc-exec` directly with the `microvm` containment backend. -//! Tests skip gracefully when prerequisites (binaries, KVM) are missing. - -use std::sync::OnceLock; -use std::time::{SystemTime, UNIX_EPOCH}; - -use serde::Serialize; -use wxc_e2e_tests::{ - has_kvm, has_lxc_exe, has_lxc_nanvix_binaries, repo_root, run_lxc_config, test_configs_dir, - CommandResult, -}; - -static HAS_LXC_EXE: OnceLock = OnceLock::new(); -static HAS_LXC_NANVIX: OnceLock = OnceLock::new(); -static HAS_KVM: OnceLock = OnceLock::new(); - -fn cached_has_lxc_exe() -> bool { - *HAS_LXC_EXE.get_or_init(has_lxc_exe) -} - -fn cached_has_lxc_nanvix() -> bool { - *HAS_LXC_NANVIX.get_or_init(has_lxc_nanvix_binaries) -} - -fn cached_has_kvm() -> bool { - *HAS_KVM.get_or_init(has_kvm) -} - -/// Guard: skip test if prerequisites are missing. -fn skip_unless_ready() -> bool { - if !cached_has_lxc_exe() { - return false; - } - if !cached_has_lxc_nanvix() { - return false; - } - if !cached_has_kvm() { - return false; - } - true -} - -// --------------------------------------------------------------------------- -// Individual tests (mirrors microvm_basic on Windows) -// --------------------------------------------------------------------------- - -#[test] -fn test_microvm_hello() { - if !skip_unless_ready() { - return; - } - let result = run_lxc_config("microvm_hello_linux.json", &["--debug", "--experimental"]); - assert_eq!( - result.code, - Some(0), - "expected exit 0, got {:?}\nstdout: {}\nstderr: {}", - result.code, - result.stdout, - result.stderr - ); - let combined = format!("{}\n{}", result.stdout, result.stderr); - assert!( - combined.contains("sum=100"), - "output missing 'sum=100'\ncombined: {}", - combined - ); -} - -#[test] -fn test_microvm_network() { - if !skip_unless_ready() { - return; - } - // Exercises the `-allow-host-networking` path on Linux/KVM: the guest runs - // a loopback TCP ping/pong round-trip and prints NET_OK. Without host - // networking the guest socket() fails (errno 134), so a clean exit plus - // the marker confirms the flag is wired through for the cold-boot path. - let result = run_lxc_config("microvm_network_linux.json", &["--debug", "--experimental"]); - assert_eq!( - result.code, - Some(0), - "expected exit 0, got {:?}\nstdout: {}\nstderr: {}", - result.code, - result.stdout, - result.stderr - ); - assert!( - result - .combined_output_with_decoded_base64() - .contains("NET_OK"), - "guest network round-trip marker missing\nstdout: {}\nstderr: {}", - result.stdout, - result.stderr - ); -} - -// --------------------------------------------------------------------------- -// Full microvm suite (mirrors test_microvm_suite on Windows) -// --------------------------------------------------------------------------- - -#[derive(Debug)] -struct MicrovmCase { - config: &'static str, - expected_exit: Option, - description: &'static str, - output_contains: Option<&'static str>, - expect_non_zero: bool, -} - -#[derive(Debug, Serialize)] -struct MicrovmPerfOutput { - commit: String, - timestamp: String, - results: Vec, -} - -#[derive(Debug, Serialize)] -struct MicrovmPerfEntry { - test: String, - description: String, - wall_time_ms: u128, - exit_code: Option, - status: String, -} - -#[test] -fn test_microvm_suite() { - if !skip_unless_ready() { - return; - } - microvm_suite(); -} - -fn microvm_suite() { - let cases = [ - MicrovmCase { - config: "microvm_hello_linux.json", - expected_exit: Some(0), - description: "Hello world", - output_contains: Some("sum=100"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_exit_code_linux.json", - expected_exit: Some(42), - description: "Exit code propagation", - output_contains: None, - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_multiline_linux.json", - expected_exit: Some(0), - description: "Multi-line script (fibonacci)", - output_contains: Some("fib("), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_stdlib_linux.json", - expected_exit: Some(0), - description: "Stdlib (json, math, hashlib)", - output_contains: Some("pi"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_large_output_linux.json", - expected_exit: Some(0), - description: "Large stdout (1000 lines)", - output_contains: Some("line 999"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_error_linux.json", - expected_exit: Some(1), - description: "Python exception", - output_contains: Some("ValueError"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_timeout_linux.json", - expected_exit: None, - description: "Timeout kills VM", - output_contains: None, - expect_non_zero: true, - }, - ]; - - let mut perf_entries = Vec::new(); - let mut failures = Vec::new(); - - for case in cases { - let config_path = test_configs_dir().join(case.config); - if !config_path.exists() { - println!("SKIPPED: config not found: {}", config_path.display()); - continue; - } - - println!("--- {} ({}) ---", case.description, case.config); - let result = run_lxc_config(case.config, &["--debug", "--experimental"]); - let status = if command_matches(&result, &case) { - "PASS" - } else { - failures.push(format!( - "{} expected {}, got {:?}", - case.config, - expected_exit_description(&case), - result.code - )); - "FAIL" - }; - - perf_entries.push(MicrovmPerfEntry { - test: case.config.to_string(), - description: case.description.to_string(), - wall_time_ms: result.wall_time_ms, - exit_code: result.code, - status: status.to_string(), - }); - - if status == "FAIL" { - println!( - "--- stdout ---\n{}\n--- stderr ---\n{}", - result.stdout, result.stderr - ); - } else { - println!(" PASS ({} ms)", result.wall_time_ms); - } - } - - write_microvm_perf_results(perf_entries); - - if !failures.is_empty() { - panic!("MicroVM Linux E2E failures:\n{}", failures.join("\n")); - } -} - -fn command_matches(result: &CommandResult, case: &MicrovmCase) -> bool { - if case.expect_non_zero { - if result.code == Some(0) { - return false; - } - } else if result.code != case.expected_exit { - return false; - } - - let Some(expected) = case.output_contains else { - return true; - }; - - result - .combined_output_with_decoded_base64() - .contains(expected) -} - -fn expected_exit_description(case: &MicrovmCase) -> String { - if case.expect_non_zero { - "non-zero exit".to_string() - } else { - format!("exit {}", case.expected_exit.unwrap_or(0)) - } -} - -// --------------------------------------------------------------------------- -// Perf results output -// --------------------------------------------------------------------------- - -fn write_microvm_perf_results(results: Vec) { - let output = MicrovmPerfOutput { - commit: std::env::var("GITHUB_SHA").unwrap_or_else(|_| "local".to_string()), - timestamp: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_secs().to_string()) - .unwrap_or_else(|_| "unknown".to_string()), - results, - }; - let json = serde_json::to_string_pretty(&output) - .expect("microvm performance results should serialize"); - let path = repo_root().join("microvm-perf-results-linux.json"); - std::fs::write(&path, json) - .unwrap_or_else(|error| panic!("failed to write {}: {error}", path.display())); - println!("Performance results written to {}", path.display()); -} - -// --------------------------------------------------------------------------- -// Stress tests (run_on_repeat — ignored by default) -// --------------------------------------------------------------------------- - -#[test] -#[ignore] -fn test_microvm_run_on_repeat() { - if !skip_unless_ready() { - return; - } - - const ITERATIONS: u32 = 10; - let mut failures = Vec::new(); - - for i in 0..ITERATIONS { - let result = run_lxc_config("microvm_hello_linux.json", &["--experimental"]); - if result.code != Some(0) { - failures.push(format!("iteration {}: exit {:?}", i, result.code)); - } - } - - if !failures.is_empty() { - panic!( - "MicroVM repeat test: {}/{} failures:\n{}", - failures.len(), - ITERATIONS, - failures.join("\n") - ); - } -} diff --git a/src/testing/wxc_e2e_tests/tests/e2e_windows.rs b/src/testing/wxc_e2e_tests/tests/e2e_windows.rs index b4dfd249a..51f962105 100644 --- a/src/testing/wxc_e2e_tests/tests/e2e_windows.rs +++ b/src/testing/wxc_e2e_tests/tests/e2e_windows.rs @@ -10,18 +10,15 @@ use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; -use serde::Serialize; use wxc_e2e_tests::{ assert_exit, assert_pwsh, assert_python, assert_success, assert_success_or_skip_missing_prerequisite, examples_dir, has_hyperlight_snapshot, - has_nanvix_binaries, has_test_driver, has_windows_sandbox_feature, has_wxc_exe, repo_root, - run_test_driver, run_wxc_config, run_wxc_config_value, run_wxc_example, run_wxc_state_aware, - test_configs_dir, TempDirs, + has_test_driver, has_windows_sandbox_feature, has_wxc_exe, run_test_driver, run_wxc_config, + run_wxc_config_value, run_wxc_example, run_wxc_state_aware, test_configs_dir, TempDirs, }; static HAS_WXC_EXE: OnceLock = OnceLock::new(); static HAS_TEST_DRIVER: OnceLock = OnceLock::new(); -static HAS_NANVIX_BINARIES: OnceLock = OnceLock::new(); static HAS_WINDOWS_SANDBOX: OnceLock = OnceLock::new(); static HAS_HYPERLIGHT: OnceLock = OnceLock::new(); static TEST_LOCK: OnceLock> = OnceLock::new(); @@ -37,11 +34,6 @@ fn cached_has_test_driver() -> bool { *HAS_TEST_DRIVER.get_or_init(has_test_driver) } -/// Caches the NanVix binary probe to avoid repeated prerequisite work. -fn cached_has_nanvix_binaries() -> bool { - *HAS_NANVIX_BINARIES.get_or_init(has_nanvix_binaries) -} - /// Caches the Windows Sandbox feature probe. fn cached_has_windows_sandbox_feature() -> bool { *HAS_WINDOWS_SANDBOX.get_or_init(has_windows_sandbox_feature) @@ -122,145 +114,6 @@ fn examples() { assert_success(&result); } -fn microvm_basic() { - assert_wxc_success("microvm_hello.json", &["--debug", "--experimental"]); -} - -fn microvm_network() { - // Drives the `-allow-host-networking` path: the guest opens a loopback TCP - // socket, completes a ping/pong round-trip, and prints NET_OK. Without host - // networking enabled the guest's socket() call fails (errno 134) and the - // process exits non-zero, so a clean exit + marker proves the flag wiring. - let result = run_wxc_config("microvm_network.json", &["--debug", "--experimental"]); - assert_eq!( - result.code, - Some(0), - "expected exit 0, got {:?}\nstdout: {}\nstderr: {}", - result.code, - result.stdout, - result.stderr - ); - let combined = result.combined_output_with_decoded_base64(); - assert!( - combined.contains("NET_OK"), - "guest network round-trip marker missing\ncombined: {}", - combined - ); -} - -/// Discovers a non-loopback IPv4 address of the host. nanvixd proxies guest -/// `connect()` calls through the host network stack, so a host-routable IP -/// gives a deterministic, internet-independent target for egress-filter tests. -fn host_lan_ipv4() -> Option { - let sock = std::net::UdpSocket::bind("0.0.0.0:0").ok()?; - // Connecting a UDP socket sends no packets; it just selects the outbound - // interface so `local_addr` reports this host's routable address. - sock.connect("10.255.255.255:9").ok()?; - match sock.local_addr().ok()?.ip() { - std::net::IpAddr::V4(v4) if !v4.is_loopback() && !v4.is_unspecified() => Some(v4), - _ => None, - } -} - -/// Returns a TCP port that is very likely closed: bind an ephemeral port, read -/// it, then drop the listener. A guest connecting there while egress is -/// permitted gets `ECONNREFUSED`, distinguishing "allowed but unreachable" -/// from "blocked by the filter" (`EACCES`). -fn likely_closed_tcp_port() -> u16 { - let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral port"); - let port = listener.local_addr().expect("read ephemeral port").port(); - drop(listener); - port -} - -/// Builds the guest Python that connects to `host:port` and prints the result -/// as `RESULT CONNECT_OK` or `RESULT ERRNO `. A blocked egress surfaces as -/// `PermissionError`/`EACCES` (errno 13); an allowed-but-closed target as -/// `ECONNREFUSED` (errno 111). -fn egress_probe_source(host: std::net::Ipv4Addr, port: u16) -> String { - format!( - "import _socket\n\ - s = _socket.socket(2, 1, 0)\n\ - try:\n\ - \x20 s.connect(('{host}', {port}))\n\ - \x20 print('RESULT CONNECT_OK', flush=True)\n\ - except OSError as e:\n\ - \x20 print('RESULT ERRNO %d' % (e.errno,), flush=True)\n", - host = host, - port = port - ) -} - -/// Negative egress test: a blocklisted host must be rejected by the guest -/// filter (Part B `-block-host`). Drives the host-networking path, asks the -/// guest to connect to a blocked host IP, and asserts the connection is denied -/// with `EACCES` (errno 13). A positive control connects to the same target -/// under an allow-all policy and asserts it is *not* denied, proving the -/// `EACCES` in the negative case comes from the filter rather than the network. -fn microvm_network_blocked() { - let Some(host_ip) = host_lan_ipv4() else { - println!("SKIPPED: microvm_network_blocked needs a non-loopback host IPv4"); - return; - }; - let port = likely_closed_tcp_port(); - let source = egress_probe_source(host_ip, port); - - // Negative case: host is on the blocklist -> guest egress denied (EACCES). - let blocked = serde_json::json!({ - "process": { "commandLine": source, "timeout": 30000 }, - "containment": "microvm", - "network": { "blockedHosts": [host_ip.to_string()] } - }); - let blocked_result = run_wxc_config_value( - "microvm_network_blocked", - &blocked, - &["--debug", "--experimental"], - ); - let blocked_out = blocked_result.combined_output_with_decoded_base64(); - assert_eq!( - blocked_result.code, - Some(0), - "blocked-egress run should exit cleanly (the guest catches the error)\ncombined: {}", - blocked_out - ); - assert!( - blocked_out.contains("RESULT ERRNO 13"), - "blocklisted host should be denied with EACCES (errno 13)\ncombined: {}", - blocked_out - ); - - // Positive control: allow-all policy -> egress permitted, so the same - // closed target yields a connection error other than EACCES (typically - // ECONNREFUSED / errno 111), never the filter's EACCES. - let allowed = serde_json::json!({ - "process": { "commandLine": source, "timeout": 30000 }, - "containment": "microvm", - "network": { "defaultPolicy": "allow" } - }); - let allowed_result = run_wxc_config_value( - "microvm_network_allowed_control", - &allowed, - &["--debug", "--experimental"], - ); - let allowed_out = allowed_result.combined_output_with_decoded_base64(); - assert_eq!( - allowed_result.code, - Some(0), - "allow-all control run should exit cleanly\ncombined: {}", - allowed_out - ); - assert!( - allowed_out.contains("RESULT "), - "allow-all control should produce a connect result\ncombined: {}", - allowed_out - ); - assert!( - !allowed_out.contains("RESULT ERRNO 13"), - "allow-all egress must not be denied with EACCES (proves filter, not network, blocks)\ncombined: {}", - allowed_out - ); -} - fn processcontainer_proxy() { let config = test_configs_dir().join("proxy_builtin_test.json"); if !config.exists() { @@ -591,39 +444,6 @@ fn test_examples() { with_test_lock(examples); } -#[test] -fn test_microvm_basic() { - if !cached_has_wxc_exe() { - return; - } - if !cached_has_nanvix_binaries() { - return; - } - with_test_lock(microvm_basic); -} - -#[test] -fn test_microvm_network() { - if !cached_has_wxc_exe() { - return; - } - if !cached_has_nanvix_binaries() { - return; - } - with_test_lock(microvm_network); -} - -#[test] -fn test_microvm_network_blocked() { - if !cached_has_wxc_exe() { - return; - } - if !cached_has_nanvix_binaries() { - return; - } - with_test_lock(microvm_network_blocked); -} - #[test] fn test_windows_sandbox() { if !cached_has_wxc_exe() { @@ -636,17 +456,6 @@ fn test_windows_sandbox() { with_test_lock(windows_sandbox_suite); } -#[test] -fn test_microvm_suite() { - if !cached_has_wxc_exe() { - return; - } - if !cached_has_nanvix_binaries() { - return; - } - with_test_lock(microvm_suite); -} - #[test] #[ignore] // Requires velocity key 61714527 (BFS deadlock fix) enabled and elevation fn test_processcontainer_proxy() { @@ -802,161 +611,6 @@ fn run_sandbox_case(case: &SandboxCase) { ); } -// --------------------------------------------------------------------------- -// MicroVM suite -// --------------------------------------------------------------------------- - -#[derive(Debug)] -struct MicrovmCase { - config: &'static str, - expected_exit: Option, - description: &'static str, - output_contains: Option<&'static str>, - expect_non_zero: bool, -} - -#[derive(Debug, Serialize)] -struct MicrovmPerfOutput { - commit: String, - timestamp: String, - results: Vec, -} - -#[derive(Debug, Serialize)] -struct MicrovmPerfEntry { - test: String, - description: String, - wall_time_ms: u128, - exit_code: Option, - status: String, -} - -fn microvm_suite() { - let cases = [ - MicrovmCase { - config: "microvm_hello.json", - expected_exit: Some(0), - description: "Hello world", - output_contains: Some("sum=100"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_exit_code.json", - expected_exit: Some(42), - description: "Exit code propagation", - output_contains: None, - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_multiline.json", - expected_exit: Some(0), - description: "Multi-line script (fibonacci)", - output_contains: Some("fib("), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_stdlib.json", - expected_exit: Some(0), - description: "Stdlib (json, math, hashlib)", - output_contains: Some("pi"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_large_output.json", - expected_exit: Some(0), - description: "Large stdout (1000 lines)", - output_contains: Some("line 999"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_error.json", - expected_exit: Some(1), - description: "Python exception", - output_contains: Some("ValueError"), - expect_non_zero: false, - }, - MicrovmCase { - config: "microvm_timeout.json", - expected_exit: None, - description: "Timeout kills VM", - output_contains: None, - expect_non_zero: true, - }, - ]; - - let mut perf_entries = Vec::new(); - let mut failures = Vec::new(); - - for case in cases { - let config_path = test_configs_dir().join(case.config); - if !config_path.exists() { - println!("SKIPPED: config not found: {}", config_path.display()); - continue; - } - - println!("--- {} ({}) ---", case.description, case.config); - let result = run_wxc_config(case.config, &["--debug", "--experimental"]); - let status = if command_matches(&result, &case) { - "PASS" - } else { - failures.push(format!( - "{} expected {}, got {:?}", - case.config, - expected_exit_description(&case), - result.code - )); - "FAIL" - }; - - perf_entries.push(MicrovmPerfEntry { - test: case.config.to_string(), - description: case.description.to_string(), - wall_time_ms: result.wall_time_ms, - exit_code: result.code, - status: status.to_string(), - }); - - if status == "FAIL" { - println!( - "--- stdout ---\n{}\n--- stderr ---\n{}", - result.stdout, result.stderr - ); - } - } - - write_microvm_perf_results(perf_entries); - - if !failures.is_empty() { - panic!("MicroVM E2E failures:\n{}", failures.join("\n")); - } -} - -fn command_matches(result: &wxc_e2e_tests::CommandResult, case: &MicrovmCase) -> bool { - if case.expect_non_zero { - if result.code == Some(0) { - return false; - } - } else if result.code != case.expected_exit { - return false; - } - - let Some(expected) = case.output_contains else { - return true; - }; - - result - .combined_output_with_decoded_base64() - .contains(expected) -} - -fn expected_exit_description(case: &MicrovmCase) -> String { - if case.expect_non_zero { - "non-zero exit".to_string() - } else { - format!("exit {}", case.expected_exit.unwrap_or(0)) - } -} - // --------------------------------------------------------------------------- // Hyperlight suite // --------------------------------------------------------------------------- @@ -1179,24 +833,3 @@ fn test_hyperlight_suite() { } with_test_lock(hyperlight_suite); } - -// --------------------------------------------------------------------------- -// MicroVM perf results -// --------------------------------------------------------------------------- - -fn write_microvm_perf_results(results: Vec) { - let output = MicrovmPerfOutput { - commit: std::env::var("GITHUB_SHA").unwrap_or_else(|_| "local".to_string()), - timestamp: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_secs().to_string()) - .unwrap_or_else(|_| "unknown".to_string()), - results, - }; - let json = serde_json::to_string_pretty(&output) - .expect("microvm performance results should serialize"); - let path = repo_root().join("microvm-perf-results.json"); - std::fs::write(&path, json) - .unwrap_or_else(|error| panic!("failed to write {}: {error}", path.display())); - println!("Performance results written to {}", path.display()); -} diff --git a/tests/configs/microvm_error.json b/tests/configs/microvm_error.json deleted file mode 100644 index 0217bc91c..000000000 --- a/tests/configs/microvm_error.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "raise ValueError('intentional test error')", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_error_linux.json b/tests/configs/microvm_error_linux.json deleted file mode 100644 index 0217bc91c..000000000 --- a/tests/configs/microvm_error_linux.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "raise ValueError('intentional test error')", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_exit_code.json b/tests/configs/microvm_exit_code.json deleted file mode 100644 index 0b4d61d08..000000000 --- a/tests/configs/microvm_exit_code.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "import sys; sys.exit(42)", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_exit_code_linux.json b/tests/configs/microvm_exit_code_linux.json deleted file mode 100644 index 0b4d61d08..000000000 --- a/tests/configs/microvm_exit_code_linux.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "import sys; sys.exit(42)", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_hello.json b/tests/configs/microvm_hello.json deleted file mode 100644 index a2bdaa59d..000000000 --- a/tests/configs/microvm_hello.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "x = 42\ny = 58\nprint('Hello from MicroVM! sum=%d' % (x + y))", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_hello_linux.json b/tests/configs/microvm_hello_linux.json deleted file mode 100644 index 21ed00595..000000000 --- a/tests/configs/microvm_hello_linux.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "x = 42\ny = 58\nprint('Hello from NanVix/KVM on Linux! sum=%d' % (x + y))", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_large_output.json b/tests/configs/microvm_large_output.json deleted file mode 100644 index 7efe25d5d..000000000 --- a/tests/configs/microvm_large_output.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "for i in range(1000):\n print(f'line {i}: ' + 'x' * 80)", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_large_output_linux.json b/tests/configs/microvm_large_output_linux.json deleted file mode 100644 index 7efe25d5d..000000000 --- a/tests/configs/microvm_large_output_linux.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "for i in range(1000):\n print(f'line {i}: ' + 'x' * 80)", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_multiline.json b/tests/configs/microvm_multiline.json deleted file mode 100644 index c11c1db31..000000000 --- a/tests/configs/microvm_multiline.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "def fib(n):\n a, b = 0, 1\n for _ in range(n):\n a, b = b, a + b\n return a\n\nfor i in range(10):\n print(f'fib({i}) = {fib(i)}')", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_multiline_linux.json b/tests/configs/microvm_multiline_linux.json deleted file mode 100644 index c11c1db31..000000000 --- a/tests/configs/microvm_multiline_linux.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "def fib(n):\n a, b = 0, 1\n for _ in range(n):\n a, b = b, a + b\n return a\n\nfor i in range(10):\n print(f'fib({i}) = {fib(i)}')", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_network.json b/tests/configs/microvm_network.json deleted file mode 100644 index 702eb151e..000000000 --- a/tests/configs/microvm_network.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "process": { - "commandLine": "import _socket\nsrv = _socket.socket(2, 1, 0)\nsrv.bind(('127.0.0.1', 8080))\nsrv.listen(1)\ncli = _socket.socket(2, 1, 0)\ncli.connect(('127.0.0.1', 8080))\nfd, addr = srv._accept()\nconn = _socket.socket(2, 1, 0, fd)\ncli.send(b'ping')\nassert conn.recv(64) == b'ping'\nconn.send(b'pong')\nassert cli.recv(64) == b'pong'\nprint('NET_OK loopback roundtrip', flush=True)", - "timeout": 30000 - }, - "containment": "microvm", - "network": { - "defaultPolicy": "allow" - } -} diff --git a/tests/configs/microvm_network_linux.json b/tests/configs/microvm_network_linux.json deleted file mode 100644 index 702eb151e..000000000 --- a/tests/configs/microvm_network_linux.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "process": { - "commandLine": "import _socket\nsrv = _socket.socket(2, 1, 0)\nsrv.bind(('127.0.0.1', 8080))\nsrv.listen(1)\ncli = _socket.socket(2, 1, 0)\ncli.connect(('127.0.0.1', 8080))\nfd, addr = srv._accept()\nconn = _socket.socket(2, 1, 0, fd)\ncli.send(b'ping')\nassert conn.recv(64) == b'ping'\nconn.send(b'pong')\nassert cli.recv(64) == b'pong'\nprint('NET_OK loopback roundtrip', flush=True)", - "timeout": 30000 - }, - "containment": "microvm", - "network": { - "defaultPolicy": "allow" - } -} diff --git a/tests/configs/microvm_stdlib.json b/tests/configs/microvm_stdlib.json deleted file mode 100644 index d159a896c..000000000 --- a/tests/configs/microvm_stdlib.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "import json, math, hashlib\ndata = {'pi': math.pi, 'e': math.e, 'hash': hashlib.sha256(b'nanvix').hexdigest()[:16]}\nprint(json.dumps(data))", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_stdlib_linux.json b/tests/configs/microvm_stdlib_linux.json deleted file mode 100644 index d159a896c..000000000 --- a/tests/configs/microvm_stdlib_linux.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "process": { - "commandLine": "import json, math, hashlib\ndata = {'pi': math.pi, 'e': math.e, 'hash': hashlib.sha256(b'nanvix').hexdigest()[:16]}\nprint(json.dumps(data))", - "timeout": 30000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_timeout.json b/tests/configs/microvm_timeout.json deleted file mode 100644 index b9c8c1c22..000000000 --- a/tests/configs/microvm_timeout.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "_comment": "Timeout test. Note: MicroVM adds a 60s boot grace to the 5s script timeout, so actual wall time is ~65s.", - "process": { - "commandLine": "import time; time.sleep(120); print('should not reach here')", - "timeout": 5000 - }, - "containment": "microvm" -} diff --git a/tests/configs/microvm_timeout_linux.json b/tests/configs/microvm_timeout_linux.json deleted file mode 100644 index c7748e18b..000000000 --- a/tests/configs/microvm_timeout_linux.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "_comment": "Timeout test. NanVix adds a 60s boot grace to the 5s script timeout, so actual wall time is ~65s.", - "process": { - "commandLine": "import time; time.sleep(120); print('should not reach here')", - "timeout": 5000 - }, - "containment": "microvm" -} diff --git a/tests/playground/package.json b/tests/playground/package.json index 2eb8c67c9..0aafb919f 100644 --- a/tests/playground/package.json +++ b/tests/playground/package.json @@ -54,30 +54,6 @@ "from": "../src/target/x86_64-pc-windows-msvc/release/wxc-sandbox-agent.exe", "to": "bin/x64/wxc-sandbox-agent.exe" }, - { - "from": "../src/target/x86_64-pc-windows-msvc/release/nanvixd.exe", - "to": "bin/x64/nanvixd.exe" - }, - { - "from": "../src/target/x86_64-pc-windows-msvc/release/bin/kernel.elf", - "to": "bin/x64/bin/kernel.elf" - }, - { - "from": "../src/target/x86_64-pc-windows-msvc/release/python3.initrd", - "to": "bin/x64/python3.initrd" - }, - { - "from": "../src/target/x86_64-pc-windows-msvc/release/nanvix_rootfs.img", - "to": "bin/x64/nanvix_rootfs.img" - }, - { - "from": "../src/target/x86_64-pc-windows-msvc/release/snapshots/kernel.vmem", - "to": "bin/x64/snapshots/kernel.vmem" - }, - { - "from": "../src/target/x86_64-pc-windows-msvc/release/snapshots/kernel.whp.cbor", - "to": "bin/x64/snapshots/kernel.whp.cbor" - }, { "from": "../sdk/node/bin/x64/vcruntime140.dll", "to": "bin/x64/vcruntime140.dll" diff --git a/tests/playground/src/main/main.ts b/tests/playground/src/main/main.ts index 1d2af9dce..f60df509b 100644 --- a/tests/playground/src/main/main.ts +++ b/tests/playground/src/main/main.ts @@ -521,45 +521,11 @@ ipcMain.handle('run-sandbox-raw', (_event, configJson: string, debug: boolean, e const config = JSON.parse(configJson); const execPath = resolveExecutablePath(); - // MicroVM (nanvixd) requires CWD to be the binary directory - let workingDir: string | undefined; - if (config.containment === 'microvm') { - const fs = require('fs'); - // Match the SDK's binary discovery layout (sdk/src/platform.ts): - // npm-packaged binaries live under sdk/bin/, local dev builds - // under src/target//{release,debug}. - const arch = process.arch === 'arm64' ? 'arm64' : 'x64'; - const triple = process.arch === 'arm64' ? 'aarch64-pc-windows-msvc' : 'x86_64-pc-windows-msvc'; - const repoRoot = path.join(__dirname, '..', '..', '..'); - const candidates = [ - execPath, - path.join(repoRoot, 'sdk', 'bin', arch), - path.join(repoRoot, 'src', 'target', triple, 'release'), - path.join(repoRoot, 'src', 'target', triple, 'debug'), - path.join(repoRoot, 'src', 'target', 'release'), - path.join(repoRoot, 'src', 'target', 'debug'), - ].filter(Boolean); - for (const c of candidates) { - const dir = c!.endsWith('.exe') ? path.dirname(c!) : c!; - if (fs.existsSync(path.join(dir, 'nanvixd.exe'))) { - workingDir = dir; - break; - } - } - if (!workingDir) { - return { - success: false, - error: `nanvixd.exe not found for arch '${arch}'. Looked in: ${candidates.join('; ')}. ` + - `MicroVM requires nanvixd.exe to be co-located with wxc-exec (and CWD must point at it).`, - }; - } - } - const ptyProcess = sdk.spawnSandboxFromConfig(config, { debug, experimental, executablePath: execPath, skipPlatformCheck: true, - }, workingDir); + }); attachPtyListeners(ptyProcess); return { success: true, config }; diff --git a/tests/playground/src/renderer/app.ts b/tests/playground/src/renderer/app.ts index 49cd8b95e..b6b050efd 100644 --- a/tests/playground/src/renderer/app.ts +++ b/tests/playground/src/renderer/app.ts @@ -18,7 +18,7 @@ interface Scenario { script: string; policy: any; shell: 'cmd' | 'ps51' | 'ps7' | 'python' | 'networking' | 'filesystem'; - containment?: 'appcontainer' | 'windows_sandbox' | 'microvm' | 'hyperlight'; + containment?: 'appcontainer' | 'windows_sandbox' | 'hyperlight'; requiresV05?: boolean; /** If set, output must contain this string for a PASS verdict */ successMarker?: string; @@ -377,77 +377,6 @@ var SCENARIOS: Scenario[] = [ script: 'ping -n 30 127.0.0.1', policy: { timeoutMs: 5000 } }, - // ========== MicroVM (NanVix) ========== - { id: 'mv-hello', name: 'Hello from MicroVM', category: 'Quick Tests', categoryIcon: '🎯', shell: 'python', - containment: 'microvm', - description: 'Runs a simple Python script inside the NanVix micro-VM.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "x = 42\ny = 58\nprint('Hello from MicroVM! sum=%d' % (x + y))", - policy: {}, successMarker: 'Hello from MicroVM!' }, - { id: 'mv-stdlib', name: 'Stdlib (json, math, hashlib)', category: 'Quick Tests', categoryIcon: '🎯', shell: 'python', - containment: 'microvm', - description: 'Imports json, math, hashlib to verify the CPython stdlib is available.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "import json, math, hashlib\ndata = {'pi': math.pi, 'e': math.e, 'hash': hashlib.sha256(b'nanvix').hexdigest()[:16]}\nprint(json.dumps(data))", - policy: {}, successMarker: 'pi' }, - { id: 'mv-multiline', name: 'Fibonacci (multiline)', category: 'Quick Tests', categoryIcon: '🎯', shell: 'python', - containment: 'microvm', - description: 'Runs a multi-line Fibonacci function to verify complex scripts work.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "def fib(n):\n a, b = 0, 1\n for _ in range(n):\n a, b = b, a + b\n return a\n\nfor i in range(10):\n print(f'fib({i}) = {fib(i)}')", - policy: {}, successMarker: 'fib(9) = 34' }, - { id: 'mv-large-output', name: 'Large output (1000 lines)', category: 'Quick Tests', categoryIcon: '🎯', shell: 'python', - containment: 'microvm', - description: 'Prints 1000 lines to verify large output streaming works.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "for i in range(1000):\n print(f'line {i}: ' + 'x' * 80)", - policy: {}, successMarker: 'line 999' }, - { id: 'mv-exit-code', name: 'Exit code 42', category: 'Error Cases', categoryIcon: '⚠️', shell: 'python', - containment: 'microvm', - description: 'Exits with code 42. Verifies exit codes propagate from the micro-VM.', - expectedOutcome: 'show-error', expectedLabel: 'Should exit 42', - script: 'import sys; sys.exit(42)', - policy: {} }, - { id: 'mv-error', name: 'Python error', category: 'Error Cases', categoryIcon: '⚠️', shell: 'python', - containment: 'microvm', - description: 'Raises a ValueError. Verifies stderr capture from the micro-VM.', - expectedOutcome: 'show-error', expectedLabel: 'Should show error', - script: "raise ValueError('intentional test error')", - policy: {} }, - { id: 'mv-timeout', name: 'Timeout', category: 'Error Cases', categoryIcon: '⚠️', shell: 'python', - containment: 'microvm', - description: 'Sleeps for 120s with a 5s timeout. MicroVM adds 60s boot grace, so actual ~65s.', - expectedOutcome: 'be-blocked', expectedLabel: 'Should be terminated', - script: "import time; time.sleep(120); print('should not reach here')", - policy: { timeoutMs: 5000 } }, - - // ========== MicroVM — Filesystem ========== - { id: 'mv-fs-write-read', name: 'Write & read file (FS mount)', category: 'Filesystem', categoryIcon: '📁', shell: 'filesystem', - containment: 'microvm', - description: 'Writes a file to a readwritePaths mount and reads it back. Verifies staging dir works.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "import os\ntest_dir = 'C:\\\\Users\\\\Public\\\\MXCPlaygroundTests'\nfpath = os.path.join(test_dir, 'mxc-mv-test.txt')\ntry:\n with open(fpath, 'w') as f:\n f.write('hello from microvm')\n with open(fpath) as f:\n data = f.read()\n print('Read back:', data)\n assert data == 'hello from microvm', 'mismatch!'\n print('MV FS write/read OK')\nfinally:\n try: os.remove(fpath)\n except OSError: pass", - policy: { filesystem: { readwritePaths: ['C:\\Users\\Public\\MXCPlaygroundTests'] } }, successMarker: 'MV FS write/read OK' }, - { id: 'mv-fs-list-dir', name: 'List directory contents', category: 'Filesystem', categoryIcon: '📁', shell: 'filesystem', - containment: 'microvm', - description: 'Creates and lists files in a readwritePaths mount. Verifies directory operations.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "import os\ntest_dir = 'C:\\\\Users\\\\Public\\\\MXCPlaygroundTests'\nnames = ['mxc-a.txt', 'mxc-b.txt', 'mxc-c.txt']\ntry:\n for name in names:\n with open(os.path.join(test_dir, name), 'w') as f:\n f.write(name)\n entries = [e for e in os.listdir(test_dir) if e.startswith('mxc-')]\n print('MXC files:', sorted(entries))\n assert 'mxc-a.txt' in entries and 'mxc-c.txt' in entries\n print('MV dir listing OK')\nfinally:\n for name in names:\n try: os.remove(os.path.join(test_dir, name))\n except OSError: pass", - policy: { filesystem: { readwritePaths: ['C:\\Users\\Public\\MXCPlaygroundTests'] } }, successMarker: 'MV dir listing OK' }, - - // ========== MicroVM — Stdlib ========== - { id: 'mv-stdlib-broad', name: 'Stdlib (re, datetime, collections)', category: 'Quick Tests', categoryIcon: '🎯', shell: 'python', - containment: 'microvm', - description: 'Imports re, datetime, collections to verify broader stdlib availability.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "import re, datetime, collections\nm = re.match(r'(\\w+)@(\\w+)', 'user@host')\nprint('regex:', m.group(1), m.group(2))\nnow = datetime.datetime(2025, 1, 15, 12, 0)\nprint('datetime:', now.isoformat())\nc = collections.Counter('abracadabra')\nprint('counter:', c.most_common(3))\nprint('broad stdlib OK')", - policy: {}, successMarker: 'broad stdlib OK' }, - { id: 'mv-memory', name: 'Memory stress (10MB list)', category: 'Quick Tests', categoryIcon: '🎯', shell: 'python', - containment: 'microvm', - description: 'Allocates a 10MB list to verify the VM handles non-trivial memory.', - expectedOutcome: 'succeed', expectedLabel: 'Should succeed', - script: "data = list(range(1_000_000))\nprint('Allocated', len(data), 'items')\nprint('Sum:', sum(data[:100]))\nprint('Memory OK')", - policy: {}, successMarker: 'Memory OK' }, { id: 'hl-hello', name: 'Hello from Hyperlight', category: 'Quick Tests', categoryIcon: '🎯', shell: 'python', containment: 'hyperlight', description: 'Runs a simple Python script inside the Hyperlight+Unikraft micro-VM.', @@ -772,7 +701,7 @@ function getCurrentScript(): string { // Replace bare 'python' with resolved full path for BaseContainer compatibility // (skip for Windows Sandbox — it uses mapped Python from the host) var containment = $sel('containmentSelect').value; - if (containment !== 'windows_sandbox' && containment !== 'microvm' && containment !== 'hyperlight' && shellPaths.python?.exe && script.match(/^python\s/)) { + if (containment !== 'windows_sandbox' && containment !== 'hyperlight' && shellPaths.python?.exe && script.match(/^python\s/)) { script = '"' + shellPaths.python.exe + '"' + script.substring(6); } @@ -786,7 +715,6 @@ function getCurrentScript(): string { var CONTAINMENT_LABELS: Record = { appcontainer: 'Base Process Container', windows_sandbox: 'Windows Sandbox', - microvm: 'MicroVM (NanVix)', hyperlight: 'Hyperlight', wslc: 'WSLC', lxc: 'LXC', @@ -825,8 +753,8 @@ var cachedPlatformSupport: { isSupported: boolean; reason?: string } | null = nu /** Update the platform badge based on the selected containment backend. */ function updatePlatformBadgeForContainment(containment: string): void { - if (containment === 'microvm' || containment === 'hyperlight') { - // HL/MV don't depend on a specific Windows build version + if (containment === 'hyperlight') { + // Hyperlight does not depend on a specific Windows build version. var label = CONTAINMENT_LABELS[containment] || containment; $('platformBadge').textContent = '✓ ' + label + ' (no OS version requirement)'; } else if (cachedPlatformSupport) { @@ -843,7 +771,7 @@ function updatePlatformBadgeForContainment(containment: string): void { function isVmBackend(): boolean { var c = $sel('containmentSelect').value; - return c === 'microvm' || c === 'hyperlight' || c === 'windows_sandbox'; + return c === 'hyperlight' || c === 'windows_sandbox'; } function getPermsSummary(): string { @@ -1062,7 +990,7 @@ function populateScenarios(): void { select.innerHTML = ''; var containment = $sel('containmentSelect').value; - var isRawBackend = containment === 'windows_sandbox' || containment === 'microvm' || containment === 'hyperlight'; + var isRawBackend = containment === 'windows_sandbox' || containment === 'hyperlight'; var filtered = SCENARIOS.filter(function(s) { if (s.shell !== shell) return false; if (isRawBackend) return s.containment === containment; @@ -1246,8 +1174,7 @@ function buildRawBackendConfig( } } // Hyperlight defaults to network-allowed when `network` is omitted; explicitly - // block unless the scenario opts in. MicroVM/NanVix rejects `defaultPolicy: block`, - // so leave its network field unset in that case. + // block unless the scenario opts in. if (containment === 'hyperlight' && !config.network) { config.network = { defaultPolicy: 'block' }; } @@ -1312,9 +1239,9 @@ async function runSandbox(): Promise { return; } - // Windows Sandbox / MicroVM mode — build raw wxc-exec JSON config and use runSandboxRaw + // VM backends build raw wxc-exec JSON config and use runSandboxRaw. var currentContainment = $sel('containmentSelect').value; - if (currentContainment === 'windows_sandbox' || currentContainment === 'microvm' || currentContainment === 'hyperlight') { + if (currentContainment === 'windows_sandbox' || currentContainment === 'hyperlight') { var rawScript = state.selectedScenario ? state.selectedScenario.script : (state.customScript || '').trim(); if (!rawScript) { termError('No script specified'); @@ -1328,31 +1255,6 @@ async function runSandbox(): Promise { state.timeoutSeconds, ); - // MicroVM staging requires readwritePaths to exist on the host. Pre-create - // any dirs the scenario mounts (e.g. C:\Users\Public\MXCPlaygroundTests). - if (currentContainment === 'microvm' && rawConfig.filesystem && rawConfig.filesystem.readwritePaths) { - try { - var ensureDirsResult = await mxc.ensureDirs(rawConfig.filesystem.readwritePaths); - if (ensureDirsResult && typeof ensureDirsResult === 'object') { - if ((ensureDirsResult as any).success === false) { - throw new Error((ensureDirsResult as any).error || (ensureDirsResult as any).message || 'Unknown error'); - } - if (Array.isArray(ensureDirsResult)) { - var failedEnsureDir = ensureDirsResult.find(function (entry: any) { - return entry && typeof entry === 'object' && entry.success === false; - }); - if (failedEnsureDir) { - throw new Error(failedEnsureDir.error || failedEnsureDir.message || 'Unknown error'); - } - } - } - } catch (e: any) { - termError('[Playground] Failed to pre-create RW mount dirs: ' + (e && e.message ? e.message : String(e))); - onSandboxExit(-1); - return; - } - } - state.running = true; if (!runAllInProgress) { ($('btnRun') as HTMLButtonElement).disabled = true; @@ -1372,8 +1274,6 @@ async function runSandbox(): Promise { termInfo('[MXC] API: spawnSandboxFromConfig (raw config)'); if (currentContainment === 'windows_sandbox') { termDim('[MXC] Note: First run may take 3-5 minutes while the sandbox VM boots.'); - } else if (currentContainment === 'microvm') { - termDim('[MXC] Note: MicroVM boot adds ~60s grace period to the script timeout.'); } else if (currentContainment === 'hyperlight') { termDim('[MXC] Note: First run may take longer while Hyperlight warms up the snapshot.'); } @@ -1539,9 +1439,9 @@ async function runAllScenarios(): Promise { // Filter scenarios: current shell, containment, available runtimes, version-appropriate var currentContainment = $sel('containmentSelect').value; - var isRawBackend = currentContainment === 'windows_sandbox' || currentContainment === 'microvm' || currentContainment === 'hyperlight'; + var isRawBackend = currentContainment === 'windows_sandbox' || currentContainment === 'hyperlight'; var scenariosToRun = SCENARIOS.filter(function(s) { - // When the user picks "python" on a raw backend (MicroVM/Hyperlight), also + // When the user picks "python" on a raw backend (Hyperlight), also // pull in the pseudo-shell categories (networking, filesystem) since those // scenarios are Python-backed and would otherwise be silently skipped. var shellMatch = (s.shell === currentShell); @@ -1551,7 +1451,7 @@ async function runAllScenarios(): Promise { } if (!shellMatch) { return false; } if (isRawBackend) { if (s.containment !== currentContainment) return false; } - else { if (s.containment === 'windows_sandbox' || s.containment === 'microvm' || s.containment === 'hyperlight') return false; } + else { if (s.containment === 'windows_sandbox' || s.containment === 'hyperlight') return false; } if (s.shell === 'ps7' && !shellAvailability['ps7']) { return false; } if (s.shell === 'python' && !shellAvailability['python']) { return false; } if (s.requiresV05 && version !== '0.5.0-dev') { return false; } @@ -1926,7 +1826,7 @@ function showJsonPanel(tab: string): void { if (tab === 'policy') { var containment = $sel('containmentSelect').value; - if (containment === 'windows_sandbox' || containment === 'microvm' || containment === 'hyperlight') { + if (containment === 'windows_sandbox' || containment === 'hyperlight') { var rawScript = state.selectedScenario ? state.selectedScenario.script : (state.customScript || '').trim(); var rawConfig = buildRawBackendConfig( containment, @@ -1941,7 +1841,7 @@ function showJsonPanel(tab: string): void { } } else { var containment2 = $sel('containmentSelect').value; - if (containment2 === 'windows_sandbox' || containment2 === 'microvm' || containment2 === 'hyperlight') { + if (containment2 === 'windows_sandbox' || containment2 === 'hyperlight') { var rawScript2 = state.selectedScenario ? state.selectedScenario.script : (state.customScript || '').trim(); var rawConfig2 = buildRawBackendConfig( containment2, @@ -1991,8 +1891,8 @@ function updateDevSidebar(): void { var currentShell = $sel('shellSelect').value; var currentContainment = $sel('containmentSelect').value; - // Windows Sandbox / MicroVM / Hyperlight mode — show the raw config - if ((currentContainment === 'windows_sandbox' || currentContainment === 'microvm' || currentContainment === 'hyperlight') && currentShell !== 'rawjson') { + // Windows Sandbox / Hyperlight mode — show the raw config + if ((currentContainment === 'windows_sandbox' || currentContainment === 'hyperlight') && currentShell !== 'rawjson') { var rawScript = state.selectedScenario ? state.selectedScenario.script : (state.customScript || '').trim(); var rawConfig = buildRawBackendConfig( currentContainment, @@ -2210,27 +2110,6 @@ function init(): void { } else { populateScenarios(); } - } else if (containment === 'microvm') { - // MicroVM (NanVix) — the policy-generator UI is hidden because MicroVM - // does not consume the SandboxPolicy schema; scenarios may still set - // filesystem (and other) fields in the raw config sent to wxc-exec. - $('runtimeRow').classList.remove('hidden'); - $sel('shellSelect').disabled = false; - $('experimentalCaution').classList.add('hidden'); - $('policySectionWrapper').classList.add('hidden'); - $('advancedSectionWrapper').classList.add('hidden'); - $('uiGroupWrapper').classList.add('hidden'); - ($('experimentalToggle') as HTMLInputElement).checked = true; - ($('experimentalToggle') as HTMLInputElement).disabled = true; - // MicroVM supports Python + Filesystem test categories - var mvOpts = $sel('shellSelect').options; - for (var mi = 0; mi < mvOpts.length; mi++) { - var v = mvOpts[mi].value; - (mvOpts[mi] as HTMLOptionElement).hidden = (v !== 'python' && v !== 'filesystem' && v !== 'custom' && v !== 'rawjson'); - } - $sel('shellSelect').value = 'python'; - $sel('shellSelect').dispatchEvent(new Event('change')); - updatePlatformBadgeForContainment(containment); } else if (containment === 'hyperlight') { // Hyperlight — the policy-generator UI is hidden because Hyperlight // does not consume the SandboxPolicy schema; scenarios may still set @@ -2258,7 +2137,7 @@ function init(): void { $('categoryRow').classList.add('hidden'); $('policySectionWrapper').classList.remove('hidden'); // Restore PowerShell runtimes (but keep pseudo-shells hidden — they only - // apply to MicroVM/Hyperlight, where Python is the runtime). + // apply to Hyperlight, where Python is the runtime). var shellOpts2 = $sel('shellSelect').options; for (var j = 0; j < shellOpts2.length; j++) { var sv2 = shellOpts2[j].value; @@ -2276,7 +2155,7 @@ function init(): void { $('policySectionWrapper').classList.remove('hidden'); ($('experimentalToggle') as HTMLInputElement).disabled = false; // Restore PowerShell runtimes (but keep pseudo-shells hidden — they only - // apply to MicroVM/Hyperlight, where Python is the runtime). + // apply to Hyperlight, where Python is the runtime). var shellOpts3 = $sel('shellSelect').options; for (var k = 0; k < shellOpts3.length; k++) { var sv3 = shellOpts3[k].value; @@ -2302,7 +2181,7 @@ function init(): void { $('categoryRow').classList.add('hidden'); $('scriptSection').classList.remove('hidden'); $('rawJsonSection').classList.add('hidden'); - if ($sel('containmentSelect').value !== 'windows_sandbox' && $sel('containmentSelect').value !== 'microvm' && $sel('containmentSelect').value !== 'hyperlight') { + if ($sel('containmentSelect').value !== 'windows_sandbox' && $sel('containmentSelect').value !== 'hyperlight') { $('policySectionWrapper').classList.remove('hidden'); } $('btnRun').classList.remove('hidden'); @@ -2337,7 +2216,7 @@ function init(): void { populateScenarios(); $('btnRun').classList.remove('hidden'); $('btnRunAll').classList.remove('hidden'); - if ($sel('containmentSelect').value !== 'windows_sandbox' && $sel('containmentSelect').value !== 'microvm' && $sel('containmentSelect').value !== 'hyperlight') { + if ($sel('containmentSelect').value !== 'windows_sandbox' && $sel('containmentSelect').value !== 'hyperlight') { $('policySectionWrapper').classList.remove('hidden'); } if (document.getElementById('advancedSectionWrapper')) { diff --git a/tests/playground/src/renderer/index.html b/tests/playground/src/renderer/index.html index e9cdf58d4..b2577a609 100644 --- a/tests/playground/src/renderer/index.html +++ b/tests/playground/src/renderer/index.html @@ -107,7 +107,6 @@

Scenario

@@ -450,7 +449,7 @@

Containment Types

  • 🛡️ Base Process Container — Default for 0.5.0-dev. Uses CreateProcessInSandbox. Supports all pre-made scenarios.
  • 🛡️ AppContainer — Default for 0.4.0-alpha. Standard Windows AppContainer isolation.
  • -
  • 🧪 Windows Sandbox / MicroVM / WSLC — Experimental. Select from the Containment dropdown — these only support MXC JSON Config mode (paste your own config).
  • +
  • 🧪 Windows Sandbox / Hyperlight / WSLC — Experimental. Select from the Containment dropdown — these only support MXC JSON Config mode (paste your own config).
diff --git a/tests/scripts/README.md b/tests/scripts/README.md index 88302976c..c5a4f177c 100644 --- a/tests/scripts/README.md +++ b/tests/scripts/README.md @@ -36,8 +36,6 @@ Linux / macOS (`.sh`): | `run_filesystem_bfs_spaces_test.ps1` | BFS path-with-spaces test | `wxc-exec.exe` | | `run_test_configs.ps1` | All test configs via wxc-test-driver | `wxc-test-driver.exe` | | `run_examples.ps1` | All examples via wxc-test-driver | `wxc-test-driver.exe` | -| `run_microvm_basic_test.ps1` | MicroVM smoke test | `wxc-exec.exe`, NanVix binaries | -| `run_microvm_tests.ps1` | Full MicroVM E2E suite | WHP enabled, NanVix binaries | | `run_windows_sandbox_one_shot_tests.ps1` | Windows Sandbox one-shot E2E suite (fresh disposable VM per test) | Windows Sandbox enabled | | `run_windows_sandbox_state_aware_tests.ps1` | Windows Sandbox state-aware lifecycle E2E (single VM held across provision/start/exec*/stop/deprovision) | Windows Sandbox enabled | | `run_processcontainer_proxy_tests.ps1` | Process container proxy tests | `wxc-exec.exe` | @@ -71,8 +69,8 @@ these dispatchers, which map a matrix backend id to the suites above: | Dispatcher | Platforms | Backend ids | |------------|-----------|-------------| -| `scripts/ci/run_backend_validation_tests.ps1` | Windows | `process-t1`, `process-t3`, `isolation-session`, `windows-sandbox`, `wslc`, `microvm`, `hyperlight` | -| `scripts/ci/run_backend_validation_tests.sh` | Linux, macOS | `bubblewrap`, `lxc`, `seatbelt`, `microvm`, `hyperlight` | +| `scripts/ci/run_backend_validation_tests.ps1` | Windows | `process-t1`, `process-t3`, `isolation-session`, `windows-sandbox`, `wslc`, `hyperlight` | +| `scripts/ci/run_backend_validation_tests.sh` | Linux, macOS | `bubblewrap`, `lxc`, `seatbelt`, `hyperlight` | Pass the backend id exactly as it appears in the catalog — there is no separate handler name. Ids that share a suite have their own case in the dispatcher: @@ -194,22 +192,3 @@ Run them explicitly on capable machines with | `test_examples` | Requires velocity key 61714527 (BFS deadlock fix) | | `test_processcontainer_proxy` | Requires velocity key 61714527 (BFS deadlock fix) and elevation | | `test_on_repeat` | Stress test (loops BFS tests) | - -## MicroVM E2E - -### Build - -```powershell -cd src -cargo build --features microvm --target x86_64-pc-windows-msvc -``` - -### Run - -```powershell -cd src -cargo test -p wxc_e2e_tests --target x86_64-pc-windows-msvc test_microvm_suite -- --nocapture -``` - -The MicroVM suite runs 6 functional tests + 1 timeout behavior test. -It generates `microvm-perf-results.json` with per-test timing and status data (uploaded as CI artifact). diff --git a/tests/scripts/run_microvm_basic_test.ps1 b/tests/scripts/run_microvm_basic_test.ps1 deleted file mode 100644 index 5dffe2214..000000000 --- a/tests/scripts/run_microvm_basic_test.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# MicroVM basic smoke test runner. -# -# Usage: -# .\run_microvm_basic_test.ps1 # debug build -# .\run_microvm_basic_test.ps1 -Release # release build - -param( - [switch]$Release, - [string]$BinDir -) - -$ErrorActionPreference = "Stop" -$RepoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) - -if (-not $BinDir) { - if ($Release) { - $BinDir = Join-Path $RepoRoot "src\target\release" - } else { - $BinDir = Join-Path $RepoRoot "src\target\debug" - } -} - -$WxcExec = Join-Path $BinDir "wxc-exec.exe" -$TestConfig = Join-Path $RepoRoot "tests\configs\microvm_hello.json" - -if (-not (Test-Path $WxcExec)) { - Write-Host "ERROR: wxc-exec.exe not found at $WxcExec" -ForegroundColor Red - Write-Host "Run 'cargo build$(if ($Release) { ' --release' })' first." -ForegroundColor Yellow - exit 1 -} - -Write-Host "Running MicroVM basic smoke test..." -ForegroundColor Cyan -& $WxcExec --debug --experimental $TestConfig -$exitCode = $LASTEXITCODE - -if ($exitCode -ne 0) { - Write-Host "FAILED: wxc-exec exited with code $exitCode" -ForegroundColor Red - exit $exitCode -} - -Write-Host "PASSED: MicroVM basic smoke test" -ForegroundColor Green diff --git a/tests/scripts/run_microvm_tests.ps1 b/tests/scripts/run_microvm_tests.ps1 deleted file mode 100644 index dcf6002b5..000000000 --- a/tests/scripts/run_microvm_tests.ps1 +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -<# -.SYNOPSIS - Runs MicroVM E2E tests. Requires WHP and Nanvix binaries next to wxc-exec.exe. - -.DESCRIPTION - - Locates wxc-exec.exe (built with --features microvm) - - Verifies Nanvix binaries are present - - Runs each test config via wxc-exec, validates exit codes and stdout content - - Reports pass/fail summary with per-test performance timing - - Writes microvm-perf-results.json for CI artifact consumption - -.PARAMETER Release - Use release build (default: debug) - -.PARAMETER BinDir - Explicit binary directory. Overrides -Release logic when provided. - -.PARAMETER ConfigDir - Path to test configs directory. Defaults to \tests\configs - -.EXAMPLE - .\run_microvm_tests.ps1 - .\run_microvm_tests.ps1 -Release - .\run_microvm_tests.ps1 -BinDir C:\build\output -#> - -param( - [switch]$Release, - [string]$BinDir, - [string]$ConfigDir -) - -$ErrorActionPreference = "Stop" -$RepoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) - -if (-not $BinDir) { - if ($Release) { - $BinDir = Join-Path $RepoRoot "src\target\release" - } else { - $BinDir = Join-Path $RepoRoot "src\target\debug" - } -} - -if (-not $ConfigDir) { - $ConfigDir = Join-Path $RepoRoot "tests\configs" -} - -$WxcExePath = Join-Path $BinDir "wxc-exec.exe" - -# -- WHP check (local runs only) --------------------------------------------- -# In CI, the workflow checks WHP and fails before reaching this script. -# For local runs, check here and skip gracefully if WHP is unavailable. - -if (-not $env:CI) { - function Test-WhpAvailable { - if (-not (Test-Path "$env:SystemRoot\System32\WinHvPlatform.dll")) { - return $false - } - try { - $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue - return ($cs -and $cs.HypervisorPresent) - } catch { - return $false - } - } - - if (-not (Test-WhpAvailable)) { - Write-Host "SKIP: Windows Hypervisor Platform (WHP) is not available." -ForegroundColor Yellow - Write-Host " Enable it with: Enable-WindowsOptionalFeature -Online -FeatureName HypervisorPlatform" - exit 0 - } -} - -Write-Host "`n=== MicroVM E2E Tests ===" -ForegroundColor Cyan - -# -- Locate wxc-exec.exe ----------------------------------------------------- - -if (-not (Test-Path $WxcExePath)) { - Write-Host "ERROR: wxc-exec.exe not found at: $WxcExePath" -ForegroundColor Red - Write-Host " Build with: cd src && cargo build --features microvm" - exit 1 -} - -$wxcExe = Resolve-Path $WxcExePath - -# -- Verify MicroVM binaries -------------------------------------------------- - -$requiredBinaries = @( - "nanvixd.exe", - "nanvix_rootfs.img", - "python3.initrd", - "bin\kernel.elf", - "snapshots\kernel.vmem", - "snapshots\kernel.whp.cbor" -) -$binDir = Split-Path $wxcExe -$missing = $requiredBinaries | Where-Object { -not (Test-Path (Join-Path $binDir $_)) } - -if ($missing) { - Write-Host "ERROR: Missing MicroVM binaries in ${binDir}:" -ForegroundColor Red - $missing | ForEach-Object { Write-Host " - $_" } - Write-Host " Build with: cd src && cargo build --features microvm" - exit 1 -} - -Write-Host "wxc-exec: $wxcExe" -Write-Host "binaries: $binDir" - -# -- Test definitions --------------------------------------------------------- - -$tests = @( - @{ Config = "microvm_hello.json"; ExpectedExit = 0; Description = "Hello world"; OutputContains = "sum=100" }, - @{ Config = "microvm_exit_code.json"; ExpectedExit = 42; Description = "Exit code propagation" }, - @{ Config = "microvm_multiline.json"; ExpectedExit = 0; Description = "Multi-line script (fibonacci)"; OutputContains = "fib(" }, - @{ Config = "microvm_stdlib.json"; ExpectedExit = 0; Description = "Stdlib (json, math, hashlib)"; OutputContains = "pi" }, - @{ Config = "microvm_large_output.json"; ExpectedExit = 0; Description = "Large stdout (1000 lines)"; OutputContains = "line 999" }, - @{ Config = "microvm_error.json"; ExpectedExit = 1; Description = "Python exception"; OutputContains = "ValueError" }, - @{ Config = "microvm_timeout.json"; ExpectedExit = -1; Description = "Timeout kills VM" } -) - -# -- Run tests ---------------------------------------------------------------- - -$passed = 0 -$failed = 0 -$results = @() - -foreach ($test in $tests) { - $configPath = Join-Path $ConfigDir $test.Config - if (-not (Test-Path $configPath)) { - Write-Host " SKIP $($test.Config) (file not found)" -ForegroundColor Yellow - continue - } - - Write-Host "`n--- $($test.Description) ($($test.Config)) ---" -ForegroundColor White - - $sw = [System.Diagnostics.Stopwatch]::StartNew() - $stdoutFile = [System.IO.Path]::GetTempFileName() - $stderrFile = [System.IO.Path]::GetTempFileName() - $process = Start-Process -FilePath $wxcExe ` - -ArgumentList "--debug", "--experimental", $configPath ` - -PassThru -Wait ` - -RedirectStandardOutput $stdoutFile ` - -RedirectStandardError $stderrFile - $sw.Stop() - - $actualExit = $process.ExitCode - $expectedExit = $test.ExpectedExit - $elapsedMs = $sw.ElapsedMilliseconds - $stdout = Get-Content $stdoutFile -Raw -ErrorAction SilentlyContinue - $stderr = Get-Content $stderrFile -Raw -ErrorAction SilentlyContinue - Remove-Item $stdoutFile, $stderrFile -ErrorAction SilentlyContinue - - $pass = ($actualExit -eq $expectedExit) - $reason = "" - - if (-not $pass) { - $reason = "expected exit=$expectedExit, got exit=$actualExit" - } - - # Check stdout content if OutputContains is specified. Not every test - # defines the key, and StrictMode makes a missing hashtable key throw, so - # test for its presence rather than accessing it directly. - if ($pass -and $test.ContainsKey('OutputContains')) { - $combined = "$stdout`n$stderr" - if ($combined -notmatch [regex]::Escape($test.OutputContains)) { - $pass = $false - $reason = "output missing '$($test.OutputContains)'" - } - } - - if ($pass) { - Write-Host " PASS (exit=$actualExit, ${elapsedMs}ms)" -ForegroundColor Green - $passed++ - $results += @{ Test = $test.Config; Status = "PASS"; Exit = $actualExit; WallTimeMs = $elapsedMs; Description = $test.Description } - } else { - Write-Host " FAIL ($reason, ${elapsedMs}ms)" -ForegroundColor Red - $combined = "$stdout`n$stderr" - $combined -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 3 | ForEach-Object { - Write-Host " > $($_.TrimEnd())" -ForegroundColor Gray - } - $failed++ - $results += @{ Test = $test.Config; Status = "FAIL"; Exit = $actualExit; WallTimeMs = $elapsedMs; Description = $test.Description } - } -} - -# -- Performance summary ------------------------------------------------------ - -Write-Host "`n=== Performance ===" -ForegroundColor Cyan -Write-Host (" {0,-35} {1,10} {2,8}" -f "Test", "Time (ms)", "Status") -Write-Host (" {0,-35} {1,10} {2,8}" -f "----", "---------", "------") -foreach ($r in $results) { - $color = if ($r.Status -eq "PASS") { "Green" } else { "Red" } - Write-Host (" {0,-35} {1,10} {2,8}" -f $r.Description, $r.WallTimeMs, $r.Status) -ForegroundColor $color -} - -# Write JSON results for CI artifact consumption -$perfOutput = @{ - commit = if ($env:GITHUB_SHA) { $env:GITHUB_SHA } else { "local" } - timestamp = (Get-Date -Format "o") - results = $results | ForEach-Object { - @{ - test = $_.Test - description = $_.Description - wall_time_ms = $_.WallTimeMs - exit_code = $_.Exit - status = $_.Status - } - } -} -$perfJsonPath = Join-Path $ConfigDir "..\microvm-perf-results.json" -$perfOutput | ConvertTo-Json -Depth 3 | Set-Content $perfJsonPath -Encoding UTF8 -Write-Host "`n Performance results written to: $perfJsonPath" - -# -- Summary ------------------------------------------------------------------ - -$total = $passed + $failed -Write-Host "`n=== Results ===" -ForegroundColor Cyan -if ($total -eq 0) { - Write-Host " ERROR: No tests were executed. Check -ConfigDir path." -ForegroundColor Red - exit 1 -} -Write-Host " Passed: $passed / $total" -if ($failed -gt 0) { - Write-Host " Failed: $failed / $total" -ForegroundColor Red - $results | Where-Object { $_.Status -eq "FAIL" } | ForEach-Object { - Write-Host " - $($_.Test) (exit=$($_.Exit))" -ForegroundColor Red - } - exit 1 -} else { - Write-Host " All MicroVM E2E tests passed!" -ForegroundColor Green - exit 0 -} From b1abbe19f14eccf2abe02ec6104b307112da734f Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 11:31:12 -0700 Subject: [PATCH 21/41] docs: document the NVX cutover Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 16 +- .github/workflows/issue-triage.md | 2 +- CONTRIBUTING.md | 2 +- README.md | 17 +- docs/backend-support-probe-api-plan.md | 6 +- docs/bwrap-support/bubblewrap-backend-plan.md | 6 +- docs/ci-validation-infrastructure.md | 17 +- docs/isolation-session/oneshot.md | 2 +- docs/linux-wsl-roadmap-june-2026.md | 2 +- .../nanvix-microvm/nanvix-integration-plan.md | 394 ------------------ docs/nanvix-microvm/nanvix.md | 224 ---------- docs/nvx-backend-gaps.md | 22 +- .../0.8.0/networking/networking.md | 2 +- docs/schema.md | 5 +- .../mxc-state-aware-sandbox-api-overview.md | 2 +- .../mxc-state-aware-sandbox-api.md | 10 +- src/core/mxc-sdk/README.md | 2 +- 17 files changed, 52 insertions(+), 679 deletions(-) delete mode 100644 docs/nanvix-microvm/nanvix-integration-plan.md delete mode 100644 docs/nanvix-microvm/nanvix.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 24953d0e9..bde8708ab 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -21,7 +21,7 @@ Building the C# SDK (`sdk/dotnet/`) additionally requires the .NET SDK; the proj build.bat # Release build for current architecture build.bat --debug # Debug build build.bat --all # Release build for both x64 and ARM64 -build.bat --with-microvm # Include NanVix micro-VM binaries +build.bat --with-nvx # Include the NVX foundation (Windows x64) ``` ### Full build (Linux) @@ -160,7 +160,7 @@ The Rust workspace (`src/`) implements multiple sandboxing backends behind the ` | AppContainer | `wxc-exec.exe` | Windows | `backends/appcontainer/common/src/appcontainer_runner.rs` | | BaseContainer (OS sandbox API) | `wxc-exec.exe` | Windows | `backends/appcontainer/common/src/base_container_runner.rs` — prefers `CreateProcessSecurityEnvironment` with PSEC whenever its runtime probe succeeds and the requested policy is compatible, independent of schema version. PSEC is the only path that receives schema 0.8 egress filters, runtime proxy, proxy peer identity, and host-loopback configuration. When PSEC is unavailable or policy-incompatible, MXC tries `Experimental_CreateProcessInSandbox`; SBOX is selected only when its legacy FlatBuffer contract can represent the request without dropping those PSEC-only features, otherwise selection continues to the AppContainer tiers. SBOX keeps legacy proxy behavior but rejects schema 0.8 runtime proxy. `captureDenials` prefers the complete compatible PSEC + V2 Learning Mode path; when that path cannot fully honor a request, MXC retains the highest compatible legacy tier and pairs it with guarded WPR using exact handle-attested process scope. Both providers honor explicit `retainEtl` after a terminal wait; abandoned processes discard the trace. | | Windows Sandbox | `wxc-exec.exe` | Windows | `backends/windows_sandbox/lifecycle/src/` (live transient one-shot `WindowsSandboxRunner` + state-aware `StatefulSandboxBackend`). Experimental — requires `--experimental`. Supports both **one-shot** (a fresh, disposable VM per invocation with guaranteed teardown, via `ScriptRunner`) and **state-aware** (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. State-aware holds a single live VM across separate `wxc-exec` phase processes behind a persistent detached host-side daemon (`backends/windows_sandbox/daemon/`); the OS enforces a single running Windows Sandbox VM per host, so the daemon owns it and reclaims an orphaned VM on restart only via positive process-identity proof. The shared boot sequence (write per-launch nonce, launch VM, capture ownership proof, wait rendezvous, connect) lives in `backends/windows_sandbox/lifecycle/src/vm.rs::launch_managed_vm`; each mode plugs in its own `LaunchObserver` for the per-caller ownership / proof bookkeeping. Honors `readwritePaths`/`readonlyPaths`/`deniedPaths` (HOST paths) at provision via `.wsb` `` entries (mapped at the same absolute host path inside the guest; rejects `deniedPaths` equal-to or nested-within a mapped share since `.wsb` has no Deny primitive); filesystem policy is immutable post-provision. Network isolation is enforced unconditionally by the in-guest agent; `network`/`ui` are not honored. ID prefix `wsb` (strict `wsb:<8-hex>` grammar). Per-launch handshake: 32-byte `Nonce` + 1-byte `ChannelRole` tag on every TCP connection (boot + reconnect); the guest pairs accepted sockets by declared role, not by accept order. The guest agent binary `wxc-windows-sandbox-guest.exe` (`backends/windows_sandbox/guest/`) is injected into the VM. | -| MicroVM (NanVix) | `wxc-exec.exe` | Windows | `backends/nanvix/runner/src/lib.rs` — feature-gated behind `microvm` | +| NVX | `wxc-exec.exe` | Windows x64 | `backends/nvx/runner/src/lib.rs` — feature-gated behind `nvx`; Phase-One runtime remains unavailable and unadvertised | | Hyperlight | `wxc-exec.exe` | Windows | `backends/hyperlight/common/src/lib.rs` — Hyperlight + Unikraft micro-VM backend | | IsolationSession | `wxc-exec.exe` | Windows | `backends/isolation_session/common/src/` — feature-gated behind `isolation_session`, experimental, uses the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API. Supports both one-shot (single-invocation lifecycle, via `ScriptRunner`) and state-aware (multi-invocation provision/start/exec/stop/deprovision, via `StatefulSandboxBackend`) modes. Rejects all filesystem policy (`readwritePaths`/`readonlyPaths`/`deniedPaths`) at every phase with `policy_validation` — the backend has no host-folder-sharing primitive. Likewise rejects any supplied `ui` policy at every phase on both surfaces (as `policy_validation` on the state-aware surface; one-shot discards the typed variant and surfaces `backend_error` with the reason in the message): the isolation session isolates the *host's* UI from contained code but does not deny it UI capabilities (window creation, GDI and the session's own clipboard all work inside it), so no `ui` posture is truthful here — there is no value combination that could be accepted instead, which is why there is no acknowledgment-style gate as there is for `network`. The check is presence-based via `ContainerPolicy::ui_specified` (twin of `network_specified`) because `UiPolicy`'s defaults are full lockdown, making an explicit lockdown `ui` indistinguishable by value from an absent one. An omitted `ui` is accepted and applies no restriction — the schema's default-deny reading does not hold on this backend. One-shot additionally rejects `lifecycle.destroyOnExit=false` and `lifecycle.preservePolicy=true` — the in-proc API has no session-lifetime knob, and the default `destroyOnExit=true` matches actual behavior so it is accepted; the state-aware parser already rejects the whole `lifecycle` section. The full per-phase honor matrix for both surfaces is in `docs/isolation-session/state-aware-rust.md`. The container's network is unrestricted (outbound open; a process inside can listen on a localhost-reachable port) and MXC has no primitive to filter or deny it, so provision (and one-shot) accept ONLY the canonical unrestricted-network acknowledgment — `network.defaultPolicy=allow` + `network.allowLocalNetwork=true`, no host rules, no proxy, default enforcement — and refuse anything else (including an absent policy, which defaults to the unenforceable deny) with `policy_validation`; post-provision phases reject any supplied network policy (fixed at provision, tracked via `ExecutionRequest.network_specified`) and inherit an absent one. State-aware provision accepts an optional `appId` (a packaged app must pass its Package Family Name in the `PFN:` format, e.g. `PFN:Contoso.App_8wekyb3d8bbwe`; an unpackaged app may pass any string), carried verbatim inside the returned `sandboxId`; the one-shot surface takes no backend configuration at all (a stray `experimental.isolation_session` payload is accepted and ignored). Streams stdout/stderr, forwards stdin, and switches to ConPTY mode when wxc-exec's stdout is a TTY for `spawnSandbox` parity. | | WSLc | `wxc-exec.exe` | Windows | `backends/wslc/common/src/` — feature-gated behind `wslc`, experimental, uses the WSLc SDK (`wslcsdk.dll`, loaded at runtime) to run Linux containers in a WSL2 VM. Supports both one-shot (`WSLContainerRunner`, via `ScriptRunner` + streaming `SandboxBackend`) and state-aware (`state_aware.rs` `WslcStateAwareRunner`, via `StatefulSandboxBackend`) modes. Because the WSLc SDK has **no cross-process re-attach**, state-aware keeps the session (VM) + container warm across separate `wxc-exec` phase processes behind a persistent per-user daemon (`wxc-wslc-daemon.exe`, `backends/wslc/daemon/`) that owns the live `WslcSession`/`WslcContainer` handles; phase processes are thin named-pipe clients (`daemon_client.rs`). The daemon runs all SDK calls on one apartment-affine worker thread (so exec is currently serialized across sandboxes — see `docs/wsl/wslc-state-aware.md`). Honors `readwritePaths`/`readonlyPaths` at provision (→ container volumes) + `network.defaultPolicy` (`Block`→`None`, `Allow`→`Bridged`; networking is all-or-nothing — no per-host filtering, since the container lacks `CAP_NET_ADMIN`); rejects `deniedPaths` nested under a mount and rejects proxy/host-filtering at provision. exec honors `network.proxy` **url-form only** (injected as `HTTP_PROXY`/`HTTPS_PROXY`); start/stop/deprovision reject all policy. Rejects, on **both** surfaces and every phase, the fields it cannot honor: any supplied `ui` (presence-based via `ContainerPolicy::ui_specified`, since the backend has no mechanism to enforce UI restrictions on a container — an omitted `ui` is accepted and applies no restriction, so the schema's default-deny reading does not hold here) and `network.enforcementMode` other than `capabilities` (no `CAP_NET_ADMIN` for in-container rules). Provision/one-shot additionally reject `network.allowLocalNetwork=true` (all-or-nothing networking; the one-shot message points at `experimental.wslc.portMappings`, which the state-aware surface does not have), and one-shot rejects `lifecycle.preservePolicy=true` and `lifecycle.destroyOnExit=false` (the container is session-scoped and the session dies with the one-shot process, so `false` cannot be honored; only the default `true` matches actual behavior). Every rejection aborts before any container is created. ID prefix `wslc` (`wslc:<32-hex>`). Idle-timeout is env-overridable via `MXC_WSLC_DAEMON_IDLE_TIMEOUT_SECS`/`MXC_WSLC_DAEMON_IDLE_POLL_SECS`. See `docs/wsl/wslc-state-aware.md`. | @@ -217,7 +217,7 @@ Per-backend guides: - `docs/wsl/wsl-container-getting-started.md` / `docs/wsl/wsl-container-support-plan.md` — WSL Container (WSLC SDK) - `docs/wsl/wslc-state-aware.md` — WSLc state-aware lifecycle (daemon-backed warm reuse, per-phase policy honor matrix, `wxc-wslc-daemon.exe`, idle-timeout env overrides) - `docs/wsl/wslc-sdk-bindings.md` — WSLC SDK FFI bindings: `src/backends/wslc/common/src/wslcsdk_sys.rs` is **generated** by bindgen from `wslcsdk.h` (do NOT hand-edit); `wslc_bindings.rs` is a thin facade over it. On every WSLC SDK version bump, regenerate via `scripts/generate-wslc-bindings.ps1` (needs libclang + `bindgen-cli`, required only on the regen machine — normal/CI builds need neither) and commit the regenerated file with the `WSLC_SDK_VERSION` + hash change. See the doc for the full runbook. -- `docs/nanvix-microvm/nanvix.md` / `docs/nanvix-microvm/nanvix-integration-plan.md` — MicroVM via NanVix +- `docs/nvx-backend-gaps.md` — NVX compatibility results, integration boundary, and remaining runtime work State-aware lifecycle: @@ -245,19 +245,19 @@ The workspace is organized into six top-level directories under `src/`: | Directory | Purpose | Examples | |-----------|---------|----------| | `core/` | Cross-platform foundation + per-platform aggregator binaries | `wxc_common/`, `wxc/`, `lxc/`, `mxc_darwin/`, `mxc_engine/`, `mxc-sdk/`, `mxc_pty/`, `mxc_build_common/`, `learning_mode_core/`, `generated/` | -| `backends/` | Backend-specific code (one subfolder per containment backend or backend support component) | `appcontainer/common`, `windows_sandbox/{daemon,guest,common,lifecycle}`, `isolation_session/{bindings,common}`, `learning_mode/windows`, `hyperlight/common`, `nanvix/{common,build_common,binaries,runner}`, `lxc/common`, `bubblewrap/common`, `wslc/common`, `seatbelt/common` | +| `backends/` | Backend-specific code (one subfolder per containment backend or backend support component) | `appcontainer/common`, `windows_sandbox/{daemon,guest,common,lifecycle}`, `isolation_session/{bindings,common}`, `learning_mode/windows`, `hyperlight/common`, `nvx/{common,build_common,binaries,runner}`, `lxc/common`, `bubblewrap/common`, `wslc/common`, `seatbelt/common` | | `ffi/` | Foreign-function-interface crates (C ABI for language bindings) | `mxc_ffi/` | | `host/` | Host-side utilities | `wxc_host_prep/`, `wxc_winhttp_proxy_shim/` | | `testing/` | Test infrastructure crates | `wxc_e2e_tests/`, `wxc_test_driver/`, `wxc_test_proxy/`, `unix_test_proxy/`, `wxc_ui_probe/`, `fuzz/` | | `tools/` | Developer/diagnostic tools | `mxc_diagnostic_console/` | - `wxc_common` is the **cross-platform foundation**: config parsing, models, errors, logger, `ScriptRunner` / `StatefulSandboxBackend` traits, state-aware dispatch helpers, validators, ids, ui-policy, encoding. Plus a few thin Windows API helpers shared by host tools and backends (`process_util`, `string_util`, `filesystem_dacl`, `diagnostic`). It must not depend on any `backends/*` crate. -- Each Windows containment backend lives in its own `backends/*/common` crate (e.g. `appcontainer_common`, `windows_sandbox_common`, `isolation_session_common`, `hyperlight_common`, `nanvix_runner`). Backend crates depend on `wxc_common`; there are no cross-edges between backend crates. Windows Sandbox additionally has `windows_sandbox_lifecycle`, which owns the one-shot and state-aware runners and depends on `windows_sandbox_common` for the wire protocol, plus separate daemon and guest binaries. +- Each Windows containment backend lives in its own `backends/*/common` crate (e.g. `appcontainer_common`, `windows_sandbox_common`, `isolation_session_common`, `hyperlight_common`, `nvx_common`). Backend crates depend on `wxc_common`; there are no cross-edges between backend crates. Windows Sandbox additionally has `windows_sandbox_lifecycle`, which owns the one-shot and state-aware runners and depends on `windows_sandbox_common` for the wire protocol, plus separate daemon and guest binaries. - `learning_mode_core` is the cross-platform learning-mode denial model and output layer. It owns denial types, summaries, analyzer abstractions, plain-JSON document emission, and the serializable output-pointer type, and must not depend on any `backends/*` crate. - `learning_mode_windows` (`backends/learning_mode/windows`) is a Windows-only backend support crate for the AppInfo-brokered Learning Mode APIs in `processmodel.dll`. It runtime-resolves the Learning Mode trace and process security-environment exports, owns their typed handle/lifecycle wrappers, decodes sealed ETL traces through `learning_mode_core`, and process-scopes guarded WPR retention with Windows Trace Relogger using exact job-attested PID/creation/exit `FILETIME` ranges. It depends on `wxc_common` plus `learning_mode_core`; runner integration consumes it from the AppContainer backend layer. The trace contract is `HRESULT Start` + retryable `HRESULT Stop` + infallible `Close`: `Stop` never consumes the trace handle, and every started trace must be closed exactly once (closing without stopping is the early-exit discard path). The process security-environment contract is `HRESULT Create` + infallible by-value `Close` and consumes a PSEC 1.0 FlatBuffer, not the legacy SBOX buffer; generated PSEC bindings live in `core/generated/process_security_environment_specification`. - `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR operations; the retained elevated guardian accepts authenticated attach and stop/analyze/discard controls over unique local PID-checked named pipes and uses the compiled-in profile from protected fixed-volume ProgramData scratch. WPR's host-wide source ETL never crosses the privilege boundary: after terminal job tracking, the guardian relogs a separate ETL containing only supported Learning Mode events from exact handle-attested process generations, analyzes that filtered ETL, and returns its bytes only when explicitly retained. Relogging failure transfers no trace. Successful authenticated stop/discard disarms the child before releasing the PLM singleton. Owner death, pipe break, or another uncertain control failure preserves the recovery marker and deliberately leaves WPR untouched for administrator recovery. - `wxc`, `lxc`, and `mxc_darwin` are thin binary crates (`wxc-exec` / `lxc-exec` / `mxc-exec-mac`) that wire up CLI args (`clap`), load/validate config, handle maintenance modes (`--probe`, `--delete`, `--setup-*`, `--audit`), and **delegate all backend dispatch to `mxc_engine`**. They contain no `match request.containment` of their own. `wxc-exec` additionally owns the Windows Ctrl-C / DACL-cleanup / telemetry orchestration around the engine call. Its `--audit` compatibility workflow synthesizes allow-mode `captureDenials` with ETL retention, then generates policy-authoring artifacts from the actionable denials document returned by the selected engine backend. -- `mxc_engine` is the **single execution engine** — the one home for "given an `ExecutionRequest`, run it". It owns: run-to-completion backend selection (`run` / `resolve_runner`, covering **all** backends, incl. the Windows ProcessContainer BaseContainer/AppContainer BFS/DACL fallback tiers via `appcontainer_common::dispatcher::dispatch_with_fallback`, and every experimental backend, feature-gated); streaming (`spawn` → `Box`); state-aware lifecycle dispatch (`run_state_aware`, including Windows Sandbox and IsolationSession); host probing (`platform_support` / `PlatformSupport`); and config building (`build_request` / `build_request_with_containment`, `SandboxPolicy` + sections, `available_tools_policy`/`user_profile_policy`/`temporary_files_policy`). It depends on the backend crates (cfg-split: appcontainer/windows_sandbox lifecycle/isolation_session/wslc/nanvix on Windows, bubblewrap/lxc/nanvix on Linux, seatbelt on macOS) so it can't live in `wxc_common`. Both the executor binaries and `mxc-sdk` call into it. `ResolvedRunner` carries the boxed runner plus (Windows only) the optional `DaclManager` guard, so `wxc-exec` can park the guard for its signal handler. +- `mxc_engine` is the **single execution engine** — the one home for "given an `ExecutionRequest`, run it". It owns: run-to-completion backend selection (`run` / `resolve_runner`, covering **all** backends, incl. the Windows ProcessContainer BaseContainer/AppContainer BFS/DACL fallback tiers via `appcontainer_common::dispatcher::dispatch_with_fallback`, and every experimental backend, feature-gated); streaming (`spawn` → `Box`); state-aware lifecycle dispatch (`run_state_aware`, including Windows Sandbox and IsolationSession); host probing (`platform_support` / `PlatformSupport`); and config building (`build_request` / `build_request_with_containment`, `SandboxPolicy` + sections, `available_tools_policy`/`user_profile_policy`/`temporary_files_policy`). It depends on the backend crates (cfg-split: appcontainer/windows_sandbox lifecycle/isolation_session/wslc/nvx on Windows, bubblewrap/lxc on Linux, seatbelt on macOS) so it can't live in `wxc_common`. Both the executor binaries and `mxc-sdk` call into it. `ResolvedRunner` carries the boxed runner plus (Windows only) the optional `DaclManager` guard, so `wxc-exec` can park the guard for its signal handler. - `mxc-sdk` is the **public Rust SDK** — a thin facade over `mxc_engine`. Build a `SandboxRequest` with `build_request`, then either `run(request)` (run-to-completion; returns an `Output` with the `WaitOutcome`, captured @@ -283,7 +283,7 @@ The workspace is organized into six top-level directories under `src/`: - `mxc_pty` is the shared pty bridge used by the LXC backend (`lxc_common::lxc_bindings::attach_run`) so the inner shell sees a real TTY and host stdio is streamed live. (Seatbelt and Bubblewrap no longer use it: they spawn directly and let the child inherit the host's stdio — a TTY when the executor binary runs under a pty — via `SandboxBackend::spawn(StdioMode::Inherit)`.) - `learning_mode_core` is the **cross-platform learning-mode / captureDenials model + output emitter**: `DeniedResource` (+ `ResourceType`/`AccessType`), `DenialSummary`, the `DenialAnalyzer` decode trait, actionable `DenialsDocument` emission, bounded sensitive-value-redacted `VerboseLoggingDocument` emission, and transactional paired-file write/relocation helpers. Every successful analysis writes the actionable artifact plus its deterministic `*.verbose.json` sibling; relocation reports destination commit separately from source-cleanup warnings so callers keep metadata truthful. The crate also defines the serializable `DenialsOutputPointer` and carries no OS-specific code (it must not depend on any `backends/*` crate). The Windows ETL decoder implementing `DenialAnalyzer` lives in `backends/learning_mode/windows`. When `processContainer.captureDenials` is set, native PSEC/V2 seals and decodes a managed ETL locally, while guarded WPR relogs its host-wide source into a process-scoped ETL before analysis; both routes write the same output pair through shared plumbing and return neutral `wxc_common` metadata. Explicit `retainEtl` preserves the native sealed trace or the guarded process-scoped relogged trace after a terminal wait; abandonment discards it. `wxc-exec` serializes the metadata as the one-line stderr pointer at the CLI boundary; Rust/C#/FFI callers receive it programmatically. Each actionable denial's `resource` field holds an authorable file path, UI category, or AppContainer capability name; registry writes and recognized Section, SymbolicLink, and Timer checks remain verbose-only because MXC has no corresponding config grants. Capability denials resolve their capability SID to a friendly name via `backends/learning_mode/windows`'s `capability_names` (well-known `S-1-15-3-…` SID → policy name; custom hashed SIDs fall back to the SID string). - `mxc_build_common` is a build-time helper crate — all Windows binary crates use it in their `build.rs` to embed VersionInfo (ProductName, FileDescription, copyright, version+commit). When adding a new Windows binary crate, add `mxc_build_common` as a build-dependency and call `mxc_build_common::embed_version_info()` from `build.rs` -- `nanvix_build_common` is a **build-only** helper crate (never linked into the runtime): it stages NanVix binaries next to the executable and resolves the `NANVIX_BIN` prefetch directory. The `nanvix_binaries`, `wxc`, and `lxc` build scripts consume it as a `[build-dependencies]` entry. Runtime constants it needs (binary/snapshot filenames) stay in `nanvix_common`. Keep build-only file-staging logic here, not in `nanvix_common` (which is a runtime dependency of `nanvix_runner`). +- `nvx_build_common` is a **build-only** helper crate (never linked into the runtime): it validates the Windows x64 target, stages the pinned NVX platform bundle next to the executor, and resolves the `NVX_BIN` offline override. `nvx_binaries` and `wxc` consume it; Linux has no NVX backend in Phase One. - Platform-specific modules use `#[cfg(target_os = "windows")]` / `#[cfg(target_os = "linux")]` - Workspace edition is 2021; shared dependencies are declared in the root `Cargo.toml` `[workspace.dependencies]` @@ -298,7 +298,7 @@ The parser deserializes JSON directly into the typed wire model (`wxc_common::wi ### Binary naming -- Windows: `wxc-exec.exe` (AppContainer / Windows Sandbox / MicroVM); `wxc-host-prep.exe` (host setup — see `docs/host-prep.md`) +- Windows: `wxc-exec.exe` (AppContainer / Windows Sandbox / NVX foundation); `wxc-host-prep.exe` (host setup — see `docs/host-prep.md`) - Linux: `lxc-exec` (LXC containers) - macOS: `mxc-exec-mac` (Seatbelt) - Target triples: `x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc`, `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `aarch64-apple-darwin` diff --git a/.github/workflows/issue-triage.md b/.github/workflows/issue-triage.md index 8843772f5..2a3022d61 100644 --- a/.github/workflows/issue-triage.md +++ b/.github/workflows/issue-triage.md @@ -90,7 +90,7 @@ Assign matching owner(s) with `assign_to_user` using this map: | @mgudgin | AppContainer / BaseContainer / process isolation/container | | @bbonaby | AppContainer / BaseContainer / process isolation/container / networking / firewall / DNS / proxy / iptables | | @SohamDas2021 | Linux / LXC / WSLC / Bubblewrap (bwrap) / proxy on Linux / iptables | -| @huzaifa-d | MicroVM / NanVix / Hyperlight / Windows Sandbox | +| @huzaifa-d | MicroVM / NVX / Hyperlight / Windows Sandbox | | @adpa-ms | IsolationSession / session isolation | | @richiemsft | macOS / Seatbelt | | @mgudgin | SDK configuration and policy (Area-SDK-Configuration, Area-SDK-Policy) | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f7ac4dc47..2f9f59b29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,7 +143,7 @@ MXC has a Rust core (under `src/`) and a TypeScript SDK (under `sdk/node/`). The build.bat :: Release build for current architecture build.bat --debug :: Debug build build.bat --all :: Release build for both x64 and ARM64 -build.bat --with-microvm :: Include NanVix micro-VM binaries +build.bat --with-nvx :: Include the NVX foundation (Windows x64) ``` **Linux** — `./build.sh`: diff --git a/README.md b/README.md index 19452a8ff..d500bc35e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ MXC is a **sandboxed code execution system** for running untrusted code (model o - **Cross-platform**: Windows, Linux, and macOS support with platform-appropriate containment backends - **JSON-based Configuration**: Define execution parameters and security policies via a versioned JSON schema -- **Multiple Containment Backends**: ProcessContainer, Windows Sandbox, LXC, Bubblewrap, Seatbelt (macOS), MicroVM (NanVix), Hyperlight, IsolationSession, and WSLC +- **Multiple Containment Backends**: ProcessContainer, Windows Sandbox, LXC, Bubblewrap, Seatbelt (macOS), NVX, Hyperlight, IsolationSession, and WSLC - **Policy-driven Sandboxing**: - **Filesystem Policy**: Read-only and read-write path lists (denied paths not yet supported on Windows) - **Network Policy**: Proxy support (cooperative on Linux/macOS), allow/block outbound, and backend-dependent host filtering @@ -26,12 +26,12 @@ MXC ships a native container wrapper plus a TypeScript SDK — see the [SDK READ | Platform | Default backend | Other backends | Minimum build | | --- | --- | --- | --- | -| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `microvm`, `hyperlight`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | -| Linux x64 / ARM64 | `bubblewrap` | `lxc`, `microvm`, `hyperlight` | — | +| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `nvx`, `hyperlight`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | +| Linux x64 / ARM64 | `bubblewrap` | `lxc`, `hyperlight` | — | | macOS ARM64 / x64 (schema `0.7.0-alpha`+) | `seatbelt` | — | — | -The stable one-shot backends (`processcontainer`, `bubblewrap`, `lxc`, and `seatbelt`) do not require experimental mode; Linux hosts also need the matching runtime installed: bwrap (Bubblewrap) for the default backend, or the lxc toolset for the lxc backend. **Experimental backends** (`windows_sandbox`, `wslc`, `microvm`, `isolation_session`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` or the `--experimental` CLI flag. +The stable one-shot backends (`processcontainer`, `bubblewrap`, `lxc`, and `seatbelt`) do not require experimental mode; Linux hosts also need the matching runtime installed: bwrap (Bubblewrap) for the default backend, or the lxc toolset for the lxc backend. **Experimental backends** (`windows_sandbox`, `wslc`, `nvx`, `isolation_session`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` or the `--experimental` CLI flag. For which filesystem, network, and UI-restriction policy aspects the Windows `processcontainer` backend can enforce on each Windows 11 release (23H2 / 24H2 / 25H2 / 25H2+), see [Windows OS-version policy support](./docs/process-container/os-version-support.md). @@ -61,19 +61,18 @@ scripts/ Build and utility scripts build.bat # Release build for current architecture build.bat --debug # Debug build build.bat --all # Release build for both x64 and ARM64 -build.bat --with-microvm # Include NanVix micro-VM binaries build.bat --with-nvx # Include the incomplete NVX foundation (Windows x64) ``` -`--with-nvx` is additive to the existing `--with-microvm` path. It acquires the -pinned NVX platform archive at build time and verifies every staged file by +`--with-nvx` acquires the pinned NVX platform archive at build time and verifies every staged file by SHA-256; sandbox launches never download artifacts. Set `NVX_BIN` to a pre-fetched, checksum-verifiable bundle directory for an offline build. The current pin, `v0.1.0-dev.5c86da3dff02`, contains only `openvmm.exe`, the guest kernel, and the guest initramfs, not the workload-image bundle. -This foundation adds `containment: "nvx"` to the development schema while -retaining `microvm`. An NVX-enabled binary parses the new value, but execution +The experimental NanVix-backed `microvm` containment value was removed. Use +`containment: "nvx"` and build with `--with-nvx`. An NVX-enabled binary parses +the new value, but execution returns a typed backend-unavailable error and capability probes do not advertise NVX. Runtime work remains blocked on NVX-produced distro/runtime EROFS images and scratch image, a proven combined managed-sandbox/virtio-fs contract, and the diff --git a/docs/backend-support-probe-api-plan.md b/docs/backend-support-probe-api-plan.md index bc814572f..db598df9a 100644 --- a/docs/backend-support-probe-api-plan.md +++ b/docs/backend-support-probe-api-plan.md @@ -98,7 +98,7 @@ for them **cannot be built just yet**. | --- | --- | --- | --- | | `windows_sandbox` | DISM/registry check of the *Containers-DisposableClientVM* optional feature | only a private "is the `.exe` on disk" check | reports available when the feature is off → launch fails | | `isolation_session` | activation of the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` runtime class succeeds (the API class is registered on the OS **and** its OS feature gate is on) **and** the backend feature is compiled | as of #761, detection queries whether the API class is registered rather than gating on a build number; a `CLASS_E_CLASSNOTAVAILABLE` / `REGDB_E_CLASSNOTREG` activation failure means unavailable | none for false-availability now — a machine without the API registered fails activation cleanly; still needs a cheap probe seam so callers don't have to attempt a real activation | -| `microvm` | feature compiled, NanVix runtime files staged, and WHP usable on Windows or `/dev/kvm` readable/writable on Linux | nothing | checking only a hypervisor can report availability when required runtime files are missing | +| `nvx` | feature compiled, pinned NVX runtime files staged, and WHP usable on Windows x64 | nothing | checking only a hypervisor can report availability when required runtime files are missing | | `hyperlight` | hypervisor present + feature compiled | nothing | same VM-boot risk | ### 4.2 The parity rule: detect once, project into TS @@ -132,7 +132,7 @@ See §7.9 for why the canonical probe stays in Rust rather than moving into the guarding against drift between this API and the tier ladder. - On Linux, `bubblewrap` and `lxc` each appear when their check passes (`bwrap --version` / `lxc-ls --version`). - `wslc` appears when `WslcSdk::load()` resolves `wslcsdk.dll`; the remaining VM group -(`windows_sandbox`, `isolation_session`, `microvm`, `hyperlight`) never appears until its detector lands. +(`windows_sandbox`, `isolation_session`, `nvx`, `hyperlight`) never appears until its detector lands. - The TypeScript `getPlatformSupport()` output matches the native probe (parity by projection, §4.2), guarding against the two layers drifting. @@ -141,7 +141,7 @@ guarding against drift between this API and the tier ladder. Writing the missing detectors and wiring the TypeScript projection, one issue each: 1. `windows_sandbox` - optional-feature (DISM/registry) detector. 2. `isolation_session` - probe whether the `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API class is registered on the OS (activation-factory resolvable), replacing the old build-number gate (see #761), and expose it to Rust. -3. `microvm` / `hyperlight` - hypervisor-presence probe. +3. `nvx` / `hyperlight` - hypervisor-presence probe. 4. `lxc` - port the `lxc-ls` presence check from TypeScript to Rust, so the probe (not just the SDK) can report it (§4.2, step 1). 5. TypeScript projection - make `getPlatformSupport()` read the native backend availability via a diff --git a/docs/bwrap-support/bubblewrap-backend-plan.md b/docs/bwrap-support/bubblewrap-backend-plan.md index 748043db9..dd7b1501d 100644 --- a/docs/bwrap-support/bubblewrap-backend-plan.md +++ b/docs/bwrap-support/bubblewrap-backend-plan.md @@ -48,7 +48,7 @@ Plus optional `validate_runner()`. This is a perfect fit for bwrap, which is fun Add `"bubblewrap"` to the `containment` enum: ```json "containment": { - "enum": ["process", "processcontainer", "windows_sandbox", "lxc", "microvm", + "enum": ["process", "processcontainer", "windows_sandbox", "lxc", "nvx", "wslc", "seatbelt", "isolation_session", "bubblewrap"] } ``` @@ -228,10 +228,10 @@ Add `bwrap_common` dependency to `lxc/Cargo.toml`. ```typescript export type ContainmentBackend = | 'processcontainer' | 'windows_sandbox' | 'wslc' - | 'lxc' | 'microvm' | 'seatbelt' | 'isolation_session' + | 'lxc' | 'nvx' | 'seatbelt' | 'isolation_session' | 'bubblewrap'; // ← add -export const ExperimentalBackends = ['microvm', 'wslc', 'seatbelt', 'bubblewrap']; +export const ExperimentalBackends = ['nvx', 'wslc', 'seatbelt', 'bubblewrap']; ``` **`sdk/node/src/platform.ts`:** diff --git a/docs/ci-validation-infrastructure.md b/docs/ci-validation-infrastructure.md index 9687fadc6..91b6b3d53 100644 --- a/docs/ci-validation-infrastructure.md +++ b/docs/ci-validation-infrastructure.md @@ -106,14 +106,14 @@ Current platforms: | Platform id | Family | x64 pool | arm64 pool | Declared backends (x64) | |-------------|--------|----------|------------|--------------------------| -| `windows-prerelease-process-container` | windows | `1es-mxc-windows-prerelease-t1-x64` | *(dormant)* | process-t1, process-t3, isolation-session, wslc, windows-sandbox, microvm, hyperlight | +| `windows-prerelease-process-container` | windows | `1es-mxc-windows-prerelease-t1-x64` | *(dormant)* | process-t1, process-t3, isolation-session, wslc, windows-sandbox, hyperlight | | `windows-prerelease-isolation-session` | windows | *(dormant)* | *(dormant)* | same as above | | `windows-canary` | windows | *(dormant)* | *(dormant)* | same as above | -| `windows-25h2` | windows | `1es-mxc-e2e-windows-25h2-pro-x64` | *(dormant)* | process-t3, wslc, windows-sandbox, microvm, hyperlight | -| `windows-24h2` | windows | `1es-mxc-e2e-windows-24h2-pro-x64` | *(dormant)* | process-t3, wslc, windows-sandbox, microvm, hyperlight | -| `windows-23h2` | windows | `1es-mxc-e2e-windows-23h2-enterprise-x64` | *(dormant)* | process-t3, wslc, windows-sandbox, microvm, hyperlight | +| `windows-25h2` | windows | `1es-mxc-e2e-windows-25h2-pro-x64` | *(dormant)* | process-t3, wslc, windows-sandbox, hyperlight | +| `windows-24h2` | windows | `1es-mxc-e2e-windows-24h2-pro-x64` | *(dormant)* | process-t3, wslc, windows-sandbox, hyperlight | +| `windows-23h2` | windows | `1es-mxc-e2e-windows-23h2-enterprise-x64` | *(dormant)* | process-t3, wslc, windows-sandbox, hyperlight | | `ubuntu-26.04` | linux | `1es-mxc-e2e-ubuntu-26.04-x64` | *(dormant)* | bubblewrap, hyperlight, lxc | -| `ubuntu-24.04` | linux | `1es-mxc-e2e-ubuntu-24.04-x64` | *(dormant)* | bubblewrap, microvm, hyperlight, lxc | +| `ubuntu-24.04` | linux | `1es-mxc-e2e-ubuntu-24.04-x64` | *(dormant)* | bubblewrap, hyperlight, lxc | | `rhel-10` | linux | `1es-mxc-e2e-rhel-10-x64` | *(dormant)* | bubblewrap, hyperlight, lxc | | `debian-13` | linux | `1es-mxc-e2e-debian-13-x64` | *(dormant)* | bubblewrap, hyperlight, lxc | | `macos-26` | macos | — | runner `macos-26` | seatbelt | @@ -215,7 +215,6 @@ get fixed or wired. | WSLC | ✅ Good | Might have to retry hung jobs - this is an issue with overzealous agent reclaiming. | | IsolationSession | ✅ Good | | | Windows Sandbox | ⛔ Blocked | Images don't support `Containers-DisposableClientVM` opt. feature | -| MicroVM | ⛔ Not working | Windows cold and warm starts hang; no Linux suite. The artifact payload is currently commented out in the build jobs. | | Hyperlight | ⛔ Not implemented | No suite on any platform. | | Seatbelt | ⛔ Not implemented | The backend itself is healthy; there is no official E2E suite to dispatch to. | @@ -229,9 +228,6 @@ every entry. - `process-t3` — runs `wxc-host-prep.exe prepare-system-drive` and `prepare-null-device --no-sacl`. -- `microvm` — asserts the NanVix payload is in the artifact, adds a Defender - exclusion for the binary directory, and requires the Windows Hypervisor - Platform feature *and* a running hypervisor. - `wslc` — asserts `wslcsdk.dll` shipped, requires the WSL and VirtualMachinePlatform optional features to be baked into the image, then installs/updates the WSL runtime (including the pre-release ring) up to the @@ -258,7 +254,6 @@ one: `Repair-Winget` re-registers the App Installer package when `winget` is on for `lxcbr0`, enables bridge netfilter, and makes sure the bridge's NAT rule is in place. On RHEL-likes it needs EPEL first, because Red Hat dropped LXC after RHEL 7 and ships no replacement. -- `microvm` — asserts the NanVix payload exists. - `hyperlight` — no-op. Every install above goes through two shared helpers rather than its own @@ -476,7 +471,7 @@ cron *and* a job condition *and* a dispatch choice. Set the ARM64 `pool` for the platform *and* remove or narrow `suppressNonMacArm64` in the resolver. Note that the resolver rejects -`hyperlight` and `microvm` on ARM64 outright (x64-only runtimes), and the WSLC +`hyperlight` on ARM64 outright (x64-only runtime), and the WSLC dispatcher still refuses non-x64. ## Testing Your Changes to the Validation Infrastructure diff --git a/docs/isolation-session/oneshot.md b/docs/isolation-session/oneshot.md index 720cb1774..073447685 100644 --- a/docs/isolation-session/oneshot.md +++ b/docs/isolation-session/oneshot.md @@ -317,7 +317,7 @@ A test runner at `tests/scripts/run_isolation_session_tests.ps1` invokes both configs via `wxc-exec.exe --experimental`, validates exit codes and expected output substrings, and reports a pass/fail summary. Pattern follows the existing per-backend integration scripts (e.g. -`run_microvm_tests.ps1`, `run_wslc_all_tests.ps1`). +`run_wslc_all_tests.ps1`). The script must run **interactively** on the test host. The OS-side service's calling-process identity check rejects network-logon tokens, so diff --git a/docs/linux-wsl-roadmap-june-2026.md b/docs/linux-wsl-roadmap-june-2026.md index 0e572a7b1..ef914b4e5 100644 --- a/docs/linux-wsl-roadmap-june-2026.md +++ b/docs/linux-wsl-roadmap-june-2026.md @@ -503,7 +503,7 @@ These items depend on the WSLC SDK team and are not unilaterally schedulable. > **Why network enforcement must be container-scoped (host vs. VM vs. container).** Network policy can be enforced at three layers: the Windows **host** (Windows Firewall), the WSL2 **VM**, or the **container** network namespace inside the VM. GA decision **D6 (per-sandbox scoping)** requires every sandbox's policy to be independent — concurrent WSLC containers must not affect each other's access — and names the container network namespace as WSLC's scoping identity. A machine-wide **host** firewall can't attribute traffic to one container vs. another, so it violates D6 (and per **D8**, host firewalls apply *on top of* enforcement, never *as* it). A **VM-wide** rule fails the same way when one utility VM hosts multiple containers — sandbox A's rules would bleed into sandbox B. Only the **container namespace** is inherently per-sandbox, which is why it's the required enforcement point. The catch: MXC can't install rules into that namespace today (`Privileged` doesn't grant `CAP_NET_ADMIN`, and the VM may lack iptables tooling). Hence SDK dep #1 — a VM-level API that applies rules **scoped to a specific container's namespace**: physically enforced at the VM boundary, logically attributed to one container. > -> **Contrast with Hyperlight/Nanvix, and the state-aware wrinkle.** Hyperlight (host-proxied sockets, per-instance) and Nanvix (per-guest egress filter) get D6 scoping for free because each sandbox *is* its own VM instance/process — no shared surface to bleed across. WSLC today is also effectively 1 sandbox : 1 VM (the one-shot flow creates a session, one container, then tears it down), but the highest-value WSLC optimization — **state-aware session reuse** (Misc #29), keeping a warm VM to amortize startup cost — makes one VM host **multiple** containers, at which point a host- or VM-wide rule genuinely bleeds across co-resident sandboxes. That is exactly when namespace-scoped enforcement (SDK dep #1) stops being merely cleaner and becomes mandatory. +> **Contrast with Hyperlight/NVX, and the state-aware wrinkle.** Hyperlight (host-proxied sockets, per-instance) and NVX (per-guest egress filter) get D6 scoping for free because each sandbox *is* its own VM instance/process — no shared surface to bleed across. WSLC today is also effectively 1 sandbox : 1 VM (the one-shot flow creates a session, one container, then tears it down), but the highest-value WSLC optimization — **state-aware session reuse** (Misc #29), keeping a warm VM to amortize startup cost — makes one VM host **multiple** containers, at which point a host- or VM-wide rule genuinely bleeds across co-resident sandboxes. That is exactly when namespace-scoped enforcement (SDK dep #1) stops being merely cleaner and becomes mandatory. --- diff --git a/docs/nanvix-microvm/nanvix-integration-plan.md b/docs/nanvix-microvm/nanvix-integration-plan.md deleted file mode 100644 index 4df3f94fb..000000000 --- a/docs/nanvix-microvm/nanvix-integration-plan.md +++ /dev/null @@ -1,394 +0,0 @@ -# MXC NanVix Integration — Design Document - -## Problem - -MXC (Microsoft eXecution Container) runs untrusted code in sandboxed environments. Today it supports multiple backends: **AppContainer** (process-level isolation), **Windows Sandbox** (full VM), **LXC** and **WSLC** (Linux containers via WSL). - -There is a need for a **micro-VM backend** that can execute various forms of code generated by agents — including Python, JavaScript, C, C++, and Rust applications — with full hardware isolation. - -## Proposed Solution - -Add a **NanVix micro-VM backend** directly into the existing `wxc-exec.exe` binary. When the JSON config specifies `"containment": "microvm"`, the binary routes to a new `NanVixScriptRunner` (NanVix is the implementation behind the `microvm` containment). The runner stages the user script into a temporary host directory, spawns `nanvixd.exe` with that directory bind-mounted into the guest, and relays stdout/stderr directly to the parent process. The guest's stdin is closed (`Stdio::null()`) — the script is loaded from the mounted staging directory, not piped over stdin. - -**NanVix** is a lightweight microkernel OS that runs inside a WHP (Windows Hypervisor Platform) virtual machine. It provides POSIX-compatible process execution with hardware-enforced isolation. NanVix supports multiple runtimes — the initial integration uses CPython 3.12 with a trimmed FAT32 stdlib filesystem, but the architecture supports JavaScript (QuickJS), C, C++, and Rust binaries as configurable runtimes. - -## How It Works - -``` -Path A — CLI (direct): - User: wxc-exec.exe config.json - └── Parses args → loads JSON → dispatches to NanVixScriptRunner - -Path B — SDK (programmatic): - App calls: spawnSandbox("print('hello')", policy, { containment: "microvm" }) - ├── Builds JSON config with containment = "microvm" - └── Spawns wxc-exec.exe with the config - -Both paths converge here: - wxc-exec.exe - ├── Parses JSON config → sees containment = "microvm" - ├── Creates NanVixScriptRunner (via existing Box dispatch) - ├── Validates paths next to wxc-exec.exe: - │ nanvixd.exe, bin/kernel.elf, nanvix_rootfs.img, python3.initrd - ├── Builds a temp staging directory containing bootstrap.py - │ (user script wrapped in a small loader preamble) plus any - │ readwrite/readonly host paths resolved by the policy. - ├── Ensures a WHP warm-start snapshot exists under /snapshots/ - │ (cold-boots once to generate kernel.vmem + kernel.whp.cbor on - │ first run; subsequent runs restore from the snapshot) - ├── Spawns nanvixd.exe as a child process (cwd = snapshot home): - │ nanvixd.exe -snapshot snapshots/kernel.whp.cbor - │ -bin-dir /bin - │ -ramfs /nanvix_rootfs.img - │ -mount - │ -- python3.initrd - ├── stdin is set to Stdio::null() (guest never reads host stdin); - │ stdout/stderr are streamed live to the parent via relay threads. - ├── Starts watchdog thread - ├── Waits for process exit or timeout - ├── Signals watchdog to cancel, joins all threads - └── Returns exit code (stdout/stderr relayed directly to parent) - -Inside the NanVix VM: - nanvixd.exe restores (or cold-boots) a WHP virtual machine: - ├── Loads kernel.elf (NanVix microkernel) from -bin-dir - ├── Loads python3.initrd as initrd payload - ├── Maps nanvix_rootfs.img (FAT32 stdlib) into guest memory - ├── Bind-mounts the host staging directory into the guest - │ (exposes bootstrap.py and any policy-mapped host paths) - ├── Kernel splits cmdline on ';': - │ argv = ["python3.initrd", "/mnt/bootstrap.py"] - │ env = ["PYTHONHOME=/sysroot"] - ├── Python executes bootstrap.py from the mounted staging dir - ├── Script output → stdout (via IKC) → host stdout - ├── Kernel traces → host stderr - └── sys.exit(N) → nanvixd exits N (exit code propagated) -``` - -## Architecture: - -``` - wxc-exec.exe - │ - config_parser.rs - reads "containment" field - │ - ┌──────────────┼──────────────┐ - │ │ │ - AppContainerScript Sandbox NanVix - Runner (existing) ScriptRunner ScriptRunner (new) - │ (existing) │ - AppContainer │ Spawn nanvixd.exe - NTFS ACLs Windows ├── Mount staging dir - WFP firewall Sandbox VM └── Relay stdout/stderr -``` - -## Key Design Decisions - -1. **Staging-directory mount for script delivery** — NanVix's cmdline has a 255-byte limit and splits on spaces, so the script cannot be passed as an argument. Instead, the runner writes a `bootstrap.py` (user script + small loader preamble) into a per-invocation temp directory and bind-mounts that directory into the guest via `nanvixd -mount `. Python then executes `bootstrap.py` from the mount. Host stdin is closed (`Stdio::null()`) — no stdin relay is involved. Zero changes to CPython or NanVix. - -2. **Raw Python source in `process.commandLine` field** — For AppContainer/Sandbox, `process.commandLine` is a shell command. For the microvm backend, it's raw Python source code. The runner handles interpreter invocation internally. This avoids users needing to understand NanVix's cmdline constraints. - -3. **Pre-built artifacts, not built from source** — NanVix binaries (`nanvixd.exe`, `kernel.elf`, `python3.initrd`, `nanvix_rootfs.img`) are downloaded from GitHub pre-releases. MXC does not compile or build NanVix components. - - -4. **Unsupported policies are rejected** — If a config specifies `network` or `processContainer` policies with `containment: "microvm"`, the runner returns a clear error. (Filesystem readwrite/readonly paths are honored via the staging-dir mount.) - -5. **Host-side I/O risk mitigated by IKC framing** — `nanvixd.exe` parses guest I/O via IKC (Inter-Kernel Communication) messages using fixed-size frames with bounds checking. A crafted guest could attempt malformed messages. Mitigation: formal fuzzing (future work). - -6. **Artifact integrity via hash verification** — Guest artifacts (`python3.initrd`, `kernel.elf`, `nanvix_rootfs.img`) are loaded from disk. If an attacker can modify these files, they control the guest. Mitigation: hash verification at install time via setup scripts (future work). - -7. **Timeout = boot + script** — Total timeout is `boot_timeout_ms` (default 60000ms, grace for VM boot + Python init) + `script_timeout` (from JSON `timeout` field). A background watchdog thread terminates the process if the total expires. - -8. **Cleanup guarantee** — On normal exit, timeout, or crash: relay threads joined (stdout/stderr fully drained), watchdog cancelled and joined, `nanvixd.exe` termination releases the WHP partition. No orphaned VMs. - -## Workspace Changes - -``` -mxc/src/ -├── Cargo.toml # Add NanVix to workspace (no new deps) -├── wxc/ -│ ├── Cargo.toml # UNCHANGED -│ └── src/main.rs # Add NanVix match arm (2 lines) -├── wxc_common/ -│ ├── Cargo.toml # UNCHANGED -│ └── src/ -│ ├── lib.rs # Add: pub mod nanvix_runner (1 line) -│ ├── models.rs # Add: NanVixConfig struct, ContainmentBackend::MicroVm -│ ├── wire.rs # Add: MicroVm containment variant (schema source); regenerate schema -│ ├── config_parser.rs # Add: map_wire_containment "microvm" arm -│ ├── error.rs # Add: WxcError::NanVix variant -│ ├── nanvix_runner.rs # NEW — NanVixScriptRunner implementation -│ ├── appcontainer.rs # UNCHANGED -│ ├── windows_sandbox_runner.rs # UNCHANGED -│ ├── script_runner.rs # UNCHANGED -│ └── ... # All other modules UNCHANGED -├── wxc_test_driver/ # UNCHANGED -├── wxc_windows_sandbox_guest/ # UNCHANGED -└── wxc_windows_sandbox_daemon/ # UNCHANGED - -mxc/docs/nanvix-microvm/ -└── nanvix-integration-plan.md # NEW — this document - -mxc/tests/configs/ -└── microvm_hello.json # NEW — example microvm config -``` - -## Error Handling & Output Semantics - -### I/O Model - -**stdout/stderr** are relayed live between nanvixd and the parent via pipe relay threads — the runner does not capture or buffer either stream. **stdin is not relayed**: the runner spawns nanvixd with `stdin = Stdio::null()`, so the guest sees no host input. - -``` -wxc-exec stdin ──╳ (closed; not relayed into the guest) -wxc-exec stdout ◀── nanvixd stdout ◀── guest python stdout -wxc-exec stderr ◀── nanvixd stderr ◀── kernel traces -``` - -**Script delivery via mount, not stdin**: The runner writes `bootstrap.py` (the user script wrapped in a loader preamble) into a per-invocation temp staging directory and passes that directory to `nanvixd -mount `. Inside the guest, Python executes `bootstrap.py` from the mounted directory. The SDK does not write the script to wxc-exec's stdin. - -### Exit Code Propagation - -`sys.exit(42)` inside the guest → `nanvixd.exe` exits with code 42. The runner returns this directly as the process exit code. - -### Error Classification - -The runner classifies errors using a `NanVixError` enum, allowing consumers to match on error types: - -```rust -enum NanVixError { - Preflight(String), // Missing binaries, invalid paths - Platform(String), // WHP unavailable, spawn failure - Runtime(String), // Stdin broken pipe, VM crash - Timeout { // Watchdog killed the process - boot_timeout_ms: u32, - script_timeout_ms: u32, - total_ms: u64, - }, -} -``` - -| Variant | Trigger | Example | -|---------|---------|---------| -| `Preflight` | Path validation before spawn | `nanvixd not found at /path/to/nanvixd.exe` | -| `Platform` | `Command::new()` fails | `Failed to spawn nanvixd: The system cannot find the file specified` | -| `Runtime` | Stdin write or process error | `Failed to write script to nanvixd stdin: Broken pipe` | -| `Timeout` | Watchdog fires | `NanVix execution timed out after 90000ms` | - -All variants are surfaced via stderr output. Preflight and Platform errors prevent the process from spawning (exit code -1). Runtime errors surface as non-zero exit codes from nanvixd. On success, the nanvixd exit code (which reflects the guest Python exit code) is returned directly. - -## Configuration Semantics - -### JSON Config Format - -```json -{ - "process": { - "commandLine": "print('Hello from NanVix!')", - "timeout": 30000 - }, - "containment": "microvm" -} -``` - -### Config Field Mapping - -| JSON Field | Rust Field (`ExecutionRequest`) | NanVix Behavior | -|------------|---------------------------|----------------| -| `process.commandLine` | `script_code: String` | ✅ **Used** — raw Python source code (not a shell command) | -| `process.timeout` | `script_timeout: u32` | ✅ **Used** — script execution timeout in ms | -| `containment` | `containment: ContainmentBackend` | ✅ **Used** — must be `"microvm"` | -| `workingDirectory` | `working_directory: String` | ❌ **Rejected** — guest has its own filesystem namespace | -| `processContainer.*` | `policy: ContainerPolicy` | ❌ **Rejected** — not applicable to NanVix | -| `filesystem.readwritePaths` / `readonlyPaths` | (part of `policy`) | ✅ **Used** — host paths bind-mounted into the guest via the staging dir | -| `network.*` | (part of `policy`) | ❌ **Rejected** — no network stack in guest | -| `sandbox.*` | `sandbox_config: WindowsSandboxConfig` | ❌ **Rejected** — microvm is not Windows Sandbox | - -**Policy validation**: If a config specifies `containment: "microvm"` alongside `network`, `processContainer`, or `workingDirectory` fields, the runner returns an error. - -## Binary Distribution & Versioning - -NanVix artifacts are **NOT bundled** in the MXC npm package. They are distributed as **pre-release binaries** from their respective GitHub repositories: - -| Artifact | Size | Source | -|----------|------|--------| -| `nanvixd.exe` | 7.5 MB | [nanvix/nanvix](https://github.com/nanvix/nanvix) releases | -| `kernel.elf` | 10.5 MB | [nanvix/nanvix](https://github.com/nanvix/nanvix) releases | -| `python3.initrd` | 9.1 MB | [nanvix/cpython](https://github.com/nanvix/cpython) releases | -| `nanvix_rootfs.img` | 35.6 MB | [nanvix/cpython](https://github.com/nanvix/cpython) releases | - -Setup scripts (PowerShell & Bash) will download matching pre-release binaries and verify checksums. - -## Security Model - -### Isolation Comparison - -| Property | AppContainer | Windows Sandbox | NanVix | -|----------|-------------|-----------------|--------| -| **Isolation level** | Process | Full VM (Hyper-V) | Micro VM (WHP) | -| **Host FS access** | Restricted by ACLs | Mapped folders only | None (read-only ramfs) | -| **Network access** | Filtered by firewall | NAT/bridged | None | -| **Writable storage** | Host FS (restricted) | VM disk | None (read-only) | -| **Guest OS** | Windows (host) | Windows (guest) | NanVix microkernel | - -## Development Phases - -### Phase 1 — Backend Implementation - -**Goal:** Add NanVix as a functional containment backend in `wxc-exec.exe`. - -**What changed:** -- `models.rs` — Added `MicroVm` variant to `ContainmentBackend`, added `NanVixConfig` struct, added `nanvix_config` field to `ExecutionRequest` -- `config_parser.rs` — Added `"microvm"` containment parsing and NanVix config section parsing (originally via `Raw*` structs; the parser has since been rewired onto the `wire::MxcConfig` model — new work maps the wire types in `convert_wire_config`) -- `error.rs` — Added `WxcError::NanVix(String)` variant -- `nanvix_runner.rs` — **NEW** — `NanVixScriptRunner` implementing `ScriptRunner` trait -- `lib.rs` — Added `pub mod nanvix_runner` -- `main.rs` — Added `ContainmentBackend::MicroVm` dispatch arm - -**Verified:** Build succeeds, all existing tests pass, E2E test produces correct output. - -### Phase 2 — SDK Types & Platform Detection - -**Goal:** Make the TypeScript SDK aware of NanVix as a backend option. - -**What changes:** -- `sdk/node/src/types.ts` — Add `'microvm'` to `SandboxingMethod` type, add `NanVixConfig` interface -- `sdk/node/src/platform.ts` — Detect NanVix availability via `wxc-exec.exe --check-platform` -- `sdk/node/src/sandbox.ts` — Accept `containment: 'microvm'` in `SandboxSpawnOptions` -- `wxc/src/main.rs` — Add `--check-platform` subcommand returning JSON capabilities - -### Phase 3 — CLI Flags & Setup Scripts - -**Goal:** Let users invoke NanVix from the CLI and set up the runtime. - -**What changes:** -- `wxc/src/main.rs` — Add `--microvm` flag (sets `containment: "microvm"` automatically) -- `scripts/setup-nanvix.ps1` — PowerShell script to download NanVix pre-release binaries -- `scripts/setup-nanvix.sh` — Bash equivalent for WSL/Linux/CI - -### Phase 4 — Testing & Hardening - -**Goal:** Comprehensive test coverage for the NanVix backend. - -**What changes:** -- Integration tests exercising full MXC→nanvixd→Python→output pipeline -- Negative tests: missing modules, network I/O, file writes, timeout -- WHP-conditional test execution (skip on runners without WHP) -- Mock `nanvixd.exe` for fast unit testing of error/timeout paths - -## Supported Workloads - -### Supported - -| Workload | Example | Notes | -|----------|---------|-------| -| Pure computation | `sum(range(1000000))` | Full Python numeric stack | -| String processing | `re.findall(r'\d+', text)` | Regex, string ops, encodings | -| JSON/data manipulation | `json.loads(data)` | json, csv, collections | -| Math/statistics | `math.factorial(100)` | math, decimal, fractions | -| Date/time operations | `datetime.datetime.now()` | datetime, calendar | -| Hash computation | `hashlib.sha256(b'data')` | hashlib | -| Data structures | `dict`, `list`, `set`, `heapq` | All built-in data structures | -| Multi-line scripts | Functions, classes, loops | Full Python syntax | - -### Not Supported - -| Workload | Why | Error User Sees | -|----------|-----|----------------| -| Network I/O (`urllib`, `socket`, `http`) | No network stack in guest | `OSError: Function not implemented` | -| File writing (`open('f','w')`, `tempfile`) | Read-only FAT32 ramfs | `OSError: Read-only file system` | -| Subprocess (`subprocess.run`, `os.system`) | `fork()` is stubbed | `OSError: Function not implemented` | -| SSL/TLS (`import ssl`) | `_ssl` module not built | `ModuleNotFoundError: No module named '_ssl'` | -| ctypes (`import ctypes`) | `_ctypes` module not built | `ModuleNotFoundError: No module named '_ctypes'` | -| GUI (`tkinter`, `turtle`) | No display server | Module removed from ramfs | -| Interactive input (`input()`) | guest stdin is closed (`Stdio::null()`); no host stdin relay | `EOFError: EOF when reading a line` | -| Large memory (>128MB) | Default VM memory limit | Process killed by kernel OOM | - -## End-User Experience - -### CLI Usage - -```bash -# Run a Python script in a NanVix micro-VM -wxc-exec.exe microvm_config.json - -# With debug output -wxc-exec.exe --debug microvm_config.json -``` - -### Example Config (`microvm_config.json`) - -```json -{ - "process": { - "commandLine": "import sys\nprint(f'Python {sys.version} on {sys.platform}')", - "timeout": 30000 - }, - "containment": "microvm" -} -``` - -### Expected Output - -``` -$ wxc-exec.exe --debug microvm_config.json -Script code length: 58 -Working directory: -Script timeout: 30000 -Container name: CLI -NanVix: nanvixd="C:\\nanvix\\bin\\nanvixd.exe" -NanVix: bin_dir="C:\\nanvix\\bin" -NanVix: ramfs="C:\\nanvix\\bin\\nanvix_rootfs.img" -NanVix: python="C:\\nanvix\\bin\\python3.initrd" -NanVix: process exited with code 0 -Exit code: 0 -Python 3.12.3 (tags/0715636-nanvix-03bba66:0715636) on nanvix -``` - -### SDK Usage (After Phase 2) - -```typescript -import { spawnSandboxAsync } from '@microsoft/mxc-sdk'; - -const result = await spawnSandboxAsync( - "print('Hello from NanVix!')", - {}, // no policy needed — NanVix is isolated by design - { containment: 'microvm' } -); - -console.log(result.stdout); // "Hello from NanVix!" -console.log(result.exitCode); // 0 -``` - - -## Testing Strategy - -### Unit Tests - -| Test | What It Validates | -|------|------------------| -| `default_config_values` | NanVixConfig defaults (python3.initrd, /sysroot, 60s) | -| `total_timeout_adds_boot_and_script` | Timeout arithmetic | -| `resolve_nanvixd_missing_returns_error` | Path resolution error handling | -| Config parser: `"microvm"` containment | JSON parsing of microvm section | - -### Integration Tests (requires WHP + NanVix binaries) - -| Test | What It Validates | -|------|------------------| -| Hello world | Basic script → stdout pipeline (script delivered via mounted staging dir) | -| Script with spaces | Mount-based delivery bypasses the cmdline space-splitting limit | -| Exit code propagation | `sys.exit(42)` → exit code 42 | -| Missing nanvixd | Preflight error message | -| Timeout | Watchdog kills after deadline | -| Large script | >255 bytes (would exceed cmdline limit if it weren't mounted) | - -## Open Design Questions - -| # | Question | Status | -|---|----------|--------| -| 1 | Should NanVix VMs be pooled/warm-started? | | -| 2 | Should there be a size limit to the VM | | -| 3 | Should there be a writable FS area in the guest? | Parked for Phase 2 | -| 4 | Should we support TypeScript or other payloads? | Architecture supports it | diff --git a/docs/nanvix-microvm/nanvix.md b/docs/nanvix-microvm/nanvix.md deleted file mode 100644 index 7e8c54df9..000000000 --- a/docs/nanvix-microvm/nanvix.md +++ /dev/null @@ -1,224 +0,0 @@ -# Nanvix MicroVM Backend - -Nanvix MicroVM is an experimental containment backend for MxC. It is powered by the -[Nanvix OS/VM](https://aka.ms/nanvix) and runs untrusted code with hardware-enforced isolation -via the Windows Hypervisor Platform (WHP) on Windows or KVM on Linux. - -## Key Features - -- **Fast cold-start** — ~100 ms from process spawn to guest code execution (Windows warm-start via WHP snapshot; Linux uses cold boot via KVM every run) -- **Low Memory Footprint** — Resident memory size of ~100 MB -- **Hardware-Enforced Isolation** — Runs guest code inside a lightweight virtual machine (VM) - -## Requirements - -### Windows - -- Windows with WHP enabled (`bcdedit /set hypervisorlaunchtype auto`) -- Nanvix runtime binaries (`nanvixd.exe`, `kernel.elf`, `python3.initrd`, `nanvix_rootfs.img`) placed next to `wxc-exec.exe` -- Build with `--with-microvm` (`build.bat --with-microvm` or `cargo build -p wxc --features microvm`) -- `--experimental` flag (Nanvix MicroVM is an experimental feature) - -### Linux - -- Linux with KVM available at `/dev/kvm` (and the invoking user has read/write access to it) -- Nanvix runtime binaries (`nanvixd.elf`, `kernel.elf`, `python3.initrd`, `nanvix_rootfs.img`) placed next to `lxc-exec` (the build script downloads and stages them automatically) -- Build with `--with-microvm` (`./build.sh --with-microvm` or `cargo build -p lxc --features microvm`) -- `--experimental` flag (Nanvix MicroVM is an experimental feature) - -> **Note:** On Linux, WHP snapshots are not used. Each invocation cold-boots -> the VM via KVM. Snapshot-based warm-start is Windows-only. - -### Offline builds - -By default the `nanvix_binaries` build script downloads the NanVix release -assets at compile time. For air-gapped or hermetic builds, pre-fetch the -binaries and point the `NANVIX_BIN` environment variable at the directory -containing them: - -``` -# Windows (PowerShell) -$env:NANVIX_BIN = "C:\path\to\nanvix-binaries" - -# Linux / macOS -export NANVIX_BIN=/path/to/nanvix-binaries -``` - -When `NANVIX_BIN` is set, the build performs no network downloads and uses the -provided directory directly. The directory must contain the required binaries -(the flat files plus the `bin/` subdirectory); their checksums are still -verified against `checksums.json`. The easiest way to produce such a directory -is to run a normal `--with-microvm` build once and copy the staged -`nanvix-binaries` directory out of `OUT_DIR`. - -> **Snapshots are not trusted in offline mode.** WHP warm-start snapshots -> (`snapshots/kernel.vmem`, `snapshots/kernel.whp.cbor`) are *not* covered by -> `checksums.json` — in a normal build they are generated locally, not -> downloaded. Any `snapshots/` directory inside `NANVIX_BIN` is therefore -> ignored (never copied next to the executable), and the runtime cold-boots on -> first use to regenerate a verified snapshot. This prevents an unverified VM -> memory image from being warm-booted against otherwise-verified binaries. - -## Quick Start - -```json -{ - "process": { - "commandLine": "print('Hello from MicroVM!')", - "timeout": 30000 - }, - "containment": "microvm" -} -``` - -```bash -wxc-exec.exe --experimental config.json -``` - -## SDK Usage - -Use `spawnSandboxFromConfig` with `usePty: false` for reliable exit codes and -separate stdout/stderr streams: - -```typescript -const child = spawnSandboxFromConfig(config, { - experimental: true, - usePty: false, -}); - -``` - -## Filesystem Policy - -### readwrite_paths - -Host directories or files listed in `readwritePaths` are copied into a private -per-run staging directory before boot. Nanvix mounts are snapshot-based — host -files are **not** modified while the guest is running. No junctions or live host -mounts are used. - -```json -{ - "process": { - "commandLine": "import os\npath = 'C:\\\\Users\\\\me\\\\work'\nwith open(os.path.join(path, 'result.txt'), 'w') as f:\n f.write('done')", - "timeout": 30000 - }, - "containment": "microvm", - "filesystem": { - "readwritePaths": ["C:\\Users\\me\\work"] - } -} -``` - -Inside the guest, host paths in the script are transparently rewritten to their -guest mount equivalents at staging time. The script uses the original host paths -and the staging layer translates them before the code reaches the VM. - -| Host path | Guest path | -| ------------------ | --------------------------- | -| `C:\Users\me\work` | `/mnt/rw/c/Users/me/work` | -| `C:\data\ref-data` | `/mnt/rw/c/data/ref-data` | - -**Copyback semantics:** After `nanvixd` exits normally, MXC copies the modified -snapshot back to the original host paths. Copyback runs for both exit code `0` -and non-zero guest exit codes. It is skipped for preflight failure, spawn -failure, watchdog timeout, and runner/runtime errors — no partial state is -leaked to the host. - -### readonly_paths - -Host directories listed in `readonlyPaths` are copied into the staging directory -with read-only file attributes. Writes return `EACCES`. Read-only paths are -never copied back to the host. - -```json -{ - "filesystem": { - "readonlyPaths": ["C:\\data\\reference"] - } -} -``` - -### denied_paths - -Not supported for MicroVM. If `deniedPaths` is specified, the config is rejected with an error. - -## Constraints - -| Constraint | Value | -| --------------------------------------- | ------------------------------------------- | -| Single file size | < 4 GB (FAT32 limit) | -| Guest RAM | 256 MB | -| Symlinks/reparse points in source paths | Not supported (rejected at preflight) | -| Junctions for staging | Not used | -| `workingDirectory` | Not supported (guest CWD is `/`) | -| Network policy | Host networking + per-host egress filtering | - -## Networking - -Host networking is **opt-in**. Set `network.defaultPolicy` to `"allow"` to -enable unrestricted egress; the runner then passes `-allow-host-networking` to -`nanvixd`. The default (`"block"`) leaves networking disabled, and a guest -socket call fails with `OSError: [Errno 134]`. - -### Per-host egress filtering - -`allowedHosts` and `blockedHosts` are supported and forwarded to the guest's -host-side socket proxy, which enforces egress at `connect()`. The guest filter -is **allow-XOR-block**, so the two lists are mutually exclusive (specifying both -is rejected at preflight). Presence of either list implies host networking, so -`defaultPolicy` is ignored when a list is set: - -| `allowedHosts` | `blockedHosts` | Effect | -| -------------- | -------------- | ------ | -| _(empty)_ | _(empty)_ | follows `defaultPolicy` (`block` = no egress, `allow` = unrestricted) | -| `[A, ...]` | _(empty)_ | **allowlist** — only the listed destinations are reachable | -| _(empty)_ | `[B, ...]` | **blocklist** — everything except the listed destinations is reachable | -| `[A, ...]` | `[B, ...]` | rejected at preflight (mutually exclusive) | - -Entries may be IPv4 literals (`93.184.216.34`), IPv4 CIDR blocks -(`10.0.0.0/8`), or hostnames. Hostnames are resolved to their IPv4 (A-record) -addresses at preflight; IPv6 (AAAA) results are dropped because the guest filter -is IPv4-only. Resolution failures are handled per direction so neither list ever -fails open: - -- **allowlist** (deny-by-default): each dropped entry is logged as a warning and - the run continues, since dropping an entry only *narrows* access. If the list - resolves to **no** IPv4 address at all, the run is rejected at preflight rather - than silently allowing all traffic. -- **blocklist** (allow-by-default): **any** entry that resolves to no IPv4 - address rejects the run at preflight. Silently dropping a blocked host would - let traffic the policy explicitly blocks flow freely, and the static preflight - filter cannot enforce a name that does not resolve — so the blocklist - fails closed. - -**DNS:** in allowlist mode the guest daemon automatically exempts the DNS port -(53), so name resolution works without adding the resolver to `allowedHosts`. - -Network proxies (`network.proxy`) are not supported and are rejected at -preflight. - -```jsonc -{ - "containment": "microvm", - "process": { "commandLine": "import urllib.request; ..." }, - // Unrestricted egress: - "network": { "defaultPolicy": "allow" } -} -``` - -```jsonc -{ - "containment": "microvm", - "process": { "commandLine": "import urllib.request; ..." }, - // Allowlist: only example.com and the 10.0.0.0/8 block are reachable. - "network": { "allowedHosts": ["example.com", "10.0.0.0/8"] } -} -``` - -## Not Supported - -| Workload | Error | -| ------------------------------- | ----------------------------------- | -| Both `allowedHosts` + `blockedHosts` | Rejected at preflight (mutually exclusive) | -| File writing outside `/mnt/rw/` | `OSError: Read-only file system` | diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index 570e94e0d..7454da16e 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -6,10 +6,11 @@ The original NVX prototype accepted a real MXC `0.9.0-dev` JSON, validated it against the schema, and adapted supported fields into typed NVX launch plans. The prototype first exercised those plans on Windows through OpenVMM and WHP. -The current MXC foundation is additive: `containment: "nvx"` parses while the -existing `microvm` backend remains available. An NVX-enabled build returns a -typed backend-unavailable error for execution, and capability probes do not -advertise NVX while the runtime is incomplete. +NVX is now the sole micro-VM backend identity in the active MXC development +contract. The retired NanVix-backed `microvm` value is rejected with migration +guidance to `nvx`. An NVX-enabled build returns a typed backend-unavailable +error for execution, and capability probes do not advertise NVX while the +runtime is incomplete. ## Current architecture @@ -85,8 +86,8 @@ them for this backend: The rolling `0.9.0-dev` and exact `0.9.0-alpha` development contracts now include `containment: "nvx"` in their generated schemas and TypeScript wire -types. The existing `microvm` identifier remains in both artifact sets. No -stable schema is changed. An `experimental.nvx.provision` section is necessary +types. The retired `microvm` identifier is absent from both development artifact sets. +Published stable schemas remain unchanged. An `experimental.nvx.provision` section is necessary only if images remain caller-configurable. Example state-aware provision request: @@ -145,9 +146,8 @@ Example state-aware provision request: ## Binary acquisition and packaging -`build.bat --with-nvx` enables the incomplete x64 Windows/WHP foundation. This -path is additive and does not replace the existing `--with-microvm` packaging -path. +`build.bat --with-nvx` enables the incomplete x64 Windows/WHP foundation. It is +the only active micro-VM packaging path. The build pins the exact `microsoft/nvx` release tag, platform asset name, and per-file SHA-256 checksums in the repository. It downloads and verifies the @@ -164,8 +164,8 @@ writable scratch image required to run a workload. ## Remaining MXC integration work The schema/wire, policy/model, typed-unavailable dispatch, and pinned Windows -artifact-acquisition foundations are now present without replacing NanVix. -PR2/runtime remains blocked on all of the following: +artifact-acquisition foundations now replace the removed NanVix integration. +Phase 2 runtime remains blocked on all of the following: - NVX-produced distro and runtime EROFS images plus a writable scratch image; - a proven combined managed-sandbox/virtio-fs contract; and diff --git a/docs/sandbox-policy/0.8.0/networking/networking.md b/docs/sandbox-policy/0.8.0/networking/networking.md index c8649be8a..f59baddf9 100644 --- a/docs/sandbox-policy/0.8.0/networking/networking.md +++ b/docs/sandbox-policy/0.8.0/networking/networking.md @@ -546,7 +546,7 @@ Model 2 permits only the proxy endpoint. reserved for the backend migration work; until that lands, callers must continue using the legacy unrestricted acknowledgment. Other network/proxy policy is rejected. In GA for process isolation only (identity, lifecycle). -- **Hyperlight, Nanvix:** Not in this GA scope doc. Additional follow up is needed to confirm their capabilities and whether they align with this doc. +- **Hyperlight, NVX:** Not in this GA scope doc. Additional follow up is needed to confirm their capabilities and whether they align with this doc. ## Gaps and limitations diff --git a/docs/schema.md b/docs/schema.md index 74094f9d9..e179aafa0 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -246,7 +246,7 @@ use: | Windows ProcessContainer (AppContainer / BaseContainer) | First `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root (`%SystemDrive%\`). Never `NULL`. | | Seatbelt (macOS) | Same precedence, with `~` expanded as the profile expands it; falls back to `/`. | | LXC / WSL Container | The container root — see [`docs/lxc-support/lxc-backend.md`](lxc-support/lxc-backend.md). | -| MicroVM (NanVix) / Hyperlight | Not applicable — these backends reject a working directory outright. | +| NVX / Hyperlight | Not applicable — these backends reject a working directory outright. | Policy entries that are blank, name a file, or do not exist yet are skipped: a process cannot be launched in any of them. @@ -344,7 +344,6 @@ force a particular backend. |-------|------------| | `"process"` | `processcontainer` on Windows, `lxc` on Linux, `seatbelt` on macOS | | `"vm"` | Full hardware-virtualised VM isolation. Resolves to `windows_sandbox` on Windows. | -| `"microvm"` | MicroVM on Windows (NanVix via the Windows Hypervisor Platform). Experimental. | #### Concrete backends @@ -354,7 +353,7 @@ force a particular backend. | `"windows_sandbox"` | Windows Sandbox VM isolation. Dual-mode: a transient **one-shot** runner that launches a fresh disposable VM per execution, and a **state-aware** lifecycle backed by a long-lived per-sandbox daemon. | | `"wslc"` | Linux containers via the WSL Container SDK | | `"lxc"` | Native LXC container isolation | -| `"microvm"` | MicroVM isolation via Windows HyperV Platform (NanVix microkernel) | +| `"nvx"` | NVX Linux micro-VM hosted by OpenVMM/WHP (experimental, Windows x64 foundation; runtime unavailable in Phase 1) | | `"hyperlight"` | MicroVM isolation via Hyperlight + Unikraft with an embedded CPython snapshot (experimental) | | `"isolation_session"` | Windows isolation session — runs the workload as a freshly-provisioned, per-execution isolated user account in its own OS-managed session (experimental). Dual-mode: one-shot and state-aware. | | `"seatbelt"` | macOS sandbox isolation (Seatbelt). Requires macOS 15 or later — see [`docs/seatbelt/seatbelt-backend.md`](seatbelt/seatbelt-backend.md). | diff --git a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md index 494fbe488..8fd1a7f5a 100644 --- a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md +++ b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md @@ -20,7 +20,7 @@ function signatures throughout. One-line summaries; full definitions live in | Type | Where | Role | |---|---|---| -| `ContainmentType` / `ContainmentBackend` | `sdk/node/src/types.ts` | Two-tier containment names: `ContainmentType` for abstract intents (`'process' \| 'vm' \| 'microvm'` today); `ContainmentBackend` for concrete runners (`'processcontainer' \| 'windows_sandbox' \| 'lxc' \| 'wslc' \| 'microvm' \| 'seatbelt' \| 'isolation_session'`). Wire `containment` accepts either. The deprecated alias `SandboxingMethod = ContainmentType \| ContainmentBackend` is retained for back-compat. | +| `ContainmentType` / `ContainmentBackend` | `sdk/node/src/types.ts` | Two-tier containment names: `ContainmentType` for abstract intents (`'process' \| 'vm'`); `ContainmentBackend` for concrete runners (`'processcontainer' \| 'windows_sandbox' \| 'lxc' \| 'wslc' \| 'nvx' \| 'hyperlight' \| 'seatbelt' \| 'isolation_session'`). Wire `containment` accepts either. The deprecated alias `SandboxingMethod = ContainmentType \| ContainmentBackend` is retained for back-compat. | | `ProcessConfig` | `sdk/node/src/types.ts` | Per-process settings: `commandLine`, `cwd`, `env`, `timeout`. Reused inside state-aware exec Configs. | | `FilesystemConfig`, `NetworkConfig`, `UiConfig` | `sdk/node/src/types.ts` | Wire-format-aligned cross-cutting interfaces. Reused inline as field types inside the per-(backend, phase) state-aware Configs. | | `SandboxSpawnOptions` | `sdk/node/src/sandbox.ts` | Existing options bag (debug, dryRun, logDir, executablePath, ptyOptions, usePty, experimental). State-aware reuses it as the third positional arg, extended with `signal?: AbortSignal` for cancellation. | diff --git a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md index c424efde4..954a95c5b 100644 --- a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md +++ b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md @@ -497,7 +497,7 @@ to learn the cross-cutting flags. Phase-specific fields on `SandboxSpawnOptions` (`ptyOptions`, `usePty`) are honored by `execInSandbox` / `execInSandboxAsync` and silently ignored on the other phases. State-awareness is not itself experimental — `experimental: true` must be set when the targeted backend is itself experimental, just -as it is today for one-shot calls against `microvm` and `wslc`. IsolationSession is +as it is today for one-shot calls against `nvx` and `wslc`. IsolationSession is experimental at the time of writing; that status is independent of the state-aware API surface (§13). @@ -1395,11 +1395,9 @@ that shape and reuses `ExecutionRequest` for five concrete reasons: 1. **The field-ignore precedent is established across every existing backend.** Every `ScriptRunner` impl in the workspace today (`AppContainer`, `BaseContainer`, - `NanVix`, `WindowsSandbox`, `IsolationSession`, `Lxc`, `Wslc`) takes - `&ExecutionRequest` and reads only the fields it needs. `NanVix` and - `IsolationSession` go further and actively reject fields they cannot honor (e.g., - `NanVixScriptRunner::validate_runner` rejects filesystem paths, network rules, - network proxy, and a non-empty working directory). State-aware follows the same + `WindowsSandbox`, `IsolationSession`, `Lxc`, `Wslc`) takes + `&ExecutionRequest` and reads only the fields it needs. `IsolationSession` + goes further and actively rejects fields it cannot honor. State-aware follows the same pattern, so the trait ergonomic stays consistent across one-shot and state-aware surfaces. diff --git a/src/core/mxc-sdk/README.md b/src/core/mxc-sdk/README.md index 0d8755d6a..8de487c72 100644 --- a/src/core/mxc-sdk/README.md +++ b/src/core/mxc-sdk/README.md @@ -397,7 +397,7 @@ Constructing the listed variants is unaffected. [`ErrorCode::UnsupportedContainment`]. Reach it through the state-aware lifecycle — `run_state_aware_json` plus `exec_attached` or `exec_sandbox`. -Backends with no variant at all — Windows Sandbox, MicroVM, Hyperlight, LXC — +Backends with no variant at all — Windows Sandbox, NVX, Hyperlight, LXC — cannot be named from this crate; use the executor binaries. Windows Sandbox is still reachable here through the state-aware lifecycle. From cb0cca9e115650fd4f96164ef36569063994a52c Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 11:31:59 -0700 Subject: [PATCH 22/41] chore: preserve Node type line endings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sdk/node/src/types.ts | 1232 ++++++++++++++++++++--------------------- 1 file changed, 616 insertions(+), 616 deletions(-) diff --git a/sdk/node/src/types.ts b/sdk/node/src/types.ts index 7b2cae92a..da2f1eacb 100644 --- a/sdk/node/src/types.ts +++ b/sdk/node/src/types.ts @@ -1,616 +1,616 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * MXC SDK Types - * These types match the wxc-exec JSON configuration schema - */ - - -/** - * Process execution settings - */ -export interface ProcessConfig { - /** Complete command line to execute (e.g., "python -c \"print('hello')\"") */ - commandLine: string; - /** Working directory for the process */ - cwd?: string; - /** Environment variables as KEY=VALUE strings */ - env?: string[]; - /** Execution timeout in milliseconds (default: 0 = no timeout) */ - timeout?: number; -} - -/** - * Container lifecycle settings shared across all backends - */ -export interface LifecycleConfig { - /** Destroy the container after execution completes (default: true) */ - destroyOnExit?: boolean; - /** Retain filesystem and network policies after execution (default: false) */ - preservePolicy?: boolean; -} - -/** - * Abstract containment intent. Names the *kind* of isolation the caller - * wants; the native binary resolves it to a concrete - * {@link ContainmentBackend} per host capability. - * - * Today's intents: - * - "process": OS-native process-level isolation. Resolves to - * `processcontainer` (Windows), `bubblewrap` (Linux), or `seatbelt` - * (macOS). On Linux, `lxc` remains available as an explicit concrete - * backend but is no longer the default for the abstract `"process"` - * intent. - * - "vm": full hardware-virtualised VM isolation. Resolves to - * `windows_sandbox` on Windows; no concrete VM backend exists on other - * platforms today. - * Concrete-only backends (such as `"wslc"`) live on - * {@link ContainmentBackend} until there is a meaningful abstraction over - * multiple implementations of the same kind. - */ -export type ContainmentType = 'process' | 'vm'; - -/** - * Runtime list of {@link ContainmentType} values. Kept in sync with the - * `ContainmentType` union via the type annotation. Use this to recognise - * abstract intents at run time (the union itself only exists at compile - * time). - */ -export const ContainmentTypes: readonly ContainmentType[] = ['process', 'vm']; - -/** - * Deprecated containment wire values, mapped to their canonical - * {@link ContainmentBackend} replacement. The native binary (wxc-exec) accepts - * the deprecated form via serde aliases; this map mirrors that behavior in - * the SDK validator so legacy configs are not rejected before reaching the - * binary. - * - * The map is intentionally partial: only deprecated keys appear. Use a - * presence check (e.g. `LegacyContainmentAliases[value] ?? value`) rather - * than indexing blindly. - * - * The wire payload is forwarded to wxc-exec unchanged — the Rust parser - * performs the final mapping at runtime. Resolution here is purely so the - * SDK's experimental-mode, platform-support, and availability checks see - * the canonical backend. - * - * Internal to the SDK; not part of the public API. Subject to removal in a - * future minor release once the deprecation window closes. - */ -export const LegacyContainmentAliases: Readonly>> = { - appcontainer: 'processcontainer', - macos_sandbox: 'seatbelt', -}; - -/** - * Concrete containment backend. Each value names a specific runner - * implementation in the native binary. Prefer a {@link ContainmentType} - * value unless you specifically need to force a particular backend. - */ -export type ContainmentBackend = - | 'processcontainer' - | 'windows_sandbox' - | 'wslc' - | 'lxc' - | 'nvx' - | 'hyperlight' - | 'seatbelt' - | 'isolation_session' - | 'bubblewrap'; - -/** - * Containment values (abstract intent or concrete backend) that require - * the `--experimental` flag. - */ -export const ExperimentalBackends: readonly (ContainmentType | ContainmentBackend)[] = [ - 'nvx', - 'windows_sandbox', - 'hyperlight', - 'wslc', - 'isolation_session', -]; - -/** - * Clipboard access policy levels - */ -export type ClipboardPolicy = "none" | "read" | "write" | "all"; - -/** - * Cross-platform UI configuration in ContainerConfig. - * Mapped from SandboxPolicy.ui by createConfigFromPolicy. - */ -export interface UiConfig { - /** Whether UI is disabled (no visible windows). Maps from !policy.ui.allowWindows. */ - disable: boolean; - /** Clipboard access level */ - clipboard: ClipboardPolicy; - /** Whether input injection is allowed */ - injection: boolean; -} - -/** - * BaseProcess-specific UI configuration (Windows only). - * Lives under processContainer.ui in ContainerConfig. - */ -export interface BaseProcessUiConfig { - /** UI isolation level for the desktop */ - isolation: "desktop" | "handles" | "atoms" | "container"; - /** Whether desktop system control is allowed */ - desktopSystemControl: boolean; - /** System settings access level */ - systemSettings: string; - /** Whether IME (Input Method Editor) is allowed */ - ime: boolean; -} - -/** - * ProcessContainer configuration for the Windows process-level backend. - * - * `processcontainer` is the abstraction layer; the runner picks between - * the legacy AppContainer implementation (which honors `capabilities`, - * `leastPrivilege`) and the newer BaseContainer implementation (which - * honors `ui`) at run time based on the host OS and the `--experimental` - * flag. - */ -export interface ProcessContainerConfig { - /** AppContainer profile name (default: "CLI"). Deprecated: use containerId instead. */ - name?: string; - /** Use least privilege mode with PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT (default: false) */ - leastPrivilege?: boolean; - /** - * Enable deny-and-record learning mode. Failed access checks are logged while - * accesses remain denied and containment remains enforced. - */ - learningMode?: boolean; - /** - * Additional AppContainer capabilities (e.g., "registryRead", "internetClient"). - * Each entry must contain one capability name and must not contain a comma. - * The reserved learning-mode capability names must not be supplied directly. - */ - capabilities?: string[]; - /** BaseProcess-specific UI settings (Windows only) */ - ui?: BaseProcessUiConfig; - /** ProcessContainer-specific networking settings. */ - network?: { - /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ - allowedProxyPeer?: string; - }; -} - -/** - * Filesystem access configuration - */ -export interface FilesystemConfig { - /** Paths the script can read and write */ - readwritePaths?: string[]; - /** Paths the script can read but not write */ - readonlyPaths?: string[]; - /** Paths the script cannot access */ - deniedPaths?: string[]; - /** Automatically remove file access policy after execution (default: true) */ - clearPolicyOnExit?: boolean; -} - -/** - * Network access configuration - */ -export interface NetworkConfig { - /** - * Network enforcement mode: - * - "capabilities": Use AppContainer capabilities only (no admin required) - * - "firewall": Use Windows Firewall rules (requires admin) - * - "both": Use both capabilities and firewall rules (requires admin) - * (default: "both") - */ - enforcementMode?: 'capabilities' | 'firewall' | 'both'; - /** Default network policy: "allow" or "block" (default: "block") */ - defaultPolicy?: 'allow' | 'block'; - /** - * Whether to allow inbound connections to local IP listeners (i.e. the - * sandboxed process may call `bind()` + `listen()` and accept incoming - * TCP/UDP). Independent of `defaultPolicy`. (default: false) - */ - allowLocalNetwork?: boolean; - /** Hostnames or IP addresses/CIDR blocks to allow (firewall mode only) */ - allowedHosts?: string[]; - /** Hostnames or IP addresses to block (firewall mode only) */ - blockedHosts?: string[]; - /** Proxy configuration (supported on Windows ProcessContainer, Linux Bubblewrap, - * macOS Seatbelt, and WSLC). On Bubblewrap/Seatbelt/WSLC it is a cooperative - * env-var proxy (HTTP_PROXY/HTTPS_PROXY): well-behaved HTTP clients honor it, - * raw-socket clients can bypass it. `builtinTestServer` activates a bundled, - * testing-only proxy; the SDK rejects it unless `allowTestingFeatures: true` is - * set in SandboxSpawnOptions (which maps to the native `--allow-testing-features` - * flag). - * - * WSLC imposes additional parse-time constraints (a violating config is - * rejected before it runs): - * - Only the `{ url }` form is accepted — its containers run in their own - * network namespace, so the `localhost` / `builtinTestServer` loopback - * forms are unreachable and rejected. - * - The `url` scheme must be `http` or `https`. - * - `defaultPolicy` must be `"allow"` and both `allowedHosts` and - * `blockedHosts` must be empty/unset — WSLC has no in-kernel iptables, so - * it cannot enforce host lists, and the container needs outbound - * networking to reach the proxy at all. - * Enforcement is cooperative (no in-kernel iptables). */ - proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; - /** Automatically remove firewall rules after execution (default: true). Deprecated: use lifecycle.preservePolicy. */ - removeRulesOnExit?: boolean; - /** Outbound network policy. */ - egress?: NetworkEgressConfig; - /** Inbound and host-loopback network policy. */ - ingress?: NetworkIngressConfig; -} - -/** Allow or deny network action. */ -export type NetworkAction = 'allow' | 'deny'; - -/** Transport protocol selector. */ -export type NetworkProtocol = 'tcp' | 'udp' | 'icmp' | 'any'; - -/** CIDR network peer. */ -export interface NetworkPeerConfig { - /** IPv4 or IPv6 CIDR. */ - cidr: string; - /** CIDRs excluded from this peer. */ - except?: string[]; -} - -/** Protocol and destination-port selector. */ -export interface NetworkPortConfig { - /** Transport protocol. Defaults to "any". */ - protocol?: NetworkProtocol; - /** Destination port. Omission matches every port. */ - port?: number; - /** Inclusive end of a destination-port range. Requires `port`. */ - endPort?: number; -} - -/** Outbound network rule. */ -export interface NetworkRuleConfig { - /** Destination CIDRs. Omission matches both IP families; an explicit array must be non-empty. */ - to?: NetworkPeerConfig[]; - /** Destination protocols and ports. Omission matches all; an explicit array must be non-empty. */ - ports?: NetworkPortConfig[]; -} - -/** Outbound network policy. */ -export interface NetworkEgressConfig { - /** Action used when no explicit rule matches. Defaults to "deny". */ - default?: NetworkAction; - /** Explicit allow rules. */ - allow?: NetworkRuleConfig[]; - /** Explicit deny rules. Deny rules take precedence. */ - deny?: NetworkRuleConfig[]; -} - -/** Inbound and host-loopback network policy. */ -export interface NetworkIngressConfig { - /** Default action for LAN/private-network inbound traffic. */ - default?: NetworkAction; - /** Bidirectional host-loopback connectivity action. */ - hostLoopback?: NetworkAction; -} - -/** Runtime values supplied separately from sandbox policy. */ -export interface RuntimeConfig { - /** HTTP/S loopback proxy URL. */ - networkProxy?: string; -} - -/** - * WSLC SDK configuration for Linux containers from Windows - */ -export interface WslcConfig { - /** OCI container image name (default: "alpine:latest") */ - image?: string; - /** Storage path for WSLC session image store */ - storagePath?: string; - /** Target OS for the container (default: "linux") */ - targetOs?: string; - /** Number of CPUs allocated to the WSLC session */ - cpuCount?: number; - /** Memory in MB allocated to the WSLC session */ - memoryMb?: number; - /** Enable GPU passthrough to the container (default: false) */ - gpu?: boolean; - /** Path to a local tar file to import as the container image */ - imageTarPath?: string; - /** - * Host↔container port mappings. - * - * Only TCP is currently supported by the WSLC SDK runtime. UDP is declared - * in the SDK header but the shipped runtime returns `E_NOTIMPL` when UDP is - * actually requested, so the parser hard-rejects `"udp"` with a clear - * message at spawn time. The `protocol` field defaults to `"tcp"` when - * omitted. - */ - portMappings?: PortMapping[]; -} - -/** - * Port mapping for host↔container port forwarding. - */ -export interface PortMapping { - /** Port on the Windows host */ - windowsPort: number; - /** Port inside the Linux container */ - containerPort: number; - /** - * Transport protocol. Only `"tcp"` is currently supported; `"udp"` is - * rejected by the parser because the WSLC SDK runtime returns `E_NOTIMPL` - * for UDP even though the header declares it. Defaults to `"tcp"` when - * omitted. - */ - protocol?: 'tcp'; -} - -/** Telemetry configuration for TraceLogging ETW support. */ -export interface TelemetryConfig { - /** - * Explicit telemetry opt-in. `true` requests telemetry subject to user - * consent and administrative policy; `false` or `undefined` keeps it off. - */ - enabled?: boolean; -} - -/** - * Main WXC configuration - */ -export interface ContainerConfig { - /** MXC config schema version. Required. */ - version: string; - /** Externally assigned container identifier */ - containerId?: string; - /** Containment intent (preferred) or concrete backend (override). */ - containment?: ContainmentType | ContainmentBackend; - /** Container lifecycle settings */ - lifecycle?: LifecycleConfig; - /** Process execution settings (required) */ - process?: ProcessConfig; - /** ProcessContainer configuration */ - processContainer?: ProcessContainerConfig; - /** - * Legacy alias of {@link processContainer}. Retained so callers - * migrating from pre-0.6 SDK versions can keep their existing code - * compiling; the native binary parses both names into the same slot - * via a serde alias. - * - * @deprecated Use {@link processContainer} instead. This alias may be - * removed in a future minor release. - */ - appContainer?: ProcessContainerConfig; - /** LXC container configuration (Linux only) */ - lxc?: LxcConfig; - /** Filesystem access configuration */ - filesystem?: FilesystemConfig; - /** Network access configuration */ - network?: NetworkConfig; - /** Runtime values supplied separately from sandbox policy. */ - runtimeConfig?: RuntimeConfig; - /** Telemetry configuration */ - telemetry?: TelemetryConfig; - /** Experimental features (only applied when --experimental flag is set) */ - experimental?: { - /** WSLC SDK configuration for Linux containers from Windows */ - wslc?: WslcConfig; - }; - /** macOS Seatbelt sandbox configuration (macOS only) */ - seatbelt?: SeatbeltConfig; - /** Cross-platform UI configuration */ - ui?: UiConfig; -} - -/** - * The main sandbox policy configuration interface for external consumers - * to define sandboxed execution environments. - * - * Policy describes *what* the caller wants restricted. Cross-platform. - * No OS-specific content. Omitted fields = most restrictive (default-deny). - */ -export type SandboxPolicy = { - /** Policy version (semver). Must match a supported schema version. */ - version: string; - /** Filesystem access restrictions */ - filesystem?: { - /** Paths that are granted read and write access */ - readwritePaths?: string[]; - /** Paths that are granted read-only access */ - readonlyPaths?: string[]; - /** Paths that are explicitly denied all access */ - deniedPaths?: string[]; - /** Whether to clear the filesystem policy when the shell exits. (default: true) */ - clearPolicyOnExit?: boolean; - }; - /** Network access restrictions. All flags default to false (no network access). */ - network?: { - /** Whether to allow outbound connections to the Internet. (default: false) Legacy network field. */ - allowOutbound?: boolean; - /** Whether to allow connections to local networks. (default: false) Legacy network field. */ - allowLocalNetwork?: boolean; - /** When set, ONLY these outbound hosts are reachable. Requires allowOutbound. Legacy network field. */ - allowedHosts?: string[]; - /** Hosts to block even when outbound is allowed. Requires allowOutbound. Legacy network field. */ - blockedHosts?: string[]; - /** - * Proxy configuration. Routes cooperating HTTP traffic through this proxy. - * Supported on Windows ProcessContainer, Linux Bubblewrap, and macOS - * Seatbelt. On Bubblewrap/Seatbelt it is a cooperative env-var proxy - * (HTTP_PROXY/HTTPS_PROXY) — raw-socket clients can bypass it. Native - * validation enforces backend-specific combination rules. - * `builtinTestServer` selects a bundled, testing-only proxy; the SDK - * rejects it unless `allowTestingFeatures: true` is set in - * SandboxSpawnOptions (which maps to the native - * `--allow-testing-features` flag). - * Legacy network field. - */ - proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; - /** Schema 0.8 outbound network policy. Cannot be combined with legacy network fields. */ - egress?: NetworkEgressConfig; - /** Schema 0.8 inbound and host-loopback policy. Cannot be combined with legacy network fields. */ - ingress?: NetworkIngressConfig; - }; - /** Schema 0.8 runtime values supplied separately from sandbox policy. */ - runtimeConfig?: RuntimeConfig; - /** Schema 0.8 ProcessContainer-specific policy. */ - processContainer?: { - /** ProcessContainer-specific networking settings. */ - network?: { - /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ - allowedProxyPeer?: string; - }; - }; - /** UI access restrictions. All flags default to denied. */ - ui?: { - /** Whether the sandbox may create visible windows. (default: false) */ - allowWindows?: boolean; - /** Clipboard access level. (default: "none") */ - clipboard?: ClipboardPolicy; - /** Whether the sandbox may inject keyboard/mouse input. (default: false) */ - allowInputInjection?: boolean; - }; - /** Execution timeout in milliseconds. Omitted = no timeout. */ - timeoutMs?: number; -} - -/** - * LXC container configuration for Linux sandbox - */ -export interface LxcConfig { - /** Container name (default: auto-generated) */ - containerName?: string; - /** Linux distribution for container rootfs (default: "alpine") */ - distribution?: string; - /** Distribution release version (default: "3.19") */ - release?: string; - /** Whether to destroy the container after execution (default: true) */ - destroyOnExit?: boolean; -} - -/** - * macOS Seatbelt sandbox configuration. Used under the top-level - * `seatbelt` key when `containment == "seatbelt"`. - */ -export interface SeatbeltConfig { - /** - * Optional override of the generated TinyScheme sandbox profile. - */ - profileOverride?: string; - /** - * Allow the inner process to allocate its own pseudo-terminals via - * `posix_openpt` (needed by tests, `git`, `gh`, REPLs, and any tool - * that wraps commands in a pty). Adds `(allow pseudo-tty)` and - * read/write/ioctl on `/dev/ptmx` to the generated profile. Defaults - * to `true`; set to `false` for the tightest possible sandbox when - * the inner command does not need to allocate new ttys. - */ - nestedPty?: boolean; - /** - * Allow the inner process to use the macOS Keychain (e.g. via - * `keytar` or `Security.framework`) end-to-end. Adds Mach lookup for - * `securityd`, `trustd`, `ocspd`, `cfprefsd`, `xpcd`, and the - * `com.apple.lsd.*` family; read access to `/private/var/db/mds` and - * `/private/var/protected/trustd`; and read+write access to - * `~/Library/Keychains` and `/private/var/folders` (XPC cache). - * Defaults to `false`; opt in only when the inner workload genuinely - * needs Keychain access. - */ - keychainAccess?: boolean; - /** - * Additional Mach service global-names to allow `mach-lookup` for. - * Escape hatch for callers that need a specific system service the - * baseline doesn't cover (e.g. opt-in agent integrations). Each entry - * is rendered as `(global-name "...")` inside a single - * `(allow mach-lookup ...)` form. - */ - extraMachLookups?: string[]; -} - -/** - * Sandboxing methods available on the platform - * - * @deprecated Prefer {@link ContainmentBackend} (concrete) or - * {@link ContainmentType} (abstract). This alias is retained for - * backward compatibility and may be removed in a future minor release. - */ -export type SandboxingMethod = ContainmentType | ContainmentBackend; - -/** - * Isolation tier selected by the runtime fallback detector. - * - * - `base-container`: full BaseContainer (Experimental_CreateProcessInSandbox) - * - `appcontainer-bfs`: AppContainer + BFS filesystem isolation - * - `appcontainer-dacl`: AppContainer + host DACL augmentation (last-resort fallback) - */ -export type IsolationTier = - | 'base-container' - | 'appcontainer-bfs' - | 'appcontainer-dacl'; - -/** - * Host support for enforcing sandbox UI restrictions. - * - * The fields describe platform-agnostic restriction intents, not the - * OS-specific primitive used to enforce them. For example, Windows derives - * these values from `JOB_OBJECT_UILIMIT_*` support. The SDK currently - * receives this object only from the Windows native probe; other platforms - * omit `PlatformSupport.uiCapabilities` until they expose equivalent probe - * data. - */ -export interface UiCapabilitySupport { - /** Whether the host can block reads from the clipboard. */ - canBlockClipboardRead: boolean; - /** Whether the host can block writes to the clipboard. */ - canBlockClipboardWrite: boolean; - /** Whether the host can block synthetic keyboard/mouse input. */ - canBlockInputInjection: boolean; - /** Whether the host can block input method / IME changes. */ - canBlockInputMethodChanges: boolean; - /** Whether the host can block access to external UI object handles. */ - canBlockExternalUiObjects: boolean; - /** Whether the host can block access to global UI namespaces. */ - canBlockGlobalUiNamespace: boolean; - /** Whether the host can block desktop switching. */ - canBlockDesktopSwitching: boolean; - /** Whether the host can block logoff or shutdown requests. */ - canBlockLogoffOrShutdown: boolean; - /** Whether the host can block system parameter changes. */ - canBlockSystemParameterChanges: boolean; - /** Whether the host can block display settings changes. */ - canBlockDisplaySettingsChanges: boolean; -} - -/** - * Platform support information - */ -export interface PlatformSupport { - /** Whether WXC is supported on the current platform */ - isSupported: boolean; - /** Reason why the platform is not supported (if applicable) */ - reason?: string; - /** Available sandboxing methods on this platform */ - availableMethods: ContainmentBackend[]; - /** - * Why individual Linux backends are unavailable, whether or not another - * backend keeps Linux supported. Omitted on other platforms and when no - * Linux backend failures were observed. - */ - unavailableReasons?: Partial>; - /** - * Tier that would be selected for an empty policy on this system. - * Omitted on non-Windows platforms or when the probe fails. - */ - isolationTier?: IsolationTier; - /** - * Tier degradation warnings (one per fall-through during selection). - * Omitted on non-Windows platforms or when the probe fails. - */ - isolationWarnings?: string[]; - /** - * Host UI-restriction capabilities. Omitted when the backend probe cannot - * determine them, including on Linux and macOS today. - */ - uiCapabilities?: UiCapabilitySupport; -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * MXC SDK Types + * These types match the wxc-exec JSON configuration schema + */ + + +/** + * Process execution settings + */ +export interface ProcessConfig { + /** Complete command line to execute (e.g., "python -c \"print('hello')\"") */ + commandLine: string; + /** Working directory for the process */ + cwd?: string; + /** Environment variables as KEY=VALUE strings */ + env?: string[]; + /** Execution timeout in milliseconds (default: 0 = no timeout) */ + timeout?: number; +} + +/** + * Container lifecycle settings shared across all backends + */ +export interface LifecycleConfig { + /** Destroy the container after execution completes (default: true) */ + destroyOnExit?: boolean; + /** Retain filesystem and network policies after execution (default: false) */ + preservePolicy?: boolean; +} + +/** + * Abstract containment intent. Names the *kind* of isolation the caller + * wants; the native binary resolves it to a concrete + * {@link ContainmentBackend} per host capability. + * + * Today's intents: + * - "process": OS-native process-level isolation. Resolves to + * `processcontainer` (Windows), `bubblewrap` (Linux), or `seatbelt` + * (macOS). On Linux, `lxc` remains available as an explicit concrete + * backend but is no longer the default for the abstract `"process"` + * intent. + * - "vm": full hardware-virtualised VM isolation. Resolves to + * `windows_sandbox` on Windows; no concrete VM backend exists on other + * platforms today. + * Concrete-only backends (such as `"wslc"`) live on + * {@link ContainmentBackend} until there is a meaningful abstraction over + * multiple implementations of the same kind. + */ +export type ContainmentType = 'process' | 'vm'; + +/** + * Runtime list of {@link ContainmentType} values. Kept in sync with the + * `ContainmentType` union via the type annotation. Use this to recognise + * abstract intents at run time (the union itself only exists at compile + * time). + */ +export const ContainmentTypes: readonly ContainmentType[] = ['process', 'vm']; + +/** + * Deprecated containment wire values, mapped to their canonical + * {@link ContainmentBackend} replacement. The native binary (wxc-exec) accepts + * the deprecated form via serde aliases; this map mirrors that behavior in + * the SDK validator so legacy configs are not rejected before reaching the + * binary. + * + * The map is intentionally partial: only deprecated keys appear. Use a + * presence check (e.g. `LegacyContainmentAliases[value] ?? value`) rather + * than indexing blindly. + * + * The wire payload is forwarded to wxc-exec unchanged — the Rust parser + * performs the final mapping at runtime. Resolution here is purely so the + * SDK's experimental-mode, platform-support, and availability checks see + * the canonical backend. + * + * Internal to the SDK; not part of the public API. Subject to removal in a + * future minor release once the deprecation window closes. + */ +export const LegacyContainmentAliases: Readonly>> = { + appcontainer: 'processcontainer', + macos_sandbox: 'seatbelt', +}; + +/** + * Concrete containment backend. Each value names a specific runner + * implementation in the native binary. Prefer a {@link ContainmentType} + * value unless you specifically need to force a particular backend. + */ +export type ContainmentBackend = + | 'processcontainer' + | 'windows_sandbox' + | 'wslc' + | 'lxc' + | 'nvx' + | 'hyperlight' + | 'seatbelt' + | 'isolation_session' + | 'bubblewrap'; + +/** + * Containment values (abstract intent or concrete backend) that require + * the `--experimental` flag. + */ +export const ExperimentalBackends: readonly (ContainmentType | ContainmentBackend)[] = [ + 'nvx', + 'windows_sandbox', + 'hyperlight', + 'wslc', + 'isolation_session', +]; + +/** + * Clipboard access policy levels + */ +export type ClipboardPolicy = "none" | "read" | "write" | "all"; + +/** + * Cross-platform UI configuration in ContainerConfig. + * Mapped from SandboxPolicy.ui by createConfigFromPolicy. + */ +export interface UiConfig { + /** Whether UI is disabled (no visible windows). Maps from !policy.ui.allowWindows. */ + disable: boolean; + /** Clipboard access level */ + clipboard: ClipboardPolicy; + /** Whether input injection is allowed */ + injection: boolean; +} + +/** + * BaseProcess-specific UI configuration (Windows only). + * Lives under processContainer.ui in ContainerConfig. + */ +export interface BaseProcessUiConfig { + /** UI isolation level for the desktop */ + isolation: "desktop" | "handles" | "atoms" | "container"; + /** Whether desktop system control is allowed */ + desktopSystemControl: boolean; + /** System settings access level */ + systemSettings: string; + /** Whether IME (Input Method Editor) is allowed */ + ime: boolean; +} + +/** + * ProcessContainer configuration for the Windows process-level backend. + * + * `processcontainer` is the abstraction layer; the runner picks between + * the legacy AppContainer implementation (which honors `capabilities`, + * `leastPrivilege`) and the newer BaseContainer implementation (which + * honors `ui`) at run time based on the host OS and the `--experimental` + * flag. + */ +export interface ProcessContainerConfig { + /** AppContainer profile name (default: "CLI"). Deprecated: use containerId instead. */ + name?: string; + /** Use least privilege mode with PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT (default: false) */ + leastPrivilege?: boolean; + /** + * Enable deny-and-record learning mode. Failed access checks are logged while + * accesses remain denied and containment remains enforced. + */ + learningMode?: boolean; + /** + * Additional AppContainer capabilities (e.g., "registryRead", "internetClient"). + * Each entry must contain one capability name and must not contain a comma. + * The reserved learning-mode capability names must not be supplied directly. + */ + capabilities?: string[]; + /** BaseProcess-specific UI settings (Windows only) */ + ui?: BaseProcessUiConfig; + /** ProcessContainer-specific networking settings. */ + network?: { + /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ + allowedProxyPeer?: string; + }; +} + +/** + * Filesystem access configuration + */ +export interface FilesystemConfig { + /** Paths the script can read and write */ + readwritePaths?: string[]; + /** Paths the script can read but not write */ + readonlyPaths?: string[]; + /** Paths the script cannot access */ + deniedPaths?: string[]; + /** Automatically remove file access policy after execution (default: true) */ + clearPolicyOnExit?: boolean; +} + +/** + * Network access configuration + */ +export interface NetworkConfig { + /** + * Network enforcement mode: + * - "capabilities": Use AppContainer capabilities only (no admin required) + * - "firewall": Use Windows Firewall rules (requires admin) + * - "both": Use both capabilities and firewall rules (requires admin) + * (default: "both") + */ + enforcementMode?: 'capabilities' | 'firewall' | 'both'; + /** Default network policy: "allow" or "block" (default: "block") */ + defaultPolicy?: 'allow' | 'block'; + /** + * Whether to allow inbound connections to local IP listeners (i.e. the + * sandboxed process may call `bind()` + `listen()` and accept incoming + * TCP/UDP). Independent of `defaultPolicy`. (default: false) + */ + allowLocalNetwork?: boolean; + /** Hostnames or IP addresses/CIDR blocks to allow (firewall mode only) */ + allowedHosts?: string[]; + /** Hostnames or IP addresses to block (firewall mode only) */ + blockedHosts?: string[]; + /** Proxy configuration (supported on Windows ProcessContainer, Linux Bubblewrap, + * macOS Seatbelt, and WSLC). On Bubblewrap/Seatbelt/WSLC it is a cooperative + * env-var proxy (HTTP_PROXY/HTTPS_PROXY): well-behaved HTTP clients honor it, + * raw-socket clients can bypass it. `builtinTestServer` activates a bundled, + * testing-only proxy; the SDK rejects it unless `allowTestingFeatures: true` is + * set in SandboxSpawnOptions (which maps to the native `--allow-testing-features` + * flag). + * + * WSLC imposes additional parse-time constraints (a violating config is + * rejected before it runs): + * - Only the `{ url }` form is accepted — its containers run in their own + * network namespace, so the `localhost` / `builtinTestServer` loopback + * forms are unreachable and rejected. + * - The `url` scheme must be `http` or `https`. + * - `defaultPolicy` must be `"allow"` and both `allowedHosts` and + * `blockedHosts` must be empty/unset — WSLC has no in-kernel iptables, so + * it cannot enforce host lists, and the container needs outbound + * networking to reach the proxy at all. + * Enforcement is cooperative (no in-kernel iptables). */ + proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; + /** Automatically remove firewall rules after execution (default: true). Deprecated: use lifecycle.preservePolicy. */ + removeRulesOnExit?: boolean; + /** Outbound network policy. */ + egress?: NetworkEgressConfig; + /** Inbound and host-loopback network policy. */ + ingress?: NetworkIngressConfig; +} + +/** Allow or deny network action. */ +export type NetworkAction = 'allow' | 'deny'; + +/** Transport protocol selector. */ +export type NetworkProtocol = 'tcp' | 'udp' | 'icmp' | 'any'; + +/** CIDR network peer. */ +export interface NetworkPeerConfig { + /** IPv4 or IPv6 CIDR. */ + cidr: string; + /** CIDRs excluded from this peer. */ + except?: string[]; +} + +/** Protocol and destination-port selector. */ +export interface NetworkPortConfig { + /** Transport protocol. Defaults to "any". */ + protocol?: NetworkProtocol; + /** Destination port. Omission matches every port. */ + port?: number; + /** Inclusive end of a destination-port range. Requires `port`. */ + endPort?: number; +} + +/** Outbound network rule. */ +export interface NetworkRuleConfig { + /** Destination CIDRs. Omission matches both IP families; an explicit array must be non-empty. */ + to?: NetworkPeerConfig[]; + /** Destination protocols and ports. Omission matches all; an explicit array must be non-empty. */ + ports?: NetworkPortConfig[]; +} + +/** Outbound network policy. */ +export interface NetworkEgressConfig { + /** Action used when no explicit rule matches. Defaults to "deny". */ + default?: NetworkAction; + /** Explicit allow rules. */ + allow?: NetworkRuleConfig[]; + /** Explicit deny rules. Deny rules take precedence. */ + deny?: NetworkRuleConfig[]; +} + +/** Inbound and host-loopback network policy. */ +export interface NetworkIngressConfig { + /** Default action for LAN/private-network inbound traffic. */ + default?: NetworkAction; + /** Bidirectional host-loopback connectivity action. */ + hostLoopback?: NetworkAction; +} + +/** Runtime values supplied separately from sandbox policy. */ +export interface RuntimeConfig { + /** HTTP/S loopback proxy URL. */ + networkProxy?: string; +} + +/** + * WSLC SDK configuration for Linux containers from Windows + */ +export interface WslcConfig { + /** OCI container image name (default: "alpine:latest") */ + image?: string; + /** Storage path for WSLC session image store */ + storagePath?: string; + /** Target OS for the container (default: "linux") */ + targetOs?: string; + /** Number of CPUs allocated to the WSLC session */ + cpuCount?: number; + /** Memory in MB allocated to the WSLC session */ + memoryMb?: number; + /** Enable GPU passthrough to the container (default: false) */ + gpu?: boolean; + /** Path to a local tar file to import as the container image */ + imageTarPath?: string; + /** + * Host↔container port mappings. + * + * Only TCP is currently supported by the WSLC SDK runtime. UDP is declared + * in the SDK header but the shipped runtime returns `E_NOTIMPL` when UDP is + * actually requested, so the parser hard-rejects `"udp"` with a clear + * message at spawn time. The `protocol` field defaults to `"tcp"` when + * omitted. + */ + portMappings?: PortMapping[]; +} + +/** + * Port mapping for host↔container port forwarding. + */ +export interface PortMapping { + /** Port on the Windows host */ + windowsPort: number; + /** Port inside the Linux container */ + containerPort: number; + /** + * Transport protocol. Only `"tcp"` is currently supported; `"udp"` is + * rejected by the parser because the WSLC SDK runtime returns `E_NOTIMPL` + * for UDP even though the header declares it. Defaults to `"tcp"` when + * omitted. + */ + protocol?: 'tcp'; +} + +/** Telemetry configuration for TraceLogging ETW support. */ +export interface TelemetryConfig { + /** + * Explicit telemetry opt-in. `true` requests telemetry subject to user + * consent and administrative policy; `false` or `undefined` keeps it off. + */ + enabled?: boolean; +} + +/** + * Main WXC configuration + */ +export interface ContainerConfig { + /** MXC config schema version. Required. */ + version: string; + /** Externally assigned container identifier */ + containerId?: string; + /** Containment intent (preferred) or concrete backend (override). */ + containment?: ContainmentType | ContainmentBackend; + /** Container lifecycle settings */ + lifecycle?: LifecycleConfig; + /** Process execution settings (required) */ + process?: ProcessConfig; + /** ProcessContainer configuration */ + processContainer?: ProcessContainerConfig; + /** + * Legacy alias of {@link processContainer}. Retained so callers + * migrating from pre-0.6 SDK versions can keep their existing code + * compiling; the native binary parses both names into the same slot + * via a serde alias. + * + * @deprecated Use {@link processContainer} instead. This alias may be + * removed in a future minor release. + */ + appContainer?: ProcessContainerConfig; + /** LXC container configuration (Linux only) */ + lxc?: LxcConfig; + /** Filesystem access configuration */ + filesystem?: FilesystemConfig; + /** Network access configuration */ + network?: NetworkConfig; + /** Runtime values supplied separately from sandbox policy. */ + runtimeConfig?: RuntimeConfig; + /** Telemetry configuration */ + telemetry?: TelemetryConfig; + /** Experimental features (only applied when --experimental flag is set) */ + experimental?: { + /** WSLC SDK configuration for Linux containers from Windows */ + wslc?: WslcConfig; + }; + /** macOS Seatbelt sandbox configuration (macOS only) */ + seatbelt?: SeatbeltConfig; + /** Cross-platform UI configuration */ + ui?: UiConfig; +} + +/** + * The main sandbox policy configuration interface for external consumers + * to define sandboxed execution environments. + * + * Policy describes *what* the caller wants restricted. Cross-platform. + * No OS-specific content. Omitted fields = most restrictive (default-deny). + */ +export type SandboxPolicy = { + /** Policy version (semver). Must match a supported schema version. */ + version: string; + /** Filesystem access restrictions */ + filesystem?: { + /** Paths that are granted read and write access */ + readwritePaths?: string[]; + /** Paths that are granted read-only access */ + readonlyPaths?: string[]; + /** Paths that are explicitly denied all access */ + deniedPaths?: string[]; + /** Whether to clear the filesystem policy when the shell exits. (default: true) */ + clearPolicyOnExit?: boolean; + }; + /** Network access restrictions. All flags default to false (no network access). */ + network?: { + /** Whether to allow outbound connections to the Internet. (default: false) Legacy network field. */ + allowOutbound?: boolean; + /** Whether to allow connections to local networks. (default: false) Legacy network field. */ + allowLocalNetwork?: boolean; + /** When set, ONLY these outbound hosts are reachable. Requires allowOutbound. Legacy network field. */ + allowedHosts?: string[]; + /** Hosts to block even when outbound is allowed. Requires allowOutbound. Legacy network field. */ + blockedHosts?: string[]; + /** + * Proxy configuration. Routes cooperating HTTP traffic through this proxy. + * Supported on Windows ProcessContainer, Linux Bubblewrap, and macOS + * Seatbelt. On Bubblewrap/Seatbelt it is a cooperative env-var proxy + * (HTTP_PROXY/HTTPS_PROXY) — raw-socket clients can bypass it. Native + * validation enforces backend-specific combination rules. + * `builtinTestServer` selects a bundled, testing-only proxy; the SDK + * rejects it unless `allowTestingFeatures: true` is set in + * SandboxSpawnOptions (which maps to the native + * `--allow-testing-features` flag). + * Legacy network field. + */ + proxy?: { builtinTestServer: true } | { localhost: number } | { url: string }; + /** Schema 0.8 outbound network policy. Cannot be combined with legacy network fields. */ + egress?: NetworkEgressConfig; + /** Schema 0.8 inbound and host-loopback policy. Cannot be combined with legacy network fields. */ + ingress?: NetworkIngressConfig; + }; + /** Schema 0.8 runtime values supplied separately from sandbox policy. */ + runtimeConfig?: RuntimeConfig; + /** Schema 0.8 ProcessContainer-specific policy. */ + processContainer?: { + /** ProcessContainer-specific networking settings. */ + network?: { + /** Package family name or AppContainer profile authorized as the loopback proxy peer. */ + allowedProxyPeer?: string; + }; + }; + /** UI access restrictions. All flags default to denied. */ + ui?: { + /** Whether the sandbox may create visible windows. (default: false) */ + allowWindows?: boolean; + /** Clipboard access level. (default: "none") */ + clipboard?: ClipboardPolicy; + /** Whether the sandbox may inject keyboard/mouse input. (default: false) */ + allowInputInjection?: boolean; + }; + /** Execution timeout in milliseconds. Omitted = no timeout. */ + timeoutMs?: number; +} + +/** + * LXC container configuration for Linux sandbox + */ +export interface LxcConfig { + /** Container name (default: auto-generated) */ + containerName?: string; + /** Linux distribution for container rootfs (default: "alpine") */ + distribution?: string; + /** Distribution release version (default: "3.19") */ + release?: string; + /** Whether to destroy the container after execution (default: true) */ + destroyOnExit?: boolean; +} + +/** + * macOS Seatbelt sandbox configuration. Used under the top-level + * `seatbelt` key when `containment == "seatbelt"`. + */ +export interface SeatbeltConfig { + /** + * Optional override of the generated TinyScheme sandbox profile. + */ + profileOverride?: string; + /** + * Allow the inner process to allocate its own pseudo-terminals via + * `posix_openpt` (needed by tests, `git`, `gh`, REPLs, and any tool + * that wraps commands in a pty). Adds `(allow pseudo-tty)` and + * read/write/ioctl on `/dev/ptmx` to the generated profile. Defaults + * to `true`; set to `false` for the tightest possible sandbox when + * the inner command does not need to allocate new ttys. + */ + nestedPty?: boolean; + /** + * Allow the inner process to use the macOS Keychain (e.g. via + * `keytar` or `Security.framework`) end-to-end. Adds Mach lookup for + * `securityd`, `trustd`, `ocspd`, `cfprefsd`, `xpcd`, and the + * `com.apple.lsd.*` family; read access to `/private/var/db/mds` and + * `/private/var/protected/trustd`; and read+write access to + * `~/Library/Keychains` and `/private/var/folders` (XPC cache). + * Defaults to `false`; opt in only when the inner workload genuinely + * needs Keychain access. + */ + keychainAccess?: boolean; + /** + * Additional Mach service global-names to allow `mach-lookup` for. + * Escape hatch for callers that need a specific system service the + * baseline doesn't cover (e.g. opt-in agent integrations). Each entry + * is rendered as `(global-name "...")` inside a single + * `(allow mach-lookup ...)` form. + */ + extraMachLookups?: string[]; +} + +/** + * Sandboxing methods available on the platform + * + * @deprecated Prefer {@link ContainmentBackend} (concrete) or + * {@link ContainmentType} (abstract). This alias is retained for + * backward compatibility and may be removed in a future minor release. + */ +export type SandboxingMethod = ContainmentType | ContainmentBackend; + +/** + * Isolation tier selected by the runtime fallback detector. + * + * - `base-container`: full BaseContainer (Experimental_CreateProcessInSandbox) + * - `appcontainer-bfs`: AppContainer + BFS filesystem isolation + * - `appcontainer-dacl`: AppContainer + host DACL augmentation (last-resort fallback) + */ +export type IsolationTier = + | 'base-container' + | 'appcontainer-bfs' + | 'appcontainer-dacl'; + +/** + * Host support for enforcing sandbox UI restrictions. + * + * The fields describe platform-agnostic restriction intents, not the + * OS-specific primitive used to enforce them. For example, Windows derives + * these values from `JOB_OBJECT_UILIMIT_*` support. The SDK currently + * receives this object only from the Windows native probe; other platforms + * omit `PlatformSupport.uiCapabilities` until they expose equivalent probe + * data. + */ +export interface UiCapabilitySupport { + /** Whether the host can block reads from the clipboard. */ + canBlockClipboardRead: boolean; + /** Whether the host can block writes to the clipboard. */ + canBlockClipboardWrite: boolean; + /** Whether the host can block synthetic keyboard/mouse input. */ + canBlockInputInjection: boolean; + /** Whether the host can block input method / IME changes. */ + canBlockInputMethodChanges: boolean; + /** Whether the host can block access to external UI object handles. */ + canBlockExternalUiObjects: boolean; + /** Whether the host can block access to global UI namespaces. */ + canBlockGlobalUiNamespace: boolean; + /** Whether the host can block desktop switching. */ + canBlockDesktopSwitching: boolean; + /** Whether the host can block logoff or shutdown requests. */ + canBlockLogoffOrShutdown: boolean; + /** Whether the host can block system parameter changes. */ + canBlockSystemParameterChanges: boolean; + /** Whether the host can block display settings changes. */ + canBlockDisplaySettingsChanges: boolean; +} + +/** + * Platform support information + */ +export interface PlatformSupport { + /** Whether WXC is supported on the current platform */ + isSupported: boolean; + /** Reason why the platform is not supported (if applicable) */ + reason?: string; + /** Available sandboxing methods on this platform */ + availableMethods: ContainmentBackend[]; + /** + * Why individual Linux backends are unavailable, whether or not another + * backend keeps Linux supported. Omitted on other platforms and when no + * Linux backend failures were observed. + */ + unavailableReasons?: Partial>; + /** + * Tier that would be selected for an empty policy on this system. + * Omitted on non-Windows platforms or when the probe fails. + */ + isolationTier?: IsolationTier; + /** + * Tier degradation warnings (one per fall-through during selection). + * Omitted on non-Windows platforms or when the probe fails. + */ + isolationWarnings?: string[]; + /** + * Host UI-restriction capabilities. Omitted when the backend probe cannot + * determine them, including on Linux and macOS today. + */ + uiCapabilities?: UiCapabilitySupport; +} From ab165a307129ee84efecceed7ba0b789b921105b Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 12:43:27 -0700 Subject: [PATCH 23/41] fix: harden NVX feature boundaries Use Cargo target metadata for artifact staging, keep engine-only NVX builds portable, and align the documented working-directory contract. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/schema.md | 3 +- schemas/dev/mxc-config.schema.0.9.0-dev.json | 2 +- sdk/node/src/generated/wire.ts | 2 +- src/backends/nvx/binaries/build.rs | 3 +- src/backends/nvx/build_common/src/lib.rs | 41 ++++++++++++++++++++ src/backends/nvx/runner/Cargo.toml | 2 +- src/core/wxc/Cargo.toml | 2 - src/core/wxc/build.rs | 25 ++++++------ src/core/wxc_common/src/wire.rs | 5 ++- 9 files changed, 63 insertions(+), 22 deletions(-) diff --git a/docs/schema.md b/docs/schema.md index e179aafa0..ba6d7d74d 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -246,7 +246,8 @@ use: | Windows ProcessContainer (AppContainer / BaseContainer) | First `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root (`%SystemDrive%\`). Never `NULL`. | | Seatbelt (macOS) | Same precedence, with `~` expanded as the profile expands it; falls back to `/`. | | LXC / WSL Container | The container root — see [`docs/lxc-support/lxc-backend.md`](lxc-support/lxc-backend.md). | -| NVX / Hyperlight | Not applicable — these backends reject a working directory outright. | +| NVX | Guest root when omitted; a caller-supplied guest path is preserved. | +| Hyperlight | Not applicable — this backend rejects a working directory outright. | Policy entries that are blank, name a file, or do not exist yet are skipped: a process cannot be launched in any of them. diff --git a/schemas/dev/mxc-config.schema.0.9.0-dev.json b/schemas/dev/mxc-config.schema.0.9.0-dev.json index ac677a089..ab2c306f6 100644 --- a/schemas/dev/mxc-config.schema.0.9.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.9.0-dev.json @@ -743,7 +743,7 @@ ] }, "cwd": { - "description": "Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NVX and Hyperlight reject a working directory outright. See `docs/schema.md` (\"Working Directory\").", + "description": "Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NVX uses the guest root and preserves a caller-supplied guest path; Hyperlight rejects a working directory outright. See `docs/schema.md` (\"Working Directory\").", "type": [ "string", "null" diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index cd6d76f9d..b33ffcfc3 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -354,7 +354,7 @@ export interface Process { */ commandLine?: string | null; /** - * Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NVX and Hyperlight reject a working directory outright. See `docs/schema.md` ("Working Directory"). + * Working directory for the process. When omitted, backends substitute a directory the sandbox can use rather than inheriting the launcher's cwd: Windows ProcessContainer picks the first `readwritePaths` entry that is an existing directory, else the first such `readonlyPaths` entry, else the system drive root; Seatbelt applies the same precedence with a `/` fallback; LXC/WSL use the container root; NVX uses the guest root and preserves a caller-supplied guest path; Hyperlight rejects a working directory outright. See `docs/schema.md` ("Working Directory"). */ cwd?: string | null; /** diff --git a/src/backends/nvx/binaries/build.rs b/src/backends/nvx/binaries/build.rs index 71919ab81..08bc82bca 100644 --- a/src/backends/nvx/binaries/build.rs +++ b/src/backends/nvx/binaries/build.rs @@ -77,8 +77,7 @@ fn ensure_supported_target() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); - let target = std::env::var("TARGET") - .unwrap_or_else(|_| format!("{target_arch}-pc-{target_os}-{target_env}")); + let target = std::env::var("TARGET").expect("nvx_binaries: TARGET is not set by Cargo"); nvx_build_common::validate_nvx_target(&target, &target_os, &target_arch, &target_env) .unwrap_or_else(|error| panic!("nvx_binaries: {error}")); } diff --git a/src/backends/nvx/build_common/src/lib.rs b/src/backends/nvx/build_common/src/lib.rs index a7c2dbbc2..7506bb6f3 100644 --- a/src/backends/nvx/build_common/src/lib.rs +++ b/src/backends/nvx/build_common/src/lib.rs @@ -56,6 +56,25 @@ pub fn validate_nvx_target( } } +/// Determines whether an executor build should stage NVX artifacts. +/// +/// Non-Windows targets do not package NVX. Windows targets must be the exact +/// supported x64 MSVC triple; unsupported Windows targets fail instead of +/// silently producing an incomplete NVX-enabled executor. +pub fn should_stage_nvx( + target: &str, + target_os: &str, + target_arch: &str, + target_env: &str, +) -> Result { + if target_os != "windows" { + return Ok(false); + } + + validate_nvx_target(target, target_os, target_arch, target_env)?; + Ok(true) +} + /// Formats the explicit error message used when NVX is requested for an /// unsupported target. pub fn unsupported_target_message(target: &str) -> String { @@ -317,4 +336,26 @@ mod tests { assert!(error.contains(SUPPORTED_NVX_TARGET_TRIPLE)); assert!(error.contains("aarch64-pc-windows-msvc")); } + + #[test] + fn staging_decision_uses_target_not_build_host() { + assert_eq!( + should_stage_nvx("x86_64-pc-windows-msvc", "windows", "x86_64", "msvc"), + Ok(true) + ); + assert_eq!( + should_stage_nvx("x86_64-unknown-linux-gnu", "linux", "x86_64", "gnu"), + Ok(false) + ); + + for (target, arch, env) in [ + ("aarch64-pc-windows-msvc", "aarch64", "msvc"), + ("x86_64-pc-windows-gnu", "x86_64", "gnu"), + ] { + let error = should_stage_nvx(target, "windows", arch, env) + .expect_err("unsupported Windows targets must fail closed"); + assert!(error.contains(SUPPORTED_NVX_TARGET_TRIPLE)); + assert!(error.contains(target)); + } + } } diff --git a/src/backends/nvx/runner/Cargo.toml b/src/backends/nvx/runner/Cargo.toml index 59339f05e..5abf46424 100644 --- a/src/backends/nvx/runner/Cargo.toml +++ b/src/backends/nvx/runner/Cargo.toml @@ -7,7 +7,7 @@ description = "NVX backend preflight checks for incomplete runtime builds" [features] default = [] -nvx = ["nvx_binaries/nvx"] +nvx = [] [dependencies] nvx_binaries = { workspace = true } diff --git a/src/core/wxc/Cargo.toml b/src/core/wxc/Cargo.toml index 38572e5ff..a6f345114 100644 --- a/src/core/wxc/Cargo.toml +++ b/src/core/wxc/Cargo.toml @@ -31,8 +31,6 @@ tempfile.workspace = true [build-dependencies] mxc_build_common.workspace = true - -[target.'cfg(windows)'.build-dependencies] nvx_build_common = { workspace = true, optional = true } [features] diff --git a/src/core/wxc/build.rs b/src/core/wxc/build.rs index 142278f6d..7574a1b24 100644 --- a/src/core/wxc/build.rs +++ b/src/core/wxc/build.rs @@ -9,11 +9,8 @@ fn main() { #[cfg(windows)] check_test_prerequisites(); - #[cfg(all(windows, feature = "nvx"))] - { - ensure_supported_nvx_target(); - copy_nvx_binaries(); - } + #[cfg(feature = "nvx")] + stage_nvx_for_target(); // Delay-load winhvplatform.dll so WHP-less hosts don't crash before main(). // CARGO_CFG_TARGET_* (not #[cfg]) because build.rs cfg gates are host, not target. @@ -82,18 +79,22 @@ fn check_test_prerequisites() { } } -#[cfg(all(windows, feature = "nvx"))] -fn ensure_supported_nvx_target() { +#[cfg(feature = "nvx")] +fn stage_nvx_for_target() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); - let target = std::env::var("TARGET") - .unwrap_or_else(|_| format!("{target_arch}-pc-{target_os}-{target_env}")); - nvx_build_common::validate_nvx_target(&target, &target_os, &target_arch, &target_env) - .unwrap_or_else(|error| panic!("wxc build.rs: {error}")); + + let target = std::env::var("TARGET").expect("wxc build.rs: TARGET is not set by Cargo"); + let should_stage = + nvx_build_common::should_stage_nvx(&target, &target_os, &target_arch, &target_env) + .unwrap_or_else(|error| panic!("wxc build.rs: {error}")); + if should_stage { + copy_nvx_binaries(); + } } -#[cfg(all(windows, feature = "nvx"))] +#[cfg(feature = "nvx")] fn copy_nvx_binaries() { use std::path::Path; diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index 1a8644aff..33a80b9bb 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -166,8 +166,9 @@ pub struct Process { /// Windows ProcessContainer picks the first `readwritePaths` entry that is /// an existing directory, else the first such `readonlyPaths` entry, else /// the system drive root; Seatbelt applies the same precedence with a `/` - /// fallback; LXC/WSL use the container root; NVX and Hyperlight reject a - /// working directory outright. See `docs/schema.md` ("Working Directory"). + /// fallback; LXC/WSL use the container root; NVX uses the guest root and + /// preserves a caller-supplied guest path; Hyperlight rejects a working + /// directory outright. See `docs/schema.md` ("Working Directory"). pub cwd: Option, /// Environment variables as `"KEY=VALUE"` strings. pub env: Option>, From 5db179ca64511df70868f95a47c2ad4d7dbb79cb Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 13:55:09 -0700 Subject: [PATCH 24/41] fix: address follow-up NVX review findings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 6 +++- sdk/node/src/sandbox.ts | 6 ++++ sdk/node/tests/unit/sandbox.test.ts | 43 ++++++++++++++++++++++++ src/backends/nvx/build_common/src/lib.rs | 15 ++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index 7454da16e..d9b80e60c 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -90,7 +90,11 @@ types. The retired `microvm` identifier is absent from both development artifact Published stable schemas remain unchanged. An `experimental.nvx.provision` section is necessary only if images remain caller-configurable. -Example state-aware provision request: +Future state-aware provision design: + +> **Not runnable in Phase 1.** The exact development state-aware registry does +> not yet include NVX, so the current parser rejects this request before +> dispatch. This example records the intended Phase 2 request shape only. ```json { diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index d38c84041..c7cb5b9b1 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -485,6 +485,12 @@ export function createConfigFromPolicy( } // Backend-specific config based on containment type + if (containment === 'nvx') { + diagLog(`createConfigFromPolicy: containment=nvx, id=${containerId}`); + config.containment = 'nvx'; + return config; + } + if (containment === 'wslc') { return buildWslcContainerConfig(config, policy, containerId); } diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 26ba7d17d..d2dbb049d 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -540,6 +540,49 @@ describe('buildSandboxPayload', () => { } }); + it('should build an NVX payload for the native backend', () => { + mockWindows(); + try { + const policy: SandboxPolicy = { + version: '0.9.0-alpha', + filesystem: { + readonlyPaths: ['C:\\workspace\\source'], + readwritePaths: ['C:\\workspace\\output'], + }, + network: { + egress: { default: 'deny' }, + ingress: { default: 'deny', hostLoopback: 'deny' }, + }, + }; + + const payload = buildSandboxPayload( + 'echo hello', + policy, + '/', + 'nvx-test', + 'nvx', + ); + + assert.strictEqual(payload.containment, 'nvx'); + assert.strictEqual(payload.containerId, 'nvx-test'); + assert.deepStrictEqual(payload.process, { + commandLine: 'echo hello', + timeout: 0, + cwd: '/', + }); + assert.deepStrictEqual(payload.filesystem, { + readonlyPaths: ['C:\\workspace\\source'], + readwritePaths: ['C:\\workspace\\output'], + deniedPaths: [], + }); + assert.deepStrictEqual(payload.network, policy.network); + assert.strictEqual(payload.processContainer, undefined); + assert.strictEqual(payload.lxc, undefined); + } finally { + restore(); + } + }); + }); describe('WSLC', () => { diff --git a/src/backends/nvx/build_common/src/lib.rs b/src/backends/nvx/build_common/src/lib.rs index 7506bb6f3..61c95c8bf 100644 --- a/src/backends/nvx/build_common/src/lib.rs +++ b/src/backends/nvx/build_common/src/lib.rs @@ -49,7 +49,9 @@ pub fn validate_nvx_target( target_arch: &str, target_env: &str, ) -> Result<(), String> { - if target_supports_nvx(target_os, target_arch, target_env) { + if target == SUPPORTED_NVX_TARGET_TRIPLE + && target_supports_nvx(target_os, target_arch, target_env) + { Ok(()) } else { Err(unsupported_target_message(target)) @@ -337,6 +339,16 @@ mod tests { assert!(error.contains("aarch64-pc-windows-msvc")); } + #[test] + fn validate_nvx_target_rejects_custom_target_with_matching_cfg_values() { + let target = "custom-windows-x64-msvc"; + let error = validate_nvx_target(target, "windows", "x86_64", "msvc") + .expect_err("only the exact supported target triple may stage NVX"); + + assert!(error.contains(SUPPORTED_NVX_TARGET_TRIPLE)); + assert!(error.contains(target)); + } + #[test] fn staging_decision_uses_target_not_build_host() { assert_eq!( @@ -351,6 +363,7 @@ mod tests { for (target, arch, env) in [ ("aarch64-pc-windows-msvc", "aarch64", "msvc"), ("x86_64-pc-windows-gnu", "x86_64", "gnu"), + ("custom-windows-x64-msvc", "x86_64", "msvc"), ] { let error = should_stage_nvx(target, "windows", arch, env) .expect_err("unsupported Windows targets must fail closed"); From cbf87780337f592d3a5fce7fab7824ba895c63b0 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 14:18:49 -0700 Subject: [PATCH 25/41] fix: complete NVX artifact packaging Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- build.bat | 65 +++++++++++++++++++++++ src/backends/nvx/build_common/src/lib.rs | 67 +++++++++++++++++++++--- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/build.bat b/build.bat index 26e455464..25a21e5b8 100644 --- a/build.bat +++ b/build.bat @@ -137,6 +137,71 @@ for %%T in (x86_64-pc-windows-msvc aarch64-pc-windows-msvc) do ( copy /Y "!BIN_DIR!\mxc_ffi.dll" "sdk\node\bin\!SDK_ARCH!\" >nul echo Copied !SDK_ARCH!\mxc_ffi.dll ) + if "%%T"=="x86_64-pc-windows-msvc" ( + if "%WITH_NVX%"=="1" ( + for %%B in (bin\openvmm.exe guest\vmlinux guest\initramfs.cpio.gz) do ( + if not exist "!BIN_DIR!\%%B" ( + echo ERROR: NVX-enabled Node runtime is missing !BIN_DIR!\%%B + exit /b 1 + ) + ) + set "NVX_WORKLOAD_IMAGE_COUNT=0" + for %%B in (images\distro.erofs images\runtime.erofs images\scratch.ext4) do ( + if exist "!BIN_DIR!\%%B" set /A NVX_WORKLOAD_IMAGE_COUNT+=1 + ) + if not "!NVX_WORKLOAD_IMAGE_COUNT!"=="0" if not "!NVX_WORKLOAD_IMAGE_COUNT!"=="3" ( + echo ERROR: NVX-enabled Node runtime has an incomplete workload-image bundle. + exit /b 1 + ) + for %%B in (images\distro.erofs images\runtime.erofs images\scratch.ext4) do ( + if exist "sdk\node\bin\!SDK_ARCH!\%%B" del /Q "sdk\node\bin\!SDK_ARCH!\%%B" + ) + if exist "sdk\node\bin\!SDK_ARCH!\images" rd "sdk\node\bin\!SDK_ARCH!\images" 2>nul + for %%B in (bin\openvmm.exe guest\vmlinux guest\initramfs.cpio.gz) do ( + for %%D in ("sdk\node\bin\!SDK_ARCH!\%%B") do ( + if not exist "%%~dpD" ( + mkdir "%%~dpD" + if errorlevel 1 ( + echo ERROR: Failed to create NVX Node runtime directory %%~dpD + exit /b 1 + ) + ) + ) + copy /Y "!BIN_DIR!\%%B" "sdk\node\bin\!SDK_ARCH!\%%B" >nul + if errorlevel 1 ( + echo ERROR: Failed to copy NVX Node runtime artifact %%B + exit /b 1 + ) + echo Copied !SDK_ARCH!\%%B + ) + if "!NVX_WORKLOAD_IMAGE_COUNT!"=="3" ( + for %%B in (images\distro.erofs images\runtime.erofs images\scratch.ext4) do ( + for %%D in ("sdk\node\bin\!SDK_ARCH!\%%B") do ( + if not exist "%%~dpD" ( + mkdir "%%~dpD" + if errorlevel 1 ( + echo ERROR: Failed to create NVX Node runtime directory %%~dpD + exit /b 1 + ) + ) + ) + copy /Y "!BIN_DIR!\%%B" "sdk\node\bin\!SDK_ARCH!\%%B" >nul + if errorlevel 1 ( + echo ERROR: Failed to copy NVX Node runtime artifact %%B + exit /b 1 + ) + echo Copied !SDK_ARCH!\%%B + ) + ) + ) else ( + for %%B in (bin\openvmm.exe guest\vmlinux guest\initramfs.cpio.gz images\distro.erofs images\runtime.erofs images\scratch.ext4) do ( + if exist "sdk\node\bin\!SDK_ARCH!\%%B" del /Q "sdk\node\bin\!SDK_ARCH!\%%B" + ) + if exist "sdk\node\bin\!SDK_ARCH!\bin" rd "sdk\node\bin\!SDK_ARCH!\bin" 2>nul + if exist "sdk\node\bin\!SDK_ARCH!\guest" rd "sdk\node\bin\!SDK_ARCH!\guest" 2>nul + if exist "sdk\node\bin\!SDK_ARCH!\images" rd "sdk\node\bin\!SDK_ARCH!\images" 2>nul + ) + ) if "!COPY_WSLC_RUNTIME!"=="1" ( if "%WITH_WSLC%"=="1" ( for %%B in (wxc-wslc-daemon.exe wslcsdk.dll) do ( diff --git a/src/backends/nvx/build_common/src/lib.rs b/src/backends/nvx/build_common/src/lib.rs index 61c95c8bf..8fde0214d 100644 --- a/src/backends/nvx/build_common/src/lib.rs +++ b/src/backends/nvx/build_common/src/lib.rs @@ -133,7 +133,15 @@ pub fn copy_artifact_paths<'a>( relative_paths: impl IntoIterator, ) -> io::Result<()> { let relative_paths: Vec<&str> = relative_paths.into_iter().collect(); - for relative_path in &relative_paths { + validate_artifact_paths(src_dir, relative_paths.iter().copied())?; + copy_validated_artifact_paths(src_dir, target_dir, relative_paths) +} + +fn validate_artifact_paths<'a>( + src_dir: &Path, + relative_paths: impl IntoIterator, +) -> io::Result<()> { + for relative_path in relative_paths { let source = src_dir.join(relative_path); if !source.is_file() { return Err(io::Error::new( @@ -142,7 +150,14 @@ pub fn copy_artifact_paths<'a>( )); } } + Ok(()) +} +fn copy_validated_artifact_paths<'a>( + src_dir: &Path, + target_dir: &Path, + relative_paths: impl IntoIterator, +) -> io::Result<()> { for relative_path in relative_paths { let source = src_dir.join(relative_path); let destination = target_dir.join(relative_path); @@ -172,17 +187,44 @@ pub fn copy_artifact_paths<'a>( Ok(()) } +fn remove_artifact_paths<'a>( + target_dir: &Path, + relative_paths: impl IntoIterator, +) -> io::Result<()> { + for relative_path in relative_paths { + let destination = target_dir.join(relative_path); + match std::fs::remove_file(&destination) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => { + return Err(io::Error::new( + error.kind(), + format!( + "failed to remove stale NVX artifact '{}': {error}", + destination.display() + ), + )) + } + } + } + Ok(()) +} + /// Stages all artifacts available in the configured release. pub fn copy_artifacts_to_target( src_dir: &Path, target_dir: &Path, workload_images_available: bool, ) -> io::Result<()> { - copy_artifact_paths( - src_dir, - target_dir, - available_artifact_rel_paths(workload_images_available), - ) + let relative_paths: Vec<&str> = + available_artifact_rel_paths(workload_images_available).collect(); + validate_artifact_paths(src_dir, relative_paths.iter().copied())?; + + if !workload_images_available { + remove_artifact_paths(target_dir, WORKLOAD_IMAGE_ARTIFACTS)?; + } + + copy_validated_artifact_paths(src_dir, target_dir, relative_paths) } /// Emits Cargo change tracking for every artifact available in this release. @@ -280,6 +322,9 @@ mod tests { for relative_path in WINDOWS_PLATFORM_ARTIFACTS { write_artifact(source.path(), relative_path, relative_path.as_bytes()); } + for relative_path in WORKLOAD_IMAGE_ARTIFACTS { + write_artifact(target.path(), relative_path, b"stale"); + } copy_artifacts_to_target(source.path(), target.path(), false) .expect("platform-only staging failed"); @@ -303,6 +348,9 @@ mod tests { for relative_path in WINDOWS_PLATFORM_ARTIFACTS { write_artifact(source.path(), relative_path, b"platform"); } + for relative_path in WORKLOAD_IMAGE_ARTIFACTS { + write_artifact(target.path(), relative_path, b"existing"); + } let error = copy_artifacts_to_target(source.path(), target.path(), true) .expect_err("missing workload images must fail"); @@ -312,6 +360,13 @@ mod tests { for relative_path in WINDOWS_PLATFORM_ARTIFACTS { assert!(!target.path().join(relative_path).exists()); } + for relative_path in WORKLOAD_IMAGE_ARTIFACTS { + assert_eq!( + std::fs::read(target.path().join(relative_path)) + .expect("failed staging must preserve existing artifacts"), + b"existing" + ); + } } #[test] From 9b74f5a2cb4b0e1bd3fa8fd20e207a7b8981f766 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 14:25:33 -0700 Subject: [PATCH 26/41] fix: reject NVX proxy peer policy Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sdk/node/src/sandbox.ts | 11 +++++++++-- sdk/node/tests/unit/sandbox.test.ts | 30 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index c7cb5b9b1..29714f3a4 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -362,6 +362,7 @@ export function createConfigFromPolicy( validateTelemetryVersion(policy); const directionalNetwork = selectDirectionalNetwork(policy); const enumeratePaths = policy.processContainer?.filesystem?.enumeratePaths; + const allowedProxyPeer = policy.processContainer?.network?.allowedProxyPeer; const containerId = containerName ?? generateRandomContainerName(); @@ -395,6 +396,12 @@ export function createConfigFromPolicy( ); } } + if (containment === 'nvx' && allowedProxyPeer !== undefined) { + throw new Error( + 'processContainer.network.allowedProxyPeer is supported only by the Windows ' + + 'ProcessContainer backend.' + ); + } config.filesystem = { readwritePaths: [...(policy.filesystem?.readwritePaths ?? [])], @@ -429,11 +436,11 @@ export function createConfigFromPolicy( networkProxy: policy.runtimeConfig.networkProxy, }; } - if (policy.processContainer?.network?.allowedProxyPeer !== undefined) { + if (allowedProxyPeer !== undefined) { config.processContainer = { ...config.processContainer, network: { - allowedProxyPeer: policy.processContainer.network.allowedProxyPeer, + allowedProxyPeer, }, }; } diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index d2dbb049d..aaefe6f2b 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -583,6 +583,36 @@ describe('buildSandboxPayload', () => { } }); + it('should reject ProcessContainer proxy peer policy for NVX', () => { + mockWindows(); + try { + assert.throws( + () => buildSandboxPayload( + 'echo hello', + { + version: '0.9.0-alpha', + runtimeConfig: { + networkProxy: 'http://127.0.0.1:8080', + }, + processContainer: { + network: { + allowedProxyPeer: 'Contoso.Proxy_1234567890abc', + }, + }, + }, + undefined, + undefined, + 'nvx', + ), + { + message: /processContainer\.network\.allowedProxyPeer is supported only by the Windows ProcessContainer backend/, + }, + ); + } finally { + restore(); + } + }); + }); describe('WSLC', () => { From 773f9f2c4905466e724c1e9b6e42f26d43946d4f Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 14:33:34 -0700 Subject: [PATCH 27/41] docs: fix NVX upstream links Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index d9b80e60c..fe45df252 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -208,8 +208,8 @@ the exact schema 0.9 request and expose their outcomes through MXC APIs. | HTTP/HTTPS proxy | An exec-time `runtimeConfig.networkProxy` endpoint is injected as controlled proxy environment variables and allowed as one exact TCP endpoint under host-loopback denial. | Caller-provided proxy variables remain rejected; UDP to the same endpoint is not allowed. | | Outcomes and telemetry | Execution, lifecycle, and teardown return bounded status and failure categories without workload secrets. | Richer MXC telemetry facts are optional future work; MXC owns telemetry policy and emission. | -See the NVX [run guide](https://github.com/microsoft/nvx/blob/main/doc/run.md) -and [command-line reference](https://github.com/microsoft/nvx/blob/main/doc/usage.md) +See the NVX [run guide](https://github.com/microsoft/nvx/blob/dev/doc/run.md) +and [command-line reference](https://github.com/microsoft/nvx/blob/dev/doc/usage.md) for the current direct runtime options. ## Appendix B: NVX issue tracking From ebe878c5984f4d739b52dc78b77b74a089695354 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Mon, 21 Sep 2026 15:16:57 -0700 Subject: [PATCH 28/41] docs: align NVX with v0.10 contracts Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 6 +++--- docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index fe45df252..429470300 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -84,7 +84,7 @@ them for this backend: ## Schema changes -The rolling `0.9.0-dev` and exact `0.9.0-alpha` development contracts now +The rolling `0.10.0-dev` and exact `0.10.0-alpha` development contracts now include `containment: "nvx"` in their generated schemas and TypeScript wire types. The retired `microvm` identifier is absent from both development artifact sets. Published stable schemas remain unchanged. An `experimental.nvx.provision` section is necessary @@ -98,8 +98,8 @@ Future state-aware provision design: ```json { - "$schema": "https://aka.ms/mxc/schemas/0.9.0-alpha.json", - "version": "0.9.0-alpha", + "$schema": "https://aka.ms/mxc/schemas/0.10.0-alpha.json", + "version": "0.10.0-alpha", "phase": "provision", "containment": "nvx", "filesystem": { diff --git a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md index eff9737b1..25caff88e 100644 --- a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md +++ b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api.md @@ -511,10 +511,10 @@ State-aware reuses the same options bag as one-shot — single mental model, sin to learn the cross-cutting flags. Phase-specific fields on `SandboxSpawnOptions` (`ptyOptions`, `usePty`) are honored by `execInSandbox` / `execInSandboxAsync` and silently ignored on the other phases. State-awareness is not itself experimental — -`experimental: true` must be set when the targeted backend is itself experimental, just -as it is today for one-shot calls against `nvx` and `wslc`. IsolationSession is -experimental at the time of writing; that status is independent of the state-aware API -surface (§13). +`experimental: true` must be set only when the targeted backend participation remains +experimental, as it is for Windows Sandbox state-aware calls and for one-shot calls +against NVX or Windows Sandbox. WSLc and IsolationSession are stable and do not require +the generic experimental opt-in (§13). ### 6.3 Example From 1b1f4eecd8d8b9bfcbadc420dd81d67c7226f395 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 09:34:28 -0700 Subject: [PATCH 29/41] fix: keep MicroVM as the public backend identity Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/templates/Fuzz.Build.Job.yml | 2 +- .azure-pipelines/templates/Rust.Build.Job.yml | 2 +- .github/copilot-instructions.md | 2 +- .github/workflows/Build.Windows.Job.yml | 6 +- .github/workflows/CodeQL.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 19 ++-- build.bat | 20 ++-- docs/backend-support-probe-api-plan.md | 6 +- docs/bwrap-support/bubblewrap-backend-plan.md | 6 +- docs/nvx-backend-gaps.md | 27 ++--- docs/schema.md | 2 +- .../mxc-state-aware-sandbox-api-overview.md | 2 +- .../dev/mxc-config.schema.0.10.0-alpha.json | 4 +- schemas/dev/mxc-config.schema.0.10.0-dev.json | 4 +- sdk/node/README.md | 14 +-- sdk/node/src/generated/v0_10_0_alpha/wire.ts | 2 +- sdk/node/src/generated/wire.ts | 2 +- sdk/node/src/helper.ts | 11 +- sdk/node/src/sandbox.ts | 10 +- sdk/node/src/types.ts | 4 +- sdk/node/tests/unit/sandbox.test.ts | 34 +++--- src/backends/hyperlight/common/src/lib.rs | 2 +- src/core/lxc/Cargo.toml | 1 + src/core/mxc-sdk/Cargo.toml | 4 +- .../mxc_config_contract/src/dev/one_shot.rs | 4 +- .../tests/v0_10_0_alpha/enums.rs | 13 ++- .../tests/version_boundaries/containment.rs | 2 +- src/core/mxc_engine/Cargo.toml | 2 +- src/core/mxc_engine/src/platform.rs | 20 ++-- src/core/mxc_engine/src/probe.rs | 20 ++-- src/core/mxc_engine/src/run.rs | 107 +++++++++--------- src/core/wxc/Cargo.toml | 4 +- src/core/wxc/build.rs | 10 +- src/core/wxc/src/main.rs | 6 +- src/core/wxc_common/src/cmdline.rs | 6 +- .../config_contract_adapters/dev/one_shot.rs | 2 +- .../dev/one_shot_tests/experimental.rs | 4 +- src/core/wxc_common/src/config_parser.rs | 67 +++-------- src/core/wxc_common/src/models.rs | 10 +- src/core/wxc_common/src/splice.rs | 2 +- src/core/wxc_common/src/wire.rs | 4 +- src/testing/fuzz/Cargo.toml | 2 + src/testing/fuzz/fuzz_targets/validator.rs | 5 + 44 files changed, 230 insertions(+), 250 deletions(-) diff --git a/.azure-pipelines/templates/Fuzz.Build.Job.yml b/.azure-pipelines/templates/Fuzz.Build.Job.yml index 560b7ddd2..397bdde93 100644 --- a/.azure-pipelines/templates/Fuzz.Build.Job.yml +++ b/.azure-pipelines/templates/Fuzz.Build.Job.yml @@ -76,7 +76,7 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "fuzz build failed for $t" } } Write-Host "===== building validator =====" - cargo +$(rustNightly) fuzz build --fuzz-dir . validator --release --features hyperlight,isolation_session + cargo +$(rustNightly) fuzz build --fuzz-dir . validator --release --features hyperlight,isolation_session,microvm if ($LASTEXITCODE -ne 0) { throw "fuzz build failed for validator" } displayName: Build fuzz targets (ASAN) diff --git a/.azure-pipelines/templates/Rust.Build.Job.yml b/.azure-pipelines/templates/Rust.Build.Job.yml index 158c31d13..6d1254308 100644 --- a/.azure-pipelines/templates/Rust.Build.Job.yml +++ b/.azure-pipelines/templates/Rust.Build.Job.yml @@ -46,7 +46,7 @@ jobs: workingDirectory: $(Build.SourcesDirectory)/src # Explicit feature set (no --all-features): tier2_bfs must NOT ship # on Win 11 25H2 (bfscfg.exe risks an OS hang). - cargoFeatures: hyperlight isolation_session wslc + cargoFeatures: hyperlight isolation_session microvm wslc signPattern: | wxc-exec.exe plm.exe diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8ee7e1590..ae563c1f8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -37,7 +37,7 @@ The Rust toolchain is pinned by `src/rust-toolchain.toml`. Run Rust commands fro ```text build.bat -build.bat --with-nvx # Include the incomplete NVX foundation (Windows x64) +build.bat --with-microvm # Include the incomplete MicroVM (NVX) foundation (Windows x64) ./build.sh diff --git a/.github/workflows/Build.Windows.Job.yml b/.github/workflows/Build.Windows.Job.yml index 24b2efcd6..ce94ecd7f 100644 --- a/.github/workflows/Build.Windows.Job.yml +++ b/.github/workflows/Build.Windows.Job.yml @@ -66,13 +66,13 @@ jobs: --no-default-features --features "${{ matrix.features }}" - # Keep NVX out of the uploaded release executable while its workload + # Keep MicroVM (NVX) out of the uploaded release executable while its workload # image bundle is unavailable, but verify its opt-in build and staging. - - name: Check opt-in NVX packaging + - name: Check opt-in MicroVM packaging if: matrix.arch == 'x64' run: cargo check --locked --target ${{ matrix.target }} -p wxc --no-default-features - --features nvx + --features microvm - name: Test telemetry consent and policy in release mode run: | diff --git a/.github/workflows/CodeQL.yml b/.github/workflows/CodeQL.yml index 9b364fe0f..338df0619 100644 --- a/.github/workflows/CodeQL.yml +++ b/.github/workflows/CodeQL.yml @@ -55,7 +55,7 @@ jobs: # surface the pinned toolchain and the MxcDependencies feed so that # dependency resolution succeeds (the private feed 401s otherwise). No # `cargo build` is run, so the feature-gated backends' build workarounds - # (hyperlight/nvx/windows_sandbox) are irrelevant here. + # (hyperlight/microvm/windows_sandbox) are irrelevant here. - name: Surface toolchain file at repo root if: matrix.language == 'rust' shell: bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c22101df7..6b1278736 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,7 +143,7 @@ MXC has a Rust core (under `src/`) and a TypeScript SDK (under `sdk/node/`). The build.bat :: Release build for current architecture build.bat --debug :: Debug build build.bat --all :: Release build for both x64 and ARM64 -build.bat --with-nvx :: Include the NVX foundation (Windows x64) +build.bat --with-microvm :: Include the MicroVM (NVX) foundation (Windows x64) ``` **Linux** — `./build.sh`: diff --git a/README.md b/README.md index 8d167fbe4..5221d36dc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ MXC is a **sandboxed code execution system** for running untrusted code (model o - **Cross-platform**: Windows, Linux, and macOS support with platform-appropriate containment backends - **JSON-based Configuration**: Define execution parameters and security policies via a versioned JSON schema -- **Multiple Containment Backends**: ProcessContainer, Windows Sandbox, LXC, Bubblewrap, Seatbelt (macOS), NVX, Hyperlight, IsolationSession, and WSLC +- **Multiple Containment Backends**: ProcessContainer, Windows Sandbox, LXC, Bubblewrap, Seatbelt (macOS), MicroVM (NVX), Hyperlight, IsolationSession, and WSLC - **Policy-driven Sandboxing**: - **Filesystem Policy**: Read-only and read-write path lists (denied paths not yet supported on Windows) - **Network Policy**: Proxy support (cooperative on Linux/macOS), allow/block outbound, and backend-dependent host filtering @@ -26,7 +26,7 @@ MXC ships a native container wrapper plus a TypeScript SDK — see the [SDK READ | Platform | Default backend | Other backends | Minimum build | | --- | --- | --- | --- | -| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `nvx`, `hyperlight`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | +| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `microvm`, `hyperlight`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | | Linux x64 / ARM64 | `bubblewrap` | `lxc`, `hyperlight` | — | | macOS ARM64 / x64 (schema `0.7.0-alpha`+) | `seatbelt` | — | — | @@ -35,7 +35,7 @@ The stable one-shot backends (`processcontainer`, `bubblewrap`, `lxc`, `seatbelt`, `wslc`, and `isolation_session`) do not require experimental mode; Linux hosts also need the matching runtime installed: bwrap (Bubblewrap) for the default backend, or the lxc toolset for the lxc backend. **Experimental -backends** (`windows_sandbox`, `nvx`, and `hyperlight`) require +backends** (`windows_sandbox`, `microvm`, and `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` or the `--experimental` CLI flag. @@ -70,20 +70,19 @@ layout, crate responsibilities, dependency direction, and execution surfaces. build.bat # Release build for current architecture build.bat --debug # Debug build build.bat --all # Release build for both x64 and ARM64 -build.bat --with-nvx # Include the incomplete NVX foundation (Windows x64) +build.bat --with-microvm # Include the incomplete MicroVM (NVX) foundation (Windows x64) ``` -`--with-nvx` acquires the pinned NVX platform archive at build time and verifies every staged file by +`--with-microvm` acquires the pinned NVX platform archive at build time and verifies every staged file by SHA-256; sandbox launches never download artifacts. Set `NVX_BIN` to a pre-fetched, checksum-verifiable bundle directory for an offline build. The current pin, `v0.1.0-dev.5c86da3dff02`, contains only `openvmm.exe`, the guest kernel, and the guest initramfs, not the workload-image bundle. -The experimental NanVix-backed `microvm` containment value was removed. Use -`containment: "nvx"` and build with `--with-nvx`. An NVX-enabled binary parses -the new value, but execution -returns a typed backend-unavailable error and capability probes do not advertise -NVX. Runtime work remains blocked on NVX-produced distro/runtime EROFS images +The experimental `microvm` containment value is implemented by NVX. Build with +`--with-microvm`; execution currently returns a typed backend-unavailable error, +and capability probes do not advertise MicroVM while the runtime is incomplete. +Runtime work remains blocked on NVX-produced distro/runtime EROFS images and scratch image, a proven combined managed-sandbox/virtio-fs contract, and the required WHP runner. diff --git a/build.bat b/build.bat index 25a21e5b8..54a5bd1e8 100644 --- a/build.bat +++ b/build.bat @@ -5,7 +5,7 @@ setlocal enabledelayedexpansion set "BUILD_CONFIG=release" set "BUILD_ARCH=" set "BUILD_ALL=0" -set "WITH_NVX=0" +set "WITH_MICROVM=0" set "WITH_WSLC=0" set "WITH_ISOLATION_SESSION=0" set "WITH_HYPERLIGHT=0" @@ -18,7 +18,7 @@ if /i "%~1"=="--release" ( set "BUILD_CONFIG=release" & shift & goto :parse_arg if /i "%~1"=="--x64" ( set "BUILD_ARCH=x86_64-pc-windows-msvc" & shift & goto :parse_args ) if /i "%~1"=="--arm64" ( set "BUILD_ARCH=aarch64-pc-windows-msvc" & shift & goto :parse_args ) if /i "%~1"=="--all" ( set "BUILD_ALL=1" & shift & goto :parse_args ) -if /i "%~1"=="--with-nvx" ( set "WITH_NVX=1" & shift & goto :parse_args ) +if /i "%~1"=="--with-microvm" ( set "WITH_MICROVM=1" & shift & goto :parse_args ) if /i "%~1"=="--with-wslc" ( set "WITH_WSLC=1" & shift & goto :parse_args ) if /i "%~1"=="--with-isolation-session" ( set "WITH_ISOLATION_SESSION=1" & shift & goto :parse_args ) if /i "%~1"=="--with-hyperlight" ( set "WITH_HYPERLIGHT=1" & shift & goto :parse_args ) @@ -37,13 +37,13 @@ if "%BUILD_ALL%"=="0" if "%BUILD_ARCH%"=="" ( ) ) -if "%WITH_NVX%"=="1" ( +if "%WITH_MICROVM%"=="1" ( if "%BUILD_ALL%"=="1" ( - echo ERROR: --with-nvx supports x64 only and cannot be combined with --all. + echo ERROR: --with-microvm supports x64 only and cannot be combined with --all. exit /b 1 ) if /i not "%BUILD_ARCH%"=="x86_64-pc-windows-msvc" ( - echo ERROR: --with-nvx supports x64 only. Use --x64 on an ARM64 host. + echo ERROR: --with-microvm supports x64 only. Use --x64 on an ARM64 host. exit /b 1 ) ) @@ -55,7 +55,7 @@ if "%BUILD_CONFIG%"=="release" set "CARGO_FLAGS=--release --target" :: workspace feature flags above, so it uses its own profile/target-only flags. set "PLM_FLAGS=--target" if "%BUILD_CONFIG%"=="release" set "PLM_FLAGS=--release --target" -if "%WITH_NVX%"=="1" set "CARGO_FLAGS=--features nvx %CARGO_FLAGS%" +if "%WITH_MICROVM%"=="1" set "CARGO_FLAGS=--features microvm %CARGO_FLAGS%" if "%WITH_WSLC%"=="1" set "CARGO_FLAGS=--features wslc %CARGO_FLAGS%" if "%WITH_ISOLATION_SESSION%"=="1" set "CARGO_FLAGS=--features isolation_session %CARGO_FLAGS%" if "%WITH_HYPERLIGHT%"=="1" set "CARGO_FLAGS=--features hyperlight %CARGO_FLAGS%" @@ -138,10 +138,10 @@ for %%T in (x86_64-pc-windows-msvc aarch64-pc-windows-msvc) do ( echo Copied !SDK_ARCH!\mxc_ffi.dll ) if "%%T"=="x86_64-pc-windows-msvc" ( - if "%WITH_NVX%"=="1" ( + if "%WITH_MICROVM%"=="1" ( for %%B in (bin\openvmm.exe guest\vmlinux guest\initramfs.cpio.gz) do ( if not exist "!BIN_DIR!\%%B" ( - echo ERROR: NVX-enabled Node runtime is missing !BIN_DIR!\%%B + echo ERROR: MicroVM ^(NVX^) Node runtime is missing !BIN_DIR!\%%B exit /b 1 ) ) @@ -150,7 +150,7 @@ for %%T in (x86_64-pc-windows-msvc aarch64-pc-windows-msvc) do ( if exist "!BIN_DIR!\%%B" set /A NVX_WORKLOAD_IMAGE_COUNT+=1 ) if not "!NVX_WORKLOAD_IMAGE_COUNT!"=="0" if not "!NVX_WORKLOAD_IMAGE_COUNT!"=="3" ( - echo ERROR: NVX-enabled Node runtime has an incomplete workload-image bundle. + echo ERROR: MicroVM ^(NVX^) Node runtime has an incomplete workload-image bundle. exit /b 1 ) for %%B in (images\distro.erofs images\runtime.erofs images\scratch.ext4) do ( @@ -333,7 +333,7 @@ echo --release Build release configuration echo --x64 Build for x64 only echo --arm64 Build for ARM64 only echo --all Build for both x64 and ARM64 -echo --with-nvx Add the incomplete NVX foundation and platform artifacts (x64) +echo --with-microvm Add the incomplete MicroVM (NVX) foundation and platform artifacts (x64) echo Runtime preflight remains unavailable until NVX publishes echo the workload image bundle echo --with-wslc Build with WSL Container (WSLC SDK) support diff --git a/docs/backend-support-probe-api-plan.md b/docs/backend-support-probe-api-plan.md index 20503d68f..1da5362e8 100644 --- a/docs/backend-support-probe-api-plan.md +++ b/docs/backend-support-probe-api-plan.md @@ -126,7 +126,7 @@ for them **cannot be built just yet**. | --- | --- | --- | --- | | `windows_sandbox` | DISM/registry check of the *Containers-DisposableClientVM* optional feature | only a private "is the `.exe` on disk" check | reports available when the feature is off → launch fails | | `isolation_session` | activation of the in-proc `Windows.AI.IsolationSession.Preview` `IsoSessionOps` runtime class succeeds (the API class is registered on the OS **and** its OS feature gate is on) **and** the backend feature is compiled | as of #761, detection queries whether the API class is registered rather than gating on a build number; a `CLASS_E_CLASSNOTAVAILABLE` / `REGDB_E_CLASSNOTREG` activation failure means unavailable | none for false-availability now — a machine without the API registered fails activation cleanly; still needs a cheap probe seam so callers don't have to attempt a real activation | -| `nvx` | feature compiled, pinned NVX runtime files staged, and WHP usable on Windows x64 | nothing | checking only a hypervisor can report availability when required runtime files are missing | +| `microvm` | feature compiled, pinned NVX runtime files staged, and WHP usable on Windows x64 | nothing | checking only a hypervisor can report availability when required runtime files are missing | | `hyperlight` | hypervisor present + feature compiled | nothing | same VM-boot risk | ### 4.2 The parity rule: detect once, project into TS @@ -167,7 +167,7 @@ test host happens to have `slirp4netns` installed. - A serde snapshot pins the camelCase capability name (`"proxyEnforcement"`) and verifies that `capabilities` and `warnings` are **omitted** when empty. - `wslc` appears when `WslcSdk::load()` resolves `wslcsdk.dll`; the remaining VM group -(`windows_sandbox`, `isolation_session`, `nvx`, `hyperlight`) never appears until its detector lands. +(`windows_sandbox`, `isolation_session`, `microvm`, `hyperlight`) never appears until its detector lands. - The TypeScript `getPlatformSupport()` output matches the native probe (parity by projection, §4.2), guarding against the two layers drifting. @@ -176,7 +176,7 @@ test host happens to have `slirp4netns` installed. Writing the missing detectors and wiring the TypeScript projection, one issue each: 1. `windows_sandbox` - optional-feature (DISM/registry) detector. 2. `isolation_session` - probe whether the `Windows.AI.IsolationSession.Preview` `IsoSessionOps` API class is registered on the OS (activation-factory resolvable), replacing the old build-number gate (see #761), and expose it to Rust. -3. `nvx` / `hyperlight` - hypervisor-presence probe. +3. `microvm` / `hyperlight` - hypervisor-presence probe. 4. `lxc` - port the `lxc-ls` presence check from TypeScript to Rust, so the probe (not just the SDK) can report it (§4.2, step 1). 5. TypeScript projection - make `getPlatformSupport()` read the native backend availability via a diff --git a/docs/bwrap-support/bubblewrap-backend-plan.md b/docs/bwrap-support/bubblewrap-backend-plan.md index dd7b1501d..748043db9 100644 --- a/docs/bwrap-support/bubblewrap-backend-plan.md +++ b/docs/bwrap-support/bubblewrap-backend-plan.md @@ -48,7 +48,7 @@ Plus optional `validate_runner()`. This is a perfect fit for bwrap, which is fun Add `"bubblewrap"` to the `containment` enum: ```json "containment": { - "enum": ["process", "processcontainer", "windows_sandbox", "lxc", "nvx", + "enum": ["process", "processcontainer", "windows_sandbox", "lxc", "microvm", "wslc", "seatbelt", "isolation_session", "bubblewrap"] } ``` @@ -228,10 +228,10 @@ Add `bwrap_common` dependency to `lxc/Cargo.toml`. ```typescript export type ContainmentBackend = | 'processcontainer' | 'windows_sandbox' | 'wslc' - | 'lxc' | 'nvx' | 'seatbelt' | 'isolation_session' + | 'lxc' | 'microvm' | 'seatbelt' | 'isolation_session' | 'bubblewrap'; // ← add -export const ExperimentalBackends = ['nvx', 'wslc', 'seatbelt', 'bubblewrap']; +export const ExperimentalBackends = ['microvm', 'wslc', 'seatbelt', 'bubblewrap']; ``` **`sdk/node/src/platform.ts`:** diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index 429470300..2d6da698c 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -6,11 +6,11 @@ The original NVX prototype accepted a real MXC `0.9.0-dev` JSON, validated it against the schema, and adapted supported fields into typed NVX launch plans. The prototype first exercised those plans on Windows through OpenVMM and WHP. -NVX is now the sole micro-VM backend identity in the active MXC development -contract. The retired NanVix-backed `microvm` value is rejected with migration -guidance to `nvx`. An NVX-enabled build returns a typed backend-unavailable -error for execution, and capability probes do not advertise NVX while the -runtime is incomplete. +MicroVM remains the sole public MXC backend identity in the active development +contract, selected with `containment: "microvm"`. NVX is the concrete +implementation behind that abstraction. A MicroVM-enabled build returns a +typed backend-unavailable error for execution, and capability probes do not +advertise MicroVM while the runtime is incomplete. ## Current architecture @@ -84,10 +84,10 @@ them for this backend: ## Schema changes -The rolling `0.10.0-dev` and exact `0.10.0-alpha` development contracts now -include `containment: "nvx"` in their generated schemas and TypeScript wire -types. The retired `microvm` identifier is absent from both development artifact sets. -Published stable schemas remain unchanged. An `experimental.nvx.provision` section is necessary +The rolling `0.10.0-dev` and exact `0.10.0-alpha` development contracts include +`containment: "microvm"` in their generated schemas and TypeScript wire types. +The internal `nvx` implementation name is not accepted as a public containment +value. Published stable schemas remain unchanged. An `experimental.nvx.provision` section is necessary only if images remain caller-configurable. Future state-aware provision design: @@ -101,7 +101,7 @@ Future state-aware provision design: "$schema": "https://aka.ms/mxc/schemas/0.10.0-alpha.json", "version": "0.10.0-alpha", "phase": "provision", - "containment": "nvx", + "containment": "microvm", "filesystem": { "readonlyPaths": [ "C:\\workspace\\source" @@ -150,7 +150,7 @@ Future state-aware provision design: ## Binary acquisition and packaging -`build.bat --with-nvx` enables the incomplete x64 Windows/WHP foundation. It is +`build.bat --with-microvm` enables the incomplete x64 Windows/WHP foundation. It is the only active micro-VM packaging path. The build pins the exact `microsoft/nvx` release tag, platform asset name, and @@ -168,7 +168,8 @@ writable scratch image required to run a workload. ## Remaining MXC integration work The schema/wire, policy/model, typed-unavailable dispatch, and pinned Windows -artifact-acquisition foundations now replace the removed NanVix integration. +artifact-acquisition foundations preserve MicroVM as the public abstraction +while replacing the removed NanVix implementation with NVX. Phase 2 runtime remains blocked on all of the following: - NVX-produced distro and runtime EROFS images plus a writable scratch image; @@ -238,7 +239,7 @@ The current GitHub Copilot CLI builds MXC policy version `0.7.0-alpha` and emits the legacy `allowOutbound`, `allowLocalNetwork`, and `network.proxy` fields. It no longer provides a raw `sandbox.config` passthrough. -Before the CLI can use the NVX backend, it must migrate its generated policy +Before the CLI can use the MicroVM (NVX) backend, it must migrate its generated policy to schema 0.8+: - map outbound allow/block to `network.egress.default`; diff --git a/docs/schema.md b/docs/schema.md index b867d5917..871118e48 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -393,7 +393,7 @@ force a particular backend. | `"windows_sandbox"` | Windows Sandbox VM isolation. Dual-mode: a transient **one-shot** runner that launches a fresh disposable VM per execution, and a **state-aware** lifecycle backed by a long-lived per-sandbox daemon. | | `"wslc"` | Linux containers via the WSL Container SDK | | `"lxc"` | Native LXC container isolation. No abstract intent resolves to LXC; request it explicitly. | -| `"nvx"` | NVX Linux micro-VM hosted by OpenVMM/WHP (experimental, Windows x64 foundation; runtime unavailable in Phase 1) | +| `"microvm"` | MicroVM isolation implemented by NVX and hosted by OpenVMM/WHP (experimental, Windows x64 foundation; runtime unavailable in Phase 1) | | `"hyperlight"` | MicroVM isolation via Hyperlight + Unikraft with an embedded CPython snapshot (experimental) | | `"isolation_session"` | Windows isolation session — runs the workload as a freshly-provisioned, per-execution isolated user account in its own OS-managed session. Dual-mode: one-shot and state-aware. | | `"seatbelt"` | macOS sandbox isolation (Seatbelt). Requires macOS 15 or later — see [`docs/seatbelt/seatbelt-backend.md`](seatbelt/seatbelt-backend.md). | diff --git a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md index fbdf3fdd1..d0d49b1ba 100644 --- a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md +++ b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md @@ -20,7 +20,7 @@ function signatures throughout. One-line summaries; full definitions live in | Type | Where | Role | |---|---|---| -| `ContainmentType` / `ContainmentBackend` | `sdk/node/src/types.ts` | Two-tier containment names: `ContainmentType` for abstract intents (`'process' \| 'vm'`); `ContainmentBackend` for concrete runners (`'processcontainer' \| 'windows_sandbox' \| 'lxc' \| 'wslc' \| 'nvx' \| 'hyperlight' \| 'seatbelt' \| 'isolation_session'`). Wire `containment` accepts either. The deprecated alias `SandboxingMethod = ContainmentType \| ContainmentBackend` is retained for back-compat. | +| `ContainmentType` / `ContainmentBackend` | `sdk/node/src/types.ts` | Two-tier containment names: `ContainmentType` for abstract intents (`'process' \| 'vm'`); `ContainmentBackend` for concrete runners (`'processcontainer' \| 'windows_sandbox' \| 'lxc' \| 'wslc' \| 'microvm' \| 'hyperlight' \| 'seatbelt' \| 'isolation_session'`). Wire `containment` accepts either. The deprecated alias `SandboxingMethod = ContainmentType \| ContainmentBackend` is retained for back-compat. | | `ProcessConfig` | `sdk/node/src/types.ts` | Per-process settings: `commandLine`, `cwd`, `env`, `timeout`. Reused inside state-aware exec Configs. | | `FilesystemConfig`, `NetworkConfig`, `UiConfig` | `sdk/node/src/types.ts` | Wire-format-aligned cross-cutting interfaces. Reused inline as field types inside the per-(backend, phase) state-aware Configs. | | `SandboxSpawnOptions` | `sdk/node/src/sandbox.ts` | Existing options bag (debug, dryRun, logDir, executablePath, ptyOptions, usePty, experimental). State-aware reuses it as the third positional arg, extended with `signal?: AbortSignal` for cancellation. | diff --git a/schemas/dev/mxc-config.schema.0.10.0-alpha.json b/schemas/dev/mxc-config.schema.0.10.0-alpha.json index dbfb85b70..b5ef266d4 100644 --- a/schemas/dev/mxc-config.schema.0.10.0-alpha.json +++ b/schemas/dev/mxc-config.schema.0.10.0-alpha.json @@ -771,9 +771,9 @@ "type": "string" }, { - "description": "Nvx", + "description": "Microvm", "enum": [ - "nvx" + "microvm" ], "type": "string" }, diff --git a/schemas/dev/mxc-config.schema.0.10.0-dev.json b/schemas/dev/mxc-config.schema.0.10.0-dev.json index 2eab5dba6..2cf0082c1 100644 --- a/schemas/dev/mxc-config.schema.0.10.0-dev.json +++ b/schemas/dev/mxc-config.schema.0.10.0-dev.json @@ -144,9 +144,9 @@ "type": "string" }, { - "description": "NVX micro-VM (experimental).", + "description": "MicroVM isolation implemented by NVX (experimental).", "enum": [ - "nvx" + "microvm" ], "type": "string" }, diff --git a/sdk/node/README.md b/sdk/node/README.md index cf77f969c..26c8dd909 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -64,10 +64,10 @@ child.on('close', (code) => console.log('exit:', code)); | `0.10.0-alpha` | Dev (remaining experimental backends and development fields) | [`schemas/dev/mxc-config.schema.0.10.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.10.0-alpha.json) | Pick `0.9.0-alpha` for new code using current stable backends. Windows Sandbox, -NVX, and Hyperlight require `0.10.0-alpha`; Seatbelt requires `0.7.0-alpha` +MicroVM, and Hyperlight require `0.10.0-alpha`; Seatbelt requires `0.7.0-alpha` or later. -> **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `nvx`, `hyperlight`) and their permanent backend sections are defined by the mutable `0.10.0-alpha` development contract. IsolationSession and WSLC, including their state-aware lifecycles, are part of exact v0.9 and do not require `--experimental`. Production executors dispatch through the exact contract selected by the declared version; the rolling `wxc_common::wire` model remains temporarily as an SDK/codegen migration oracle. +> **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `microvm`, `hyperlight`) and their permanent backend sections are defined by the mutable `0.10.0-alpha` development contract. IsolationSession and WSLC, including their state-aware lifecycles, are part of exact v0.9 and do not require `--experimental`. Production executors dispatch through the exact contract selected by the declared version; the rolling `wxc_common::wire` model remains temporarily as an SDK/codegen migration oracle. > **Network host allow/block lists are not implemented on Windows.** Exact > v0.9/v0.10 requests use `network.egress` / `network.ingress` for directional @@ -144,11 +144,11 @@ for all three connectivity modes and backend-specific support. | Platform | Default backend | Other backends | Minimum build | | --- | --- | --- | --- | -| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `nvx`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | +| Windows 11 24H2+ (verified on 25H2) | `processcontainer` | `windows_sandbox`, `wslc`, `microvm`, `isolation_session` | `processcontainer`: 26100 (24H2)
`isolation_session`: 26340.9212 ([Insider Preview](https://learn.microsoft.com/en-us/windows-insider/release-notes/experimental/preview-build-26340-9212)) | | Linux x64 / ARM64 | `bubblewrap` | `lxc` | — | | macOS ARM64 (schema `0.7.0-alpha`+) | `seatbelt` | — | — | -The default `processcontainer`, `bubblewrap`, `lxc`, `seatbelt`, `wslc`, and `isolation_session` backends work without an experimental opt-in. **Experimental backends** (`windows_sandbox`, `nvx`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend). +The default `processcontainer`, `bubblewrap`, `lxc`, `seatbelt`, `wslc`, and `isolation_session` backends work without an experimental opt-in. **Experimental backends** (`windows_sandbox`, `microvm`, `hyperlight`) require `{ experimental: true }` in `SandboxSpawnOptions` when you spawn — see [Choosing a Backend](#choosing-a-backend). > **Hyperlight** is an opt-in build flavor (Linux x64 and Windows x64) gated by the `--with-hyperlight` cargo feature. Default shipped binaries do not include it; build from source with `build.bat --with-hyperlight` (Windows) or the equivalent cargo invocation on Linux. @@ -301,7 +301,7 @@ console.log(result.stdout); | `lxc` | (concrete only) | Linux | `0.6.0-alpha` | ✅ | [`docs/lxc-support/lxc-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/lxc-support/lxc-backend.md) | | `seatbelt` | `process` | macOS | `0.7.0-alpha` | ✅ | [`docs/seatbelt/seatbelt-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/seatbelt/seatbelt-backend.md) | | `windows_sandbox` | `vm` | Windows | `0.10.0-alpha` | Experimental | [`docs/windows-sandbox/windows-sandbox.md`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.md) | -| `nvx` | (concrete only) | Windows x64 | `0.10.0-alpha` | Experimental | [`docs/nvx-backend-gaps.md`](https://github.com/microsoft/mxc/blob/main/docs/nvx-backend-gaps.md) — structural foundation only; runtime unavailable in Phase 1 | +| `microvm` | (concrete only) | Windows x64 | `0.10.0-alpha` | Experimental | [`docs/nvx-backend-gaps.md`](https://github.com/microsoft/mxc/blob/main/docs/nvx-backend-gaps.md) — implemented by NVX; structural foundation only; runtime unavailable in Phase 1 | | `hyperlight` | (concrete only) | Windows x64 / Linux x64 | `0.10.0-alpha` | Experimental | No dedicated guide | | `wslc` | (concrete only) | Windows | `0.9.0-alpha` | Stable | [`docs/wsl/wsl-container-getting-started.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wsl-container-getting-started.md) | | `isolation_session` | (concrete only) | Windows | `0.9.0-alpha` | Stable | [`docs/isolation-session/oneshot.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) | @@ -543,13 +543,13 @@ granting file content reads. It requires a BaseContainer host with PSEC 1.1 | `MXC is not supported on this platform` | `getPlatformSupport()` returned `isSupported: false`. On Linux, neither LXC nor a usable Bubblewrap 0.5.0+ installation is available. On macOS, the Seatbelt platform probe could not find `/usr/bin/sandbox-exec`. | Inspect `support.reason`. On Linux, also inspect `support.unavailableReasons` and install LXC or Bubblewrap 0.5.0+. On macOS, verify that `/usr/bin/sandbox-exec` exists; its absence indicates an incomplete or unsupported macOS installation. | | `wxc-exec.exe not found` / `lxc-exec not found` | The SDK couldn't locate the native binary. | Set `MXC_BIN_DIR=` so `//wxc-exec.exe` (or `lxc-exec`) exists, or pass `options.executablePath` explicitly. | | `Invalid containment value ''` | `containment` field doesn't match the parser's accepted values. | Use one of the abstract intents (`process`, `vm`) or a concrete backend listed in [Choosing a Backend](#choosing-a-backend). | -| `'' containment requires experimental mode` | A `windows_sandbox` / `nvx` / `hyperlight` backend was selected without the flag. | Pass `{ experimental: true }` in `SandboxSpawnOptions`. | +| `'' containment requires experimental mode` | A `windows_sandbox` / `microvm` / `hyperlight` backend was selected without the flag. | Pass `{ experimental: true }` in `SandboxSpawnOptions`. | | `process.commandLine starts with an unquoted Windows path containing a space` | `wxc-exec` rejects unquoted paths with spaces at parse time. | Quote the executable: `'"C:\\Program Files\\…\\foo.exe" args'`. | | `CreateProcessW(PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT) failed: ...` | The process security environment launch returned an OS-level error. Backend-unavailable failures automatically fall through to an AppContainer tier during selection. | Check the Windows build requirements for the backend you selected. | | Process exits `-1` / `4294967295` with no stdout | Native binary terminated abnormally. | Re-run with `options.debug: true` (or `options.logDir: ''`) to capture diagnostic logs. | | `Policy version '' is older than supported` / `newer than supported` | Version is outside the supported version lines. | Use an exact registered version: `0.6.0-alpha`, `0.7.0-alpha`, `0.8.0-alpha`, `0.9.0-alpha`, or `0.10.0-alpha`. See [Compatibility](#compatibility). | | `Policy version '' is not a registered schema contract` / `Unsupported contract version` | The declaration is not registered, even if it falls between supported versions (for example, `0.6.1-alpha`). | Use an exact version from [Compatibility](#compatibility); IsolationSession and WSLC state-aware requests use `0.9.0-alpha`, while Windows Sandbox uses `0.10.0-alpha`. | -| `Schema does not support containment ''` | The selected backend was introduced after the declared schema version. | Use the backend's minimum version from [Choosing a Backend](#choosing-a-backend). Seatbelt requires `0.7.0-alpha`; IsolationSession and WSLC require `0.9.0-alpha`; Windows Sandbox, NVX, and Hyperlight require `0.10.0-alpha`. | +| `Schema does not support containment ''` | The selected backend was introduced after the declared schema version. | Use the backend's minimum version from [Choosing a Backend](#choosing-a-backend). Seatbelt requires `0.7.0-alpha`; IsolationSession and WSLC require `0.9.0-alpha`; Windows Sandbox, MicroVM, and Hyperlight require `0.10.0-alpha`. | For backend-specific errors, see the per-backend guide linked from the [Choosing a Backend](#choosing-a-backend) table. diff --git a/sdk/node/src/generated/v0_10_0_alpha/wire.ts b/sdk/node/src/generated/v0_10_0_alpha/wire.ts index 8fc6260a8..866d68669 100644 --- a/sdk/node/src/generated/v0_10_0_alpha/wire.ts +++ b/sdk/node/src/generated/v0_10_0_alpha/wire.ts @@ -354,7 +354,7 @@ export interface NetworkRule { export type NonEmptyString = string; -export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "vm" | "windows_sandbox" | "nvx" | "hyperlight" | "wslc" | "isolation_session"; +export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "vm" | "windows_sandbox" | "microvm" | "hyperlight" | "wslc" | "isolation_session"; /** * A complete one-shot `0.10.0-alpha` configuration request. diff --git a/sdk/node/src/generated/wire.ts b/sdk/node/src/generated/wire.ts index 156183485..ec9de6662 100644 --- a/sdk/node/src/generated/wire.ts +++ b/sdk/node/src/generated/wire.ts @@ -69,7 +69,7 @@ export type ClipboardPolicy = "none" | "read" | "write" | "all"; /** * Containment backend (abstract intent or concrete backend). */ -export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "nvx" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap"; +export type Containment = "process" | "processcontainer" | "vm" | "windows_sandbox" | "lxc" | "microvm" | "hyperlight" | "wslc" | "seatbelt" | "isolation_session" | "bubblewrap"; /** * Experimental features (only honored with `--experimental`). This block is intentionally **permissive** (no `deny_unknown_fields`): experimental backends are in flux, so the schema documents the known shapes for editor help without rejecting in-progress fields. The strict, closed contract is the stable (top-level) surface. diff --git a/sdk/node/src/helper.ts b/sdk/node/src/helper.ts index 8cc70e21f..21ae1dc4f 100644 --- a/sdk/node/src/helper.ts +++ b/sdk/node/src/helper.ts @@ -243,11 +243,6 @@ export function resolveExecutableAndArgs( // ExperimentalBackends under their alias) and produce a confusing // "not available on this platform" error instead. const rawContainment: string | undefined = config.containment; - if (rawContainment === 'microvm') { - throw new Error( - 'containment "microvm" was removed; use "nvx" for the NVX micro-VM backend' - ); - } const effectiveContainment = rawContainment ? (LegacyContainmentAliases[rawContainment] ?? rawContainment) : undefined; @@ -272,12 +267,12 @@ export function resolveExecutableAndArgs( throw new Error(`MXC is not supported on this platform: ${platformSupport.reason}`); } - // Hard platform requirement: NVX needs OpenVMM/WHP on Windows x64. This guard + // Hard platform requirement: MicroVM (NVX) needs OpenVMM/WHP on Windows x64. This guard // runs even when `skipPlatformCheck` is set because it's not a build-version // check — the backend literally cannot run on non-Windows hosts. - if (effectiveContainment === 'nvx' && + if (effectiveContainment === 'microvm' && (os.platform() !== 'win32' || os.arch() !== 'x64')) { - throw new Error('The nvx backend is only supported on Windows x64.'); + throw new Error('The microvm backend is only supported on Windows x64.'); } // Validate containment against platform diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index 3bbcbf79f..53318df7c 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -121,7 +121,7 @@ function validateContainmentVersion( || effectiveContainment === 'wslc' ? '0.9.0-alpha' : effectiveContainment === 'vm' || - effectiveContainment === 'nvx' || + effectiveContainment === 'microvm' || effectiveContainment === 'windows_sandbox' || effectiveContainment === 'hyperlight' ? '0.10.0-alpha' @@ -415,7 +415,7 @@ export function createConfigFromPolicy( ); } } - if (containment === 'nvx' && allowedProxyPeer !== undefined) { + if (containment === 'microvm' && allowedProxyPeer !== undefined) { throw new Error( 'processContainer.network.allowedProxyPeer is supported only by the Windows ' + 'ProcessContainer backend.' @@ -512,9 +512,9 @@ export function createConfigFromPolicy( } // Backend-specific config based on containment type - if (containment === 'nvx') { - diagLog(`createConfigFromPolicy: containment=nvx, id=${containerId}`); - config.containment = 'nvx'; + if (containment === 'microvm') { + diagLog(`createConfigFromPolicy: containment=microvm, id=${containerId}`); + config.containment = 'microvm'; return config; } diff --git a/sdk/node/src/types.ts b/sdk/node/src/types.ts index 654bf1363..a89260f8c 100644 --- a/sdk/node/src/types.ts +++ b/sdk/node/src/types.ts @@ -112,7 +112,7 @@ export type ContainmentBackend = | 'windows_sandbox' | 'wslc' | 'lxc' - | 'nvx' + | 'microvm' | 'hyperlight' | 'seatbelt' | 'isolation_session' @@ -123,7 +123,7 @@ export type ContainmentBackend = * the `--experimental` flag. */ export const ExperimentalBackends: readonly (ContainmentType | ContainmentBackend)[] = [ - 'nvx', + 'microvm', 'windows_sandbox', 'hyperlight', ]; diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index bcee745ca..4b15d439b 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -79,9 +79,9 @@ describe('exact-version network authoring', () => { }); describe('containment exports', () => { - it('exposes nvx and no longer exposes microvm', () => { + it('exposes microvm and not the internal nvx implementation name', () => { assert.deepStrictEqual(ContainmentTypes, ['process', 'vm']); - assert.deepStrictEqual(ExperimentalBackends, ['nvx', 'windows_sandbox', 'hyperlight']); + assert.deepStrictEqual(ExperimentalBackends, ['microvm', 'windows_sandbox', 'hyperlight']); }); }); @@ -254,7 +254,7 @@ describe('buildSandboxPayload', () => { try { for (const containment of [ 'vm', - 'nvx', + 'microvm', 'windows_sandbox', 'wslc', 'hyperlight', @@ -283,7 +283,7 @@ describe('buildSandboxPayload', () => { try { for (const containment of [ 'vm', - 'nvx', + 'microvm', 'windows_sandbox', 'wslc', 'hyperlight', @@ -564,12 +564,12 @@ describe('buildSandboxPayload', () => { 'echo hello', policy, '/', - 'nvx-test', - 'nvx', + 'microvm-test', + 'microvm', ); - assert.strictEqual(payload.containment, 'nvx'); - assert.strictEqual(payload.containerId, 'nvx-test'); + assert.strictEqual(payload.containment, 'microvm'); + assert.strictEqual(payload.containerId, 'microvm-test'); assert.deepStrictEqual(payload.process, { commandLine: 'echo hello', timeout: 0, @@ -588,7 +588,7 @@ describe('buildSandboxPayload', () => { } }); - it('should reject ProcessContainer proxy peer policy for NVX', () => { + it('should reject ProcessContainer proxy peer policy for MicroVM', () => { mockWindows(); try { assert.throws( @@ -607,7 +607,7 @@ describe('buildSandboxPayload', () => { }, undefined, undefined, - 'nvx', + 'microvm', ), { message: /processContainer\.network\.allowedProxyPeer is supported only by the Windows ProcessContainer backend/, @@ -1990,7 +1990,7 @@ describe('resolveExecutableAndArgs (containment validation)', { skip: platformSk const version = containment === 'isolation_session' || containment === 'wslc' ? '0.9.0-alpha' - : ['nvx', 'vm', 'hyperlight', 'windows_sandbox'].includes(containment) + : ['microvm', 'vm', 'hyperlight', 'windows_sandbox'].includes(containment) ? '0.10.0-alpha' : ['seatbelt', 'macos_sandbox'].includes(containment) ? '0.7.0-alpha' @@ -2011,23 +2011,23 @@ describe('resolveExecutableAndArgs (containment validation)', { skip: platformSk ); }); - it('should reject removed microvm containment with NVX migration guidance', () => { + it('should reject the internal nvx implementation name', () => { assert.throws( - () => resolveExecutableAndArgs(makeConfig('microvm'), { + () => resolveExecutableAndArgs(makeConfig('nvx'), { executablePath: fakeExe, experimental: true, }), - { message: /microvm.*removed.*nvx/i }, + { message: /nvx.*not available/i }, ); }); - it('should accept nvx with experimental mode on Windows x64', function (this: { skip: (reason?: string) => void }) { + it('should accept microvm with experimental mode on Windows x64', function (this: { skip: (reason?: string) => void }) { if (process.platform !== 'win32' || process.arch !== 'x64') { - this.skip('nvx is Windows x64-only'); + this.skip('microvm is Windows x64-only'); return; } assert.doesNotThrow(() => - resolveExecutableAndArgs(makeConfig('nvx'), { + resolveExecutableAndArgs(makeConfig('microvm'), { executablePath: fakeExe, experimental: true, }), diff --git a/src/backends/hyperlight/common/src/lib.rs b/src/backends/hyperlight/common/src/lib.rs index 0b7a43fd4..b313ad088 100644 --- a/src/backends/hyperlight/common/src/lib.rs +++ b/src/backends/hyperlight/common/src/lib.rs @@ -6,7 +6,7 @@ // `hyperlight` cargo feature. On other targets, or when the feature is // disabled, this crate compiles to an empty library so workspace builds // (`cargo build --workspace`) on ARM64 hosts and feature-less builds -// (e.g. `cargo build --features nvx`) succeed without pulling in +// (e.g. `cargo build --features microvm`) succeed without pulling in // hyperlight-host. Consumers gate their use of `HyperlightScriptRunner` // on `target_arch = "x86_64"` and the `hyperlight` cargo feature. #![cfg(all(feature = "hyperlight", target_arch = "x86_64"))] diff --git a/src/core/lxc/Cargo.toml b/src/core/lxc/Cargo.toml index 05bcb33ae..0a25eaa26 100644 --- a/src/core/lxc/Cargo.toml +++ b/src/core/lxc/Cargo.toml @@ -10,6 +10,7 @@ path = "src/main.rs" [features] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight", "mxc_engine/hyperlight"] +microvm = ["mxc_engine/microvm"] [build-dependencies] mxc_build_common.workspace = true diff --git a/src/core/mxc-sdk/Cargo.toml b/src/core/mxc-sdk/Cargo.toml index b2e22592b..043232092 100644 --- a/src/core/mxc-sdk/Cargo.toml +++ b/src/core/mxc-sdk/Cargo.toml @@ -16,8 +16,8 @@ default = [] wslc = ["mxc_engine/wslc"] # IsolationSession backend (Windows only). isolation_session = ["mxc_engine/isolation_session"] -# NVX backend foundation (experimental). -nvx = ["mxc_engine/nvx"] +# MicroVM backend foundation implemented by NVX (experimental). +microvm = ["mxc_engine/microvm"] # Exposes the debug-only telemetry test seams to downstream binding tests. test-support = ["wxc_common/test-support"] diff --git a/src/core/mxc_config_contract/src/dev/one_shot.rs b/src/core/mxc_config_contract/src/dev/one_shot.rs index adf8c0959..9b9793265 100644 --- a/src/core/mxc_config_contract/src/dev/one_shot.rs +++ b/src/core/mxc_config_contract/src/dev/one_shot.rs @@ -31,8 +31,8 @@ string_enum! { Vm => ["vm"], /// Select the Windows Sandbox backend. WindowsSandbox => ["windows_sandbox"], - /// Select the NVX micro-VM backend. - Nvx => ["nvx"], + /// Select the MicroVM backend implemented by NVX. + Microvm => ["microvm"], /// Select the Hyperlight micro-VM backend. Hyperlight => ["hyperlight"], /// Select the WSL container backend. diff --git a/src/core/mxc_config_contract/tests/v0_10_0_alpha/enums.rs b/src/core/mxc_config_contract/tests/v0_10_0_alpha/enums.rs index 6b5b03c79..f3cc8a027 100644 --- a/src/core/mxc_config_contract/tests/v0_10_0_alpha/enums.rs +++ b/src/core/mxc_config_contract/tests/v0_10_0_alpha/enums.rs @@ -14,7 +14,7 @@ fn accepts_every_containment_value() { "seatbelt", "vm", "windows_sandbox", - "nvx", + "microvm", "hyperlight", "isolation_session", "wslc", @@ -42,6 +42,17 @@ fn rejects_invalid_containment_value() { ); } +#[test] +fn rejects_internal_nvx_containment_value() { + assert_invalid( + r#"{ + "version": "0.10.0-alpha", + "containment": "nvx", + "process": {"commandLine": "echo"} + }"#, + ); +} + #[test] fn rejects_every_removed_default_network_policy_value() { for default_network_policy in ["allow", "block"] { diff --git a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs index a85bf5dc6..fee1fa3c2 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs @@ -15,7 +15,7 @@ fn wslc_containment_is_introduced_in_v09() { #[test] fn development_containment_values_are_introduced_in_v010() { - for containment in ["vm", "windows_sandbox", "nvx", "hyperlight"] { + for containment in ["vm", "windows_sandbox", "microvm", "hyperlight"] { assert_v10_introduces(&format!(r#""containment": "{containment}""#)); } } diff --git a/src/core/mxc_engine/Cargo.toml b/src/core/mxc_engine/Cargo.toml index 973b04a6e..c3dd41d52 100644 --- a/src/core/mxc_engine/Cargo.toml +++ b/src/core/mxc_engine/Cargo.toml @@ -49,7 +49,7 @@ default = [] # Enables constructing the run-to-completion runner for optional backends. # Mirrors the executor binaries' feature set so backend selection can live in # one place. Build-time binary staging remains with the executor crates. -nvx = ["dep:nvx_runner", "nvx_runner/nvx"] +microvm = ["dep:nvx_runner", "nvx_runner/nvx"] wslc = ["dep:wslc_common", "wslc_common/link-wslcsdk"] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight"] isolation_session = [ diff --git a/src/core/mxc_engine/src/platform.rs b/src/core/mxc_engine/src/platform.rs index 7bd5f8cc6..cc1a8433c 100644 --- a/src/core/mxc_engine/src/platform.rs +++ b/src/core/mxc_engine/src/platform.rs @@ -282,7 +282,7 @@ mod tests { Containment::Vm, Containment::WindowsSandbox, Containment::Lxc, - Containment::Nvx, + Containment::Microvm, Containment::Hyperlight, Containment::Wslc, Containment::Seatbelt, @@ -321,27 +321,27 @@ mod tests { } } - fn assert_nvx_method_is_omitted() { + fn assert_microvm_method_is_omitted() { let support = platform_support(); assert!( support .available_methods .iter() - .all(|method| method != "nvx"), - "platform_support must not advertise NVX while runtime is incomplete" + .all(|method| method != "microvm"), + "platform_support must not advertise MicroVM while runtime is incomplete" ); } - #[cfg(not(feature = "nvx"))] + #[cfg(not(feature = "microvm"))] #[test] - fn nvx_is_not_advertised_without_feature() { - assert_nvx_method_is_omitted(); + fn microvm_is_not_advertised_without_feature() { + assert_microvm_method_is_omitted(); } - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] #[test] - fn nvx_is_not_advertised_with_feature_enabled() { - assert_nvx_method_is_omitted(); + fn microvm_is_not_advertised_with_feature_enabled() { + assert_microvm_method_is_omitted(); } #[cfg(target_os = "linux")] diff --git a/src/core/mxc_engine/src/probe.rs b/src/core/mxc_engine/src/probe.rs index 7230273b2..4bc0459d1 100644 --- a/src/core/mxc_engine/src/probe.rs +++ b/src/core/mxc_engine/src/probe.rs @@ -285,7 +285,7 @@ mod tests { Containment::Vm, Containment::WindowsSandbox, Containment::Lxc, - Containment::Nvx, + Containment::Microvm, Containment::Hyperlight, Containment::Wslc, Containment::Seatbelt, @@ -407,25 +407,25 @@ mod tests { } } - fn assert_nvx_backend_is_omitted() { + fn assert_microvm_backend_is_omitted() { assert!( available_backends() .iter() - .all(|backend| backend.backend != "nvx"), - "available_backends must not advertise NVX while runtime is incomplete" + .all(|backend| backend.backend != "microvm"), + "available_backends must not advertise MicroVM while runtime is incomplete" ); } - #[cfg(not(feature = "nvx"))] + #[cfg(not(feature = "microvm"))] #[test] - fn nvx_is_not_advertised_without_feature() { - assert_nvx_backend_is_omitted(); + fn microvm_is_not_advertised_without_feature() { + assert_microvm_backend_is_omitted(); } - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] #[test] - fn nvx_is_not_advertised_with_feature_enabled() { - assert_nvx_backend_is_omitted(); + fn microvm_is_not_advertised_with_feature_enabled() { + assert_microvm_backend_is_omitted(); } /// Every backend the probe can emit, across all platforms/features — derived diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index 46a62630d..6ee92f589 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -36,19 +36,20 @@ use wxc_common::script_runner::ScriptRunner; use crate::error::Error; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -const ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED: &str = - "NVX is an experimental feature. Use --experimental flag."; +const ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED: &str = + "MicroVM (NVX) is an experimental feature. Use --experimental flag."; #[cfg(all( - not(feature = "nvx"), + not(feature = "microvm"), any(target_os = "windows", target_os = "linux", target_os = "macos") ))] -const ERR_NVX_FEATURE_REQUIRED: &str = "NVX backend not compiled in (build with --features nvx)"; +const ERR_MICROVM_FEATURE_REQUIRED: &str = + "MicroVM backend not compiled in (build with --features microvm)"; #[cfg(all( - feature = "nvx", + feature = "microvm", any(target_os = "windows", target_os = "linux", target_os = "macos") ))] -const ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING: &str = - "NVX runtime implementation is not present in this build"; +const ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING: &str = + "MicroVM (NVX) runtime implementation is not present in this build"; /// A backend runner resolved for an [`ExecutionRequest`], ready to run. /// @@ -254,7 +255,7 @@ fn resolve_runner_inner_windows( ContainmentBackend::Vm => Err(MxcError::unsupported_containment( "VM backend not yet implemented", )), - ContainmentBackend::Nvx => resolve_nvx_backend(request), + ContainmentBackend::Microvm => resolve_microvm_backend(request), ContainmentBackend::Hyperlight => resolve_hyperlight(request), ContainmentBackend::WindowsSandbox => { if !request.experimental_enabled { @@ -310,7 +311,7 @@ fn resolve_runner_inner( match request.containment { ContainmentBackend::Hyperlight => resolve_hyperlight(request), - ContainmentBackend::Nvx => resolve_nvx_backend(request), + ContainmentBackend::Microvm => resolve_microvm_backend(request), ContainmentBackend::Bubblewrap => Ok(ResolvedRunner::without_guard(Box::new(Runner::new( bwrap_common::bwrap_runner::BubblewrapScriptRunner::new(), )))), @@ -340,8 +341,8 @@ fn resolve_runner_inner( ) -> Result { use wxc_common::sandbox_process::Runner; - if request.containment == ContainmentBackend::Nvx { - return resolve_nvx_backend(request); + if request.containment == ContainmentBackend::Microvm { + return resolve_microvm_backend(request); } if request.containment != ContainmentBackend::Seatbelt { @@ -423,35 +424,37 @@ fn resolve_runner_inner( } #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] -fn resolve_nvx_backend(request: &ExecutionRequest) -> Result { +fn resolve_microvm_backend(request: &ExecutionRequest) -> Result { if !request.experimental_enabled { return Err(MxcError::malformed_request( - ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED, + ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED, )); } - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] { - resolve_nvx_backend_with_preflight(nvx_runner::preflight) + resolve_microvm_backend_with_preflight(nvx_runner::preflight) } - #[cfg(not(feature = "nvx"))] + #[cfg(not(feature = "microvm"))] { - Err(MxcError::unsupported_containment(ERR_NVX_FEATURE_REQUIRED)) + Err(MxcError::unsupported_containment( + ERR_MICROVM_FEATURE_REQUIRED, + )) } } #[cfg(all( - feature = "nvx", + feature = "microvm", any(target_os = "windows", target_os = "linux", target_os = "macos") ))] -fn resolve_nvx_backend_with_preflight(preflight: F) -> Result +fn resolve_microvm_backend_with_preflight(preflight: F) -> Result where F: FnOnce() -> Result<(), MxcError>, { preflight()?; Err(MxcError::backend_unavailable( - ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING, + ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING, )) } @@ -509,9 +512,9 @@ mod tests { } } - fn nvx_request(experimental_enabled: bool) -> ExecutionRequest { + fn microvm_request(experimental_enabled: bool) -> ExecutionRequest { ExecutionRequest { - containment: ContainmentBackend::Nvx, + containment: ContainmentBackend::Microvm, experimental_enabled, ..Default::default() } @@ -571,8 +574,8 @@ mod tests { } #[test] - fn nvx_without_experimental_opt_in_is_rejected_as_malformed_request() { - let request = nvx_request(false); + fn microvm_without_experimental_opt_in_is_rejected_as_malformed_request() { + let request = microvm_request(false); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner_windows(&request, &mut logger) { @@ -581,13 +584,13 @@ mod tests { }; assert_eq!(err.code, MxcErrorCode::MalformedRequest); - assert_eq!(err.message, ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED); + assert_eq!(err.message, ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED); } - #[cfg(not(feature = "nvx"))] + #[cfg(not(feature = "microvm"))] #[test] - fn nvx_without_feature_returns_typed_unsupported_containment() { - let request = nvx_request(true); + fn microvm_without_feature_returns_typed_unsupported_containment() { + let request = microvm_request(true); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner_windows(&request, &mut logger) { @@ -596,13 +599,13 @@ mod tests { }; assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); - assert_eq!(err.message, ERR_NVX_FEATURE_REQUIRED); + assert_eq!(err.message, ERR_MICROVM_FEATURE_REQUIRED); } - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] #[test] - fn nvx_with_feature_propagates_preflight_backend_unavailable() { - let request = nvx_request(true); + fn microvm_with_feature_propagates_preflight_backend_unavailable() { + let request = microvm_request(true); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner_windows(&request, &mut logger) { @@ -617,16 +620,16 @@ mod tests { ); } - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] #[test] - fn nvx_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { - let err = match resolve_nvx_backend_with_preflight(|| Ok(())) { + fn microvm_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { + let err = match resolve_microvm_backend_with_preflight(|| Ok(())) { Ok(_) => panic!("runtime stub must not resolve a runner"), Err(err) => err, }; assert_eq!(err.code, MxcErrorCode::BackendUnavailable); - assert_eq!(err.message, ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING); + assert_eq!(err.message, ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING); } } @@ -636,17 +639,17 @@ mod nvx_tests { use wxc_common::logger::Mode; use wxc_common::mxc_error::MxcErrorCode; - fn nvx_request(experimental_enabled: bool) -> ExecutionRequest { + fn microvm_request(experimental_enabled: bool) -> ExecutionRequest { ExecutionRequest { - containment: ContainmentBackend::Nvx, + containment: ContainmentBackend::Microvm, experimental_enabled, ..Default::default() } } #[test] - fn nvx_without_experimental_opt_in_is_rejected_as_malformed_request() { - let request = nvx_request(false); + fn microvm_without_experimental_opt_in_is_rejected_as_malformed_request() { + let request = microvm_request(false); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner(&request, &mut logger) { @@ -655,13 +658,13 @@ mod nvx_tests { }; assert_eq!(err.code, MxcErrorCode::MalformedRequest); - assert_eq!(err.message, ERR_NVX_EXPERIMENTAL_OPT_IN_REQUIRED); + assert_eq!(err.message, ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED); } - #[cfg(not(feature = "nvx"))] + #[cfg(not(feature = "microvm"))] #[test] - fn nvx_without_feature_returns_typed_unsupported_containment() { - let request = nvx_request(true); + fn microvm_without_feature_returns_typed_unsupported_containment() { + let request = microvm_request(true); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner(&request, &mut logger) { @@ -670,13 +673,13 @@ mod nvx_tests { }; assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); - assert_eq!(err.message, ERR_NVX_FEATURE_REQUIRED); + assert_eq!(err.message, ERR_MICROVM_FEATURE_REQUIRED); } - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] #[test] - fn nvx_with_feature_propagates_preflight_backend_unavailable() { - let request = nvx_request(true); + fn microvm_with_feature_propagates_preflight_backend_unavailable() { + let request = microvm_request(true); let mut logger = Logger::new(Mode::Buffer); let err = match resolve_runner_inner(&request, &mut logger) { @@ -691,15 +694,15 @@ mod nvx_tests { ); } - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] #[test] - fn nvx_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { - let err = match resolve_nvx_backend_with_preflight(|| Ok(())) { + fn microvm_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { + let err = match resolve_microvm_backend_with_preflight(|| Ok(())) { Ok(_) => panic!("runtime stub must not resolve a runner"), Err(err) => err, }; assert_eq!(err.code, MxcErrorCode::BackendUnavailable); - assert_eq!(err.message, ERR_NVX_RUNTIME_IMPLEMENTATION_MISSING); + assert_eq!(err.message, ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING); } } diff --git a/src/core/wxc/Cargo.toml b/src/core/wxc/Cargo.toml index 62ec2daa8..a354185e2 100644 --- a/src/core/wxc/Cargo.toml +++ b/src/core/wxc/Cargo.toml @@ -36,11 +36,11 @@ nvx_build_common = { workspace = true, optional = true } [features] default = [] test-support = ["wxc_common/test-support"] -nvx = [ +microvm = [ "dep:nvx_binaries", "nvx_binaries/nvx", "dep:nvx_build_common", - "mxc_engine/nvx", + "mxc_engine/microvm", ] wslc = ["dep:wslc_common", "wslc_common/link-wslcsdk", "mxc_engine/wslc"] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight", "mxc_engine/hyperlight"] diff --git a/src/core/wxc/build.rs b/src/core/wxc/build.rs index 7574a1b24..685abd089 100644 --- a/src/core/wxc/build.rs +++ b/src/core/wxc/build.rs @@ -9,7 +9,7 @@ fn main() { #[cfg(windows)] check_test_prerequisites(); - #[cfg(feature = "nvx")] + #[cfg(feature = "microvm")] stage_nvx_for_target(); // Delay-load winhvplatform.dll so WHP-less hosts don't crash before main(). @@ -79,7 +79,7 @@ fn check_test_prerequisites() { } } -#[cfg(feature = "nvx")] +#[cfg(feature = "microvm")] fn stage_nvx_for_target() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); @@ -94,12 +94,14 @@ fn stage_nvx_for_target() { } } -#[cfg(feature = "nvx")] +#[cfg(feature = "microvm")] fn copy_nvx_binaries() { use std::path::Path; let nvx_bin_dir = std::env::var("DEP_NVX_BINARIES_BIN_DIR").unwrap_or_else(|error| { - panic!("wxc build.rs: DEP_NVX_BINARIES_BIN_DIR is required for the nvx feature: {error}") + panic!( + "wxc build.rs: DEP_NVX_BINARIES_BIN_DIR is required for the microvm feature: {error}" + ) }); nvx_build_common::stage_artifacts_next_to_exe(Path::new(&nvx_bin_dir)) diff --git a/src/core/wxc/src/main.rs b/src/core/wxc/src/main.rs index b72f898be..d7192515c 100644 --- a/src/core/wxc/src/main.rs +++ b/src/core/wxc/src/main.rs @@ -2019,7 +2019,7 @@ mod tests { ContainmentBackend::WindowsSandbox, ContainmentBackend::Wslc, ContainmentBackend::IsolationSession, - ContainmentBackend::Nvx, + ContainmentBackend::Microvm, ] { let containment_name = containment.wire_name(); request.containment = containment; @@ -2576,11 +2576,11 @@ mod tests { } #[test] - fn nvx_cli_command_uses_posix_shell_quoting() { + fn microvm_cli_command_uses_posix_shell_quoting() { let cli = parse_cli(&["wxc-exec", "policy.json", "--", "echo", "safe&whoami"]); let command_override = cmdline_from_argv_for_context( &cli.command, - CommandLineContext::for_backend(&ContainmentBackend::Nvx), + CommandLineContext::for_backend(&ContainmentBackend::Microvm), ) .unwrap(); diff --git a/src/core/wxc_common/src/cmdline.rs b/src/core/wxc_common/src/cmdline.rs index 707e7841d..456c8aed5 100644 --- a/src/core/wxc_common/src/cmdline.rs +++ b/src/core/wxc_common/src/cmdline.rs @@ -46,7 +46,7 @@ impl CommandLineContext { ContainmentBackend::ProcessContainer | ContainmentBackend::Vm | ContainmentBackend::Hyperlight => Self::WindowsCreateProcess, - ContainmentBackend::Nvx => Self::PosixShell, + ContainmentBackend::Microvm => Self::PosixShell, } } } @@ -382,9 +382,9 @@ mod tests { } #[test] - fn nvx_backend_uses_posix_shell_context() { + fn microvm_backend_uses_posix_shell_context() { assert_eq!( - CommandLineContext::for_backend(&ContainmentBackend::Nvx), + CommandLineContext::for_backend(&ContainmentBackend::Microvm), CommandLineContext::PosixShell ); } diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs index d37f7b200..a52b85930 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot.rs @@ -17,7 +17,7 @@ fn convert_containment(value: contract::OneShotContainment) -> wire::Containment contract::OneShotContainment::Seatbelt => wire::Containment::Seatbelt, contract::OneShotContainment::Vm => wire::Containment::Vm, contract::OneShotContainment::WindowsSandbox => wire::Containment::WindowsSandbox, - contract::OneShotContainment::Nvx => wire::Containment::Nvx, + contract::OneShotContainment::Microvm => wire::Containment::Microvm, contract::OneShotContainment::Hyperlight => wire::Containment::Hyperlight, contract::OneShotContainment::Wslc => wire::Containment::Wslc, contract::OneShotContainment::IsolationSession => wire::Containment::IsolationSession, diff --git a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs index 666ba1df8..320b98b97 100644 --- a/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs +++ b/src/core/wxc_common/src/config_contract_adapters/dev/one_shot_tests/experimental.rs @@ -163,8 +163,8 @@ const DEVELOPMENT_CONTAINMENT_CASES: &[DevelopmentContainmentCase] = &[ expected: "vm", }, DevelopmentContainmentCase { - input: "nvx", - expected: "nvx", + input: "microvm", + expected: "microvm", }, DevelopmentContainmentCase { input: "windows_sandbox", diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index 40759df49..b1fa09e47 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -422,7 +422,6 @@ fn deserialize_development_request( fn parse_exact_development(json: &str, logger: &mut Logger) -> Result { let phase = mxc_config_contract::dev::probe_phase(json).map_err(exact_phase_error)?; - reject_removed_microvm(json, phase)?; let request = deserialize_development_request(json, phase)?; let adapted = crate::config_contract_adapters::dev::adapt_request(request) .map_err(|error| ParseError::StateAware(MxcError::malformed_request(error.to_string())))?; @@ -444,27 +443,6 @@ fn parse_exact_development(json: &str, logger: &mut Logger) -> Result, -) -> Result<(), ParseError> { - let Ok(root) = serde_json::from_str::(json) else { - return Ok(()); - }; - if root.get("containment").and_then(serde_json::Value::as_str) != Some("microvm") { - return Ok(()); - } - - let message = "containment \"microvm\" was removed; use \"nvx\" for the NVX micro-VM backend"; - if phase.is_some() { - Err(ParseError::StateAware(MxcError::malformed_request(message))) - } else { - Err(ParseError::OneShot(WxcError::ConfigParse( - message.to_string(), - ))) - } -} - fn parse_exact_mxc_request_json(json: &str, logger: &mut Logger) -> Result { match probe_version(json).map_err(exact_version_error)? { ContractVersion::V0_6_0Alpha => parse_exact_published_one_shot( @@ -1078,7 +1056,7 @@ fn requested_sandbox_kind(c: Option<&wire::Containment>) -> &'static str { Some(wire::Containment::Vm) => "vm", Some(wire::Containment::WindowsSandbox) => "windows_sandbox", Some(wire::Containment::Lxc) => "lxc", - Some(wire::Containment::Nvx) => "nvx", + Some(wire::Containment::Microvm) => "microvm", Some(wire::Containment::Hyperlight) => "hyperlight", Some(wire::Containment::Wslc) => "wslc", Some(wire::Containment::Seatbelt) => "seatbelt", @@ -6434,50 +6412,33 @@ mod tests { } #[test] - fn containment_microvm_rejected_with_nvx_migration_guidance() { - let json = r#"{ - "version": "0.10.0-alpha", - "process": {"commandLine": "echo hi"}, - "containment": "microvm" - }"#; - - let error = - parse_exact_for_test(json).expect_err("removed microvm containment must be rejected"); - assert!(matches!(error, ParseError::OneShot(_))); - assert!(error.message().contains("microvm")); - assert!(error.message().contains("nvx")); - assert!(error.message().contains("removed")); - } - - #[test] - fn state_aware_microvm_rejected_with_nvx_migration_guidance() { + fn containment_microvm_accepted() { let json = r#"{ "version": "0.10.0-alpha", - "phase": "provision", + "process": {"commandLine": "/bin/true"}, "containment": "microvm" }"#; - let error = parse_exact_for_test(json) - .expect_err("removed state-aware microvm containment must be rejected"); - assert!(matches!(error, ParseError::StateAware(_))); - assert!(error.message().contains("microvm")); - assert!(error.message().contains("nvx")); - assert!(error.message().contains("removed")); + let request = parse_exact_for_test(json).unwrap(); + let MxcRequest::OneShot(request) = request else { + panic!("expected one-shot MicroVM request"); + }; + assert_eq!(request.containment, ContainmentBackend::Microvm); } #[test] - fn containment_nvx_accepted() { + fn containment_nvx_gets_normal_unknown_enum_rejection() { let json = r#"{ "version": "0.10.0-alpha", "process": {"commandLine": "/bin/true"}, "containment": "nvx" }"#; - let request = parse_exact_for_test(json).unwrap(); - let MxcRequest::OneShot(request) = request else { - panic!("expected one-shot NVX request"); - }; - assert_eq!(request.containment, ContainmentBackend::Nvx); + let error = parse_exact_for_test(json) + .expect_err("internal NVX implementation name must not be accepted"); + assert!(matches!(error, ParseError::OneShot(_))); + assert!(error.message().contains("unknown variant `nvx`")); + assert!(!error.message().contains("use")); } #[test] diff --git a/src/core/wxc_common/src/models.rs b/src/core/wxc_common/src/models.rs index f65b2ce3d..ac71cf3ad 100644 --- a/src/core/wxc_common/src/models.rs +++ b/src/core/wxc_common/src/models.rs @@ -25,8 +25,8 @@ pub enum ContainmentBackend { Lxc, /// VM-based isolation. Vm, - /// NVX Linux micro-VM hosted by OpenVMM. - Nvx, + /// MicroVM isolation implemented by NVX and hosted by OpenVMM. + Microvm, /// MicroVM isolation via Hyperlight + Unikraft, using an embedded /// warmed-up CPython snapshot. ~100 ms cold start per invocation, /// hermetic via snapshot restore. Experimental — requires @@ -56,7 +56,7 @@ impl ContainmentBackend { ContainmentBackend::Wslc => "wslc", ContainmentBackend::Lxc => "lxc", ContainmentBackend::Vm => "vm", - ContainmentBackend::Nvx => "nvx", + ContainmentBackend::Microvm => "microvm", ContainmentBackend::Hyperlight => "hyperlight", ContainmentBackend::WindowsSandbox => "windows_sandbox", ContainmentBackend::IsolationSession => "isolation_session", @@ -78,7 +78,7 @@ impl ContainmentBackend { ContainmentBackend::IsolationSession => Some("isolationSession"), ContainmentBackend::Bubblewrap | ContainmentBackend::Hyperlight - | ContainmentBackend::Nvx + | ContainmentBackend::Microvm | ContainmentBackend::Vm => None, } } @@ -122,7 +122,7 @@ impl From for ContainmentBackend { } W::WindowsSandbox => Self::WindowsSandbox, W::Lxc => Self::Lxc, - W::Nvx => Self::Nvx, + W::Microvm => Self::Microvm, W::Hyperlight => Self::Hyperlight, W::Wslc => Self::Wslc, W::Seatbelt => Self::Seatbelt, diff --git a/src/core/wxc_common/src/splice.rs b/src/core/wxc_common/src/splice.rs index acbe8d273..a45de8722 100644 --- a/src/core/wxc_common/src/splice.rs +++ b/src/core/wxc_common/src/splice.rs @@ -225,7 +225,7 @@ mod tests { "vm", "windows_sandbox", "lxc", - "nvx", + "microvm", "hyperlight", "wslc", "seatbelt", diff --git a/src/core/wxc_common/src/wire.rs b/src/core/wxc_common/src/wire.rs index 89f26e7df..45185af08 100644 --- a/src/core/wxc_common/src/wire.rs +++ b/src/core/wxc_common/src/wire.rs @@ -139,8 +139,8 @@ pub enum Containment { WindowsSandbox, /// Full Linux container. Lxc, - /// NVX micro-VM (experimental). - Nvx, + /// MicroVM isolation implemented by NVX (experimental). + Microvm, /// Hyperlight micro-VM (experimental). Hyperlight, /// WSL container. diff --git a/src/testing/fuzz/Cargo.toml b/src/testing/fuzz/Cargo.toml index 4689ccc93..b52d920e7 100644 --- a/src/testing/fuzz/Cargo.toml +++ b/src/testing/fuzz/Cargo.toml @@ -18,12 +18,14 @@ cargo-fuzz = true # These pull in heavier deps so they're opt-in for local dev. hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight"] isolation_session = ["dep:isolation_session_common"] +microvm = ["dep:nvx_runner", "nvx_runner/nvx"] [dependencies] libfuzzer-sys = "0.4" wxc_common = { path = "../../core/wxc_common" } hyperlight_common = { path = "../../backends/hyperlight/common", optional = true } isolation_session_common = { path = "../../backends/isolation_session/common", optional = true } +nvx_runner = { path = "../../backends/nvx/runner", optional = true } [[bin]] name = "config_parser" diff --git a/src/testing/fuzz/fuzz_targets/validator.rs b/src/testing/fuzz/fuzz_targets/validator.rs index 4818d665e..934bf10bd 100644 --- a/src/testing/fuzz/fuzz_targets/validator.rs +++ b/src/testing/fuzz/fuzz_targets/validator.rs @@ -7,6 +7,7 @@ // path that `--dry-run` takes through the binary. // // Runner-specific validation coverage: +// - MicroVM (NVX): requires `--features microvm` // - Hyperlight: requires `--features hyperlight` // - IsolationSession: requires `--features isolation_session` // - Seatbelt: macOS-only, not available in Windows fuzz builds @@ -33,6 +34,10 @@ fuzz_target!(|data: &[u8]| { // Dispatch to runner-specific validation based on backend. #[cfg(target_os = "windows")] match req.containment { + #[cfg(feature = "microvm")] + ContainmentBackend::Microvm => { + let _ = nvx_runner::preflight(); + } #[cfg(feature = "hyperlight")] ContainmentBackend::Hyperlight => { let runner = hyperlight_common::HyperlightScriptRunner::new(); From b6c4307c4f901da0f78c1458058d873da32e19a1 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 10:03:18 -0700 Subject: [PATCH 30/41] fix: keep NVX dispatch Windows-only Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/core/lxc/Cargo.toml | 1 - src/core/mxc_engine/Cargo.toml | 2 +- src/core/mxc_engine/src/run.rs | 148 ++++++++------------- src/testing/fuzz/Cargo.toml | 2 + src/testing/fuzz/fuzz_targets/validator.rs | 4 + 5 files changed, 61 insertions(+), 96 deletions(-) diff --git a/src/core/lxc/Cargo.toml b/src/core/lxc/Cargo.toml index 0a25eaa26..05bcb33ae 100644 --- a/src/core/lxc/Cargo.toml +++ b/src/core/lxc/Cargo.toml @@ -10,7 +10,6 @@ path = "src/main.rs" [features] hyperlight = ["dep:hyperlight_common", "hyperlight_common/hyperlight", "mxc_engine/hyperlight"] -microvm = ["mxc_engine/microvm"] [build-dependencies] mxc_build_common.workspace = true diff --git a/src/core/mxc_engine/Cargo.toml b/src/core/mxc_engine/Cargo.toml index c3dd41d52..8462f705b 100644 --- a/src/core/mxc_engine/Cargo.toml +++ b/src/core/mxc_engine/Cargo.toml @@ -14,12 +14,12 @@ wxc_common.workspace = true mxc_config_contract.workspace = true serde = { workspace = true } serde_json = { workspace = true } -nvx_runner = { workspace = true, optional = true } [target.'cfg(target_arch = "x86_64")'.dependencies] hyperlight_common = { workspace = true, optional = true } [target.'cfg(target_os = "windows")'.dependencies] +nvx_runner = { workspace = true, optional = true } appcontainer_common = { workspace = true } windows_sandbox_lifecycle = { workspace = true } isolation_session_common = { workspace = true, optional = true } diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index 90db324d3..7b4b382db 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -35,19 +35,13 @@ use wxc_common::script_runner::ScriptRunner; use crate::error::Error; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(target_os = "windows")] const ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED: &str = "MicroVM (NVX) is an experimental feature. Use --experimental flag."; -#[cfg(all( - not(feature = "microvm"), - any(target_os = "windows", target_os = "linux", target_os = "macos") -))] +#[cfg(all(not(feature = "microvm"), target_os = "windows"))] const ERR_MICROVM_FEATURE_REQUIRED: &str = "MicroVM backend not compiled in (build with --features microvm)"; -#[cfg(all( - feature = "microvm", - any(target_os = "windows", target_os = "linux", target_os = "macos") -))] +#[cfg(all(feature = "microvm", target_os = "windows"))] const ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING: &str = "MicroVM (NVX) runtime implementation is not present in this build"; @@ -347,7 +341,7 @@ fn resolve_runner_inner_windows( } // --------------------------------------------------------------------------- -// Linux: Bubblewrap, LXC, and the experimental Hyperlight / NVX backends. +// Linux: Bubblewrap, LXC, and the experimental Hyperlight backend. // A concrete backend selected for another host must fail closed. // --------------------------------------------------------------------------- @@ -360,7 +354,6 @@ fn resolve_runner_inner( match request.containment { ContainmentBackend::Hyperlight => resolve_hyperlight(request), - ContainmentBackend::Microvm => resolve_microvm_backend(request), ContainmentBackend::Bubblewrap => Ok(ResolvedRunner::without_guard(Box::new(Runner::new( bwrap_common::bwrap_runner::BubblewrapScriptRunner::new(), )))), @@ -390,10 +383,6 @@ fn resolve_runner_inner( ) -> Result { use wxc_common::sandbox_process::Runner; - if request.containment == ContainmentBackend::Microvm { - return resolve_microvm_backend(request); - } - if request.containment != ContainmentBackend::Seatbelt { return Err(MxcError::unsupported_containment(format!( "the '{}' backend is not available on macOS", @@ -429,6 +418,30 @@ mod linux_tests { ); assert!(error.message.contains("seatbelt")); } + + #[test] + fn microvm_is_rejected_as_unavailable_on_linux() { + let request = ExecutionRequest { + containment: ContainmentBackend::Microvm, + experimental_enabled: true, + ..Default::default() + }; + let mut logger = Logger::new(Mode::Buffer); + + let error = match resolve_runner_inner(&request, &mut logger) { + Ok(_) => panic!("NVX-backed MicroVM must remain Windows-only"), + Err(error) => error, + }; + + assert_eq!( + error.code, + wxc_common::mxc_error::MxcErrorCode::UnsupportedContainment + ); + assert_eq!( + error.message, + "the 'microvm' backend is not available on Linux" + ); + } } #[cfg(all(test, target_os = "macos"))] @@ -455,6 +468,30 @@ mod macos_tests { ); assert!(error.message.contains("lxc")); } + + #[test] + fn microvm_is_rejected_as_unavailable_on_macos() { + let request = ExecutionRequest { + containment: ContainmentBackend::Microvm, + experimental_enabled: true, + ..Default::default() + }; + let mut logger = Logger::new(Mode::Buffer); + + let error = match resolve_runner_inner(&request, &mut logger) { + Ok(_) => panic!("NVX-backed MicroVM must remain Windows-only"), + Err(error) => error, + }; + + assert_eq!( + error.code, + wxc_common::mxc_error::MxcErrorCode::UnsupportedContainment + ); + assert_eq!( + error.message, + "the 'microvm' backend is not available on macOS" + ); + } } // --------------------------------------------------------------------------- @@ -472,7 +509,7 @@ fn resolve_runner_inner( )) } -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(target_os = "windows")] fn resolve_microvm_backend(request: &ExecutionRequest) -> Result { if !request.experimental_enabled { return Err(MxcError::malformed_request( @@ -493,10 +530,7 @@ fn resolve_microvm_backend(request: &ExecutionRequest) -> Result(preflight: F) -> Result where F: FnOnce() -> Result<(), MxcError>, @@ -678,77 +712,3 @@ mod tests { assert_eq!(err.message, ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING); } } - -#[cfg(all(test, any(target_os = "linux", target_os = "macos")))] -mod nvx_tests { - use super::*; - use wxc_common::logger::Mode; - use wxc_common::mxc_error::MxcErrorCode; - - fn microvm_request(experimental_enabled: bool) -> ExecutionRequest { - ExecutionRequest { - containment: ContainmentBackend::Microvm, - experimental_enabled, - ..Default::default() - } - } - - #[test] - fn microvm_without_experimental_opt_in_is_rejected_as_malformed_request() { - let request = microvm_request(false); - let mut logger = Logger::new(Mode::Buffer); - - let err = match resolve_runner_inner(&request, &mut logger) { - Ok(_) => panic!("expected malformed_request"), - Err(err) => err, - }; - - assert_eq!(err.code, MxcErrorCode::MalformedRequest); - assert_eq!(err.message, ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED); - } - - #[cfg(not(feature = "microvm"))] - #[test] - fn microvm_without_feature_returns_typed_unsupported_containment() { - let request = microvm_request(true); - let mut logger = Logger::new(Mode::Buffer); - - let err = match resolve_runner_inner(&request, &mut logger) { - Ok(_) => panic!("expected unsupported_containment"), - Err(err) => err, - }; - - assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); - assert_eq!(err.message, ERR_MICROVM_FEATURE_REQUIRED); - } - - #[cfg(feature = "microvm")] - #[test] - fn microvm_with_feature_propagates_preflight_backend_unavailable() { - let request = microvm_request(true); - let mut logger = Logger::new(Mode::Buffer); - - let err = match resolve_runner_inner(&request, &mut logger) { - Ok(_) => panic!("expected backend_unavailable"), - Err(err) => err, - }; - - assert_eq!(err.code, MxcErrorCode::BackendUnavailable); - assert_eq!( - err.message, - nvx_runner::ERR_WORKLOAD_IMAGE_ASSET_UNAVAILABLE - ); - } - - #[cfg(feature = "microvm")] - #[test] - fn microvm_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { - let err = match resolve_microvm_backend_with_preflight(|| Ok(())) { - Ok(_) => panic!("runtime stub must not resolve a runner"), - Err(err) => err, - }; - - assert_eq!(err.code, MxcErrorCode::BackendUnavailable); - assert_eq!(err.message, ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING); - } -} diff --git a/src/testing/fuzz/Cargo.toml b/src/testing/fuzz/Cargo.toml index b52d920e7..0ae16d58d 100644 --- a/src/testing/fuzz/Cargo.toml +++ b/src/testing/fuzz/Cargo.toml @@ -25,6 +25,8 @@ libfuzzer-sys = "0.4" wxc_common = { path = "../../core/wxc_common" } hyperlight_common = { path = "../../backends/hyperlight/common", optional = true } isolation_session_common = { path = "../../backends/isolation_session/common", optional = true } + +[target.'cfg(target_os = "windows")'.dependencies] nvx_runner = { path = "../../backends/nvx/runner", optional = true } [[bin]] diff --git a/src/testing/fuzz/fuzz_targets/validator.rs b/src/testing/fuzz/fuzz_targets/validator.rs index 934bf10bd..e2c47b798 100644 --- a/src/testing/fuzz/fuzz_targets/validator.rs +++ b/src/testing/fuzz/fuzz_targets/validator.rs @@ -19,6 +19,10 @@ use libfuzzer_sys::fuzz_target; use wxc_common::config_parser::load_mxc_request; use wxc_common::logger::{Logger, Mode}; use wxc_common::models::ContainmentBackend; +#[cfg(all( + target_os = "windows", + any(feature = "hyperlight", feature = "isolation_session") +))] use wxc_common::script_runner::ScriptRunner; use wxc_common::state_aware_request::MxcRequest; use wxc_common::validator::validate_common; From 3f81ba348c66be322fa823d33f1851085745ed35 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 10:12:05 -0700 Subject: [PATCH 31/41] fix: reject MicroVM on Windows ARM64 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/core/mxc_engine/Cargo.toml | 4 +- src/core/mxc_engine/src/run.rs | 45 ++++++++++++++++++---- src/testing/fuzz/Cargo.toml | 2 +- src/testing/fuzz/fuzz_targets/validator.rs | 2 +- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/core/mxc_engine/Cargo.toml b/src/core/mxc_engine/Cargo.toml index 8462f705b..8a9dc430b 100644 --- a/src/core/mxc_engine/Cargo.toml +++ b/src/core/mxc_engine/Cargo.toml @@ -18,8 +18,10 @@ serde_json = { workspace = true } [target.'cfg(target_arch = "x86_64")'.dependencies] hyperlight_common = { workspace = true, optional = true } -[target.'cfg(target_os = "windows")'.dependencies] +[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.dependencies] nvx_runner = { workspace = true, optional = true } + +[target.'cfg(target_os = "windows")'.dependencies] appcontainer_common = { workspace = true } windows_sandbox_lifecycle = { workspace = true } isolation_session_common = { workspace = true, optional = true } diff --git a/src/core/mxc_engine/src/run.rs b/src/core/mxc_engine/src/run.rs index 7b4b382db..f703ec838 100644 --- a/src/core/mxc_engine/src/run.rs +++ b/src/core/mxc_engine/src/run.rs @@ -38,12 +38,19 @@ use crate::error::Error; #[cfg(target_os = "windows")] const ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED: &str = "MicroVM (NVX) is an experimental feature. Use --experimental flag."; -#[cfg(all(not(feature = "microvm"), target_os = "windows"))] +#[cfg(all( + not(feature = "microvm"), + target_os = "windows", + target_arch = "x86_64" +))] const ERR_MICROVM_FEATURE_REQUIRED: &str = "MicroVM backend not compiled in (build with --features microvm)"; -#[cfg(all(feature = "microvm", target_os = "windows"))] +#[cfg(all(feature = "microvm", target_os = "windows", target_arch = "x86_64"))] const ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING: &str = "MicroVM (NVX) runtime implementation is not present in this build"; +#[cfg(all(target_os = "windows", not(target_arch = "x86_64")))] +const ERR_MICROVM_ARCHITECTURE_UNSUPPORTED: &str = + "MicroVM (NVX) requires Windows x64; this Windows architecture is unsupported"; /// A backend runner resolved for an [`ExecutionRequest`], ready to run. /// @@ -517,20 +524,27 @@ fn resolve_microvm_backend(request: &ExecutionRequest) -> Result(preflight: F) -> Result where F: FnOnce() -> Result<(), MxcError>, @@ -667,7 +681,7 @@ mod tests { assert_eq!(err.message, ERR_MICROVM_EXPERIMENTAL_OPT_IN_REQUIRED); } - #[cfg(not(feature = "microvm"))] + #[cfg(all(not(feature = "microvm"), target_arch = "x86_64"))] #[test] fn microvm_without_feature_returns_typed_unsupported_containment() { let request = microvm_request(true); @@ -682,7 +696,7 @@ mod tests { assert_eq!(err.message, ERR_MICROVM_FEATURE_REQUIRED); } - #[cfg(feature = "microvm")] + #[cfg(all(feature = "microvm", target_arch = "x86_64"))] #[test] fn microvm_with_feature_propagates_preflight_backend_unavailable() { let request = microvm_request(true); @@ -700,7 +714,7 @@ mod tests { ); } - #[cfg(feature = "microvm")] + #[cfg(all(feature = "microvm", target_arch = "x86_64"))] #[test] fn microvm_without_runtime_returns_backend_unavailable_even_if_preflight_succeeds() { let err = match resolve_microvm_backend_with_preflight(|| Ok(())) { @@ -711,4 +725,19 @@ mod tests { assert_eq!(err.code, MxcErrorCode::BackendUnavailable); assert_eq!(err.message, ERR_MICROVM_RUNTIME_IMPLEMENTATION_MISSING); } + + #[cfg(not(target_arch = "x86_64"))] + #[test] + fn microvm_is_rejected_on_unsupported_windows_architecture() { + let request = microvm_request(true); + let mut logger = Logger::new(Mode::Buffer); + + let err = match resolve_runner_inner_windows(&request, &mut logger) { + Ok(_) => panic!("expected unsupported_containment"), + Err(err) => err, + }; + + assert_eq!(err.code, MxcErrorCode::UnsupportedContainment); + assert_eq!(err.message, ERR_MICROVM_ARCHITECTURE_UNSUPPORTED); + } } diff --git a/src/testing/fuzz/Cargo.toml b/src/testing/fuzz/Cargo.toml index 0ae16d58d..036c04ebd 100644 --- a/src/testing/fuzz/Cargo.toml +++ b/src/testing/fuzz/Cargo.toml @@ -26,7 +26,7 @@ wxc_common = { path = "../../core/wxc_common" } hyperlight_common = { path = "../../backends/hyperlight/common", optional = true } isolation_session_common = { path = "../../backends/isolation_session/common", optional = true } -[target.'cfg(target_os = "windows")'.dependencies] +[target.'cfg(all(target_os = "windows", target_arch = "x86_64"))'.dependencies] nvx_runner = { path = "../../backends/nvx/runner", optional = true } [[bin]] diff --git a/src/testing/fuzz/fuzz_targets/validator.rs b/src/testing/fuzz/fuzz_targets/validator.rs index e2c47b798..7aac4935b 100644 --- a/src/testing/fuzz/fuzz_targets/validator.rs +++ b/src/testing/fuzz/fuzz_targets/validator.rs @@ -38,7 +38,7 @@ fuzz_target!(|data: &[u8]| { // Dispatch to runner-specific validation based on backend. #[cfg(target_os = "windows")] match req.containment { - #[cfg(feature = "microvm")] + #[cfg(all(feature = "microvm", target_arch = "x86_64"))] ContainmentBackend::Microvm => { let _ = nvx_runner::preflight(); } From 062e3016b331b57a3edbd6176fba9b7c6cc106f9 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 10:20:29 -0700 Subject: [PATCH 32/41] docs: name MicroVM in SDK backend guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/core/mxc-sdk/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core/mxc-sdk/README.md b/src/core/mxc-sdk/README.md index e0d777692..9988d04c4 100644 --- a/src/core/mxc-sdk/README.md +++ b/src/core/mxc-sdk/README.md @@ -517,9 +517,10 @@ dropping the handle tears the session down synchronously rather than in the background. Reach its multi-call lifecycle through `run_state_aware_json` plus `exec_attached` or `exec_sandbox`. -Backends with no variant at all — Windows Sandbox, NVX, and Hyperlight — -cannot be named from this crate; use the executor binaries. Windows Sandbox is -still reachable here through the state-aware lifecycle. +Backends with no variant at all — Windows Sandbox, MicroVM (implemented by +NVX), and Hyperlight — cannot be named from this crate; use the executor +binaries. Windows Sandbox is still reachable here through the state-aware +lifecycle. `Containment::Lxc` models explicit LXC distribution settings, but `run` and `spawn_sandbox` reject it because the LXC backend does not expose captured From fa2d7b82617b5c24437d61a24c7095771532e332 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 10:33:45 -0700 Subject: [PATCH 33/41] fix: keep MicroVM packaging x64-only Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/templates/Rust.Build.Job.yml | 5 ++++- build.bat | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/templates/Rust.Build.Job.yml b/.azure-pipelines/templates/Rust.Build.Job.yml index 6d1254308..0c4ecfb86 100644 --- a/.azure-pipelines/templates/Rust.Build.Job.yml +++ b/.azure-pipelines/templates/Rust.Build.Job.yml @@ -46,7 +46,10 @@ jobs: workingDirectory: $(Build.SourcesDirectory)/src # Explicit feature set (no --all-features): tier2_bfs must NOT ship # on Win 11 25H2 (bfscfg.exe risks an OS hang). - cargoFeatures: hyperlight isolation_session microvm wslc + ${{ if eq(item.arch, 'x64') }}: + cargoFeatures: hyperlight isolation_session microvm wslc + ${{ else }}: + cargoFeatures: hyperlight isolation_session wslc signPattern: | wxc-exec.exe plm.exe diff --git a/build.bat b/build.bat index 54a5bd1e8..2d7ffba33 100644 --- a/build.bat +++ b/build.bat @@ -137,6 +137,11 @@ for %%T in (x86_64-pc-windows-msvc aarch64-pc-windows-msvc) do ( copy /Y "!BIN_DIR!\mxc_ffi.dll" "sdk\node\bin\!SDK_ARCH!\" >nul echo Copied !SDK_ARCH!\mxc_ffi.dll ) + for %%B in (nanvixd.exe nanvix_rootfs.img python3.initrd bin\kernel.elf snapshots\kernel.vmem snapshots\kernel.whp.cbor) do ( + if exist "sdk\node\bin\!SDK_ARCH!\%%B" del /Q "sdk\node\bin\!SDK_ARCH!\%%B" + ) + if exist "sdk\node\bin\!SDK_ARCH!\snapshots" rd "sdk\node\bin\!SDK_ARCH!\snapshots" 2>nul + if exist "sdk\node\bin\!SDK_ARCH!\bin" rd "sdk\node\bin\!SDK_ARCH!\bin" 2>nul if "%%T"=="x86_64-pc-windows-msvc" ( if "%WITH_MICROVM%"=="1" ( for %%B in (bin\openvmm.exe guest\vmlinux guest\initramfs.cpio.gz) do ( From aa7c83b43555da818d34df038b2dd2cb20069a66 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 10:46:59 -0700 Subject: [PATCH 34/41] fix: publish NVX runtime artifacts in Azure Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/templates/Rust.Build.Job.yml | 12 ++++++++++++ src/core/mxc_engine/src/dispatch.rs | 11 ++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.azure-pipelines/templates/Rust.Build.Job.yml b/.azure-pipelines/templates/Rust.Build.Job.yml index 0c4ecfb86..64ef1b0dd 100644 --- a/.azure-pipelines/templates/Rust.Build.Job.yml +++ b/.azure-pipelines/templates/Rust.Build.Job.yml @@ -48,8 +48,16 @@ jobs: # on Win 11 25H2 (bfscfg.exe risks an OS hang). ${{ if eq(item.arch, 'x64') }}: cargoFeatures: hyperlight isolation_session microvm wslc + microvmSignPattern: bin/openvmm.exe + microvmArtifactPattern: | + bin/openvmm.exe + guest/vmlinux + guest/initramfs.cpio.gz + images/** ${{ else }}: cargoFeatures: hyperlight isolation_session wslc + microvmSignPattern: '' + microvmArtifactPattern: '' signPattern: | wxc-exec.exe plm.exe @@ -68,6 +76,8 @@ jobs: artifactPrefix: lxc-binaries workingDirectory: $(Build.SourcesDirectory)/src/core/lxc cargoFeatures: hyperlight + microvmSignPattern: '' + microvmArtifactPattern: '' signPattern: | lxc-exec unix-test-proxy @@ -172,6 +182,7 @@ jobs: FolderPath: $(targetTripleDir) Pattern: | $(signPattern) + $(microvmSignPattern) UseMinimatch: true signConfigType: 'inlineSignParams' inlineOperation: >- @@ -204,6 +215,7 @@ jobs: sourceFolder: $(targetTripleDir) contents: | $(signPattern) + $(microvmArtifactPattern) targetFolder: $(outputDirectory)/$(targetTriple) # Copy wslcsdk.dll diff --git a/src/core/mxc_engine/src/dispatch.rs b/src/core/mxc_engine/src/dispatch.rs index ecad144fd..73c4d24db 100644 --- a/src/core/mxc_engine/src/dispatch.rs +++ b/src/core/mxc_engine/src/dispatch.rs @@ -15,11 +15,12 @@ //! run-to-completion path via `appcontainer_common::dispatcher`), Bubblewrap //! (Linux), Seatbelt (macOS), WSLC, and IsolationSession (Windows, //! behind the `wslc` and `isolation_session` features). Every other backend — -//! including the remaining experimental ones (Windows Sandbox, NVX, -//! Hyperlight) and LXC (no streaming path suitable for the library) — returns -//! [`MxcError::unsupported_containment`]; callers that need those must drive the -//! standalone executor binaries (whose run-to-completion path will, in a later -//! increment, also route through this engine). +//! including the remaining experimental ones (Windows Sandbox, MicroVM +//! implemented by NVX, and Hyperlight) and LXC (no streaming path suitable for +//! the library) — returns [`MxcError::unsupported_containment`]; callers that +//! need those must drive the standalone executor binaries (whose +//! run-to-completion path will, in a later increment, also route through this +//! engine). use wxc_common::logger::Logger; use wxc_common::models::{ContainmentBackend, ExecutionRequest, ScriptResponse}; From 78fa49bb594d2fdcaf6ea5162821204e689644d0 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 10:52:46 -0700 Subject: [PATCH 35/41] ci: document MicroVM artifact coupling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .azure-pipelines/templates/Rust.Build.Job.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.azure-pipelines/templates/Rust.Build.Job.yml b/.azure-pipelines/templates/Rust.Build.Job.yml index 64ef1b0dd..ae1fe2e83 100644 --- a/.azure-pipelines/templates/Rust.Build.Job.yml +++ b/.azure-pipelines/templates/Rust.Build.Job.yml @@ -46,6 +46,7 @@ jobs: workingDirectory: $(Build.SourcesDirectory)/src # Explicit feature set (no --all-features): tier2_bfs must NOT ship # on Win 11 25H2 (bfscfg.exe risks an OS hang). + # Keep the x64-only MicroVM feature coupled to its published NVX payload. ${{ if eq(item.arch, 'x64') }}: cargoFeatures: hyperlight isolation_session microvm wslc microvmSignPattern: bin/openvmm.exe From ee940df9db554d2df9603b773a12e087a84b4125 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 11:00:46 -0700 Subject: [PATCH 36/41] docs: point MicroVM integration at exact v0.10 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/nvx-backend-gaps.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index 2d6da698c..fe546c467 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -84,11 +84,12 @@ them for this backend: ## Schema changes -The rolling `0.10.0-dev` and exact `0.10.0-alpha` development contracts include -`containment: "microvm"` in their generated schemas and TypeScript wire types. -The internal `nvx` implementation name is not accepted as a public containment -value. Published stable schemas remain unchanged. An `experimental.nvx.provision` section is necessary -only if images remain caller-configurable. +The exact `0.10.0-alpha` development contract includes +`containment: "microvm"` in its generated schema and TypeScript wire types. The +internal `nvx` implementation name is not accepted as a public containment +value. Published stable schemas remain unchanged. An +`experimental.nvx.provision` section is necessary only if images remain +caller-configurable. Future state-aware provision design: @@ -198,7 +199,7 @@ surfaces, capability advertisement, and MXC-native E2E/CI coverage. This is the current direct OpenVMM/NVX capability baseline after the merged policy work. An MXC backend adapter still needs to select these controls from -the exact schema 0.9 request and expose their outcomes through MXC APIs. +the exact `0.10.0-alpha` request and expose their outcomes through MXC APIs. | Area | Supported today | Limitation for MXC | | --- | --- | --- | @@ -240,7 +241,7 @@ emits the legacy `allowOutbound`, `allowLocalNetwork`, and `network.proxy` fields. It no longer provides a raw `sandbox.config` passthrough. Before the CLI can use the MicroVM (NVX) backend, it must migrate its generated policy -to schema 0.8+: +to the exact `0.10.0-alpha` development contract: - map outbound allow/block to `network.egress.default`; - map local-network intent to `network.ingress.default` and From f0c88d5e7feabec280f7a8b775816fde3c1bc12a Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 11:42:21 -0700 Subject: [PATCH 37/41] fix: restore MicroVM SDK and NVX feature wiring Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- sdk/node/README.md | 9 +++++---- sdk/node/src/types.ts | 8 ++++++-- sdk/node/tests/unit/sandbox.test.ts | 2 +- src/backends/nvx/runner/Cargo.toml | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/sdk/node/README.md b/sdk/node/README.md index 2cf4443e3..899b2154e 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -295,7 +295,7 @@ console.log(result.stdout);
Table of all backends and links to per-backend guides — click to expand. -`SandboxPolicy` is cross-platform. The backend is selected by the second argument to `createConfigFromPolicy(policy, containment)`. Pass an **abstract intent** (`"process"`, `"vm"`) whenever possible — the SDK and native binary resolve it to the right concrete backend for the host. Pass a **concrete backend name** when you need a specific runner. +`SandboxPolicy` is cross-platform. The backend is selected by the second argument to `createConfigFromPolicy(policy, containment)`. Pass an **abstract intent** (`"process"`, `"vm"`, or the compatibility-preserved `"microvm"` public identity) whenever possible — the SDK and native binary resolve it to the right concrete backend for the host. Pass a **concrete backend name** when you need a specific runner. | Backend | Intent | Platforms | Minimum schema | Stable? | Guide | | --- | --- | --- | --- | --- | --- | @@ -304,14 +304,15 @@ console.log(result.stdout); | `lxc` | (concrete only) | Linux | `0.6.0-alpha` | ✅ | [`docs/lxc-support/lxc-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/lxc-support/lxc-backend.md) | | `seatbelt` | `process` | macOS | `0.7.0-alpha` | ✅ | [`docs/seatbelt/seatbelt-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/seatbelt/seatbelt-backend.md) | | `windows_sandbox` | `vm` | Windows | `0.10.0-alpha` | Experimental | [`docs/windows-sandbox/windows-sandbox.md`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.md) | -| `microvm` | (concrete only) | Windows x64 | `0.10.0-alpha` | Experimental | [`docs/nvx-backend-gaps.md`](https://github.com/microsoft/mxc/blob/main/docs/nvx-backend-gaps.md) — implemented by NVX; structural foundation only; runtime unavailable in Phase 1 | +| `microvm` | `microvm` | Windows x64 | `0.10.0-alpha` | Experimental | [`docs/nvx-backend-gaps.md`](https://github.com/microsoft/mxc/blob/main/docs/nvx-backend-gaps.md) — public MicroVM identity implemented internally by NVX; structural foundation only; runtime unavailable in Phase 1 | | `hyperlight` | (concrete only) | Windows x64 / Linux x64 | `0.10.0-alpha` | Experimental | No dedicated guide | | `wslc` | (concrete only) | Windows | `0.9.0-alpha` | Stable | [`docs/wsl/wsl-container-getting-started.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wsl-container-getting-started.md) | | `isolation_session` | (concrete only) | Windows | `0.9.0-alpha` | Stable | [`docs/isolation-session/oneshot.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) | The abstract `process` intent therefore requires `0.7.0-alpha` on macOS, where it resolves to Seatbelt, but retains the `0.6.0-alpha` floor on Windows -and Linux. The abstract `vm` intent requires `0.10.0-alpha`. +and Linux. The abstract `vm` intent and the compatibility-preserved `microvm` +identity require `0.10.0-alpha`. Experimental backends require `{ experimental: true }` in `SandboxSpawnOptions`: @@ -545,7 +546,7 @@ granting file content reads. It requires a BaseContainer host with PSEC 1.1 | --- | --- | --- | | `MXC is not supported on this platform` | `getPlatformSupport()` returned `isSupported: false`. On Linux, neither LXC nor a usable Bubblewrap 0.5.0+ installation is available. On macOS, the Seatbelt platform probe could not find `/usr/bin/sandbox-exec`. | Inspect `support.reason`. On Linux, also inspect `support.unavailableReasons` and install LXC or Bubblewrap 0.5.0+. On macOS, verify that `/usr/bin/sandbox-exec` exists; its absence indicates an incomplete or unsupported macOS installation. | | `wxc-exec.exe not found` / `lxc-exec not found` | The SDK couldn't locate the native binary. | Set `MXC_BIN_DIR=` so `//wxc-exec.exe` (or `lxc-exec`) exists, or pass `options.executablePath` explicitly. | -| `Invalid containment value ''` | `containment` field doesn't match the parser's accepted values. | Use one of the abstract intents (`process`, `vm`) or a concrete backend listed in [Choosing a Backend](#choosing-a-backend). | +| `Invalid containment value ''` | `containment` field doesn't match the parser's accepted values. | Use one of the public intents (`process`, `vm`, `microvm`) or a concrete backend listed in [Choosing a Backend](#choosing-a-backend). | | `'' containment requires experimental mode` | A `windows_sandbox` / `microvm` / `hyperlight` backend was selected without the flag. | Pass `{ experimental: true }` in `SandboxSpawnOptions`. | | `process.commandLine starts with an unquoted Windows path containing a space` | `wxc-exec` rejects unquoted paths with spaces at parse time. | Quote the executable: `'"C:\\Program Files\\…\\foo.exe" args'`. | | `CreateProcessW(PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT) failed: ...` | The process security environment launch returned an OS-level error. Backend-unavailable failures automatically fall through to an AppContainer tier during selection. | Check the Windows build requirements for the backend you selected. | diff --git a/sdk/node/src/types.ts b/sdk/node/src/types.ts index a89260f8c..01e5c7858 100644 --- a/sdk/node/src/types.ts +++ b/sdk/node/src/types.ts @@ -64,11 +64,15 @@ export interface LifecycleConfig { * - "vm": full hardware-virtualised VM isolation. Resolves to * `windows_sandbox` on Windows; no concrete VM backend exists on other * platforms today. + * - "microvm": the public MicroVM identity. It is retained as an intent for + * source and runtime compatibility while NVX remains its internal + * implementation. The same wire value also appears in + * {@link ContainmentBackend} because it directly selects that backend. * Concrete-only backends (such as `"wslc"`) live on * {@link ContainmentBackend} until there is a meaningful abstraction over * multiple implementations of the same kind. */ -export type ContainmentType = 'process' | 'vm'; +export type ContainmentType = 'process' | 'vm' | 'microvm'; /** * Runtime list of {@link ContainmentType} values. Kept in sync with the @@ -76,7 +80,7 @@ export type ContainmentType = 'process' | 'vm'; * abstract intents at run time (the union itself only exists at compile * time). */ -export const ContainmentTypes: readonly ContainmentType[] = ['process', 'vm']; +export const ContainmentTypes: readonly ContainmentType[] = ['process', 'vm', 'microvm']; /** * Deprecated containment wire values, mapped to their canonical diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 4b15d439b..424522aa5 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -80,7 +80,7 @@ describe('exact-version network authoring', () => { describe('containment exports', () => { it('exposes microvm and not the internal nvx implementation name', () => { - assert.deepStrictEqual(ContainmentTypes, ['process', 'vm']); + assert.deepStrictEqual(ContainmentTypes, ['process', 'vm', 'microvm']); assert.deepStrictEqual(ExperimentalBackends, ['microvm', 'windows_sandbox', 'hyperlight']); }); }); diff --git a/src/backends/nvx/runner/Cargo.toml b/src/backends/nvx/runner/Cargo.toml index 5abf46424..59339f05e 100644 --- a/src/backends/nvx/runner/Cargo.toml +++ b/src/backends/nvx/runner/Cargo.toml @@ -7,7 +7,7 @@ description = "NVX backend preflight checks for incomplete runtime builds" [features] default = [] -nvx = [] +nvx = ["nvx_binaries/nvx"] [dependencies] nvx_binaries = { workspace = true } From 200eb0e092cb4fbbc843cfb4356aa7ab13b90fed Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 11:55:29 -0700 Subject: [PATCH 38/41] fix: align MicroVM SDK intent and payload Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../mxc-state-aware-sandbox-api-overview.md | 2 +- sdk/node/README.md | 2 +- sdk/node/src/sandbox.ts | 17 +++++++---- sdk/node/tests/unit/sandbox.test.ts | 28 +++++++++++++++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md index 97cceca7f..cb63d98c9 100644 --- a/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md +++ b/docs/state-aware-lifecycle/mxc-state-aware-sandbox-api-overview.md @@ -20,7 +20,7 @@ function signatures throughout. One-line summaries; full definitions live in | Type | Where | Role | |---|---|---| -| `ContainmentType` / `ContainmentBackend` | `sdk/node/src/types.ts` | Two-tier containment names: `ContainmentType` for abstract intents (`'process' \| 'vm'`); `ContainmentBackend` for concrete runners (`'processcontainer' \| 'windows_sandbox' \| 'lxc' \| 'wslc' \| 'microvm' \| 'hyperlight' \| 'seatbelt' \| 'isolation_session'`). Wire `containment` accepts either. The deprecated alias `SandboxingMethod = ContainmentType \| ContainmentBackend` is retained for back-compat. | +| `ContainmentType` / `ContainmentBackend` | `sdk/node/src/types.ts` | Two-tier containment names: `ContainmentType` for public intents (`'process' \| 'vm' \| 'microvm'`); `ContainmentBackend` for concrete runners (`'processcontainer' \| 'windows_sandbox' \| 'lxc' \| 'wslc' \| 'microvm' \| 'hyperlight' \| 'seatbelt' \| 'isolation_session'`). The compatibility-preserved `microvm` wire value appears in both because it is the public MicroVM identity and directly selects that backend; NVX remains internal. Wire `containment` accepts either. The deprecated alias `SandboxingMethod = ContainmentType \| ContainmentBackend` is retained for back-compat. | | `ProcessConfig` | `sdk/node/src/types.ts` | Per-process settings: `commandLine`, `cwd`, `env`, `timeout`. Reused inside state-aware exec Configs. | | `FilesystemConfig`, `NetworkConfig`, `UiConfig` | `sdk/node/src/types.ts` | Wire-format-aligned cross-cutting interfaces. Reused inline as field types inside the per-(backend, phase) state-aware Configs. | | `SandboxSpawnOptions` | `sdk/node/src/sandbox.ts` | Existing options bag (debug, dryRun, logDir, executablePath, ptyOptions, usePty, experimental). State-aware reuses it as the third positional arg, extended with `signal?: AbortSignal` for cancellation. | diff --git a/sdk/node/README.md b/sdk/node/README.md index 899b2154e..792acc0b5 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -200,7 +200,7 @@ const config = createConfigFromPolicy( network: { allowOutbound: true }, timeoutMs: 30_000, }, - 'process', // intent: "process" | "vm" + 'process', // intent: "process" | "vm" | "microvm" ); // Add the script and any backend-specific runtime settings on the returned config. diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index 53318df7c..4f96a92fa 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -421,6 +421,9 @@ export function createConfigFromPolicy( 'ProcessContainer backend.' ); } + if (containment === 'microvm' && policy.ui !== undefined) { + throw new Error('SandboxPolicy.ui is not supported by the MicroVM backend.'); + } config.filesystem = { readwritePaths: [...(policy.filesystem?.readwritePaths ?? [])], @@ -435,12 +438,14 @@ export function createConfigFromPolicy( }; } - // SandboxPolicy defaults are fail-closed, so omission still emits lockdown. - config.ui = { - disable: !(policy.ui?.allowWindows ?? false), - clipboard: policy.ui?.clipboard ?? "none", - injection: policy.ui?.allowInputInjection ?? false, - }; + if (containment !== 'microvm') { + // SandboxPolicy defaults are fail-closed, so omission still emits lockdown. + config.ui = { + disable: !(policy.ui?.allowWindows ?? false), + clipboard: policy.ui?.clipboard ?? "none", + injection: policy.ui?.allowInputInjection ?? false, + }; + } if (directionalNetwork) { if ((requiresDirectionalNetwork(policy.version) && diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 424522aa5..95f8c793f 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -581,6 +581,7 @@ describe('buildSandboxPayload', () => { deniedPaths: [], }); assert.deepStrictEqual(payload.network, policy.network); + assert.strictEqual(payload.ui, undefined); assert.strictEqual(payload.processContainer, undefined); assert.strictEqual(payload.lxc, undefined); } finally { @@ -618,6 +619,33 @@ describe('buildSandboxPayload', () => { } }); + it('should reject UI policy for MicroVM', () => { + mockWindows(); + try { + assert.throws( + () => buildSandboxPayload( + 'echo hello', + { + version: '0.10.0-alpha', + ui: { + allowWindows: false, + clipboard: 'none', + allowInputInjection: false, + }, + }, + undefined, + undefined, + 'microvm', + ), + { + message: /SandboxPolicy\.ui is not supported by the MicroVM backend/, + }, + ); + } finally { + restore(); + } + }); + }); describe('WSLC', () => { From 28c573408e6eb949875e511596af42e2e6ed79a9 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 12:07:34 -0700 Subject: [PATCH 39/41] docs: document MicroVM intent compatibility Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/schema.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/schema.md b/docs/schema.md index 871118e48..0b9f3cb7c 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -384,6 +384,7 @@ force a particular backend. |-------|------------| | `"process"` | `processcontainer` on Windows, `bubblewrap` on Linux, `seatbelt` on macOS | | `"vm"` | Full hardware-virtualised VM isolation. Resolves to `windows_sandbox` on Windows. | +| `"microvm"` | Compatibility-preserved public MicroVM identity. The same wire value directly selects the concrete MicroVM backend, implemented internally by NVX. | #### Concrete backends From 6cf7650fc22bad5ca97ecaa72916016f06c4cff8 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Tue, 22 Sep 2026 13:54:50 -0700 Subject: [PATCH 40/41] fix: remove unreachable Rust SDK MicroVM feature Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/core/mxc-sdk/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/mxc-sdk/Cargo.toml b/src/core/mxc-sdk/Cargo.toml index 043232092..de7bd7559 100644 --- a/src/core/mxc-sdk/Cargo.toml +++ b/src/core/mxc-sdk/Cargo.toml @@ -16,8 +16,6 @@ default = [] wslc = ["mxc_engine/wslc"] # IsolationSession backend (Windows only). isolation_session = ["mxc_engine/isolation_session"] -# MicroVM backend foundation implemented by NVX (experimental). -microvm = ["mxc_engine/microvm"] # Exposes the debug-only telemetry test seams to downstream binding tests. test-support = ["wxc_common/test-support"] From f79ea59513b29e4038735ebd1504639483d40ef7 Mon Sep 17 00:00:00 2001 From: Huzaifa Danish Date: Wed, 23 Sep 2026 11:42:00 -0700 Subject: [PATCH 41/41] fix: publish MicroVM in the v0.9 contract Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 9 +- docs/nvx-backend-gaps.md | 26 +++--- docs/schema-codegen.md | 13 +-- docs/schema.md | 7 +- docs/versioning.md | 40 +++++---- schemas/schema-version.json | 12 ++- .../stable/mxc-config.schema.0.9.0-alpha.json | 7 ++ scripts/versioning/check-contract-codegen.js | 89 ++++++++++++++++++- .../tests/check-contract-codegen.test.js | 51 ++++++++++- sdk/node/README.md | 24 +++-- sdk/node/src/generated/v0_9_0_alpha/wire.ts | 2 +- sdk/node/src/sandbox.ts | 2 +- sdk/node/tests/unit/sandbox.test.ts | 26 +++--- .../mxc_config_contract/src/dev/one_shot.rs | 2 +- .../src/published/v0_9_0_alpha/one_shot.rs | 2 + .../tests/v0_9_0_alpha/enums.rs | 12 +++ .../one_shot/invalid/nvx_internal_name.json | 7 ++ .../fixtures/one_shot/valid/microvm.json | 7 ++ .../tests/version_boundaries/containment.rs | 7 +- .../config_contract_adapters/v0_9/one_shot.rs | 1 + .../v0_9/one_shot_tests/microvm.rs | 22 +++++ .../v0_9/one_shot_tests/mod.rs | 1 + src/core/wxc_common/src/config_parser.rs | 4 +- 23 files changed, 303 insertions(+), 70 deletions(-) create mode 100644 src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/invalid/nvx_internal_name.json create mode 100644 src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/valid/microvm.json create mode 100644 src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/microvm.rs diff --git a/README.md b/README.md index a6f364eb6..5a1a9aa5d 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,10 @@ pre-fetched, checksum-verifiable bundle directory for an offline build. The current pin, `v0.1.0-dev.5c86da3dff02`, contains only `openvmm.exe`, the guest kernel, and the guest initramfs, not the workload-image bundle. -The experimental `microvm` containment value is implemented by NVX. Build with -`--with-microvm`; execution currently returns a typed backend-unavailable error, -and capability probes do not advertise MicroVM while the runtime is incomplete. +The exact `0.9.0-alpha` contract exposes the experimental `microvm` containment +value, implemented internally by NVX. Build with `--with-microvm`; execution +currently returns a typed backend-unavailable error, and capability probes do +not advertise MicroVM while the runtime is incomplete. Runtime work remains blocked on NVX-produced distro/runtime EROFS images and scratch image, a proven combined managed-sandbox/virtio-fs contract, and the required WHP runner. @@ -240,7 +241,7 @@ See the [SDK README](sdk/node/README.md) for full API documentation. ## Schema Versions -Released, immutable stable schemas live in [`schemas/stable/`](schemas/stable); the in-progress dev schema (experimental backends, state-aware lifecycle) lives in [`schemas/dev/`](schemas/dev). The current stable and dev versions are tracked canonically in [`schemas/schema-version.json`](schemas/schema-version.json). +Released, revision-locked stable schemas live in [`schemas/stable/`](schemas/stable); the in-progress dev schema (remaining development backends and fields) lives in [`schemas/dev/`](schemas/dev). The current stable and dev versions, plus auditable publication revisions, are tracked canonically in [`schemas/schema-version.json`](schemas/schema-version.json). Pick the latest stable schema for new code on any supported platform. See [docs/versioning.md](docs/versioning.md) for the full versioning design. diff --git a/docs/nvx-backend-gaps.md b/docs/nvx-backend-gaps.md index fe546c467..7c0806058 100644 --- a/docs/nvx-backend-gaps.md +++ b/docs/nvx-backend-gaps.md @@ -6,11 +6,11 @@ The original NVX prototype accepted a real MXC `0.9.0-dev` JSON, validated it against the schema, and adapted supported fields into typed NVX launch plans. The prototype first exercised those plans on Windows through OpenVMM and WHP. -MicroVM remains the sole public MXC backend identity in the active development -contract, selected with `containment: "microvm"`. NVX is the concrete -implementation behind that abstraction. A MicroVM-enabled build returns a -typed backend-unavailable error for execution, and capability probes do not -advertise MicroVM while the runtime is incomplete. +MicroVM remains the sole public MXC backend identity in the published exact +`0.9.0-alpha` one-shot contract, selected with `containment: "microvm"`. NVX is +the concrete implementation behind that abstraction. A MicroVM-enabled build +returns a typed backend-unavailable error for execution, and capability probes +do not advertise MicroVM while the runtime is incomplete. ## Current architecture @@ -84,12 +84,12 @@ them for this backend: ## Schema changes -The exact `0.10.0-alpha` development contract includes -`containment: "microvm"` in its generated schema and TypeScript wire types. The -internal `nvx` implementation name is not accepted as a public containment -value. Published stable schemas remain unchanged. An -`experimental.nvx.provision` section is necessary only if images remain -caller-configurable. +The published exact `0.9.0-alpha` one-shot contract includes +`containment: "microvm"` in its generated schema and TypeScript wire types. +The broader `0.10.0-alpha` development contract accepts the same public value +for forward compatibility. The internal `nvx` implementation name is not +accepted as a public containment value. An `experimental.nvx.provision` +section is necessary only if images remain caller-configurable. Future state-aware provision design: @@ -240,8 +240,8 @@ The current GitHub Copilot CLI builds MXC policy version `0.7.0-alpha` and emits the legacy `allowOutbound`, `allowLocalNetwork`, and `network.proxy` fields. It no longer provides a raw `sandbox.config` passthrough. -Before the CLI can use the MicroVM (NVX) backend, it must migrate its generated policy -to the exact `0.10.0-alpha` development contract: +Before the CLI can use the MicroVM (NVX) backend, it must migrate its generated +policy to the exact published `0.9.0-alpha` contract: - map outbound allow/block to `network.egress.default`; - map local-network intent to `network.ingress.default` and diff --git a/docs/schema-codegen.md b/docs/schema-codegen.md index 036feafce..a9b94a125 100644 --- a/docs/schema-codegen.md +++ b/docs/schema-codegen.md @@ -7,12 +7,15 @@ MXC generates artifacts from exact registered configuration contracts: | Exact `0.9.0-alpha` | `src/core/mxc_config_contract/src/published/v0_9_0_alpha/` | Authoritative closed published contract and versioned TypeScript oracle | | Exact `0.10.0-alpha` | `src/core/mxc_config_contract/src/dev/` | Authoritative closed development contract and versioned TypeScript oracle | -Published schemas under `schemas/stable/` are immutable release artifacts. +Published schemas under `schemas/stable/` are revision-locked release artifacts. `mxc_schema_gen` renders published v0.9 into temporary output so `check-contract-codegen.js` can compare the enforcing Rust model with the committed stable schema and TypeScript oracle. The gate also compares pre-existing stable schemas with the merge base and validates their registry -identities. +identities. A deliberate published-contract amendment must increment that +version's `stableRevisions` entry in `schemas/schema-version.json` exactly once; +an artifact change without the bump, or a bump without an artifact change, +fails validation. ## Sources of truth @@ -23,9 +26,9 @@ and `string_marker!` macros implement `JsonSchema` so deserialization and generated constants cannot drift. `src/core/mxc_config_contract/src/published/v0_9_0_alpha/` defines the exact -published v0.9 contract, including IsolationSession and WSLC one-shot and -state-aware roots. It remains renderable for verification; generation does -not make the stable artifact mutable. +published v0.9 contract, including MicroVM one-shot and IsolationSession and +WSLC one-shot and state-aware roots. It remains renderable for verification; +generation does not by itself authorize a published-contract amendment. `mxc_schema_support` owns shared integer normalization, deterministic root rendering, and TypeScript emission. `mxc_schema_gen` uses those helpers for diff --git a/docs/schema.md b/docs/schema.md index 0b9f3cb7c..aa1eb5b68 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -386,6 +386,11 @@ force a particular backend. | `"vm"` | Full hardware-virtualised VM isolation. Resolves to `windows_sandbox` on Windows. | | `"microvm"` | Compatibility-preserved public MicroVM identity. The same wire value directly selects the concrete MicroVM backend, implemented internally by NVX. | +The one-shot `microvm` value is available in the published exact +`0.9.0-alpha` contract. It remains runtime-experimental and Windows x64-only; +publication does not remove the `--experimental` authorization requirement. +The abstract `vm` intent remains development-only in `0.10.0-alpha`. + #### Concrete backends | Value | Description | @@ -475,7 +480,7 @@ Registered contracts: | `"0.6.0-alpha"` | Published; minimum supported | | `"0.7.0-alpha"` | Published | | `"0.8.0-alpha"` | Published | -| `"0.9.0-alpha"` | Published; current stable | +| `"0.9.0-alpha"` | Published; current stable; includes one-shot `microvm` | | `"0.10.0-alpha"` | Mutable development contract | An absent version, a retired version, or any unregistered spelling such as diff --git a/docs/versioning.md b/docs/versioning.md index 278dd1f3d..658ef8cee 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -92,9 +92,12 @@ mxc/schemas/ └── mxc-config.schema.0.10.0-alpha.json (exact closed development contract) ``` -Retired stable schema files are **kept as immutable historical artifacts** — the -parser simply stops accepting those versions (the supported floor is -`0.6.0-alpha`). Released schemas are never edited or deleted. +Retired stable schema files are **kept as revision-locked historical +artifacts** — the parser simply stops accepting those versions (the supported +floor is `0.6.0-alpha`). Published schemas are never edited by hand or deleted. +An exceptional contract amendment requires an intentional one-step publication +revision bump in `schemas/schema-version.json`, and CI rejects either side of +that change without the other. The development artifact is generated from the exact `mxc_config_contract::dev` model. It describes all eight closed one-shot and @@ -106,9 +109,11 @@ contract registered for the declared version. Corpus validation selects the exact registered schema from each document's `version`. Only the v0.10 file under `schemas/dev/` is a generated development artifact. -Published v0.9 is represented by its exact Rust contract and immutable stable -schema. Exact fixtures and adapter/runtime tests remain ordinary mutable tests -so they can gain regression coverage as implementations evolve. See +Published v0.9 is represented by its exact Rust contract and revision-locked +stable schema. Its second publication revision adds the experimental one-shot +`microvm` value while preserving NVX as an internal implementation detail. +Exact fixtures and adapter/runtime tests remain ordinary mutable tests so they +can gain regression coverage as implementations evolve. See [Schema Code Generation](schema-codegen.md) for the regeneration commands and independent drift/history gates. @@ -188,10 +193,11 @@ published JSON contract. ### Trust boundary vs schema defaults -Schemas in `stable/` are immutable: they document the input shape that was -promised at release. They are **not** authoritative for runtime security -defaults. `wxc-exec` is the trust boundary and may apply stricter defaults -than a stable schema declares when a security issue requires it. +Schemas in `stable/` are revision-locked: they document the input shape +promised by a specific publication revision. They are **not** authoritative +for runtime security defaults. `wxc-exec` is the trust boundary and may apply +stricter defaults than a stable schema declares when a security issue requires +it. For example, an older stable schema may declare `network.defaultPolicy` defaulting to `"allow"`. The runtime may treat an @@ -212,7 +218,8 @@ features which still require authorization; per-feature gating is under consideration. **Rules:** -- **Published contract contents** — shipped, stable, and immutable. +- **Published contract contents** — shipped, stable, and revision-locked. + Exceptional amendments require an explicit publication revision increment. - **Development contract contents** — mutable fields and roots at their permanent locations. Inclusion does not imply runtime authorization. - **Promotion:** When a feature is ready to ship, include it in the published @@ -223,8 +230,10 @@ consideration. `scripts/versioning/check-contract-codegen.js` compares every stable schema present at the merge base with the current tree. A published schema cannot be -changed or removed. New stable schemas are allowed because they have no -merge-base predecessor. +removed. It can change only when its positive integer in +`schema-version.json::stableRevisions` increments by exactly one in the same +change; a revision-only bump also fails. New stable schemas are allowed because +they have no merge-base predecessor. The same gate requires exactly one development contract and verifies that every supported stable schema has a published registry entry with the same @@ -337,8 +346,9 @@ fn run(&mut self, request: &ExecutionRequest, logger: &mut Logger) -> ScriptResp `normalize_common_request_ir` domain normalization 3. Remove the `if request.experimental_enabled` guard 4. Bump the minor version -5. Preserve every published contract unchanged; older contracts continue to - reject the field structurally. +5. Preserve every published contract unchanged unless an exceptional amendment + follows the explicit publication-revision process; older contracts continue + to reject the field structurally. Backend-section validation does not move during promotion: an experimental backend's exact-contract section already occupies its permanent top-level diff --git a/schemas/schema-version.json b/schemas/schema-version.json index da9982021..4cf1cf0d9 100644 --- a/schemas/schema-version.json +++ b/schemas/schema-version.json @@ -1,9 +1,17 @@ { - "$comment": "Canonical source of truth for MXC CONFIG SCHEMA versions. min/maxSupported define exact parser and SDK support; stableLatest is the newest immutable schema; stateAware is the IsolationSession lifecycle version; stateAwareWindowsSandbox and stateAwareWslc are the independently versioned backend lifecycle defaults.", + "$comment": "Canonical source of truth for MXC CONFIG SCHEMA versions. min/maxSupported define exact parser and SDK support; stableLatest is the newest published schema; stableRevisions makes deliberate published-contract amendments auditable while preserving history checks; stateAware is the IsolationSession lifecycle version; stateAwareWindowsSandbox and stateAwareWslc are the independently versioned backend lifecycle defaults.", "min": "0.6.0-alpha", "maxSupported": "0.10.0-alpha", "stateAware": "0.9.0-alpha", "stateAwareWindowsSandbox": "0.10.0-alpha", "stateAwareWslc": "0.9.0-alpha", - "stableLatest": "0.9.0-alpha" + "stableLatest": "0.9.0-alpha", + "stableRevisions": { + "0.4.0-alpha": 1, + "0.5.0-alpha": 1, + "0.6.0-alpha": 1, + "0.7.0-alpha": 1, + "0.8.0-alpha": 1, + "0.9.0-alpha": 2 + } } diff --git a/schemas/stable/mxc-config.schema.0.9.0-alpha.json b/schemas/stable/mxc-config.schema.0.9.0-alpha.json index 9e4455748..18d6833c2 100644 --- a/schemas/stable/mxc-config.schema.0.9.0-alpha.json +++ b/schemas/stable/mxc-config.schema.0.9.0-alpha.json @@ -741,6 +741,13 @@ ], "type": "string" }, + { + "description": "Microvm", + "enum": [ + "microvm" + ], + "type": "string" + }, { "description": "IsolationSession", "enum": [ diff --git a/scripts/versioning/check-contract-codegen.js b/scripts/versioning/check-contract-codegen.js index e0e318546..42da11a5d 100644 --- a/scripts/versioning/check-contract-codegen.js +++ b/scripts/versioning/check-contract-codegen.js @@ -79,14 +79,81 @@ function stableSchemaVersion(path) { null; } -function validateStableHistory(baseSchemas, currentSchemas) { +function stableRevision(revisions, version) { + const revision = revisions?.[version] ?? 1; + return Number.isSafeInteger(revision) && revision > 0 ? revision : null; +} + +function validateStableHistory( + baseSchemas, + currentSchemas, + baseRevisions = {}, + currentRevisions = {} +) { const errors = []; + const changedVersions = new Set(); for (const [path, before] of baseSchemas) { const after = currentSchemas.get(path); if (after === undefined) { errors.push(`stable schema was removed: ${path}`); } else if (normalize(before) !== normalize(after)) { - errors.push(`stable schema changed after publication: ${path}`); + const version = stableSchemaVersion(path); + const beforeRevision = stableRevision(baseRevisions, version); + const afterRevision = stableRevision(currentRevisions, version); + if (beforeRevision === null || afterRevision === null) { + errors.push(`stable schema ${path} has an invalid publication revision`); + } else if (afterRevision !== beforeRevision + 1) { + errors.push( + `stable schema changed after publication without incrementing its ` + + `publication revision exactly once: ${path} ` + + `(${beforeRevision} -> ${afterRevision})` + ); + } else { + changedVersions.add(version); + } + } + } + + for (const [version, after] of Object.entries(currentRevisions ?? {})) { + const before = stableRevision(baseRevisions, version); + const current = stableRevision(currentRevisions, version); + if (before === null || current === null) { + errors.push(`stable schema ${version} has an invalid publication revision`); + } else if (current !== before && !changedVersions.has(version)) { + errors.push( + `stable schema publication revision changed without an artifact ` + + `amendment: ${version} (${before} -> ${after})` + ); + } + } + return errors; +} + +function validateStableRevisions(stableSchemas, revisions) { + const errors = []; + if ( + revisions === null || + typeof revisions !== "object" || + Array.isArray(revisions) + ) { + return ["schema-version.json stableRevisions must be an object"]; + } + + const schemaVersions = new Set( + [...stableSchemas.keys()].map(stableSchemaVersion).filter(Boolean) + ); + for (const version of schemaVersions) { + if (stableRevision(revisions, version) === null || !(version in revisions)) { + errors.push( + `stable schema ${version} has no positive integer publication revision` + ); + } + } + for (const version of Object.keys(revisions)) { + if (!schemaVersions.has(version)) { + errors.push( + `publication revision ${version} has no matching stable schema` + ); } } return errors; @@ -185,11 +252,26 @@ function validatePublishedHistory(registry) { }) ); const currentSchemas = currentStableSchemas(); + const baseSchemaVersionsText = readFileAtCommit( + repoRoot, + commit, + "schemas/schema-version.json" + ); + if (baseSchemaVersionsText === null) { + fail(`could not read schemas/schema-version.json at ${commit}`); + } + const baseSchemaVersions = JSON.parse(baseSchemaVersionsText); const schemaVersions = JSON.parse( readFileSync(join(repoRoot, "schemas", "schema-version.json"), "utf8") ); const errors = [ - ...validateStableHistory(baseSchemas, currentSchemas), + ...validateStableRevisions(currentSchemas, schemaVersions.stableRevisions), + ...validateStableHistory( + baseSchemas, + currentSchemas, + baseSchemaVersions.stableRevisions, + schemaVersions.stableRevisions + ), ...validatePublishedRegistry(registry, currentSchemas, schemaVersions.min), ]; if (errors.length > 0) { @@ -469,6 +551,7 @@ module.exports = { validateFixtures, validatePublishedRegistry, validateStableHistory, + validateStableRevisions, }; if (require.main === module) { try { diff --git a/scripts/versioning/tests/check-contract-codegen.test.js b/scripts/versioning/tests/check-contract-codegen.test.js index ae8ec4596..6f41c4793 100644 --- a/scripts/versioning/tests/check-contract-codegen.test.js +++ b/scripts/versioning/tests/check-contract-codegen.test.js @@ -19,6 +19,7 @@ const { validateFixtures, validatePublishedRegistry, validateStableHistory, + validateStableRevisions, } = require("../check-contract-codegen.js"); const roots = ["OneShotRequest", "WindowsSandboxProvisionRequest", @@ -200,7 +201,7 @@ test("stable schema history allows initial publication", () => { assert.deepEqual(validateStableHistory(base, unchanged), []); }); -test("stable schema history rejects mutation and removal", () => { +test("stable schema history requires an explicit publication revision", () => { const base = new Map([ ["schemas/stable/mxc-config.schema.0.8.0-alpha.json", "{\"v\":1}\r\n"], ]); @@ -212,7 +213,27 @@ test("stable schema history rejects mutation and removal", () => { ["schemas/stable/mxc-config.schema.0.8.0-alpha.json", "{\"v\":2}\n"], ]) ).join("\n"), - /changed after publication/ + /changed after publication without incrementing its publication revision/ + ); + assert.deepEqual( + validateStableHistory( + base, + new Map([ + ["schemas/stable/mxc-config.schema.0.8.0-alpha.json", "{\"v\":2}\n"], + ]), + { "0.8.0-alpha": 1 }, + { "0.8.0-alpha": 2 } + ), + [] + ); + assert.match( + validateStableHistory( + base, + base, + { "0.8.0-alpha": 1 }, + { "0.8.0-alpha": 2 } + ).join("\n"), + /publication revision changed without an artifact amendment/ ); assert.match( validateStableHistory(base, new Map()).join("\n"), @@ -220,6 +241,32 @@ test("stable schema history rejects mutation and removal", () => { ); }); +test("stable publication revisions cover exactly the stable artifacts", () => { + const stable = new Map([ + ["schemas/stable/mxc-config.schema.0.8.0-alpha.json", "{}"], + ["schemas/stable/mxc-config.schema.0.9.0-alpha.json", "{}"], + ]); + assert.deepEqual( + validateStableRevisions(stable, { + "0.8.0-alpha": 1, + "0.9.0-alpha": 2, + }), + [] + ); + assert.match( + validateStableRevisions(stable, { "0.8.0-alpha": 1 }).join("\n"), + /0\.9\.0-alpha has no positive integer publication revision/ + ); + assert.match( + validateStableRevisions(stable, { + "0.8.0-alpha": 1, + "0.9.0-alpha": 2, + "1.0.0-alpha": 1, + }).join("\n"), + /1\.0\.0-alpha has no matching stable schema/ + ); +}); + test("CLI failures use the normal contract-codegen diagnostic", () => { const output = formatFailure(new Error("could not parse stable schema")); assert.equal( diff --git a/sdk/node/README.md b/sdk/node/README.md index 369c6d968..94d23c6a2 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -65,14 +65,20 @@ Node.js 26.8.0 or later is recommended. | `0.6.0-alpha` | Stable (minimum supported) | [`schemas/stable/mxc-config.schema.0.6.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.6.0-alpha.json) | | `0.7.0-alpha` | Stable | [`schemas/stable/mxc-config.schema.0.7.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.7.0-alpha.json) | | `0.8.0-alpha` | Stable | [`schemas/stable/mxc-config.schema.0.8.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.8.0-alpha.json) | -| `0.9.0-alpha` | Stable (includes IsolationSession and WSLC one-shot and state-aware lifecycle) | [`schemas/stable/mxc-config.schema.0.9.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.9.0-alpha.json) | +| `0.9.0-alpha` | Stable (includes MicroVM one-shot plus IsolationSession and WSLC one-shot and state-aware lifecycle) | [`schemas/stable/mxc-config.schema.0.9.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/stable/mxc-config.schema.0.9.0-alpha.json) | | `0.10.0-alpha` | Dev (remaining experimental backends and development fields) | [`schemas/dev/mxc-config.schema.0.10.0-alpha.json`](https://github.com/microsoft/mxc/blob/main/schemas/dev/mxc-config.schema.0.10.0-alpha.json) | -Pick `0.9.0-alpha` for new code using current stable backends. Windows Sandbox, -MicroVM, and Hyperlight require `0.10.0-alpha`; Seatbelt requires `0.7.0-alpha` -or later. +Pick `0.9.0-alpha` for new code using current stable backends or the +experimental MicroVM one-shot backend. Windows Sandbox and Hyperlight require +`0.10.0-alpha`; Seatbelt requires `0.7.0-alpha` or later. -> **Stable schemas document only the non-experimental surface.** Experimental backends (`windows_sandbox`, `microvm`, `hyperlight`) and their permanent backend sections are defined by the mutable development contract. IsolationSession and WSLC, including their state-aware lifecycles, are part of exact v0.9 and do not require `--experimental`. Production executors dispatch through the exact contract selected by the declared version, whose adapter normalizes it into the private runtime input. +> **Contract publication and runtime authorization are separate.** The +> published v0.9 contract includes the experimental `microvm` one-shot wire +> value, while `windows_sandbox`, `vm`, and `hyperlight` remain in the mutable +> v0.10 development contract. MicroVM still requires `--experimental`; +> IsolationSession and WSLC, including their state-aware lifecycles, do not. +> Production executors dispatch through the exact contract selected by the +> declared version, whose adapter normalizes it into the private runtime input. > **Network host allow/block lists are not implemented on Windows.** Exact > v0.9/v0.10 requests use `network.egress` / `network.ingress` for directional @@ -302,15 +308,15 @@ console.log(result.stdout); | `lxc` | (concrete only) | Linux | `0.6.0-alpha` | ✅ | [`docs/lxc-support/lxc-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/lxc-support/lxc-backend.md) | | `seatbelt` | `process` | macOS | `0.7.0-alpha` | ✅ | [`docs/seatbelt/seatbelt-backend.md`](https://github.com/microsoft/mxc/blob/main/docs/seatbelt/seatbelt-backend.md) | | `windows_sandbox` | `vm` | Windows | `0.10.0-alpha` | Experimental | [`docs/windows-sandbox/windows-sandbox.md`](https://github.com/microsoft/mxc/blob/main/docs/windows-sandbox/windows-sandbox.md) | -| `microvm` | `microvm` | Windows x64 | `0.10.0-alpha` | Experimental | [`docs/nvx-backend-gaps.md`](https://github.com/microsoft/mxc/blob/main/docs/nvx-backend-gaps.md) — public MicroVM identity implemented internally by NVX; structural foundation only; runtime unavailable in Phase 1 | +| `microvm` | `microvm` | Windows x64 | `0.9.0-alpha` | Experimental | [`docs/nvx-backend-gaps.md`](https://github.com/microsoft/mxc/blob/main/docs/nvx-backend-gaps.md) — public MicroVM identity implemented internally by NVX; structural foundation only; runtime unavailable in Phase 1 | | `hyperlight` | (concrete only) | Windows x64 / Linux x64 | `0.10.0-alpha` | Experimental | No dedicated guide | | `wslc` | (concrete only) | Windows | `0.9.0-alpha` | Stable | [`docs/wsl/wsl-container-getting-started.md`](https://github.com/microsoft/mxc/blob/main/docs/wsl/wsl-container-getting-started.md) | | `isolation_session` | (concrete only) | Windows | `0.9.0-alpha` | Stable | [`docs/isolation-session/oneshot.md`](https://github.com/microsoft/mxc/blob/main/docs/isolation-session/oneshot.md) | The abstract `process` intent therefore requires `0.7.0-alpha` on macOS, where it resolves to Seatbelt, but retains the `0.6.0-alpha` floor on Windows -and Linux. The abstract `vm` intent and the compatibility-preserved `microvm` -identity require `0.10.0-alpha`. +and Linux. The compatibility-preserved `microvm` identity requires +`0.9.0-alpha`; the abstract `vm` intent requires `0.10.0-alpha`. Experimental backends require `{ experimental: true }` in `SandboxSpawnOptions`: @@ -554,7 +560,7 @@ granting file content reads. It requires a BaseContainer host with PSEC 1.1 | Process exits `-1` / `4294967295` with no stdout | Native binary terminated abnormally. | Re-run with `options.debug: true` (or `options.logDir: ''`) to capture diagnostic logs. | | `Policy version '' is older than supported` / `newer than supported` | Version is outside the supported version lines. | Use an exact registered version: `0.6.0-alpha`, `0.7.0-alpha`, `0.8.0-alpha`, `0.9.0-alpha`, or `0.10.0-alpha`. See [Compatibility](#compatibility). | | `Policy version '' is not a registered schema contract` / `Unsupported contract version` | The declaration is not registered, even if it falls between supported versions (for example, `0.6.1-alpha`). | Use an exact version from [Compatibility](#compatibility); IsolationSession and WSLC state-aware requests use `0.9.0-alpha`, while Windows Sandbox uses `0.10.0-alpha`. | -| `Schema does not support containment ''` | The selected backend was introduced after the declared schema version. | Use the backend's minimum version from [Choosing a Backend](#choosing-a-backend). Seatbelt requires `0.7.0-alpha`; IsolationSession and WSLC require `0.9.0-alpha`; Windows Sandbox, MicroVM, and Hyperlight require `0.10.0-alpha`. | +| `Schema does not support containment ''` | The selected backend was introduced after the declared schema version. | Use the backend's minimum version from [Choosing a Backend](#choosing-a-backend). Seatbelt requires `0.7.0-alpha`; IsolationSession, WSLC, and MicroVM require `0.9.0-alpha`; Windows Sandbox and Hyperlight require `0.10.0-alpha`. | For backend-specific errors, see the per-backend guide linked from the [Choosing a Backend](#choosing-a-backend) table. diff --git a/sdk/node/src/generated/v0_9_0_alpha/wire.ts b/sdk/node/src/generated/v0_9_0_alpha/wire.ts index e32d76aed..5da863275 100644 --- a/sdk/node/src/generated/v0_9_0_alpha/wire.ts +++ b/sdk/node/src/generated/v0_9_0_alpha/wire.ts @@ -354,7 +354,7 @@ export interface NetworkRule { export type NonEmptyString = string; -export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "isolation_session" | "wslc"; +export type OneShotContainment = "process" | "processcontainer" | "appcontainer" | "lxc" | "bubblewrap" | "seatbelt" | "macos_sandbox" | "microvm" | "isolation_session" | "wslc"; /** * A complete one-shot `0.9.0-alpha` configuration request. diff --git a/sdk/node/src/sandbox.ts b/sdk/node/src/sandbox.ts index 9d2d8e1fb..ceaa10851 100644 --- a/sdk/node/src/sandbox.ts +++ b/sdk/node/src/sandbox.ts @@ -119,9 +119,9 @@ function validateContainmentVersion( ? '0.7.0-alpha' : effectiveContainment === 'isolation_session' || effectiveContainment === 'wslc' + || effectiveContainment === 'microvm' ? '0.9.0-alpha' : effectiveContainment === 'vm' || - effectiveContainment === 'microvm' || effectiveContainment === 'windows_sandbox' || effectiveContainment === 'hyperlight' ? '0.10.0-alpha' diff --git a/sdk/node/tests/unit/sandbox.test.ts b/sdk/node/tests/unit/sandbox.test.ts index 95f8c793f..953d6d655 100644 --- a/sdk/node/tests/unit/sandbox.test.ts +++ b/sdk/node/tests/unit/sandbox.test.ts @@ -249,7 +249,7 @@ describe('buildSandboxPayload', () => { } }); - it('should enforce the minimum schema for every development-only containment', () => { + it('should enforce the minimum schema for each versioned containment', () => { mockWindows(); try { for (const containment of [ @@ -261,7 +261,9 @@ describe('buildSandboxPayload', () => { 'isolation_session', ] as const) { const minimumVersion = - containment === 'isolation_session' || containment === 'wslc' + containment === 'isolation_session' || + containment === 'wslc' || + containment === 'microvm' ? '0.9.0-alpha' : '0.10.0-alpha'; assert.throws( @@ -278,7 +280,7 @@ describe('buildSandboxPayload', () => { } }); - it('should accept each development containment at its exact boundary', () => { + it('should accept each versioned containment at its exact boundary', () => { mockWindows(); try { for (const containment of [ @@ -290,7 +292,9 @@ describe('buildSandboxPayload', () => { 'isolation_session', ] as const) { const version = - containment === 'isolation_session' || containment === 'wslc' + containment === 'isolation_session' || + containment === 'wslc' || + containment === 'microvm' ? '0.9.0-alpha' : '0.10.0-alpha'; try { @@ -299,7 +303,7 @@ describe('buildSandboxPayload', () => { assert.doesNotMatch( (error as Error).message, /Schema .* does not support containment/, - `${containment} must pass the 0.9 schema floor before backend-specific validation`, + `${containment} must pass its ${version} schema floor before backend-specific validation`, ); } } @@ -549,7 +553,7 @@ describe('buildSandboxPayload', () => { mockWindows(); try { const policy: SandboxPolicy = { - version: '0.10.0-alpha', + version: '0.9.0-alpha', filesystem: { readonlyPaths: ['C:\\workspace\\source'], readwritePaths: ['C:\\workspace\\output'], @@ -596,7 +600,7 @@ describe('buildSandboxPayload', () => { () => buildSandboxPayload( 'echo hello', { - version: '0.10.0-alpha', + version: '0.9.0-alpha', runtimeConfig: { networkProxy: 'http://127.0.0.1:8080', }, @@ -626,7 +630,7 @@ describe('buildSandboxPayload', () => { () => buildSandboxPayload( 'echo hello', { - version: '0.10.0-alpha', + version: '0.9.0-alpha', ui: { allowWindows: false, clipboard: 'none', @@ -2016,9 +2020,11 @@ describe('resolveExecutableAndArgs (containment validation)', { skip: platformSk function makeConfig(containment: string): ContainerConfig { const version = - containment === 'isolation_session' || containment === 'wslc' + containment === 'isolation_session' || + containment === 'wslc' || + containment === 'microvm' ? '0.9.0-alpha' - : ['microvm', 'vm', 'hyperlight', 'windows_sandbox'].includes(containment) + : ['vm', 'hyperlight', 'windows_sandbox'].includes(containment) ? '0.10.0-alpha' : ['seatbelt', 'macos_sandbox'].includes(containment) ? '0.7.0-alpha' diff --git a/src/core/mxc_config_contract/src/dev/one_shot.rs b/src/core/mxc_config_contract/src/dev/one_shot.rs index 9b9793265..af045b560 100644 --- a/src/core/mxc_config_contract/src/dev/one_shot.rs +++ b/src/core/mxc_config_contract/src/dev/one_shot.rs @@ -26,7 +26,7 @@ string_enum! { /// Select the macOS Seatbelt backend. Seatbelt => ["seatbelt", "macos_sandbox"], - // Development-only values. + // Additional values carried by the development contract. /// Select the host's VM-class containment backend. Vm => ["vm"], /// Select the Windows Sandbox backend. diff --git a/src/core/mxc_config_contract/src/published/v0_9_0_alpha/one_shot.rs b/src/core/mxc_config_contract/src/published/v0_9_0_alpha/one_shot.rs index 2e2b1838a..10172163e 100644 --- a/src/core/mxc_config_contract/src/published/v0_9_0_alpha/one_shot.rs +++ b/src/core/mxc_config_contract/src/published/v0_9_0_alpha/one_shot.rs @@ -25,6 +25,8 @@ string_enum! { Bubblewrap => ["bubblewrap"], /// Select the macOS Seatbelt backend. Seatbelt => ["seatbelt", "macos_sandbox"], + /// Select the MicroVM backend implemented by NVX. + Microvm => ["microvm"], /// Select the Windows IsolationSession backend. IsolationSession => ["isolation_session"], diff --git a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs index 8c392c0c1..3c9b58762 100644 --- a/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs +++ b/src/core/mxc_config_contract/tests/v0_9_0_alpha/enums.rs @@ -12,6 +12,7 @@ fn accepts_every_containment_value() { "lxc", "bubblewrap", "seatbelt", + "microvm", "isolation_session", "wslc", ] { @@ -38,6 +39,17 @@ fn rejects_invalid_containment_value() { ); } +#[test] +fn rejects_internal_nvx_containment_value() { + assert_invalid( + r#"{ + "version": "0.9.0-alpha", + "containment": "nvx", + "process": {"commandLine": "echo"} + }"#, + ); +} + #[test] fn rejects_every_removed_default_network_policy_value() { for default_network_policy in ["allow", "block"] { diff --git a/src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/invalid/nvx_internal_name.json b/src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/invalid/nvx_internal_name.json new file mode 100644 index 000000000..12b59e65c --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/invalid/nvx_internal_name.json @@ -0,0 +1,7 @@ +{ + "version": "0.9.0-alpha", + "containment": "nvx", + "process": { + "commandLine": "echo nvx" + } +} diff --git a/src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/valid/microvm.json b/src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/valid/microvm.json new file mode 100644 index 000000000..d1eb04778 --- /dev/null +++ b/src/core/mxc_config_contract/tests/v0_9_0_alpha/fixtures/one_shot/valid/microvm.json @@ -0,0 +1,7 @@ +{ + "version": "0.9.0-alpha", + "containment": "microvm", + "process": { + "commandLine": "echo microvm" + } +} diff --git a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs index fee1fa3c2..e98f4f593 100644 --- a/src/core/mxc_config_contract/tests/version_boundaries/containment.rs +++ b/src/core/mxc_config_contract/tests/version_boundaries/containment.rs @@ -13,9 +13,14 @@ fn wslc_containment_is_introduced_in_v09() { assert_v09_introduces(r#""containment": "wslc""#); } +#[test] +fn microvm_containment_is_introduced_in_v09() { + assert_v09_introduces(r#""containment": "microvm""#); +} + #[test] fn development_containment_values_are_introduced_in_v010() { - for containment in ["vm", "windows_sandbox", "microvm", "hyperlight"] { + for containment in ["vm", "windows_sandbox", "hyperlight"] { assert_v10_introduces(&format!(r#""containment": "{containment}""#)); } } diff --git a/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot.rs b/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot.rs index b0f09d25d..3189416af 100644 --- a/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot.rs +++ b/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot.rs @@ -14,6 +14,7 @@ fn convert_containment(value: contract::OneShotContainment) -> wire::Containment contract::OneShotContainment::Lxc => wire::Containment::Lxc, contract::OneShotContainment::Bubblewrap => wire::Containment::Bubblewrap, contract::OneShotContainment::Seatbelt => wire::Containment::Seatbelt, + contract::OneShotContainment::Microvm => wire::Containment::Microvm, contract::OneShotContainment::IsolationSession => wire::Containment::IsolationSession, contract::OneShotContainment::Wslc => wire::Containment::Wslc, } diff --git a/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/microvm.rs b/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/microvm.rs new file mode 100644 index 000000000..52eeadb66 --- /dev/null +++ b/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/microvm.rs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{contract, into_common_request_ir, wire}; + +#[test] +fn microvm_maps_to_the_public_wire_identity() { + let request = serde_json::from_str::( + r#"{ + "version": "0.9.0-alpha", + "containment": "microvm", + "process": {"commandLine": "echo hello"} + }"#, + ) + .unwrap(); + + let adapted = into_common_request_ir(request); + assert!(matches!( + adapted.containment, + Some(wire::Containment::Microvm) + )); +} diff --git a/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/mod.rs b/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/mod.rs index c92c2eae2..8d8e99983 100644 --- a/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/mod.rs +++ b/src/core/wxc_common/src/config_contract_adapters/v0_9/one_shot_tests/mod.rs @@ -5,5 +5,6 @@ use super::{contract, into_common_request_ir, wire}; mod common; mod isolation_session; +mod microvm; mod stable_candidate; mod wslc; diff --git a/src/core/wxc_common/src/config_parser.rs b/src/core/wxc_common/src/config_parser.rs index e6f014611..ac36849f8 100644 --- a/src/core/wxc_common/src/config_parser.rs +++ b/src/core/wxc_common/src/config_parser.rs @@ -2275,7 +2275,7 @@ mod tests { #[test] fn containment_microvm_accepted() { let json = r#"{ - "version": "0.10.0-alpha", + "version": "0.9.0-alpha", "process": {"commandLine": "/bin/true"}, "containment": "microvm" }"#; @@ -2290,7 +2290,7 @@ mod tests { #[test] fn containment_nvx_gets_normal_unknown_enum_rejection() { let json = r#"{ - "version": "0.10.0-alpha", + "version": "0.9.0-alpha", "process": {"commandLine": "/bin/true"}, "containment": "nvx" }"#;