Skip to content

nvx-rs: retire the egress policy fixture and read policies as strict JSON - #828

Merged
Pedro Henrique Penna (ppenna) merged 8 commits into
devfrom
agents/issue-729-retire-egress-fixture
Oct 25, 2026
Merged

Pedro Henrique Penna (ppenna) merged 8 commits into
devfrom
agents/issue-729-retire-egress-fixture

Conversation

@ppenna

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

Copy link
Copy Markdown
Contributor

Stacked on #824. This branch is #824's head (ed3d733e9) plus four commits. Until #824 merges, the diff here includes its four commits, so review only the last four: 306a863c2, d79dff947, df0c7da14, and 8d08b871f.

Fixes #729, as the second of its two pull requests. #824 added tests whose expectations come from the policy format instead of from tests/fixtures/egress-policy.json, the recording of what the retired Python compiler did. This retires the fixture and makes the compiler Rust-native: it reads policy files as strict RFC 8259 JSON, quotes values in its messages as Rust does, and names files as they are given. Every policy that compiled compiles to the same rules.

What changed

Retire the fixture (306a863c2). Deletes egress-policy.json, NVX_EGRESS_POLICY_FIXTURE, and the tests that read it: every_fixture_case_compiles_as_python_compiled_it, unreadable_files_fail_as_in_python, paths_print_as_pathlib_prints_them, the run of the properties over its inputs, and compiled_rules_read_back_as_native_rules_lower_to_themselves, whose property now reads back the rules of generated policies. The fixture's other sections become tests: a grammar test checks that a missing file and a directory fail with the error code io and name the path, and the unit test of python::path gains the fixture's 30 paths that it lacked. The "Egress policies" coverage family leaves scripts/nvx_rs_coverage.py and doc/ci.md.

Strict JSON (d79dff947). The new policy/json.rs reads a file with serde_json, through a seed that refuses a repeated field, naming its object's path, and arrays and objects nested more than MAX_JSON_DEPTH, 32, deep (a policy needs six). The compiler checks the serde_json::Value with the checks, order, and messages of before, and no longer goes through the CPython emulation of python::json, so lowering, address, and openvmm_arguments take &str instead of PyText, and Document::from_value goes. Another crate of a build can enable serde_json's arbitrary_precision feature for every crate that uses it, with which serde_json hands a visitor each number that isn't a 64-bit integer as a map of one field, and -0 as the integer 0; the seed then reads those numbers as serde_json reads them without the feature, so what a file holds doesn't depend on the crates beside nvx. It tells such a map from an object of the document with that field, which serde_json's own Value takes for a number, by the field's value: serde_json hands the text of a number as an owned string, and never a string of a document so.

  • Now refused as invalid JSON: NaN, Infinity, numbers beyond a double, unpaired surrogates, a byte-order mark, and trailing text. No policy that compiled used them.
  • New messages: egress policy file PATH is not UTF-8: REASON and … is not valid JSON: REASON, with serde_json's reason and position, where every refused document was failed to read egress policy file: PATH; … nests arrays and objects more than 32 deep at line L column C; and OBJECT has duplicate field …, where OBJECT is the object's path, or egress policy for the root.
  • serde_json reads -0, and integers beyond 64 bits, as doubles: -0 is not an integer, and an integer beyond 64 bits is out of range, where it was "more than 64 digits".

Rust's quoting (df0c7da14). Unknown and repeated fields, and CIDRs that are invalid or name an IPv6 scope, are quoted as {:?} quotes them: egress policy has unknown field "other" and allow[0].to[1].cidr is not a valid IPv4 or IPv6 CIDR: "10.0.0.256/8". openvmm_arguments shares these messages, so those of NetworkPolicy and aci_edge_sandboxes change too, and the breaking changes of the aci_edge_sandboxes README quote them.

Paths as given (8d08b871f). The compiler opens the path that it's given and names it as Path::display shows it, instead of as pathlib normalized it: {root}/./missing.json stays so. python_path, PythonPath, render_utf16, and render_surrogateescape go, since the compiler was their only user.

The golden scenarios of tests/fixtures/cli.json change by hand, as #722 allows, one or two records of each system for each change: a policy file that isn't JSON, an unknown field, and a missing file named with ./. The README's "Egress policy files" and doc/run.md describe the strict format.

The gate of #729

Validation

  • nvx-rs: cargo fmt --check; cargo clippy -D warnings with --all-targets --all-features at each commit, --lib --no-default-features, and for x86_64-unknown-linux-gnu; cargo test with all, default, and no features, the CLI's golden scenarios included; cargo doc with -D warnings; cargo +1.89 check --all-targets --all-features; and cargo check --target aarch64-apple-darwin.
  • aci_edge_sandboxes: cargo fmt --check, cargo clippy, and cargo test with all features.
  • With --features serde_json/arbitrary_precision, the tests of policy/json.rs and the grammar tests pass, and {"port": {"$serde_json::private::Number": "1e400"}} reads as an object, as without it. The reference model of the properties reads JSON with a visitor of its own, which assumes serde_json's default features, as the tests do.
  • scripts/test_nvx_tools.py and scripts/test_microvm_tests.py, with NVX_RS set to this build; scripts/test_nvx_rs_coverage.py, scripts/test_markdown_links.py, scripts/check_markdown_links.py, and ruff.
  • The microVM scenario that refuses policy files with a bad endPort matches their error code and field path, which don't change, so scripts/nvx_tools/microvm_tests.py needs no change. The hypervisor scenarios are left to CI.

