Repository navigation
Make nvx-rs the backend of nvx.py sandbox, egress, and artifact lookup - #597
Merged
Pedro Henrique Penna (ppenna) merged 14 commits intoOct 19, 2026
Merged
Conversation
Pedro Henrique Penna (ppenna)
requested review from
Enrique Saurez (esaurez)
and
a balanced review from Copilot
October 17, 2026 08:13
This was referenced Oct 17, 2026
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
The cross-platform runtime and guest-build migration needs human review and complete CI validation, including the currently skipped real-binary integration checks.
1 open issue
What changed in this PR
Moves NVX runtime responsibilities into nvx-rs as part of #359, reducing duplicated Python logic while preserving the command-line contract.
Changes:
- Adds Rust CLI support, compatibility helpers, and cross-platform process handling.
- Integrates Rust guest binaries into builds, provenance, and packaging.
- Updates sandbox restrictions, CI executable handoff, tests, and documentation.
| File | Description |
|---|---|
| THIRD_PARTY_NOTICES.md | Documents Rust guest dependencies and licenses. |
| SOURCE-MANIFEST.json | Records Rust toolchain and guest sources. |
| scripts/test_tool_versions.py | Checks musl target installation. |
| scripts/test_guest_binaries.py | Tests source identity and staged-build validation. |
| scripts/setup/setup-linux-runner.sh | Explicitly selects the target toolchain. |
| scripts/setup/setup-linux-mshv.sh | Installs and verifies the musl target. |
| scripts/nvx_tools/ubuntu.py | Includes Rust sources in provenance. |
| scripts/nvx_tools/sandbox_lifecycle_tests.py | Replaces lifecycle imports and strengthens report checks. |
| scripts/nvx_tools/release.py | Validates Rust source metadata. |
| scripts/nvx_tools/microvm_test_scripts/sandbox-blocks.sh | Checks distro and scratch devices only. |
| scripts/nvx_tools/guest_binaries.py | Adds source-root support and staged-build inspection. |
| scripts/nvx_tools/ci.py | Tracks Rust guest inputs for CI selection. |
| scripts/nvx_tools/build.py | Installs Rust guest binaries. |
| scripts/nvx_tools/build_constants.py | Defines Rust build and packaging inputs. |
| scripts/nvx_tools/benchmark.py | Records Rust guest provenance. |
| scripts/nvx_tools/azurelinux.py | Includes Rust build inputs in provenance. |
| scripts/nvx_tools/archive.py | Excludes generated Rust build outputs. |
| README.md | Explains Rust binaries and first-use builds. |
| nvx-rs/src/protocol/tests.rs | Pins wire values and layered feature requirements. |
| nvx-rs/src/protocol/mod.rs | Documents shared Rust protocol definitions. |
| nvx-rs/src/protocol/features.rs | Defines layered sandbox feature requirements. |
| nvx-rs/src/host/sys/mod.rs | Exposes Unix interrupt handling. |
| nvx-rs/src/host/sys/interrupt.rs | Records Unix interrupts for CLI polling. |
| nvx-rs/src/host/session.rs | Exposes request and response primitives. |
| nvx-rs/src/host/sandbox/layer.rs | Implements distro-only layer parsing. |
| nvx-rs/src/host/python/uuid.rs | Preserves Python UUID parsing behavior. |
| nvx-rs/src/host/python/shell.rs | Implements Python-compatible command quoting. |
| nvx-rs/src/host/python/mod.rs | Organizes Python compatibility helpers. |
| nvx-rs/src/host/python/dumps.rs | Implements Python-compatible JSON output. |
| nvx-rs/src/host/process.rs | Adds CLI process waiting and termination. |
| nvx-rs/src/host/platform/windows.rs | Adds Windows process, interrupt, and lock helpers. |
| nvx-rs/src/host/platform/other.rs | Supplies unsupported-platform interrupt behavior. |
| nvx-rs/src/host/platform/linux.rs | Exposes Linux interrupt helpers. |
| nvx-rs/src/host/mod.rs | Exposes CLI and sandbox modules. |
| nvx-rs/src/host/cli/sandbox/identity.rs | Checks recorded OpenVMM process identities. |
| nvx-rs/src/host/cli/output.rs | Preserves CLI stream formatting. |
| nvx-rs/src/host/cli/egress.rs | Adds egress compilation commands. |
| nvx-rs/src/host/cli/deadline.rs | Adds interruptible deadline handling. |
| nvx-rs/src/host/cli/artifacts.rs | Locates artifacts and emits JSON paths. |
| nvx-rs/src/guest/time/mod.rs | Updates historical implementation comments. |
| nvx-rs/src/guest/time_probe/mod.rs | Updates historical implementation comments. |
| nvx-rs/src/guest/reseed.rs | Clarifies existing behavior in comments. |
| nvx-rs/src/bin/nvx-rs.rs | Adds the CLI entry point. |
| nvx-rs/Cargo.toml | Enables CLI features and dependencies. |
| guest/common/nvx-reseed.c | Removes the replaced C implementation. |
| guest/common/nvx-mmio-write.c | Removes the replaced C implementation. |
| guest/common/nvx-init-agent | Restricts layers and reserves runtime share targets. |
| guest/common/nvx-exit.c | Removes the replaced C implementation. |
| guest/common/nvx-console-pending.c | Removes the replaced C implementation. |
| docker/Dockerfile | Builds and installs shared Rust guest binaries. |
| doc/usage.md | Documents delegation and sandbox restrictions. |
| doc/setup.md | Documents Rust build prerequisites. |
| doc/run.md | Updates sandbox layers and reserved targets. |
| doc/project-structure.md | Describes the Rust-backed repository structure. |
| doc/guests/ubuntu-guest.md | Documents Rust guest installation and provenance. |
| doc/guests/azurelinux-guest.md | Documents Rust guest installation and provenance. |
| doc/contribute.md | Explains runtime ownership and test placement. |
| doc/ci.md | Documents guest caches and executable handoff. |
| doc/build.md | Documents Rust builds and packaging. |
| doc/benchmarks.md | Updates guest-binary provenance documentation. |
| aci_edge_sandboxes/src/openvmm/mod.rs | Documents shared lowering and tests its errors. |
| aci_edge_sandboxes/README.md | Explains egress lowering compatibility changes. |
| .github/workflows/run-nvx-microvm-tests.yml | Downloads the platform’s Rust CLI executable. |
| .github/workflows/copilot-setup-steps.yml | Tracks all Rust source changes. |
| .github/workflows/ci.yml | Builds and distributes platform CLI executables. |
| .github/copilot-instructions.md | Documents Rust prerequisites and rebuild guidance. |
| .github/actions/validate-nvx/action.yml | Removes retired Python test suites. |
| .github/actions/build-guest-artifacts/action.yml | Keys guest caches on Rust source identity. |
| .gitattributes | Marks frozen compatibility fixtures as generated. |
Review effort: Balanced
Pedro Henrique Penna (ppenna)
added a commit
that referenced
this pull request
Oct 17, 2026
Three CLI tests run the real nvx-rs only when NVX_RS names it: the egress compilation of nvx.py run, the JSON that nvx.py reads from nvx-rs, and the options that nvx.py sandbox passes. The NVX CLI jobs built nvx-rs where nvx.py finds it, but set no NVX_RS, so those tests skipped and the executables that the jobs hand to the microVM jobs left unexercised by them (review of #597). Each Build nvx-rs step now exports NVX_RS for the rest of its job, naming the executable that it built: with `>> "${GITHUB_ENV}"` on Linux, and on Windows with `Out-File -Encoding utf8 -Append`, as setup-curl writes GITHUB_ENV under Windows PowerShell. The workflow test checks both, and doc/ci.md says so. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
added a commit
that referenced
this pull request
Oct 17, 2026
The executor validates and hashes the nvx-rs that NVX_RS names before the baseline boot, and nvx.py resolves a relative NVX_RS against its working directory: the executor's, at that point. The scenarios inherit NVX_RS unchanged but run in the repository root, so with a relative NVX_RS they ran another executable than the one validated, or found none. sanitized_environment() now makes NVX_RS absolute against the executor's working directory, so that the scenarios run the executable that the initialization checked. An empty NVX_RS, which nvx.py ignores, passes unchanged. A new test validates a relative NVX_RS in one working directory and resolves it in a scenario process in another; it fails without the fix. The environment test now expects the absolute path. Copilot's review of #598 flagged this. #597 added NVX_RS to the scenarios' environment, and the earlier commit of this series that validates the executor's nvx-rs made the mismatch matter. Closes #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 19, 2026
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-359-delegate
branch
from
October 19, 2026 16:35
403c7e8 to
6f31aa4
Compare
Pedro Henrique Penna (ppenna)
added a commit
that referenced
this pull request
Oct 19, 2026
Three CLI tests run the real nvx-rs only when NVX_RS names it: the egress compilation of nvx.py run, the JSON that nvx.py reads from nvx-rs, and the options that nvx.py sandbox passes. The NVX CLI jobs built nvx-rs where nvx.py finds it, but set no NVX_RS, so those tests skipped and the executables that the jobs hand to the microVM jobs left unexercised by them (review of #597). Each Build nvx-rs step now exports NVX_RS for the rest of its job, naming the executable that it built: with `>> "${GITHUB_ENV}"` on Linux, and on Windows with `Out-File -Encoding utf8 -Append`, as setup-curl writes GITHUB_ENV under Windows PowerShell. The workflow test checks both, and doc/ci.md says so. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-359-delegate
branch
from
October 19, 2026 17:06
6f31aa4 to
910e9e6
Compare
Pedro Henrique Penna (ppenna)
added a commit
that referenced
this pull request
Oct 19, 2026
Three CLI tests run the real nvx-rs only when NVX_RS names it: the egress compilation of nvx.py run, the JSON that nvx.py reads from nvx-rs, and the options that nvx.py sandbox passes. The NVX CLI jobs built nvx-rs where nvx.py finds it, but set no NVX_RS, so those tests skipped and the executables that the jobs hand to the microVM jobs left unexercised by them (review of #597). Each Build nvx-rs step now exports NVX_RS for the rest of its job, naming the executable that it built: with `>> "${GITHUB_ENV}"` on Linux, and on Windows with `Out-File -Encoding utf8 -Append`, as setup-curl writes GITHUB_ENV under Windows PowerShell. The workflow test checks both, and doc/ci.md says so. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-359-delegate
branch
from
October 19, 2026 17:10
910e9e6 to
9273885
Compare
nvx.py sandbox will run nvx-rs sandbox with the options that its own argparse front end read, and its type conversions, such as --layer, --workload-user, and the integer options, move to nvx-rs. A value that nvx-rs then refuses would read "usage: nvx-rs sandbox ..." and "nvx-rs sandbox: error: ...", where nvx.py printed the usage and name of its own parser. A new global option, --prog PROG, names the program in usage, help, and error messages, nvx-rs by default; nvx.py will pass its own name. Each parser now describes its syntax as a Syntax value, its command after the program's name, the usage parts of its options and positionals, and the help after its usage, instead of static usage and help text, and Exit::Usage and Exit::Help carry the text that the syntax formats for the program. format_usage ports argparse's HelpFormatter._format_usage for 80 columns: a usage that fits stays on one line; otherwise a name of at most three quarters of the width is followed by the options, and the positionals start a new line, while a longer name takes a line of its own. For the default name, every usage and help is byte for byte what the static text printed, which the golden scenarios check, and for nvx.py, whose name has the length of nvx-rs, the usage wraps as Python's does. The top-level usage lists the new option, so the eight golden steps whose unrecognized arguments the top-level parser reports, and the generator's copy of that usage, change accordingly. The top-level help describes --prog and wraps the help of --root as argparse wraps it; it had one line of 79 columns. Unit tests compare the wrapping of short and long names, and of usages without options, with CPython 3.14's argparse, and a CLI test checks the usage error of nvx.py sandbox for a root --workload-user, recorded from CPython, a long name, the top-level parser, and help. The README documents the option. This changes the CLI of #596; it adds the option and keeps every default output. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nvx.py will delegate its sandbox command, and the egress policy and artifact steps of its run command, to the nvx-rs CLI (#359). The new module scripts/nvx_tools/nvx_rs.py finds that executable, builds it when it is missing, and runs it, so that the commands that follow only choose what to delegate. Discovery runs, in order: - the executable that the NVX_RS environment variable names, which must exist; - nvx-rs/target/release/nvx-rs[.exe] in the checkout, binary_path(), where a release build puts it; and - that executable after `cargo build --release --locked --features cli --manifest-path nvx-rs/Cargo.toml --target-dir nvx-rs/target`, which runs from the repository root, so that rust-toolchain.toml selects the pinned Rust release, with its target directory pinned against CARGO_TARGET_DIR. The command line goes to standard error, and so does Cargo's standard output, so that a build never mixes with the output of the command that needs the executable. Without Cargo, nvx.py fails with a ScriptError that names NVX_RS, the build command, rustup, and `nvx.py download`, which PR E makes install a release's bin/nvx-rs[.exe] at binary_path(), as it installs OpenVMM where the OpenVMM build puts it. run() runs `nvx-rs --prog PROGRAM --root ROOT ...` on nvx.py's own standard streams, with nvx.py's repository root, and returns the status that nvx.py then exits with: nvx-rs's own, 128 + N for a signal N that ends nvx-rs, as a shell reports it, and a Windows status above 2^31 as the signed value that SystemExit passes on with every Python. Ctrl-C belongs to nvx-rs, which prints Interrupted and exits with 130. While nvx-rs runs, nvx.py replaces its SIGINT handler, so that it neither raises KeyboardInterrupt, prints Interrupted again, nor kills nvx-rs before it finishes, as subprocess.run would after 0.25 s. On POSIX hosts, it hands nvx-rs the SIGINT, SIGTERM, and SIGHUP that reach nvx.py alone, which a terminal's Ctrl-C to the whole group makes a harmless repeat, including a signal that arrives before nvx-rs has started; if SIGINT ends nvx-rs before it can report it, nvx.py prints Interrupted for it. On Windows, Ctrl-C reaches every process of the console, so nvx.py only ignores it. The handlers are installed before nvx-rs starts and restored when it ends. A signal that nvx.py ignores keeps no handler, so that nvx-rs inherits it ignored, rather than at the default that exec leaves in place of a handler: a shell without job control starts a background command with SIGINT ignored, and nohup starts one with SIGHUP ignored. query() runs nvx-rs for a JSON result, with standard input closed and its output captured, and raises a ScriptError with the message that nvx-rs printed after "error: ", so that nvx.py prints what it printed for the Python code that nvx-rs replaces. compile_egress_policy() and artifacts() read `nvx-rs egress compile` and `nvx-rs artifacts` into typed results and refuse JSON of another shape. The policy's path follows `--`, so that a path that starts with `-` stays a path. NvxRsTests covers discovery and its order, the build command, its working directory, and its output, the missing-Cargo and failed-build errors, the command line, the exit statuses, and the JSON readers. Process tests run nvx_rs.run in a fresh Python with a stand-in for nvx-rs: standard streams and statuses pass through, and on POSIX hosts, SIGINT to the process group or to nvx.py alone ends the command with one Interrupted and status 130, and signals that end nvx-rs give a shell's statuses, and nvx-rs starts with the SIGINT and SIGHUP that nvx.py ignores still ignored. Mocks cover an interrupted wait, the restoration of the handlers, an ignored SIGINT that stays ignored, and the `--` before a policy's path. With NVX_RS set, a test checks the JSON of the real nvx-rs against what nvx.py reads, also for a relative policy path that starts with `-`. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nvx.py sandbox now runs `nvx-rs --prog nvx.py --root REPO sandbox` for every operation, with nvx.py's standard streams, and exits with the status of nvx-rs, which ports command_sandbox, the lifecycle of sandbox_lifecycle.py, and the control session of control_session.py (#596). Python no longer checks, converts, or runs anything for the command; the modules that nvx-rs replaced stay for the microVM tests until a later commit moves those onto nvx-rs. argparse stays the front end, so that the help, abbreviations, and the usage errors that it reports (exit 2) work as before, and the help is unchanged byte for byte at 80 and 120 columns. Every option keeps its name, metavar, choices, and help, but none has a type converter or a default: --layer, --workload-user, the integer options, --timeout, and the paths are plain strings that nvx-rs converts and checks, and nvx-rs applies the defaults, so Python duplicates neither. A _SandboxOption action records each option as nvx-rs receives it: - under its canonical name, so that `--dry` reaches nvx-rs as --dry-run, since nvx-rs does not expand abbreviations; - in command-line order, so that each --mount-deny, --mount-allow, and --mount-write follows the --mount whose path it names, which nvx-rs attributes as nvx.py did; and - with its value attached, --option=VALUE, so that a value that starts with "-" stays a value. The operation, run by default, comes first. Only options that the user gave are passed, so that the defaults live in nvx-rs alone. --prog makes the usage errors that nvx-rs reports for the values that it converts read "usage: nvx.py sandbox ..." and "nvx.py sandbox: error: ...", as argparse printed them; the name has the length of nvx-rs, so the usage wraps the same. Usage errors keep their status and form, but which of two errors in one command line is reported changes: argparse reports an invalid choice or an unrecognized argument before nvx-rs sees a value that it refuses, where it used to report whichever came first. Ctrl-C ends the command with nvx-rs's Interrupted and status 130, once. scripts/nvx_rs_fixtures.py recorded the golden fixtures of nvx-rs from the Python implementation of nvx.py sandbox, the egress compiler, and the Python semantics that the ports keep, and imported the helpers that this commit removes from nvx.py. With nvx.py running nvx-rs, the recordings are final, so the generator goes, and the fixtures stay as frozen references that a deliberate change updates by hand. Their metadata, .gitattributes, the README, the module comments that named the generator, and doc/project-structure.md say so. The CLI tests of the Python implementation go, since the golden scenarios of nvx-rs cover each of their cases, as do the systemd, environment, network-option, and policy checks, and the tests of `run` that also ran sandbox keep their `run` half. New tests check the recorded arguments for abbreviations, attached values, order, and the default operation, the call of nvx-rs and its status through main(), the usage errors that argparse keeps, a missing nvx-rs, and, through a fresh nvx.py with a stand-in for nvx-rs, shared standard streams and, on POSIX hosts, one Interrupted and status 130 after Ctrl-C to the process group. With NVX_RS set, the real nvx-rs runs a dry run of options that need an attached value, an abbreviation, and a share policy, a usage error with nvx.py's name, and a lifecycle error. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nvx.py run keeps assembling OpenVMM's command line, but the two steps of it that Python duplicated now go through nvx-rs (#359): - --network-egress-policy-file compiles with `nvx-rs egress compile`, whose JSON lists the native rules of each action in the order in which OpenVMM receives them, in place of compile_policy_file of egress_policy.py. The option checks before it stay in nvx.py, as they belong to run's own options, and a refused policy fails with the compiler's message, as before. - The OpenVMM executable, the kernel, and the initramfs of the guest come from `nvx-rs --root REPO artifacts`, in place of openvmm_binary_path and artifact_path of common.py. nvx-rs prints absolute, unchecked paths of the same files; nvx.py still checks each with require_file, in the same order and with the same messages, and names the guest's distribution as nvx-rs prints it. The lookup runs after the egress policy compiles and before the first check, so a run that fails its checks never needs nvx-rs, and the command lines, outputs, and messages of `run` are unchanged. One visible difference: NVX_OPENVMM, which names another OpenVMM for nvx-rs, now also selects the OpenVMM of nvx.py run, as it does for nvx.py sandbox. The microVM scenarios and the build, release, and benchmark code keep using artifact_path and openvmm_binary_path: they build, package, and measure the checkout's artifacts. Tests of run that pass the option checks look up the checkout's artifacts through a mixin that answers nvx_rs.artifacts without nvx-rs. The egress test checks that run passes the compiler's rules, and with NVX_RS set, that the real nvx-rs lowers the policy as the Python compiler did and refuses a scoped IPv6 network with its message before any artifact lookup. New tests check that a refused policy fails before the lookup, and that run takes the paths that nvx-rs prints, checks them in order with nvx.py's messages, and reports a missing executable as before. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two microVM scenarios still used Python code that nvx-rs replaced (#359), which a later commit deletes: - The L3/L4 policy, port-range, and IPv6 egress scenarios compiled their policy files with compile_policy_file of egress_policy.py to record and check the native rules that `nvx.py run` passes to OpenVMM. They now compile them with `nvx-rs egress compile`, through compile_egress_policy of scripts/nvx_tools/nvx_rs.py, which nvx.py run itself uses, so the scenarios check the rules of the compiler that runs. - The public sandbox lifecycle took the names of the state files and runtime_process_running from sandbox_lifecycle.py. The names are now its own constants, as nvx.py sandbox documents them, and openvmm_running replaces the process check with the scan that the scenario already makes: an OpenVMM process with the recorded ID whose arguments name the scenario's fixture, which no other process names. kill_openvmm takes that name too. The tests of the scenarios compile their cases with nvx-rs, through discovery, as the scenarios do: the check that each port range lowers to one native rule, the IPv6 case table, and the bounded L3/L4 policy. The port-range test, which replaces subprocess.run for the public dry runs, lets nvx-rs's own runs through, and counts only the dry runs. The L3/L4 policy tests, which replace threading.Thread, which subprocess uses on Windows, take their policy's rules from a stub, and a reservation that fails before any policy compiles. The process check of the lifecycle needs no patch of its own, since it follows the scan that the tests already replace, and the kill test ends a real process that a replaced scan reports. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-359-delegate
branch
from
October 19, 2026 17:57
8234946 to
afcbc7e
Compare
Pedro Henrique Penna (ppenna)
added a commit
that referenced
this pull request
Oct 19, 2026
Three CLI tests run the real nvx-rs only when NVX_RS names it: the egress compilation of nvx.py run, the JSON that nvx.py reads from nvx-rs, and the options that nvx.py sandbox passes. The NVX CLI jobs built nvx-rs where nvx.py finds it, but set no NVX_RS, so those tests skipped and the executables that the jobs hand to the microVM jobs left unexercised by them (review of #597). Each Build nvx-rs step now exports NVX_RS for the rest of its job, naming the executable that it built: with `>> "${GITHUB_ENV}"` on Linux, and on Windows with `Out-File -Encoding utf8 -Append`, as setup-curl writes GITHUB_ENV under Windows PowerShell. The workflow test checks both, and doc/ci.md says so. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The managed-lifecycle scenario of `nvx.py test-microvm` boots a guest whose init hands the boot to the managed agent, and drove its control session with control_session.py, the Python client of the protocol that #359 removes. Its checks of the protocol move to an ignored integration test, tests/managed_lifecycle.rs, which drives the session with nvx::host::session, as nvx::host::instance does for nvx-rs sandbox: - A first session pings the agent and runs a workload in /tmp with exactly EMPTY= and an entry with spaces, an equals sign, and a snowman, and the workload prints them back. - A second session reaches the same guest: an explicitly empty environment holds no variable, and an exact one holds exactly its entries. - The timeouts 0, 3600001, 86400000, and 4294967295 ms are accepted. - Workloads run as the configured identity, 65534:65534, which the scenario checked only through the refused /root before. - State persists across requests, while working directories and environments do not, and a direct-mode guest has no workload machine ID. The workload chains these checks with &&, so that a failed one withholds its marker; the Python scenario chained them with `;`, which let only the status of the last command count. - A workload that outlives its 100 ms timeout ends with the timeout category and status 124, and the guest stays usable. - The guest refuses /does-not-exist, /etc/passwd, and /root as working directories with the cwd-failed category, the errors 2, 20, and 13, no output, and a diagnostic that names the directory, and nothing of the refused workload runs. - Stop is acknowledged, and OpenVMM exits with status 0. The test starts OpenVMM itself, with the command line, endpoint, timeout, and output directory that the scenario passes in NVX_MANAGED_LIFECYCLE_* variables, which the next commit sets. OpenVMM reads its capability from a pipe that already holds it and its end, as nvx::host::process::spawn prepares it (#440), writes its output to managed-lifecycle.log, and is killed if the test fails. The guest console of a microVM listens on TCP, and the test copies it, from the address in NVX_MANAGED_LIFECYCLE_CONSOLE, to managed-lifecycle-guest.log until OpenVMM closes or resets it, so that the scenario can scan it; any other error of the copy, such as a full disk, fails the test, so that the scenario never scans an incomplete log. The test records OpenVMM's process ID in NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL, as the scenario did, for the adversarial campaigns. Like the Python client, it bounds each exchange with the scenario's timeout. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The managed-lifecycle scenario was the last user of control_session.py, the Python client of the control protocol, which #359 leaves to nvx-rs alone. Its orchestration stays in Python, and its protocol checks run in nvx-rs's ignored test tests/managed_lifecycle.rs: - run_managed_lifecycle builds OpenVMM's command line as before, with absolute paths, since Cargo runs the test in the nvx-rs directory, and runs `cargo test --manifest-path nvx-rs/Cargo.toml --locked --test managed_lifecycle -- --ignored --nocapture` from the repository root, so that rust-toolchain.toml selects the Rust release, as test-aci-edge-sandboxes runs the aci_edge_sandboxes test. Without Cargo, it fails and says why. If its wait for Cargo ends with an exception, such as the KeyboardInterrupt of a SIGINT that reached nvx.py alone, it ends and reaps Cargo's whole tree, the test and the OpenVMM that it started, before the temporary directory that they use goes, as it ended the OpenVMM that it started before. It leaves Cargo in its process group, which a terminal's Ctrl-C and an adversarial campaign's cleanup still reach. - The guest console still listens on TCP, since a microVM console takes no file, and the test, which starts OpenVMM, copies it to managed-lifecycle-guest.log. The time ABI monitor scans that log once the test ends, so that a violation still fails the run, before the test's failure, which it may explain. - The scenario still checks and keeps managed-outcome.json, which the adversarial campaigns collect, and that OpenVMM refuses the managed lifecycle without a control endpoint. The checks of OpenVMM's report that also hold for the layered root now run through nvx.py sandbox too: the sandbox-lifecycle scenario checks that the report of a stopped sandbox names the backend, that no network policy was requested, and that all seven teardown steps completed, besides the managed success that it checked. managed-exec-config already covers the working directories, environments, timeouts, and refused directories for the layered root. The unit tests replace the capability-pipe test of the Python spawn with tests of the Cargo command, its environment, the absolute paths, the missing Cargo, an interrupted wait, which ends the tree while the temporary directory remains, a failed test, a violation on the console, an invalid report, and an unrefused transition. The sandbox stand-in writes a full report, and four new regressions show that the lifecycle acceptance catches another backend, a requested network policy, an incomplete teardown, and a teardown that names only some of its steps. The structural test of the TCP consoles counts five, and checks that the managed lifecycle monitors its file console. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nvx.py now runs nvx-rs for the sandbox command and for the egress and artifact steps of run, and the microVM tests drive the control protocol through nvx-rs, so the Python implementations that nvx-rs ported have no caller left. #359 asks that Python duplicate none of the control-protocol, lifecycle, or egress logic, so they go: - scripts/nvx_tools/control_session.py, the control-session client, which nvx::host::session and the CLI's session replace, and its tests, scripts/test_control_session.py, which nvx-rs ported to src/host/cli/session/tests.rs; - scripts/nvx_tools/egress_policy.py, the egress policy compiler, which nvx::host::network::policy and `nvx-rs egress compile` replace, and its tests, scripts/test_egress_policy.py, which nvx-rs ported to src/host/network/policy/tests.rs; - scripts/nvx_tools/sandbox_lifecycle.py, the managed lifecycle, which the nvx-rs sandbox command replaces, all of it; - the launch profile of scripts/nvx_tools/sandbox.py: SandboxLaunch, SandboxLayer, SandboxMount, the workload identity, the reserved targets, and the checks of shares, which nvx::host::sandbox replaces. sandbox.py keeps what `nvx.py run` builds its own OpenVMM command line with, as #359 leaves that assembly in Python: the --mount values, the names, arguments, and kernel command-line tokens of the aggregate of several shares, the mount owners and access-policy options, and the kernel command-line budget. Its docstring says so. The unit tests of the deleted code go with it: the managed lifecycle, lock, state, and process-identity tests and the launch-profile tests of SandboxTests, the helpers that only they used, and ControlSessionTests of test_microvm_tests.py. What remains of SandboxTests tests the share helpers of run, as RunShareTests, with the --mount parser and mount kind tested directly rather than through SandboxMount. The Validate NVX action no longer runs the two deleted test modules, and the check that it runs every scripts/test_*.py module looks for one that still exists. nvx-rs comments and README sections that named the Python files now call them former, and keep why the ports match them: the state directories, the lifecycle lock, and the rule output that an older nvx.py shares with nvx-rs. doc/project-structure.md drops the deleted modules. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Open PR #579 (jay23606, fixes #486) reserves the guest paths that the container runtime of a layered sandbox creates or binds over: it creates /run and /run/nvx below the container root, and binds its time ABI state over /run/nvx/time. A live share at one of them would be shadowed, or would expose host data to the runtime's writes. #579 changes scripts/nvx_tools/sandbox.py, which #359 retires, so this ports its behavior to the launch profile that replaced it, as the maintainer asked while #579 is open: - nvx::host::sandbox::mount: RESERVED_MOUNT_TARGETS gains the /run/nvx/time tree, and RESERVED_EXACT_MOUNT_TARGETS /run and /run/nvx, with #579's reasons. validate_mount_target refuses them with the messages that the Python check gave: `sandbox mount target /run is reserved`, and `... overlaps the reserved /run/nvx/time tree` for the tree. Other paths below /run, such as /run/secrets, /run/nvx/other, or /run/nvx/timer, stay valid. - guest/common/nvx-init-agent refuses the same targets, as #579 does, so that a guest refuses them even from a host that would not. - The guest-agent test of aggregate children gains #579's rows for /run, /run/nvx, and /run/nvx/time/state. - doc/usage.md and doc/run.md list the new reserved targets, which #579 left undocumented. tests/fixtures/sandbox.json, which the retired Python launch profile recorded, held nine targets and four shares that #579 now refuses. Its records of those cases are re-evaluated with that Python code, taken from the commit before #359 retired it, with #579's change to sandbox.py applied: 7 targets at /run, one at /run/nvx, and one at /run/nvx/time become refusals, as do 4 shares at /run, and no other record changes. The Rust unit tests check the new refusals, and paths beside them that stay valid. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR #576 (jay23606, refs #556) narrowed the sandbox to one EROFS distro layer and the scratch disk: the runtime and custom layers went, and their MMIO windows and interrupts stay unassigned. It merged after #595 had ported the launch profile to nvx-rs, and it changed scripts/nvx_tools/sandbox.py, which #359 retires, the guest agent, the sandbox-blocks scenario, and the docs, but not the launch profile of nvx-rs, which still accepted the three roles. Now that nvx.py sandbox runs nvx-rs, this ports #576's change to that launch profile, with the messages and checks of #576: - nvx::host::sandbox::layer: LAYER_ROLES, the roles that --layer accepts and a launch holds, is distro alone, so `--layer runtime,...` fails with `unsupported layer role 'runtime'; choose distro`, as does any other role. LayerRole keeps the retired Runtime and Custom roles, which from_name still reads, because the configuration of a sandbox that an older nvx.py provisioned may name them. - SandboxLaunch::new checks the duplicate roles first, over every role, and then requires exactly one layer, the distro layer: `a sandbox requires exactly one read-only distro layer`, which replaces `one to three read-only layers`. As in #576, the check holds for a saved launch as much as for parsed options, so `nvx.py sandbox start` of such a sandbox fails before OpenVMM starts. The golden records follow. In tests/fixtures/sandbox.json, the layer and launch cases are re-evaluated with the retired Python launch profile, from the commit before #359 retired it, which holds #576, with #579's change to sandbox.py applied. 168 layer values of the retired roles, 146 of which parsed and 22 of which had an invalid UUID, become refusals of their role, which is checked first; the refusals of 28 values of unknown roles list only distro; and 136 launches with retired roles now fail with the duplicate roles or the distro-layer requirement, which come before their other checks. No other record changes. The launch harness builds a retired role's layer as a saved configuration names it, since --layer no longer parses it. In tests/fixtures/cli.json, by hand, for POSIX and Windows alike: - `validation: invalid layer role` lists only distro, and a new `validation: retired layer role` shows `--layer runtime,...` refused with the same usage error; - `dry run: every layer and option` takes the distro layer alone, so its OpenVMM command loses the runtime and custom blocks and their nvx_layer tokens; - `lifecycle: network and launch options` provisions the distro layer alone, so its config.json and its OpenVMM arguments lose the runtime layer. A new CLI test edits a provisioned sandbox's configuration to add a runtime layer, as an older nvx.py wrote it, and checks that start refuses it with status 1 and the message above, leaving no runtime state, and that deprovision still removes it. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nvx.py now runs nvx-rs for its sandbox command and for the egress and artifact steps of its run command, and the microVM jobs drive both, in their scenarios and in the steps that call nvx.py sandbox directly. Without an executable, nvx.py would build one with Cargo in every microVM job, so CI builds it once per operating system and hands it over, as it hands over OpenVMM: - The NVX CLI jobs, which every microVM job of their operating system already waits for, build it with `--release --locked --features cli` before the CLI tests, at nvx-rs/target/release/, where nvx.py looks for it, so the tests run with it rather than building it. Linux builds a static x86_64-unknown-linux-musl executable, which runs on every self-hosted Linux runner whatever its C library, and Windows an executable with a static C runtime, which needs no Visual C++ redistributable on the runners. The Windows job already installs the pinned Rust release with the minimal profile. - After the tests pass, each job uploads its executable as a one-day artifact: nvx-rs-linux-musl-executable or nvx-rs-windows-msvc-executable. - run-nvx-microvm-tests.yml takes the artifact's name as a new required input, nvx-rs-artifact, which every microVM job, the debug-kernel ones included, sets for its operating system. It downloads the executable into nvx-rs/target/release/ and restores its executable permission on Linux, before any step runs nvx.py. The platform jobs run neither nvx.py sandbox nor nvx.py run, so they take nothing. The change classification needs no change: nvx-rs/, nvx.py, scripts/nvx_tools/, and these workflows are already test inputs. A new workflow test checks the builds, their targets and their order around the CLI tests, the artifact names and paths, that each microVM job takes the executable of its operating system and the platform jobs none, that the reusable workflow downloads it where nvx_rs.binary_path() looks, and that it does so before it runs nvx.py. The handoff test counts the microVM workflow's fifth download. doc/ci.md describes the handoff, and notes that the Copilot setup does not build nvx-rs, which nvx.py builds on first use. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nvx.py now runs nvx-rs for its sandbox command and for the egress and artifact steps of its run command, and test-microvm drives the managed lifecycle through an nvx-rs test, so the documentation says where the executable comes from, what needs Cargo, and what the delegation keeps or changes: - doc/usage.md gains an nvx-rs section: NVX_RS, then nvx-rs/target/release/nvx-rs[.exe], then a Cargo build with the pinned release and its output on standard error, the failure that names these choices without Cargo, and that nvx.py does not rebuild an existing executable after a change. The sandbox section gives the exit statuses and says which usage errors argparse still reports, that abbreviations still work, and that nvx-rs's usage wraps at 80 columns; run says which steps nvx-rs performs; test-microvm describes the managed-lifecycle scenario's nvx-rs test and its need for Cargo. - README.md and doc/setup.md list rustup as a prerequisite of nvx.py run and nvx.py sandbox, whose nvx-rs nvx.py builds on first use until releases include it, and doc/run.md points the sandbox section at the nvx-rs section. - doc/contribute.md says that the control protocol, lifecycle, and egress logic belong to nvx-rs, which the microVM scenarios drive as commands, and that no Python client of them may return. - doc/project-structure.md lists scripts/nvx_tools/nvx_rs.py and the hypervisor test among nvx-rs's tests. - nvx-rs/README.md says how nvx.py finds and invokes the CLI, and describes tests/managed_lifecycle.rs. - .github/copilot-instructions.md tells cloud sessions that nvx.py builds nvx-rs on first use, how to rebuild it after a change, and that the managed-lifecycle scenario needs Cargo. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three CLI tests run the real nvx-rs only when NVX_RS names it: the egress compilation of nvx.py run, the JSON that nvx.py reads from nvx-rs, and the options that nvx.py sandbox passes. The NVX CLI jobs built nvx-rs where nvx.py finds it, but set no NVX_RS, so those tests skipped and the executables that the jobs hand to the microVM jobs left unexercised by them (review of #597). Each Build nvx-rs step now exports NVX_RS for the rest of its job, naming the executable that it built: with `>> "${GITHUB_ENV}"` on Linux, and on Windows with `Out-File -Encoding utf8 -Append`, as setup-curl writes GITHUB_ENV under Windows PowerShell. The workflow test checks both, and doc/ci.md says so. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The adversarial executor runs each scenario with an allowlist of environment variables, which left out everything that nvx-rs needs now that the control protocol and the egress compilation live there. Of the catalogued scenarios, managed-lifecycle runs the nvx-rs test managed_lifecycle with Cargo, and l3-l4-egress-policy compiles its policy with nvx-rs and runs nvx.py run, which runs nvx-rs too. Under the executor, they ignored a prebuilt nvx-rs that NVX_RS named, and they could not find a Rust toolchain installed outside the default rustup and Cargo homes: without RUSTUP_HOME, rustup starts installing the pinned release into the home directory, within the scenario's timeout. The allowlist now passes NVX_RS, CARGO_HOME, and RUSTUP_HOME, which are paths, not secrets. It still drops RUSTUP_TOOLCHAIN, so that rust-toolchain.toml selects the release, and Cargo's other settings and credentials, such as CARGO_TARGET_DIR, RUSTFLAGS, and registry tokens. Tests check both, and that nvx.py finds NVX_RS in a process that has only the executor's environment. The rest of the executor's contract holds, and is now tested: - managed-lifecycle still preserves OpenVMM's report as managed-outcome.json, with its teardown fields, in the scenario's output directory, where the executor's outcome inventory requires it. A test checks the preserved report against that inventory. - The nvx-rs test records OpenVMM in the journal that NVX_ADVERSARIAL_OPENVMM_PID_JOURNAL names, as the Python scenarios do, but it runs in nvx-rs/, so the scenario now hands it an absolute path: a relative journal resolves where nvx.py runs, as for the Python scenarios. The test reads the variable as an OsString, so a path that is not UTF-8 is no longer skipped, and a new test that needs no hypervisor checks that its entries are exactly what the executor reads. doc/adversarial-testing.md describes what a target needs: the Rust toolchain, with Cargo on the executor's PATH, and nvx-rs and the managed-lifecycle test built at the staged commit before exposure, since a scenario's timeout includes any build, nvx.py never rebuilds an existing nvx-rs, and the target may have no network. doc/ci.md and doc/setup.md point there. The nvx-adversary agent is the strategist's prompt, which names no host prerequisites, so it is unchanged. Verified by driving the local executor on a Linux/MSHV host through the managed-control-lifecycle and l3-l4-network-boundary cases of workload-isolation, with source verification, provenance, and the baseline and post-campaign boots passing: once with NVX_RS naming an nvx-rs outside the checkout, which the scenario ran without building, and once without it, when the scenario built nvx-rs with Cargo. Every journal held the nvx-rs test's OpenVMM, and the inventory found managed-outcome.json with complete teardown in each repetition. On Windows, with HOME and USERPROFILE pointing at an empty directory, the executor's environment built nvx-rs and the managed_lifecycle test through CARGO_HOME and RUSTUP_HOME, with the release that rust-toolchain.toml pins despite a RUSTUP_TOOLCHAIN of 1.89; without those two variables, rustup began installing the release into the empty home. Part of #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/fix-issue-359-delegate
branch
from
October 19, 2026 18:13
afcbc7e to
cd387d4
Compare
Pedro Henrique Penna (ppenna)
deleted the
agents/fix-issue-359-delegate
branch
October 19, 2026 19:01
Pedro Henrique Penna (ppenna)
added a commit
that referenced
this pull request
Oct 19, 2026
The executor validates and hashes the nvx-rs that NVX_RS names before the baseline boot, and nvx.py resolves a relative NVX_RS against its working directory: the executor's, at that point. The scenarios inherit NVX_RS unchanged but run in the repository root, so with a relative NVX_RS they ran another executable than the one validated, or found none. sanitized_environment() now makes NVX_RS absolute against the executor's working directory, so that the scenarios run the executable that the initialization checked. An empty NVX_RS, which nvx.py ignores, passes unchanged. A new test validates a relative NVX_RS in one working directory and resolves it in a scenario process in another; it fails without the fix. The environment test now expects the absolute path. Copilot's review of #598 flagged this. #597 added NVX_RS to the scenarios' environment, and the earlier commit of this series that validates the executor's nvx-rs made the mismatch matter. Closes #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
added a commit
that referenced
this pull request
Oct 19, 2026
The executor validates and hashes the nvx-rs that NVX_RS names before the baseline boot, and nvx.py resolves a relative NVX_RS against its working directory: the executor's, at that point. The scenarios inherit NVX_RS unchanged but run in the repository root, so with a relative NVX_RS they ran another executable than the one validated, or found none. sanitized_environment() now makes NVX_RS absolute against the executor's working directory, so that the scenarios run the executable that the initialization checked. An empty NVX_RS, which nvx.py ignores, passes unchanged. A new test validates a relative NVX_RS in one working directory and resolves it in a scenario process in another; it fails without the fix. The environment test now expects the absolute path. Copilot's review of #598 flagged this. #597 added NVX_RS to the scenarios' environment, and the earlier commit of this series that validates the executor's nvx-rs made the mismatch matter. Closes #359. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Part of #359 (PR 4 of 5).
Summary
nvx.pynow runs thenvx-rsCLI of #596 for its wholesandboxcommand and for the egress-policy and artifact steps ofrun, and the Python implementations thatnvx-rsreplaced are gone: the control-protocol client, the egress compiler, the managed lifecycle, and the sandbox launch profile.test-microvmkeeps its orchestration in Python but no longer speaks the control protocol: the egress scenarios compile withnvx-rs egress compile, the public sandbox scenarios runnvx.py sandbox, andmanaged-lifecycleruns a new ignorednvx-rshypervisor test. CI buildsnvx-rsonce per OS in the NVX CLI jobs and hands it to the microVM jobs, as it hands over OpenVMM. The PR also ports the open PRs #576 and #579 into the Rust launch profile.How
nvx.pyfindsnvx-rs(scripts/nvx_tools/nvx_rs.py; documented in thenvx-rssection ofdoc/usage.md):NVX_RSnames;nvx-rs/target/release/nvx-rs[.exe], wherecargo build --releaseputs it, and where PR E'snvx.py downloadshould install a release'sbin/nvx-rs[.exe](nvx_rs.binary_path()is the hook);cargo build --release --locked --features cli --manifest-path nvx-rs/Cargo.toml --target-dir nvx-rs/target, from the repository root, with Cargo's output on standard error so that it never mixes with the command's output. Without Cargo, aScriptErrornamesNVX_RS, the build command,nvx.py download, and rustup.nvx.py sandboxkeeps argparse as its front end, for--help(byte-identical at 80 and 120 columns), abbreviations, and the usage errors of unknown options, invalid choices, and missing values. It drops every converter and check thatnvx-rsperforms, rebuilds the canonical argv from the options the user gave (in full,--name=value, in command order), and runsnvx-rs --root REPO --prog "nvx.py sandbox" sandbox OPERATION ...with inherited stdio, passing its exit status through. The new global--progoption ofnvx-rsmakes its usage and errors namenvx.py sandbox. Ctrl-C reachesnvx-rs, which prints the onlyInterruptedand exits with 130, whichnvx.pyreturns unchanged.What moved where
scripts/nvx_tools/control_session.pynvx::host::sessionandnvx::host::cli::session(#596), whichnvx-rs sandboxandnvx-rs/tests/managed_lifecycle.rsusescripts/nvx_tools/egress_policy.pynvx::host::network::policyandnvx-rs egress compile, whichnvx.py runand the egress scenarios runscripts/nvx_tools/sandbox_lifecycle.py(all of it)nvx-rs sandbox(nvx::host::cli::sandbox);sandbox_lifecycle_tests.pyuses local state-file names and a process scan insteadscripts/nvx_tools/sandbox.py:SandboxLaunch,SandboxLayer,SandboxMount, the workload identity, the reserved targets, the share checksnvx::host::sandbox(#595).sandbox.pykeeps only the share helpers with whichnvx.py runbuilds its own OpenVMM commandcommand_sandboxofscripts/nvx.pyand its helpersscripts/nvx_tools/nvx_rs.pynvx.py run's artifact lookups and egress compilationnvx-rs --root REPO artifactsandnvx-rs egress compile PATH(JSON);run's OpenVMM command assembly stays in Pythonscripts/test_control_session.py,scripts/test_egress_policy.pynvx-rs/src/host/cli/session/tests.rsandnvx-rs/src/host/network/policy/tests.rsscripts/nvx_rs_fixtures.py(the fixture generator)nvx-rs/tests/fixtures, which a behavior change now updates by hand (see below)test_nvx_tools.pyandtest_microvm_tests.pyrunshare tests remain asRunShareTestsWhere each
managed-lifecycleassertion wentThe scenario still boots a guest whose init hands the boot to the managed agent; the ignored test
nvx-rs/tests/managed_lifecycle.rsnow drives it. CI runs the scenario for Alpine (default suite, every backend), Azure Linux (every backend), and Ubuntu (KVM extended).run_managed_lifecyclenvx.py sandbox)/tmpwith exactlyEMPTY=and an entry with spaces,=, and a snowmanmanaged-exec-config(--cwd,--environment)nvx.py sandbox execis a new session (sandbox-lifecycle)managed-exec-config(empty and exact environment files)managed-exec-config/run/nvx/workload-machine-idsandbox-lifecyclewarm statesleep 5with category timeout and status 124, guest still usablemanaged-exec-configtimeout outcomecwd-failed, errors 2, 20, 13, no output, a diagnostic naming the directory, nothing of the workload runsmanaged-exec-configrefused directoriessandbox-lifecyclestop/rootrefusal)managed-exec-configidentity probenvx-rs sandbox startmanaged-outcome.jsonsandbox-lifecyclestop now checks backend, network policy, and teardown too (new)Stack
#596, #595, #594, and #561, on which this PR was stacked, have merged, and it is rebased on
dev(4f5e255b4), so it holds only its own commits:nvx-rs: name the calling program in usage messages(the--proghook)scripts: find, build, and run nvx-rs for nvx.py(with review fixes)nvx.py: run the sandbox command with nvx-rs(with a review fix)nvx.py: look up run's artifacts and egress rules with nvx-rstest: compile the microVM egress policies with nvx-rsnvx-rs: test the direct-mode managed lifecycle on a hypervisor(with review fixes)test: run the managed lifecycle through nvx-rs(with review fixes)scripts: delete the Python duplicates of nvx-rsnvx-rs: reserve /run, /run/nvx, and /run/nvx/time as share targets(fix(sandbox): reserve /run, /run/nvx, and the /run/nvx/time tree as share targets #579)nvx-rs: accept only the distro layer and the scratch disk(sandbox: accept only the distro layer and scratch #576)ci: hand the nvx-rs executable to the microVM jobsdoc: describe how nvx.py finds, builds, and runs nvx-rsci: name the nvx-rs build for the CLI tests that run it(review)adversarial: run nvx-rs scenarios with Cargo and NVX_RS: the adversarial executor's environment allowlist passesNVX_RS,CARGO_HOME, andRUSTUP_HOME(notRUSTUP_TOOLCHAIN),managed-lifecyclehands the nvx-rs test an absolute PID journal, and doc/adversarial-testing.md lists what a target needsCommits 1, 9, and 10 change code that #596 and #595 brought: the CLI's usage plumbing, the launch profile, and the golden records. Each commit body explains why.
Contract notes
nvx.py sandboxoptions, stdout and stderr, exit codes (1, 2, 130, and the workload's or OpenVMM's status), the outcome reports, and the state-directory layout are unchanged; the golden scenarios ofnvx-rs/tests/cli.rsreplay them.nvx-rsreports, such as an invalid--layeror--workload-user, printnvx-rs's usage, which namesnvx.py sandboxbut wraps at 80 columns whatever the terminal's width. The usage errors of unknown options, invalid choices, and missing values still come from argparse, and so now come before the value errors of other options.nvx.pypasses them tonvx-rsin full.nvx.py runandnvx.py sandboxnow neednvx-rs. Until PR E ships it in releases, a downloaded checkout builds it with Cargo on first use; README.md and doc/setup.md say so.NVX_OPENVMM, the OpenVMM override ofnvx-rs, now also affectsnvx.py run's artifact lookup.managed-lifecyclescenario needs Cargo, astest-aci-edge-sandboxesdoes.--layer runtime,...orcustom,...fails withunsupported layer role 'runtime'; choose distro, and a sandbox whose saved configuration names a retired role failsstartwitha sandbox requires exactly one read-only distro layer./run,/run/nvx, and the/run/nvx/timetree are reserved share targets.nvx.pyignores, such as SIGINT for a background command of a shell without job control or SIGHUP under nohup, stays ignored, andnvx-rsinherits it ignored (review).nvx.py runpasses the egress policy's path tonvx-rs egress compileafter--, so a path that starts with-is still a path, as it was for the Python compiler (review).managed-lifecyclescenario ends and reaps Cargo, thenvx-rstest, and its OpenVMM before it removes its temporary directory, as it ended the OpenVMM that it started before (review).managed-lifecycletest now fails if a second request loses the workload's state, sees the first request's environment, or finds a workload machine ID: the Python scenario chained those checks with;, so none could fail, and the port chains them with&&(review).sandbox-lifecyclerequires all seven teardown steps in a stopped sandbox's report, not only that the steps it names completed (review).managed-lifecycletest fails if it cannot copy the whole guest console, which the scenario scans for time ABI violations, rather than leave an incomplete log; a reset still ends the console (review).sandbox.json's affected cases were re-evaluated with the retired Python launch profile from the commit before this PR retires it, which holds the merged sandbox: accept only the distro layer and scratch #576, patched with fix(sandbox): reserve /run, /run/nvx, and the /run/nvx/time tree as share targets #579's change tosandbox.py(no other record changed; after the rebase, regenerating them that way reproducessandbox.jsonbyte for byte);cli.jsonchanges three scenarios by hand and addsvalidation: retired layer role, on both platforms.#576 and #579
Both were still open when this PR was opened, so this PR implemented their behavior, as the maintainer asked: in the Rust launch profile with their messages and check order, in
guest/common/nvx-init-agentverbatim, and, for #576, insandbox-blocks.sh, its scenario and unit test, and the docs. #576 has since merged intodevwith exactly those guest, scenario, test, and doc changes, so after the rebase commit 10 holds only what #576 left out: the Rust launch profile, which ondevstill accepts the runtime and custom roles thatnvx.py sandboxwould otherwise accept again once it runsnvx-rs, with its golden records and tests. #579 is still open; commit 9 implements it as before, and documents its reserved targets in doc/usage.md and doc/run.md. I commented on both PRs: #576 (comment) and #579 (comment).Validation
dev4f5e255b4and the review fixesnvx.py verify; the commands of check-nvx-rs (the bundled-artifacts test included; its macOS and guest-binary steps run on Linux only) and of check-aci-edge-sandboxes, withtest-agent-bundling; Ruff, Ruff format, Pyright (Linux and Windows), compileall, the Markdown link check; the validate-nvx unittest list withNVX_RSnaming the static-CRT release build (984 tests, the three real-binary tests included),test_adversarial.py,test_hosts.py, and the--helpchecks; andsandbox.json, regenerated fromdev's Python launch profile with #579's change: identicalNvxRsTestsandSandboxDelegationTests, with every POSIX signal test, and the managed-lifecycle unit tests; the two new signal tests fail without their fix; a SIGINT sent only to the Python process ofrun_managed_lifecycle, with a fake Cargo whose child sleeps, leaves neither running and then removes the temporary directory6f31aa4a4, before the review fixes, run 39286746353, and9273885a1, with the first three, run 39291857952: every job passed, the microVM suites and OpenVMM VMM tests on KVM, MSHV, and WHP and the performance gate included. The rows below ran before the rebase, which changed none of this PR's own changes; the review fixes change onlynvx_rs.py's signal hand-over and egress arguments, the cleanup of the managed-lifecycle scenario, the checks and console copy of themanaged-lifecycletest, the teardown check ofsandbox-lifecycle, and a.gitattributescommentnvx.py verify; nvx-rs: fmt, clippy (all features, and lib without defaults), tests (all features and default), rustdoc with-D warnings, 1.89 check, the bundled-artifacts test,aarch64-apple-darwincheck; aci_edge_sandboxes: the commands of check-aci-edge-sandboxes andtest-agent-bundling; Ruff, Ruff format, Pyright (Linux and Windows), the Markdown link check, compileall, the validate-nvx unittest list,test_adversarial.py,test_hosts.pycheck-guest-binaries, and the validate-nvx list on Python 3.14, 3.12, and 3.10.test_adversarial.pyfails one subtest on 3.10 (test_contained_native_process_preserves_child_signal_status, sigkill), as it does onorigin/dev; it passes on 3.12, which CI usesnvx-adversarial-controllerlabel)test-microvmscenariossandbox-lifecycle,managed-exec-config,managed-lifecycle,l3-l4-egress-policy,l3-l4-egress-port-ranges,l3-l4-egress-protocols,ipv6-egress-policy,directional-network-policy,sandbox-blocks,filesystem-comma-paths: pass;managed-lifecyclefor Azure Linux and Ubuntu: pass;test-aci-edge-sandboxes: 18 passedworkload-isolation'smanaged-control-lifecycleandl3-l4-network-boundary: initialize (verify, provenance, baseline boot), both cases, and finalize (post-campaign boot) pass, once withNVX_RSnaming an nvx-rs outside the checkout and once building nvx-rs with Cargo under the executor's environment; every PID journal and outcome inventory completeHardware artifacts: OpenVMM at the pinned
5ab8a84and the kernel from #594's validation, the Ubuntu and Azure Linux images and distro layer from #561's, and an Alpine initramfs rebuilt natively from this branch, so the hosts ran the new guest agent;nvx-rswas the static musl build that CI hands to Linux runners, and on Windows the static-CRT build that CI makes.