Skip to content

Port the sandbox launch profile and artifact lookup to nvx-rs - #595

Merged
Pedro Henrique Penna (ppenna) merged 5 commits into
devfrom
agents/fix-issue-359-launch-profile
Oct 19, 2026
Merged

Pedro Henrique Penna (ppenna) merged 5 commits into
devfrom
agents/fix-issue-359-launch-profile

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Part of #359 (PR 2 of 5).

Summary

This PR ports the launch profile of nvx.py sandbox to nvx-rs byte for byte, and extends artifact discovery to the guests that nvx.py runs, so that the nvx-rs CLI (PR C) can take over nvx.py sandbox, and the artifact steps of nvx.py run, as a thin layer over the library. Python stays authoritative and unchanged until PR D.

  • nvx::host::sandbox ports SandboxLaunch, SandboxLayer, SandboxMount, and the helpers of scripts/nvx_tools/sandbox.py: one to three EROFS layers and the ext4 scratch disk; the entrypoint, arguments, hostname, workload identity, and resource limits; live shares with their reserved targets, access policies, owner, aggregate, and host-path checks; the --microvm-sandbox-block and --microvm-workload-identity arguments; and the kernel command line within its 1024-byte budget, all with Python's messages.
  • nvx::host::sandbox::options ports the pure option checks of command_sandbox and the helpers of scripts/nvx.py: _sandbox_mounts, _validate_sandbox_systemd_policy with SYSTEMD_ENTRYPOINTS, _validate_run_share_targets, parse_workload_identity, and the managed exec environment rules.
  • nvx::host::artifacts gains ArtifactDir, Guest, and ArtifactPaths: the OpenVMM executable, the kernel, the Alpine, Ubuntu, and Azure Linux initramfs images, and the Ubuntu distro layer, in the repository layout (build/, openvmm/target/release/) and the release layout (guest/, bin/), checked in the order and with the messages of nvx.py. ArtifactPaths serializes as JSON for nvx-rs artifacts. The Artifacts discovery of aci_edge_sandboxes is unchanged.
  • Golden fixtures that CPython 3.14 recorded from the Python code, nvx-rs/tests/fixtures/{python,sandbox,sandbox_options}.json, hold fixed cases and seeded random corpora for both path flavors, and the Rust tests replay them without Python.
  • One generator: scripts/generate_egress_policy_fixtures.py from Port egress policies to nvx-rs #594 becomes scripts/nvx_rs_fixtures.py, which writes every Python-parity file of nvx-rs and checks them all with one --check. The egress policy fixture regenerates byte for byte, except its comment line, which now names the script.

Stack

This PR builds on #594 (PR A, the egress policy compiler) and #561, which have both merged. It is rebased on dev, so its diff holds only its own commits:

  1. cc402eeef nvx-rs: port the sandbox launch profile
  2. a23c5ceea nvx-rs: port the sandbox option checks of nvx.py
  3. a89a2ef66 nvx-rs: check the sandbox port against golden Python fixtures
  4. cba18338f nvx-rs: look up the guest artifacts that nvx.py runs
  5. 50f3d1859 doc: describe the sandbox port and guest artifact lookup of nvx-rs

PR C adds the nvx-rs CLI, PR D makes nvx.py delegate to it, and PR E ships it in releases and completes #359. #576 and #579, which change sandbox.py, are still open, so this PR ports dev's current behavior; whichever merges needs a port and regenerated fixtures.

Faithful-port notes

The python module