Interactions

#825, the first pull request of #728, adds CLI scenario tests, two of which expect forms that this changes: the "unknown field" case of tests/cli/egress.rs expects 'other', and tests/cli/validation.rs expects {root/missing.json} for a file given as {root/./missing.json}. Whichever of the two lands second updates them.

The tests that are to replace the egress policy fixture (#729) check
that an action is refused exactly when it needs more than the 256
native rules that OpenVMM accepts. A refused action emits nothing, so
a test has to learn how many rules the action needs some other way:
by lowering its rules under a higher limit.

Split the two steps that need it out of the functions that run them,
without changing what they do:

- lowering::lower_within lowers the rules of an action within a limit
  of native rules that its caller chooses. lower keeps OpenVMM's limit,
  MAX_RULES_PER_ACTION, and the message of a refusal still names it.
- policy::parse_category parses the rules of an action, which
  compile_category then lowers, so that a test can parse an action and
  lower it under another limit.

Every policy compiles to the same rules or fails with the same message
as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The egress policy compiler is checked against a fixture of what the
retired Python compiler did with 784 inputs (#722). Its expectations
are recorded output, which a deliberate change of behavior can only
edit by hand. Add tests whose expectations come from the format that
doc/run.md documents instead (#729), without changing the compiler:

- A reference model, in policy/tests/model.rs, which shares no code
  with the compiler. It reads a policy file as the format describes it,
  with serde_json and the standard library's address parsers, and
  refuses what the format refuses. It decides whether a rule matches a
  packet of a family, address, protocol (TCP, UDP, ICMP, or another),
  and port, and parses native rules, CIDR[:PROTOCOL[:PORT[-LAST]]],
  which it matches as doc/run.md describes OpenVMM's rules.
- An exact comparison: for each family and protocol, the boundaries of
  the source and native networks, exceptions, and port ranges partition
  the packets into cells within which every rule matches all packets or
  none, and the model evaluates one point of each cell, ports 0 and
  65535 included. The model also finds redundant native rules, and
  checks that native rules are canonical: written as it writes them,
  without host bits, sorted without duplicates, with no network that
  holds or merges with another of the same selector, and no overlapping
  or adjacent port ranges of one network.
- A proptest generator, in policy/tests/generator.rs, ported with its
  probabilities from Corpus, which drew the fixture's seed-359 policies
  in the retired scripts/nvx_rs_fixtures.py (36c5bf7^): overlapping
  networks in dense IPv4 and IPv6 neighborhoods, exceptions, ports at
  range boundaries, the MXC and flat forms, every spelling of a
  network, crowds of rules near the limit, broken policies, and files
  written as json.dumps writes them and sometimes broken. A crowd of
  address-only and protocol-wide rules, which Corpus did not draw,
  takes the first steps of the lowering to the limit too.
- Properties, in policy/tests/properties.rs: each action lowers to
  canonical native rules, none redundant, that match exactly what the
  model matches; an action is refused exactly when it needs more than
  256 native rules, which the tests learn by lowering it within a
  lifted limit of 1024, and then under either name, alone or beside the
  other action, so allow and deny lower independently; the compiler
  refuses what the model refuses, and compiles what it reads unless an
  action needs too many rules; reordering rules, repeating them, and
  splitting a network into its halves change no lowering; a policy's
  text, its value, and the NetworkPolicy rules of openvmm_arguments
  compile alike and deterministically, and with the cli feature
  compiled rules read back as native rules lower to themselves; and
  arbitrary bytes and broken policies compile or fail with an error.
- networks_at_the_ends_of_each_family_have_the_properties checks the
  properties of networks at the ends of each family's addresses, such
  as ::/0 less its last address, without ports and with them, which
  the generator draws only by chance. Without it, a summary that stops
  one network short when the next address is ffff:...:fffe passes
  every other test, the fixture's included.
- every_fixture_case_has_the_properties_that_replace_the_fixture runs
  the properties once over the fixture's 784 inputs, where the model
  agrees with the compiler on every one, before a later change deletes
  the fixture.

Checks of the model itself pin how it reads CIDRs and native rules,
that it finds what it looks for, and that it reads every spelling of
the generator as the network that it spells.

Applied one at a time to the lowering and the address parsing, mutants
that write a range of one port as a range, extend a swept range one
port too far, leave the rules unsorted, keep networks that other rules
cover, leave adjacent intervals unmerged, ignore exceptions, lower
`any` with ports to TCP alone, forget what protocol-wide rules cover,
stop summaries at /16, accept ports for ICMP, or let the families or
the address-only step overrun the limit each fail several of the
properties. The properties take about 12 seconds in a debug build.

The README describes the tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The fixture of the retired Python compiler holds hand-written cases
that reach every message of the policy format, and checks each message
byte for byte (#722). Messages may change once the compiler is
Rust-native, but what a refusal names and its error code may not, so
add a test for each rule of the format, in policy/tests/grammar.rs,
that checks them instead (#729):

- the root is an object of allow and deny arrays, and rules are
  objects;
- MXC and flat fields do not mix, fields of no form are refused, case
  included, and a field appears once in an object;
- a destination needs one CIDR string, which is an IPv4 or IPv6
  network, in every spelling that the format reads, whose host bits
  are cleared, and which names no IPv6 scope, with or without host
  bits (#462); exceptions lie within their network and family;
- protocols are tcp, udp, icmp, or any, a null protocol included;
  ports need tcp, udp, or any, and a flat rule's port needs a protocol;
  ports are integers from 1 to 65535, which have at most 64 digits;
  endPort needs a port and does not precede it;
- to and ports are nonempty arrays, and each action lowers to at most
  256 native rules, which both families share;
- files are at most 1 MiB of UTF-8 JSON, and deep nesting is refused
  without exhausting the stack.

Each refusal must have the error code unsupported (#724),
ErrorCode::Unsupported, name the field path, or the file or the
repeated field where messages name those, and give a fragment of its
reason that tells apart the rules that refusals at one path break.
Without the fragment, a mutant that refuses a rule that mixes forms as
one with unknown fields, at the same path and with the same code, would
pass. Each test also checks what the rule accepts.

Applied one at a time, mutants that accept IPv6 scopes, unknown or
repeated fields, mixed forms, port 0, ports below their first port,
IPv4 addresses with leading zeros, or a file of one byte over the
limit, refuse hostmasks, or move the limits of integers and native
rules by one each fail one of these tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The coverage measurement of #726 selects the egress policy family of
golden tests by name, so that each step of #722 can show what only the
family's tests reach: the lines and branches that all tests reach and
the tests outside the family don't. The family named three tests, but
two more replay the fixture's policies:
compiled_rules_read_back_as_native_rules_lower_to_themselves, with the
cli feature that the measurement enables, and
every_fixture_case_has_the_properties_that_replace_the_fixture, which
runs the properties of #729 over its inputs. Outside the family, they
would reach whatever only the fixture's inputs reach, so the
measurement would understate what deleting the fixture loses.

Name all five tests in the family, and in its table in doc/ci.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tests/fixtures/egress-policy.json recorded what the retired Python
compiler did with 784 inputs, and its tests replayed them byte for byte
(#722). The reference model, properties, and grammar tests of the
previous change check the compiler from the format instead, and ran
once over the fixture's inputs, so retire the fixture without changing
the compiler (#729):

- Delete the fixture, NVX_EGRESS_POLICY_FIXTURE, and the tests that
  read it: every_fixture_case_compiles_as_python_compiled_it,
  unreadable_files_fail_as_in_python,
  paths_print_as_pathlib_prints_them, the run of the properties over
  its inputs, and
  compiled_rules_read_back_as_native_rules_lower_to_themselves, whose
  property now reads back the rules of generated policies.
- Map the fixture's other sections to tests: a grammar test checks
  that a missing file and a directory fail with the error code io and a
  message that names them, and the unit test of python::path checks the
  fixture's 30 paths that it lacked, with their posix and windows
  forms. The `\\?\` device paths among them reach a branch of the
  Windows parser, which PurePath shares, that only the fixture reached.
- Drop the fixture's family from scripts/nvx_rs_coverage.py and
  doc/ci.md, and its mentions from the README, .gitattributes, and the
  documentation of python::json.

The coverage measurement of the previous change (run 40018023086) shows
what only the fixture's tests reached, on Linux and Windows alike: the
branch of python/path.rs that the paths now reach, and the saturation
of integers beyond an i128 in policy::port_value, which reading
policies as strict JSON replaces.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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.

🟢 Approval recommended

Static review of the four scoped commits found no blocking defects in strict parsing, fixture retirement, or downstream diagnostic updates.

No open issues 🎉

What changed in this PR

Completes the egress-policy fixture retirement from #729, building on #824’s specification-based tests. The four scoped commits make NVX’s policy reader Rust-native while preserving the compiled rules of previously accepted policies.

Changes:

  • Retires the recorded policy fixture and its replay tests, retaining grammar and property-based coverage.
  • Adds strict JSON parsing with duplicate-field rejection and a 32-level nesting limit.
  • Uses Rust-style quoting and paths as supplied, updating diagnostics, expectations, and documentation.
File Description
scripts/​nvx_rs_coverage.py Removes the retired egress fixture coverage family.
nvx-rs/​tests/​fixtures/​cli.json Updates recorded JSON, quoting, and path diagnostics.
nvx-rs/​src/​host/​sandbox/​plan/​tests.rs Updates the quoted CIDR error expectation.
nvx-rs/​src/​host/​python/​path.rs Removes policy-only path emulation and preserves path cases in unit tests.
nvx-rs/​src/​host/​python/​json.rs Removes the unused value adapter and updates commentary.
nvx-rs/​src/​host/​network/​policy/​tests/​properties.rs Checks native-rule read-back using generated policies.
nvx-rs/​src/​host/​network/​policy/​tests/​model.rs Aligns the reference reader with strict JSON.
nvx-rs/​src/​host/​network/​policy/​tests/​grammar.rs Covers strict parsing, diagnostics, and unreadable files.
nvx-rs/​src/​host/​network/​policy/​tests/​generator.rs Updates commentary to identify the retired fixture.
nvx-rs/​src/​host/​network/​policy/​tests.rs Removes fixture replay tests and updates error expectations.
nvx-rs/​src/​host/​network/​policy/​mod.rs Compiles JSON values directly and opens paths as supplied.
nvx-rs/​src/​host/​network/​policy/​json.rs Adds strict parsing with duplicate and depth checks.
nvx-rs/​src/​host/​network/​mod.rs Removes Python text conversion and updates diagnostics tests.
nvx-rs/​src/​host/​network/​lowering.rs Accepts string slices and quotes invalid CIDRs.
nvx-rs/​src/​host/​network/​address.rs Replaces Python text parameters with string slices.
nvx-rs/​README.md Documents strict policies and specification-based testing.
doc/​run.md Documents policy parsing limits and rejection messages.
doc/​ci.md Removes the retired coverage family listing.
aci_edge_sandboxes/​src/​openvmm/​mod.rs Updates the quoted IPv6-scope error expectation.
aci_edge_sandboxes/​README.md Updates documented CIDR diagnostic wording.
.gitattributes Removes the retired fixture’s generated-file designation.

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-729-retire-egress-fixture branch from 926ff50 to bad2ef8 Compare October 23, 2026 22:13
Copilot AI balanced review requested due to automatic review settings October 23, 2026 22:13

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.

🟡 Changes recommended

The arbitrary-precision adapter confuses user-supplied objects with synthetic number maps, making validation depend on dependency features.

1 open issue

Review effort: Balanced

Comment thread nvx-rs/src/host/network/policy/json.rs Resolved
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-729-retire-egress-fixture branch from bad2ef8 to 07c85bb Compare October 23, 2026 22:54
Copilot AI balanced review requested due to automatic review settings October 23, 2026 22:54

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 security-sensitive JSON reader relies on serde_json’s private arbitrary-precision representation, warranting human review of feature-dependent parsing and depth enforcement.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

The egress policy compiler read files as Python's json.loads reads
them, through the CPython emulation of nvx::host::python::json, so that
it failed with Python's messages: NaN and Infinity were numbers, an
integer of more than 64 digits failed as such, a repeated field failed
without saying where, and every document that json.loads refused failed
as `failed to read egress policy file: PATH`, without a reason. With
the fixture retired, read policy files as RFC 8259 JSON instead, as

- policy/json.rs reads a file with serde_json, which refuses NaN,
  Infinity, numbers beyond the range of a double, unpaired surrogates, a
  byte-order mark, and trailing text, through a seed that refuses an
  object that repeats a field, naming the object's path, and arrays and
  objects nested more than MAX_JSON_DEPTH, 32, deep, which a stack would
  never reach. A policy needs six. Another crate of a build can enable
  serde_json's arbitrary_precision feature, with which serde_json hands
  the seed each number that is not a 64-bit integer as a map of one
  field, and -0 as the integer 0; the seed then reads those numbers as
  serde_json reads them without the feature, so that what a file holds
  does not depend on the crates beside nvx. It tells such a map from an
  object of the document with that field, which serde_json's own Value
  takes for a number, by the field's value: serde_json hands the text of
  a number as an owned string, and never a string of a document so.
- The compiler checks and lowers the serde_json::Value that it reads,
  which compile_policy already took, with the checks, order, and
  messages of before. It no longer parses a Python document or text,
  so lowering, address, and openvmm_arguments take &str instead of
  PyText, and python::json loses Document::from_value, which only the
  compiler used.
- A file that is not UTF-8 or not JSON fails as `egress policy file
  PATH is not UTF-8: REASON` or `... is not valid JSON: REASON`, with
  serde_json's reason and position, one nested too deep as `egress
  policy file PATH nests arrays and objects more than 32 deep at line L
  column C`, and a repeated field as `OBJECT has duplicate field
  'NAME'`, where OBJECT is the path of the object, or `egress policy`
  for the root. Missing and unreadable files and oversized ones fail as
  before.
- serde_json reads -0, and integers beyond 64 bits, as doubles. A port
  is an integer written without a fraction or an exponent, so -0 fails
  as not an integer, while a double beyond 64 bits, which only an
  integer that large or an exponent writes, is out of range.

Every policy that compiled compiles to the same rules: the properties
and grammar tests, which the fixture's 784 inputs all satisfied, check
the lowering. The grammar tests check the new refusals: repeated fields
by their object's path, NaN, Infinity, numbers beyond a double, and
unpaired surrogates as invalid JSON, -0 and integers beyond 64 bits as
ports, and nesting up to and beyond the limit; the model reads files
within the limit too. Unit tests of policy/json.rs check that it reads
values as serde_json reads them, and numbers alike when serde_json hands
them as with arbitrary_precision, but not an object with the field that
holds them, refuses what RFC 8259 refuses, names the object of a
repeated field, and limits nesting. Two ported tests now expect
`duplicate field`, and an integer beyond a double as invalid JSON. The
CLI golden scenarios of a policy file that is not JSON, two for each
system, give the reason, as #722 lets a change edit them by hand. The
README and doc/run.md describe the format.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Messages of the egress policy compiler quoted values as Python's did:
unknown fields in single quotes, as `egress policy has unknown field
'other'`, and CIDRs not at all, as in `allow[0].to[1].cidr is not a
valid IPv4 or IPv6 CIDR: 10.0.0.256/8`, so that a value with a space,
a quote, or nothing at all read ambiguously. #729 asks for Rust's
quoting, so quote these values as `{:?}` does:

- unknown fields, as in `egress policy has unknown field "other"`;
- repeated fields, as in `allow[0].ports[0] has duplicate field
  "port"`; and
- CIDRs that are invalid or name an IPv6 scope, as in
  `allow[0].to[1].cidr is not a valid IPv4 or IPv6 CIDR:
  "10.0.0.256/8"`.

Field paths, networks that the compiler wrote, and numbers stay
unquoted. openvmm_arguments shares the messages, so the rules of
NetworkPolicy, and of aci_edge_sandboxes, quote these values too.

The grammar tests check that each of these messages ends with the
quoted value, and the tests that compare whole messages, of the
compiler, of NetworkPolicy, of persisted sandbox plans, and of
aci_edge_sandboxes, expect the quotes. The CLI golden scenarios of an
unknown field, one for each system, quote it, as #722 lets a change
edit them by hand. The nvx-rs README and the breaking changes of the
aci_edge_sandboxes README quote the values of their examples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The compiler named and opened a policy file as Python's
str(pathlib.Path(path)) normalized it, through python::path's
python_path: it dropped empty and `.` components, wrote `\` for `/` on
Windows, and opened `policy.json` for `policy.json/`. With the
fixture's paths test retired, nothing asks for pathlib's spelling, and
the compiler was python_path's only user, so open the path as given and
name it as Path::display shows it: a policy file at `{root}/./x.json`
is now named so, instead of `{root}/x.json` (#729).

The compiler no longer uses the CPython emulation of nvx::host::python.
python_path and PythonPath go, and so do render_utf16 and
render_surrogateescape, which printed its paths, and their tests;
posix_str and windows_str stay as helpers of the tests of the parser
that PurePath shares with them.

The CLI golden scenarios of a missing policy file named with `./`, one
for each system, keep its `./`, as #722 lets a change edit them by
hand, and the README says that messages name files as they were given.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/issue-729-retire-egress-fixture branch from 07c85bb to 8d08b87 Compare October 23, 2026 23:17
Copilot AI balanced review requested due to automatic review settings October 23, 2026 23:17

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 custom JSON deserializer relies on feature-dependent private serde_json number conventions that warrant final human review.

No open issues 🎉

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit 90e029d into dev Oct 25, 2026
42 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/issue-729-retire-egress-fixture branch October 25, 2026 02:14
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
tests/fixtures/cli.json held the 315 scenarios that scripts recorded
from the Python implementations of nvx.py sandbox and nvx.py run, and
nvx_rs_reproduces_the_recorded_scenarios replayed them byte for byte
once placeholders replaced the root and volatile values. The scenario
tests of tests/cli/ check what callers rely on in their stead (#728):
every scenario, and every regression that #722 traces, maps to them;
on Linux and Windows, the replay reaches no line or branch of
production code beyond the tests outside its family; and cargo-mutants
finds no mutant of src/host/cli that the replay catches and the other
tests of the CLI miss.

The fixture goes, with the replay, its normalization and population
helpers, and the mapping of tests/cli/scenarios.rs, whose tables of
cases are private again, as is Root::empty(), which only Root::new()
calls now. .gitattributes no longer marks the fixture as generated,
and the test of the guest binaries' source identity names another file
of tests/fixtures, root-tokens.json, which holds the cases of a root's
boot tokens rather than behavior recorded from Python, so that no
retirement of #722 removes it. The README describes the scenario tests
in a section of their own, which replaces Golden scenarios and records
how they replaced the transcripts, and its Python parity and Testing
sections point to it.

#828, which merged after #825 added the scenario tests, changed two
messages that they check, and recorded the change in the transcripts
alone: an unknown field of an egress policy is quoted as Rust quotes
it, "other" where Python wrote 'other', and a policy file that cannot
be read is named as it was given, ./ included, where Python normalized
its path. The tests of egress compile and of the refusals of run expect
these messages, which the transcripts no longer check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Build on the behavior-preserving specifications of #823 for the
retirement half of #730. Remove the six Python-oracle tests of
src/host/sandbox/golden.rs and their fixtures,
tests/fixtures/sandbox.json and tests/fixtures/sandbox_options.json.
Preserve all four accepted launch shapes, eight POSIX and Windows
records with identical contracts, as explicit argument, token, and
exact-budget regressions in the specification module rather than as
fixtures.

Remove the launch-profile family from the coverage inventory of
scripts/nvx_rs_coverage.py, and update its inventory test to recognize
the specification module. #828 and #830 retired the egress-policy and
CLI-scenario families on dev, so the inventory and its table in
doc/ci.md keep only the Python-semantics family, and doc/ci.md notes
this retirement beside that of the CLI family. Update the coverage and
mutation documentation, remove the temporary section mapping that #823
kept in the README, and drop the sandbox fixtures from the README's
list of Python records. The native input and API changes follow in
separate commits.

Validation on Windows, rebased on dev: 701 library tests and 51 CLI
tests pass with all features, and the coverage-script tests, Ruff, and
the Ruff format check pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Retire the Python-backed scalar compatibility layer of #730 in favor of
native sandbox values while preserving the existing wire contracts.

- Remove the public `Int` and `IntError` exports; use `Option<u64>` for
  the memory limit and `Option<u32>` for the process limit, including
  the bound that leaves room for the guest's init process.
- Share ASCII-digit-guarded `FromStr` parsing for UID and GID, the CLI
  resource limits, and the limits that sandbox configurations persist,
  which they keep writing as JSON integers.
- Use the canonical native `Uuid` parser while preserving tokens and
  all valid plan and configuration wire encodings.
- Reject malformed limits at the CLI usage stage with clear native
  errors: `--memory-max` and `--pids-max` fail with `argument-invalid`
  and exit status 2, naming the accepted ASCII decimal range, instead
  of Python's `invalid int value` and the later `--pids-max must be
  positive` refusal with exit status 1.
- Quote layer and persisted-plan values with Rust debug formatting.
- Remove the now-unused Python integer arithmetic, keep only the
  test-only historical Python UUID oracle, and narrow the visibility of
  the remaining private integer helpers.
- Update the scenario tests that replaced the CLI transcripts (#825,
  #830) to match: the usage table expects the double-quoted layer UUID
  and roles and the native `--memory-max` message, and the
  `--pids-max 0` case moves from the validation table, whose cases exit
  with status 1, to the usage table.
- Document the input contract and the API change in the README, whose
  Python parity section no longer lists the sandbox, nor UUIDs among
  the values that it checks as Python does; #828 already removed the
  egress policy compiler from that section.

Validation on Windows, rebased on dev: 702 library tests and 51 CLI
tests pass with all features, and `cargo fmt --all --check` passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Complete the kernel-token part of #730 with the existing
protocol::cmdline Linux lexer rather than Python's whitespace
splitting. Refuse control characters, Unicode whitespace, quotes, and
the bytes that Linux treats as whitespace, including the 0xa0 inside a
UTF-8 sequence, in entrypoints, workload arguments, and guest targets,
including the target of the single directory share of run.

Check reserved names after Linux's quote splitting and its aliasing of
hyphens and underscores, and also in the view of the guest's shell
bootstrap, which splits the expanded command line on IFS whitespace
without interpreting quotes, so that a quoted value such as
extra="ignored nvx_memory_max=max ignored" cannot override a configured
entrypoint, memory limit, process limit, or share; ordinary quoted
caller values remain accepted. Trim the caller's command line once,
before both reservation checks and rendering, so that the checks see
exactly the text that is rendered and leading Unicode whitespace, such
as U+2003 EM SPACE, cannot hide a reserved name. Confirm that Linux can
still see every generated root parameter, so that -- and unmatched
quotes cannot hide the appended boot settings. Keep the guest root
parser and the exact 1024-byte budgets for both share-delivery modes
and adapter plans.

Broaden the properties to quoted and dashed reserved inputs, leading
Unicode whitespace, and the known separator bytes, add explicit
hidden-root and quoted-override regressions for both share-delivery
modes, and make the host-path unit tests native-only. Update the
documentation, and the validation table of the scenario tests that
replaced the CLI transcripts (#825, #830): a sandbox argument with a
space is now refused for its whitespace rather than its spaces.

Build the Python compatibility code that only the CLI and the
historical tests use for them alone, so that the default-feature
library has no dead code, and add `cargo clippy --lib -- -D warnings`
to check-nvx-rs so that it stays so. Gate the Python split and strip
oracles to tests, and the integer, codec, and Unicode-digit helpers to
the cli feature and tests. Since #828 moved the egress policy compiler
to serde_json, and the previous commit the sandbox metadata readers,
only the CLI's state files use the Python JSON reader and Python
strings, so gate the json and text modules whole, as their dumps,
shell, and value siblings are. The README's Python parity section now
credits the nvx-rs CLI, their only remaining user, with the whitespace,
int, and json.loads semantics, and the module documentation of the JSON
reader no longer cites the retired egress and distro-manifest readers.

Validation on Windows, rebased on dev: 706 library tests and 51 CLI
tests pass with all features, and 630 library tests with default
features; `cargo fmt --all --check`, Clippy with warnings denied for
all targets with all features, for the default-feature library, and
for the library without default features, rustdoc with warnings
denied, and the Markdown link check pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Build on the behavior-preserving specifications of #823 for the
retirement half of #730. Remove the six Python-oracle tests of
src/host/sandbox/golden.rs and their fixtures,
tests/fixtures/sandbox.json and tests/fixtures/sandbox_options.json.
Preserve all four accepted launch shapes, eight POSIX and Windows
records with identical contracts, as explicit argument, token, and
exact-budget regressions in the specification module rather than as
fixtures.

Remove the launch-profile family from the coverage inventory of
scripts/nvx_rs_coverage.py, and update its inventory test to recognize
the specification module. #828 and #830 retired the egress-policy and
CLI-scenario families on dev, so the inventory and its table in
doc/ci.md keep only the Python-semantics family, and doc/ci.md notes
this retirement beside that of the CLI family. Update the coverage and
mutation documentation, remove the temporary section mapping that #823
kept in the README, and drop the sandbox fixtures from the README's
list of Python records. The native input and API changes follow in
separate commits.

Validation on Windows, rebased on dev: 701 library tests and 51 CLI
tests pass with all features, and the coverage-script tests, Ruff, and
the Ruff format check pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Retire the Python-backed scalar compatibility layer of #730 in favor of
native sandbox values while preserving the existing wire contracts.

- Remove the public `Int` and `IntError` exports; use `Option<u64>` for
  the memory limit and `Option<u32>` for the process limit, including
  the bound that leaves room for the guest's init process.
- Share ASCII-digit-guarded `FromStr` parsing for UID and GID, the CLI
  resource limits, and the limits that sandbox configurations persist,
  which they keep writing as JSON integers.
- Use the canonical native `Uuid` parser while preserving tokens and
  all valid plan and configuration wire encodings.
- Reject malformed limits at the CLI usage stage with clear native
  errors: `--memory-max` and `--pids-max` fail with `argument-invalid`
  and exit status 2, naming the accepted ASCII decimal range, instead
  of Python's `invalid int value` and the later `--pids-max must be
  positive` refusal with exit status 1.
- Quote layer and persisted-plan values with Rust debug formatting.
- Remove the now-unused Python integer arithmetic, keep only the
  test-only historical Python UUID oracle, and narrow the visibility of
  the remaining private integer helpers.
- Update the scenario tests that replaced the CLI transcripts (#825,
  #830) to match: the usage table expects the double-quoted layer UUID
  and roles and the native `--memory-max` message, and the
  `--pids-max 0` case moves from the validation table, whose cases exit
  with status 1, to the usage table.
- Document the input contract and the API change in the README, whose
  Python parity section no longer lists the sandbox, nor UUIDs among
  the values that it checks as Python does; #828 already removed the
  egress policy compiler from that section.

Validation on Windows, rebased on dev: 702 library tests and 51 CLI
tests pass with all features, and `cargo fmt --all --check` passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Retire the Python JSON and surrogate behavior from the sandbox metadata
checks for #730. Parse bounded environment files directly as
Vec<String> with serde_json and validate ordinary UTF-8 KEY=VALUE
strings. Read distro manifests as strict JSON, require the integer
format 1, and refuse non-finite numbers and unpaired surrogate escapes,
even in unused fields. Preserve the file, entry, and byte limits, the
binding of the artifact name and digest, the file seals, and the
independent refusal of systemd.

Read distro manifests with the strict JSON reader of egress policy
files that #828 added, which host::network::policy::json now shares as
read_document, with the name that its errors give the root of the
document. serde_json's own Value depends on the features of serde_json,
which another crate of a build may enable: with arbitrary_precision, it
reads an object that holds a number as that number, so that a format of
{"$serde_json::private::Number": "1"} would pass the integer check, and
it accepts numbers beyond the range of a double in unused fields. The
shared reader reads both as it does without the feature. It also
refuses an object that repeats a field, as `manifest has duplicate
field "format"` or `packages[0] has duplicate field "name"`, and arrays
and objects nested more than MAX_JSON_DEPTH, 32, deep, and a manifest
that is not UTF-8 now fails as such.

Quote sandbox values and paths with Rust debug formatting, and keep the
native JSON and I/O causes in diagnostics. Move the now-unused Python
JSON lookup and UTF-8 encode-oracle helpers out of production code; the
unrelated historical Python family keeps its test-only oracles. Update
the focused checks of tests/cli.rs that name these values and paths;
the transcripts of tests/fixtures/cli.json, which #830 retired, no
longer record them. Document the accepted-input and diagnostic changes.

Update the scenario tests that replaced the transcripts (#825, #830) to
match. The validation table expects the double-quoted --mount modes and
targets, and the serde_json causes of environment files that hold an
object or a non-string entry. Its environment-file cases name the file
as nvx-rs now quotes it: Root::expand, in the shared harness, gains the
{root:?} and {root/RELATIVE:?} placeholders, which quote a path as
Rust's Debug quotes it, so that the fragments match the escaped
backslashes of Windows paths as well as POSIX paths.

The manifest tests refuse that object-valued format as a mismatch, and
refuse numbers beyond a double, repeated fields at the root and in a
package, nesting beyond the limit, and a manifest that is not UTF-8. A
test of the shared reader simulates arbitrary_precision, as its other
tests do, and checks that the object stays an object there, and
another that read_document names the root of a document in its errors.

Validation on Windows, rebased on dev: 705 library tests and 51 CLI
tests pass with all features, `cargo fmt --all --check` passes, Clippy
passes with warnings denied for all targets with all features, and the
Markdown link check passes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 25, 2026
Complete the kernel-token part of #730 with the existing
protocol::cmdline Linux lexer rather than Python's whitespace
splitting. Refuse control characters, Unicode whitespace, quotes, and
the bytes that Linux treats as whitespace, including the 0xa0 inside a
UTF-8 sequence, in entrypoints, workload arguments, and guest targets,
including the target of the single directory share of run.

Check reserved names after Linux's quote splitting and its aliasing of
hyphens and underscores, and also in the view of the guest's shell
bootstrap, which splits the expanded command line on IFS whitespace
without interpreting quotes, so that a quoted value such as
extra="ignored nvx_memory_max=max ignored" cannot override a configured
entrypoint, memory limit, process limit, or share; ordinary quoted
caller values remain accepted. Trim the caller's command line once,
before both reservation checks and rendering, so that the checks see
exactly the text that is rendered and leading Unicode whitespace, such
as U+2003 EM SPACE, cannot hide a reserved name. Confirm that Linux can
still see every generated root parameter, so that -- and unmatched
quotes cannot hide the appended boot settings. Keep the guest root
parser and the exact 1024-byte budgets for both share-delivery modes
and adapter plans.

Broaden the properties to quoted and dashed reserved inputs, leading
Unicode whitespace, and the known separator bytes, add explicit
hidden-root and quoted-override regressions for both share-delivery
modes, and make the host-path unit tests native-only. Update the
documentation, and the validation table of the scenario tests that
replaced the CLI transcripts (#825, #830): a sandbox argument with a
space is now refused for its whitespace rather than its spaces.

Build the Python compatibility code that only the CLI and the
historical tests use for them alone, so that the default-feature
library has no dead code, and add `cargo clippy --lib -- -D warnings`
to check-nvx-rs so that it stays so. Gate the Python split and strip
oracles to tests, and the integer, codec, and Unicode-digit helpers to
the cli feature and tests. Since #828 moved the egress policy compiler
to serde_json, and the previous commit the sandbox metadata readers,
only the CLI's state files use the Python JSON reader and Python
strings, so gate the json and text modules whole, as their dumps,
shell, and value siblings are. The README's Python parity section now
credits the nvx-rs CLI, their only remaining user, with the whitespace,
int, and json.loads semantics, and the module documentation of the JSON
reader no longer cites the retired egress and distro-manifest readers.

Validation on Windows, rebased on dev: 708 library tests and 51 CLI
tests pass with all features, and 632 library tests with default
features; `cargo fmt --all --check`, Clippy with warnings denied for
all targets with all features, for the default-feature library, and
for the library without default features, rustdoc with warnings
denied, and the Markdown link check pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Oct 30, 2026
dev requires Required status check, but neither a merge queue nor
branches that are up to date with it, so each pull request is tested on
dev as dev was when its run started, and two pull requests that pass on
their own can break dev together, as #825 and #828 did on October 25
(#895). A merge queue tests each queued pull request on the latest dev
and the pull requests ahead of it, in a merge group whose run reports
the required check on the commit that the queue then merges. The two
commands through which CI selects and checks its jobs reject the
merge_group event, though, so a merge group's run could neither
classify its change nor pass its required check.

ci.py now names the events that CI distinguishes:

- CI_PRE_MERGE_EVENTS, pull_request and merge_group, whose runs check a
  change before it merges;
- CI_CLASSIFIED_EVENTS, those and push, whose changes the detector
  classifies; and
- CI_EVENTS, those and the full runs of schedule and workflow_dispatch.

classify_ci_changes accepts a merge group and classifies its change as
a pull request's: each suite runs only when one of its own inputs
changed, the platform jobs included, since a merge group publishes no
development release and records no performance history. Only a push
runs the platform jobs for every change but documentation.

required_ci_expected_results expects of a merge group the jobs of a
same-repository pull request, with the regression gate after the
platform jobs and the debug kernel on KVM alone, but not the rehearsal
of the guest artifact reuse, which the pull request's own run made. A
merge group runs in the repository whatever repository its pull requests
come from, so the policy expects the repository's jobs whatever
--same-repository says, as it does for a push.

The --event-name options of classify-ci-changes and check-required-ci
take their choices from these tuples. The CLI tests parse every event
and reject the others, check that a merge group classifies every case
of the classification tests as a pull request does and explains it the
same way, run the detector's script and the artifact invariants for all
three classified events, and extend the required-result policy cases
with merge groups. doc/usage.md documents the new choices.

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.

nvx-rs: replace the golden egress-policy corpus with a reference model and property tests

2 participants