nvx.py checks its options with Python's own string, integer, UUID, path, and JSON operations, and its messages show values as Python formats them. The ports keep those semantics through the private nvx::host::python module that #594 introduced, which this PR extends with one implementation per concern:

  • text (PyText, from Port egress policies to nvx-rs #594) stays the one string type that holds surrogates, and gains the errors of Python's UTF-8 codec: PyText::encode_utf8 and decode_utf8.
  • json (from Port egress policies to nvx-rs #594) keeps its API, hooks, and arena, but its reader is now a port of the C scanner of CPython 3.14, so that a refusal carries the message and position of Python's JSONDecodeError. The previous reader reported where it stopped, which is not always Python's position: an unterminated string fails at its opening quote. Loads are the hooks of plain json.loads, whose int refuses more than 4300 digits. Its "Why not serde_json" section, from Port egress policies to nvx-rs #594, gains this reason: messages such as invalid sandbox distro manifest: Expecting value: line 1 column 1 (char 0) show the JSONDecodeError itself. The egress policy fixture and Port egress policies to nvx-rs #594's tests pass unchanged.
  • path (from Port egress policies to nvx-rs #594) keeps python_path, posix_str, and windows_str, and gains PurePath and Flavor: pathlib's pure paths and the splitroot, join, isabs, and split of os.path, and ntpath.normpath, on one parser with python_path. Windows paths follow CPython on Windows, whose C splitroot and normpath index UTF-16 code units.
  • New modules: builtins (str.isspace, which unlike Rust's whitespace includes U+001C to U+001F, str.split(), str.strip(), repr, and int), uuid (uuid.UUID), unicode (the generated Unicode 16.0.0 tables), fs (the queries of os.path, os.stat identities, and Path.resolve()), and windows (the Win32 calls of CPython's os.stat).

Deviations from Python

  • Operating-system strings that are not valid Unicode, such as paths, the current directory, and link targets, are refused, where Python would carry them as surrogates. python_path still prints such paths as Python does.
  • I/O errors take Python's [Errno N] text: 'path' and [WinError N] text: 'path' forms, but the text comes from Rust's io::Error, which may word an error differently from CPython.
  • The JSON reader has no nesting limit, where Python raises RecursionError, as in Port egress policies to nvx-rs #594.
  • On Windows, os.stat follows CPython's classic path through FILE_ID_INFO, not the GetFileInformationByName fast path of recent Windows builds; the identities agree.
  • Windows paths compare case-insensitively with Rust's Unicode 17 lowercase mapping instead of Python's 16.0.0 one.
  • On hosts other than Windows, PureWindowsPath indexes code points, so a character beyond the Basic Multilingual Plane before : reads as a drive there. The port follows Windows hosts, where it does not.
  • The workload identity is (u32, u32) and the layer roles are an enum, so values that Python could hold but never accepts from options cannot be represented. A --mount-deny, --mount-allow, or --mount-write index beyond the --mount options, which argparse cannot produce, is an error instead of Python's IndexError.
  • ArtifactDir makes its root absolute without resolving links, unlike REPO_ROOT's resolve(), so callers pass the resolved root.

What PR C still needs from command_sandbox and parse_args

argparse parsing and its usage errors (exit 2), type conversions and choices, the egress options through #594, building the launch from the options, the run, provision, start, exec, stop, and deprovision operations with the state-directory layout, configuration formats, and outcome reports, --dry-run printing, how the CLI learns the artifact root, FEATURES negotiation, and the control session.

Validation

Environment Evidence
Windows 11, Rust 1.95.0 and 1.89 nvx-rs: fmt; Clippy with -D warnings for all targets and features, and for the library without default features; tests with all and with default features (259 and 256 library tests); rustdoc with -D warnings; MSRV 1.89; the synthetic-release bundled test; the aarch64-apple-darwin check
Windows 11 aci_edge_sandboxes: fmt, Clippy, tests, docs, MSRV, the macOS check, and nvx.py test-agent-bundling
Ubuntu 26.04 under WSL, Rust 1.95.0 and 1.89 nvx-rs: fmt, Clippy, tests (470 library tests with the guest modules, and 33 guest-tool tests), rustdoc, MSRV, and the macOS check; aci_edge_sandboxes: Clippy and tests
Windows, CPython 3.14.7 nvx.py verify; Ruff check and format; Pyright for Linux and Windows; check_markdown_links.py; the validate-nvx unittest list (1110 tests), test_adversarial.py, and test_hosts.py; nvx_rs_fixtures.py --check
Ubuntu under WSL, CPython 3.14.4 nvx_rs_fixtures.py --check passes on the files that Windows wrote

After the rebase on dev at 7e96632b3, the Windows rows were rerun on 50f3d1859, except nvx.py verify, which needs the OpenVMM submodule. nvx-rs ran 261 and 258 library tests, and nvx_rs_fixtures.py --check still passes with CPython 3.14.7.

The tests need no Python and no hypervisor. Left to CI: the Linux guest-binary steps of check-nvx-rs, the microVM and workload jobs, MSHV, and the benchmarks.

CI's Pythons predate 3.14, so CI does not run nvx_rs_fixtures.py --check, and nothing in CI notices fixtures that no longer match the Python code. Until PR D retires the generator with the Python code, a change to that code, or to the generator, needs a manual regeneration with CPython 3.14.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-platform behavioral compatibility, unsafe Win32 filesystem calls, and guest-build migration require final human review with CI evidence.

No open issues 🎉

What changed in this PR

Advances #359 by porting Python’s sandbox behavior and artifact lookup into nvx-rs, preparing for later CLI delegation while Python remains authoritative.

Changes:

  • Adds sandbox launch types, Python-compatible semantics, and golden parity tests.
  • Extends guest artifact discovery and documents the migration.
  • Includes stacked changes for shared egress lowering and Rust guest-binary builds, provenance, and CI.
File Description
THIRD_PARTY_NOTICES.md Records Rust guest runtime licenses.
SOURCE-MANIFEST.json Adds Rust toolchain and source identities.
scripts/​test_tool_versions.py Checks musl target installation.
scripts/​test_guest_ingress.py Removes C-based ingress tests.
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.py Adds the guest-source digest command.
scripts/​nvx_tools/​ubuntu.py Includes Rust inputs in Ubuntu provenance.
scripts/​nvx_tools/​release.py Validates Rust source and toolchain metadata.
scripts/​nvx_tools/​guest_binaries.py Adds staged-build validation and normalized digests.
scripts/​nvx_tools/​ci.py Tracks Rust inputs for VMM tests.
scripts/​nvx_tools/​build.py Installs Rust binaries instead of compiling C.
scripts/​nvx_tools/​build_constants.py Defines Rust build and packaging inputs.
scripts/​nvx_tools/​benchmark.py Records Rust guest-binary provenance.
scripts/​nvx_tools/​azurelinux.py Includes Rust inputs in image digests.
scripts/​nvx_tools/​archive.py Excludes Rust build outputs from archives.
README.md Documents shipped Rust guest binaries.
nvx-rs/​src/​protocol/​tests.rs Pins wire constants with literal assertions.
nvx-rs/​src/​protocol/​mod.rs Updates shared-protocol documentation.
nvx-rs/​src/​protocol/​features.rs Documents shared guest feature definitions.
nvx-rs/​src/​host/​sandbox/​mod.rs Adds sandbox launch validation and arguments.
nvx-rs/​src/​host/​sandbox/​layer.rs Adds EROFS layer parsing and roles.
nvx-rs/​src/​host/​sandbox/​golden.rs Replays Python sandbox parity fixtures.
nvx-rs/​src/​host/​python/​windows.rs Implements Python-compatible Win32 filesystem queries.
nvx-rs/​src/​host/​python/​uuid.rs Implements Python-compatible UUID parsing.
nvx-rs/​src/​host/​python/​text.rs Preserves Python strings and codec errors.
nvx-rs/​src/​host/​python/​mod.rs Organizes Python compatibility helpers.
nvx-rs/​src/​host/​python/​golden.rs Checks Python semantics against fixtures.
nvx-rs/​src/​host/​mod.rs Exposes sandbox and compatibility modules.
nvx-rs/​src/​guest/​time/​mod.rs Updates comments after C retirement.
nvx-rs/​src/​guest/​time_probe/​mod.rs Updates compatibility documentation.
nvx-rs/​src/​guest/​reseed.rs Removes obsolete C references from comments.
nvx-rs/​README.md Documents sandbox, parity, and guest builds.
nvx-rs/​Cargo.toml Enables SHA-256 for sandbox share naming.
guest/​common/​nvx-reseed.c Removes the replaced C implementation.
guest/​common/​nvx-mmio-write.c Removes the replaced C implementation.
guest/​common/​nvx-exit.c Removes the replaced C implementation.
guest/​common/​nvx-device-io.c Removes the replaced C implementation.
guest/​common/​nvx-console-pending.c Removes the replaced C implementation.
docker/​Dockerfile Shares one Rust build across guest images.
doc/​usage.md Documents the guest-source digest command.
doc/​setup.md Documents Rust musl prerequisites.
doc/​project-structure.md Updates module and source ownership descriptions.
doc/​guests/​ubuntu-guest.md Documents Rust binaries and provenance.
doc/​guests/​azurelinux-guest.md Documents shared binaries and image inputs.
doc/​contribute.md Updates guest testing guidance.
doc/​ci.md Documents Rust-aware caching and setup.
doc/​build.md Explains Rust builds and packaging provenance.
doc/​benchmarks.md Updates guest-binary provenance descriptions.
aci_edge_sandboxes/​src/​openvmm/​mod.rs Updates lowering documentation and error tests.
aci_edge_sandboxes/​README.md Documents shared egress lowering changes.
.github/​workflows/​copilot-setup-steps.yml Triggers setup validation for Rust changes.
.github/​copilot-instructions.md Documents musl target provisioning.
.github/​actions/​validate-nvx/​action.yml Removes retired C test suites.
.github/​actions/​build-guest-artifacts/​action.yml Adds Rust-aware guest cache identities.
.gitattributes Marks parity fixtures and tables as generated.

Review effort: Balanced

Port SandboxLaunch, SandboxLayer, SandboxMount, and the helpers of
scripts/nvx_tools/sandbox.py to a new nvx::host::sandbox module, so
that the nvx-rs CLI can launch sandboxes as nvx.py sandbox does.
Python stays authoritative and unchanged until nvx.py delegates to
nvx-rs.

The port keeps the Python contract byte for byte: the limits, option
semantics, OpenVMM arguments, kernel command line, and messages, with
refused requests as Unsupported, missing images as Unavailable, and
failed host queries as Io errors in Python's [Errno N] form.

- Layers: one to three EROFS layers, distro, runtime, and custom,
  ordered by role, with duplicate roles refused in sorted order. A
  --layer value is ROLE,PATH,EROFS_UUID, and the UUID takes every form
  that uuid.UUID reads: braces, urn:uuid:, no hyphens, and uppercase.
  Layer and scratch paths with ',' or ';' are refused.
- Settings: an absolute entrypoint and nonempty arguments without
  whitespace, a lowercase RFC 1123 hostname label, a non-root workload
  identity, 65534:65534 by default, which parse_workload_identity
  reads as int(field, 10) does, and positive limits, with
  nvx_pids_max one above the request.
- Shares: --mount GUEST_TARGET,HOST_PATH[,ro|rw], whose target ends at
  the first comma and whose mode follows the last one; the target
  rules, at most 4096 UTF-8 bytes, and the reserved targets; at most
  128 denied, allowed, and writable paths each; the overlap and
  single-owner checks; caller ownership refused on Windows; file and
  directory shares; the aggregate at /run/nvx/shares with children
  named INDEX-SHA256(TARGET)[:32] and its nvx_share= tokens; and the
  host-path checks, by Path.resolve() and (st_dev, st_ino) lineage.
- Kernel command line: NUL and the nvx_* and virtfs_* tokens refused,
  the tokens in Python's order, and the 1024 bytes that OpenVMM leaves
  of the 2048-byte x86 command line, which also hold OpenVMM's
  bootstrap tokens for the shares and the NUL.

The private python module that the egress policy compiler introduced
gains what these rules rely on, after CPython 3.14: builtins, with
str.isspace, which unlike char::is_whitespace includes U+001C to
U+001F, str.split() and str.strip(), repr of a str, and int, with
signs, underscores, Unicode digits, surrounding whitespace, and the
4300-digit limit; uuid, for uuid.UUID; unicode, the Unicode 16.0.0
tables of str.isprintable and int; and fs, the queries of os.path and
os.stat, with Path.resolve() ported from posixpath.realpath and
ntpath.realpath. On Windows, os.stat identifies a file by the 64-bit
volume serial number and 128-bit file ID of FILE_ID_INFO, as Python
3.12 and later do, through the windows-sys features that the crate
already enables. Operating-system strings that are not valid Unicode,
which Python would carry as surrogates, are refused.

python::path gains PurePath, a Unicode path of either pathlib flavor
with its parts, parents, joins, and case-insensitive comparison on
Windows, and Flavor, with the splitroot, join, isabs, and split of
posixpath and ntpath, and ntpath.normpath. One parser serves both
python_path and PurePath, so the printed and the pure paths cannot
disagree. Windows paths follow CPython on Windows, whose splitroot and
normpath are C functions that index UTF-16 code units: a character
beyond the Basic Multilingual Plane before a colon is no drive there,
although Python's own ntpath, which other hosts use, reads one.

scripts/generate_egress_policy_fixtures.py becomes
scripts/nvx_rs_fixtures.py, one generator for every Python-parity
file of nvx-rs, which now also writes the Unicode tables, with one
--check for all of them. It needs CPython 3.14, whose Unicode database
the tables record, and says so; --output and --random-cases still
write a larger egress policy corpus. The egress policy fixture
regenerates byte for byte, except its comment, which names the
script.

Aggregate child names need SHA-256 on the host, so the host feature
enables sha2 0.10, which the bundled build script already uses and
locks, as does the aci_edge_sandboxes agent, instead of another crate.

The unit tests port the launch-profile cases of
scripts/test_nvx_tools.py, and check require_file, share kinds,
host-path identity, and bind and hard-link identity on temporary
directories on Linux and Windows.

Part of #359.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the pure checks that scripts/nvx.py applies to the options of
nvx.py sandbox and nvx.py run to nvx::host::sandbox::options, so that
the nvx-rs CLI keeps their order, semantics, and messages and stays a
thin layer over the library.

- SandboxOptions::check: the checks of command_sandbox that precede
  the launch, in its order: --dry-run outside run, --environment with
  --environment-file, managed exec options outside exec, the managed
  exec environment, network policy options outside run and provision,
  systemd entrypoints, --outcome-report outside run and exec, share
  options without --mount, --mount outside run and provision, --net
  without --network-profile or the reverse, and a missing --layer or
  --scratch.
- sandbox_mounts, from _sandbox_mounts: with one --mount, every
  --mount-deny, --mount-allow, and --mount-write names a path in it;
  with several, each names a path in the --mount before it. Each
  share takes the kind that mount_kind finds for its host path.
- validate_sandbox_systemd_policy and SYSTEMD_ENTRYPOINTS: a distro
  layer's manifest, next to it, must be a JSON object of format 1 that
  names the layer and its SHA-256 digest and lists packages with
  names, none of them systemd. The format is compared as == compares
  it, so true and 1.0 match, and the manifest is read with universal
  newlines, as read_text reads it.
- validate_run_share_targets, from _validate_run_share_targets: the
  guest targets of several nvx.py run shares, or of a shared file.
- validate_exec_environment and exec_environment_from_file: at most
  256 nonempty KEY=VALUE entries of at most 4096 UTF-8 bytes, without
  NUL or a repeated key, from --environment or from a JSON array in a
  file of at most 1 MiB, as encode_exec_environment checks them.

These checks fail with the messages of Python's JSON decoder and UTF-8
codec, so the private python module gains them:

- python::json keeps its API, its hooks, and its arena, but its reader
  is now a port of the C scanner of CPython 3.14, scan_once_unicode
  and the functions that it calls, with the checks of
  JSONDecoder.decode around them. It accepts what the previous reader
  accepted, as the egress policy fixture and tests confirm, and a
  refusal now carries the message and the position of Python's
  JSONDecodeError, such as Expecting ',' delimiter, instead of the
  offset at which reading stopped, which is not always Python's
  position: an unterminated string fails at its opening quote.
  syntax_error formats it as str() of the error, with its line,
  column, and character. Loads are the hooks of json.loads without
  arguments, whose int refuses more than 4300 digits, and
  Document::get reads a member as the dict that Python builds does.
- python::text gains PyText::encode_utf8 and decode_utf8, with the
  messages of UnicodeEncodeError and UnicodeDecodeError, so PyText is
  the one string type that holds surrogates.
- python::path gains name and with_name, and python::fs exists.

Argument parsing and type conversion stay with the CLI, as do the
lifecycle operations.

The unit tests port the option cases of scripts/test_nvx_tools.py:
the attribution of policy paths to shares, file shares, the systemd
policy, the run share targets, and the environment rules, with the
environment files and distro manifests on temporary files. The JSON
and codec errors have tests of their own.

Part of #359.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Record what the Python code does with fixed and seeded random inputs,
and compare the Rust port with those records in golden tests, so that
the port stays faithful byte for byte without Python at test time,
also after nvx.py delegates to nvx-rs and the Python code goes away.

scripts/nvx_rs_fixtures.py now also writes three fixtures to
nvx-rs/tests/fixtures, which its --check covers with the others:

- python.json: the str.isspace, decimal digit, and str.isprintable
  tables; repr, str.split(), str.strip(), int, and uuid.UUID of
  strings; str(), parts, name, parents, and is_absolute of pure paths,
  joins, and Windows path equality; ntpath.normpath; and the values
  and errors of json.loads and of the UTF-8 codec.
- sandbox.json: parse_workload_identity, SandboxLayer.parse,
  parse_mount_value, validate_mount_target, aggregate_child_name,
  SandboxMount with its OpenVMM arguments, command-line fragment, and
  absolute paths, and SandboxLaunch with its OpenVMM arguments and
  kernel command line. The launches include, for each kind of share
  set, command lines that fill the 1024-byte budget exactly and that
  exceed it by one byte.
- sandbox_options.json: _validate_run_share_targets, _sandbox_mounts,
  encode_exec_environment, the --environment-file rules of
  command_sandbox, which runs with exec_workload patched, and
  _validate_sandbox_systemd_policy, on temporary files.

The generator patches sandbox.Path with pure POSIX and Windows paths
whose current directory is fixed, so that the cases of both flavors
come out the same on any host: CPython 3.14 writes identical files on
Linux and Windows. It writes ASCII JSON with one case per line. The
random corpus is mostly valid input, so that most cases reach the
arguments and command lines, and invalid at a fixed rate. Each
fixture has a random generator of its own, so the egress policy
fixture is unchanged.

The Rust golden tests read the fixtures with serde_json, run the same
inputs through the port, and report each case that differs. They run
the cases of both path flavors on every host, through the internal
constructors that take a flavor and a fixed current directory, and
the file cases on temporary files. The JSON cases go through
python::json with the hooks of json.loads, so they check the reader
that the egress policy compiler shares. .gitattributes marks the
fixtures, about 1.2 MB, as generated, so that reviews collapse them.

Part of #359.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend nvx::host::artifacts to every artifact of the guests that
nvx.py runs, so that nvx-rs can stand in for the runtime lookups of
scripts/nvx_tools/common.py: artifact_path, openvmm_binary_path, and
the require_file checks of nvx.py run and nvx.py sandbox.

- Guest describes the Alpine, Ubuntu, and Azure Linux guests of
  scripts/nvx_tools/guests.py: their names, distributions, initramfs
  images and package manifests, and the Ubuntu distro layer,
  ubuntu-distro.erofs, with its manifest. Guest::from_name fails as
  guest_descriptor does.
- ArtifactDir names them in a repository checkout, as build/NAME and
  openvmm/target/release/openvmm[.exe], where local builds write them
  and where nvx.py download installs a release, or in an extracted
  release package, as guest/NAME and bin/openvmm[.exe], where
  package_release stages them. ArtifactDir::from_dir tells the
  layouts apart as Artifacts::from_dir does, and makes the directory
  absolute without resolving links, so callers pass the resolved
  REPO_ROOT of nvx.py as it is.
- require_openvmm, require_kernel, and require_initramfs check the
  files with the messages of nvx.py: "OpenVMM release binary not
  found: PATH", "Linux direct kernel not found: PATH", and
  "DISTRIBUTION initramfs not found: PATH". sandbox_artifacts checks
  the OpenVMM executable, the kernel, and the Alpine initramfs in the
  order of nvx.py sandbox run and start, which call the last
  "initramfs".
- paths lists every artifact as ArtifactPaths, which serializes as
  JSON, with the guests in nvx.py's order, for nvx-rs artifacts.

Like nvx.py, the lookup reads no SOURCE-MANIFEST.json, and checks only
the files that a command needs. Artifacts and its discovery, which
aci_edge_sandboxes uses, are unchanged: Artifacts::from_dir only
shares its layout detection, and the systemd policy of the sandbox
options now takes the distro manifest suffix from here.

The tests cover both layouts, the layout of this repository, the
order and messages of the checks, and the JSON form.

Part of #359.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Describe in nvx-rs/README.md the nvx::host::sandbox module, with its
mount and options submodules, and the guest artifact lookup that
ArtifactDir, Guest, and ArtifactPaths add to nvx::host::artifacts,
beside the discovery that aci_edge_sandboxes uses. A new Python parity
section explains which Python semantics the ports keep, including the
egress policy compiler, the private python module that implements
them, and the fixtures that scripts/nvx_rs_fixtures.py records, with
the commands that regenerate and check them, the CPython 3.14 that
they need, and why CI does not check them; the egress policy section
points to it. The features table names sha2, which the host feature
now enables.

Update the trees and the nvx-rs summary of doc/project-structure.md
for the sandbox launch profile and the golden fixtures.

Part of #359.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/fix-issue-359-launch-profile branch from 1fa32b8 to 50f3d18 Compare October 19, 2026 12:20
Copilot AI balanced review requested due to automatic review settings October 19, 2026 12:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The extensive cross-platform compatibility layer and unsafe Win32 filesystem calls warrant platform-specific human review.

No open issues 🎉

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit f7e8e3e into dev Oct 19, 2026
33 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/fix-issue-359-launch-profile branch October 19, 2026 13:12
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 19, 2026
Add the cli feature, which implies host, and the nvx-rs binary, with
required-features = ["cli"]: the command-line interface to which
nvx.py will delegate its sandbox, egress-policy, and artifact steps
(#359). This commit adds the command-line framework and three
commands; the managed lifecycle of nvx-rs sandbox follows in the next
commit, and until then provision, start, exec, stop, and deprovision
fail after their option checks.

- nvx-rs [--root PATH] artifacts prints ArtifactDir::paths as JSON,
  as json.dumps(value, indent=2, sort_keys=True) writes it. The root
  is --root, which nvx.py will pass, or else the release whose bin/
  holds the executable, or the checkout whose nvx-rs/target/ holds
  it, either with its SOURCE-MANIFEST.json. The NVX_OPENVMM variable
  names an OpenVMM executable to run instead of the root's, such as a
  test double; a relative path is made absolute.
- nvx-rs egress compile PATH prints the rules of compile_policy_file
  as {"allow": [...], "deny": [...]}, each list in the order in which
  OpenVMM receives them, and fails with the compiler's message.
- nvx-rs sandbox ports the sandbox parser and command_sandbox of
  scripts/nvx.py: every option with its name, default, type, and
  choices, and the checks of SandboxOptions::check, the egress policy
  file, the mounts, the launch, and the systemd policy, in Python's
  order and with its messages. A one-shot run prints ">> COMMAND",
  quoted as subprocess.list2cmdline quotes it on Windows and as
  shlex.join does elsewhere, and unless --dry-run runs OpenVMM with
  the standard streams and exits with its status. The artifact checks
  keep the messages and order of nvx.py, and --hypervisor auto
  resolves to whp on Windows and to kvm elsewhere.

The parser reproduces argparse step by step: --opt value and
--opt=value; values that start with a dash only after "=", unless
they look like negative numbers or hold a space; the alternation of
options and positionals; the optional operation positional, which
defaults to run; repeated options that accumulate; and the messages
and status 2 of usage errors, with unrecognized arguments reported by
the top-level parser after the subcommand parsed. It does not expand
abbreviations, since nvx.py will pass canonical options; that is an
intended deviation. Help and usage are argparse's for 80 columns.

Errors print "error: MESSAGE" and exit with status 1, as main() of
nvx.py does for a ScriptError. Ctrl-C prints "Interrupted" and exits
with status 130. A SIGINT handler on Unix, installed only if SIGINT is
not ignored, and a console control handler on Windows record it, and
a run then gives OpenVMM 0.25 s to exit before it kills it on POSIX,
as subprocess.run does, and waits for it on Windows, where Python
cannot interrupt the wait either. Text ends its lines as Python's
print ends them, with \r\n on Windows.

The private python module of #594 and #595 gains dumps, json.dumps
with indent=2, sort_keys=True, and ensure_ascii; shell,
subprocess.list2cmdline, shlex.quote, and shlex.join; and float() in
builtins for positive_float. The sys and platform modules gain the
interrupt primitives, and windows-sys its console bindings.

Unit tests compare the parser with argparse, the Python helpers with
CPython, and cover the discovery of the artifact root. Golden
scenarios recorded from nvx.py follow in a later commit.

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 19, 2026
Record what nvx.py sandbox does, as a command, and replay it with
nvx-rs in tests that need no Python, so that the CLI stays faithful to
nvx.py after PR D deletes the Python code.

scripts/nvx_rs_fixtures.py, the generator of #595, now also writes
nvx-rs/tests/fixtures/cli.json, which its single --check covers. It
builds nvx-rs and nvx-fake-openvmm with cargo and plays scenarios in
fresh temporary roots, each with fake guest artifacts and a copy of
nvx-fake-openvmm as the root's OpenVMM executable:

- 24 usage errors and helps of the sandbox parser, 69 option and
  launch checks of command_sandbox, 11 dry runs, 11 one-shot runs,
  the lifecycles of sandboxes in every configuration format, and 8
  scenarios of the refusals and failures of the lifecycle operations,
  215 steps in all on POSIX and 209 on Windows, where a caller-owned
  share, and so format 3, is refused;
- nvx-rs egress compile and nvx-rs artifacts, from the Python
  functions that they port, compile_policy_file and the paths of
  scripts/nvx_tools/common.py and guests.py.

Each step records its exit status, standard output and error, and
the files that it leaves, such as config.json in formats 1 to 6,
runtime.json, the state directory's entries and modes, outcome
reports, and the command line that start passed to OpenVMM. The
generator runs nvx.py in a subprocess, with artifact_path and
openvmm_binary_path patched, unbuffered and with UTF-8 standard
streams, no terminal size, and no colors. It replaces the root, as
printed and as JSON escapes it, process IDs, start times, and random
identifiers with placeholders, and the program names of nvx.py with
those of nvx-rs, whose parser names have the same length, so usage
wraps alike. The posix and windows sections hold the scenarios of each
kind of host, recorded there; the script rewrites or checks the
section of the host that runs it and keeps the other. Recording needs
CPython 3.14.5 or later, whose argparse quotes the choices of an
invalid choice, as nvx-rs does.

The script then plays every scenario with nvx-rs, and each lifecycle
with nvx.py and nvx-rs taking turns both ways, so that each provisions,
starts, executes in, stops, and deprovisions a sandbox of the other,
and fails if any step differs from nvx.py. Both sections pass on
Windows and Linux.

nvx-rs/tests/cli.rs replays the section of its host with nvx-rs,
four scenarios at a time, in canonical temporary roots. It also
checks what nvx-rs adds: a start against guests that lack per-execution
environments or predate the features request, the default root of the
executable in a checkout, a relative NVX_OPENVMM, the top-level usage
errors, and on Linux, Ctrl-C during a run, which ends OpenVMM, and
during an exec, which leaves the sandbox running. .gitattributes marks
the fixture, about 370 KB, as generated.

This changes the generator of #595 only by adding the CLI section.

Part of #359.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants