Skip to content

nvx-rs: serve NVXC v2 in the guest loop and the fake OpenVMM (SL4.4b) - #1531

Merged
Pedro Henrique Penna (ppenna) merged 3 commits into
feature/nvx-refactorfrom
agents/slim-sl4-4b-nvxc-v2-serve
Nov 12, 2026
Merged

Pedro Henrique Penna (ppenna) merged 3 commits into
feature/nvx-refactorfrom
agents/slim-sl4-4b-nvxc-v2-serve

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1128 (SL4.4b, "nvx-init and the fake VMM serve NVXC v2"). Parent #1089, umbrella #1076.

Was stacked on #1528 (SL4.4a), which merged as 992164142d8: this PR is rebased onto feature/nvx-refactor at that commit, and shows only its own three commits (see "Rebase", below).

What and why

SL4.4a's NVXC v2 codec has no serving peer, so no host can stream an execution's input and output. As SL4.4b's Scope says, this PR has the guest loop that nvx-init and the managed agent share, and the fake OpenVMM, serve v2 beside v1, alike: one execution at a time, with BUSY for a second EXEC; CANCEL and STOP while it runs; standard input with an explicit end, acknowledged only once its bytes entered the child's pipe, with at most 64 KiB unacknowledged; output without a total cap, which the guest stops reading while its send queue is full; and PING, MAPS through the MappingTable, capture and activation over v2, in their existing order. Every host path still speaks v1, which both servers keep serving unchanged, FEATURES and GuestFeatures included, until SL4.4c switches the host and SL4.4d deletes v1.

Each request in its own version (guest/managed_agent/session.rs). Session::accept_request decodes a request of either version, by its header's version byte, and the session answers it in that version. v2 keeps v1's header and the codes of the kinds that both have, so a v2 PING, EXEC, MAPS, CAPTURE, ACTIVATE, CANCEL or STOP reaches the existing handler, through the same order of capture and restore, and only its framing changes: send_app encodes READY with the protocol version (and, after ACTIVATE, ReadyActivation's bytes), the busy refusal as BUSY, and EXIT with v2's outcome (exit, signal, timeout, cancelled, otherwise failed). STDIN and STDIN_END come back as Request::Stdin. ACTIVATE requests that join one activation each get READY in their own version, and a STDIN or STDIN_END while a restored activation's record applies fails the activation, as any request but ACTIVATE does since #1515. A v2 FEATURES fails to decode, as any malformed frame does.

One execution, its input and its output (guest/managed_agent/exec.rs, dispatch.rs). nvx-init's plain-process core runs a v2 execution with a pipe on its standard input; no second engine or multiplexer comes in:

  • As in v1, a second EXEC gets BUSY, as does any request but the execution's CANCEL and nvx-init's CAPTURE and ACTIVATE, which keep their handling. A v2 STOP cancels the execution: its EXIT reports cancelled, then STOPPED follows, and the session ends. A STOP of v1, or during an execution of v1, still gets BUSY, and a RESET abandons the STOP with the rest of the earlier epoch's work.
  • Input keeps what STDIN sent until the pipe takes it: each turn of the loop writes what the pipe takes without blocking, acknowledges it with STDIN_ACK (bytes written since the previous acknowledgement), and closes the pipe once STDIN_END came and every byte went in. Bytes beyond the 64 KiB window, or after the end, end the session, as a malformed request does; bytes that a closed pipe never took stay unacknowledged.
  • The output has no cap, and each turn reads at most one 32 KiB chunk of it, so that it can't keep the loop from serving control requests or the timeout.

Backpressure (session.rs, exec.rs, guest/sys/poll.rs). While a v2 execution runs, the session streams: its records go through a send queue that never waits for the console, so that the agent keeps serving CANCEL, STOP and STDIN while the host reads nothing, and the loop polls the console writable while the queue holds bytes (sys::ready, which also polls the child's stdin for writing). While the queue holds 64 KiB, the loop reads none of the child's stdout or stderr, which then wait on their pipes, and holds back STDIN_ACK, which it sends before EXIT for what the pipe took meanwhile. A RESET sends the queue before its ACK, and the EXIT, the first record sent without streaming, waits until the console took the queue and itself. The control descriptor is now nonblocking in every mode, as nvx-init's layered session already made it, so the session's reads and writes always poll; v1's behavior doesn't change.

The fake OpenVMM (testing/fake_openvmm.rs) serves the same v2 behavior: it answers each request in its version; a v2 workload runs alone, with each refusal in the refused request's version, and has no output cap; its standard input is what STDIN sent, which the new script command cat copies to standard output, and which the fake acknowledges as cat reads it, until STDIN_END or the workload's timeout; more than the window unacknowledged, or input after its end, ends the session; a v2 STOP while the workload sleeps or reads ends it with cancelled, then STOPPED, unless fake_ignore_stop=1 ignores it or fake_drop_stop=1 closes the connection after the EXIT. A v1 workload reads no input and keeps its 1 MiB cap; FEATURES and GuestFeatures stay v1's alone.

Docs. The codec's module docs say what NVX's peers do with a v2 execution, the managed agent's module docs and nvx-rs/README.md's nvx::protocol section which programs serve v2, and the fake's module docs its v2 behavior and cat.

Not in this PR, as the Scope says: switching the host or the CLI's tests to v2 (SL4.4c), deleting either server's v1, FEATURES or GuestFeatures (SL4.4d), more than one concurrent execution, a total output limit, dropping output or reading through a full send queue, and any change to capture or restore sequencing. BOOT_CONTRACT_VERSION and CONTROL_CONTRACT_REVISION stay unchanged: neither #1076 nor #1128 asks v2 to bump them. Everything is crate-private, so the public-API snapshots don't change.

Judgment calls

  • The version is per request. Each server answers a request in its version, and an execution's frames take the execution's version, so v1 and v2 requests can share one session, as the tests do; nothing negotiates.
  • STOP during a v2 execution cancels it and answers STOPPED after its EXIT, so the host sees how the execution ended before the session does.
  • Late stream frames get no answer. STDIN and STDIN_END of an execution that ended are dropped, as its late CANCEL is in v1, since the host can't know that the EXIT crossed them.
  • A window violation ends the session, as any malformed request does, rather than a refusal that a host would have to handle.
  • The codec is gated by its users. With both servers in place, mod v2 compiles where testing or guest on Linux x86-64 does, like SL6.12's gates, so its expect(dead_code) and its seam in scripts/test-layout-allowlist.json go now, rather than when SL4.4c adds the host; SL4.4c adds cli back to the gate with the host's use. (A cli-only build doesn't compile the codec or its tests meanwhile; --all-features and testing builds run them.)

Rebase

Rebased from beb8611de91 onto feature/nvx-refactor at 992164142d8 (SL4.4a's merged commit, whose nvx-rs patch is the one that this PR was stacked on), with these resolutions:

Size

Measured: 460 core lines, 454 by #1076's rule, and 5 counted files (estimate 450 and 5; stop at 600), counting the added or changed lines that are neither blank nor comments: guest/managed_agent/exec.rs 139, guest/managed_agent/session.rs 135, testing/fake_openvmm.rs 135, guest/managed_agent/dispatch.rs 26 and guest/sys/poll.rs 18. protocol/mod.rs's 6 lines gate re-exports by the features that use them, and protocol/app.rs only regates mod v2, so neither counts; guest/sys/mod.rs adds ready to its re-exports (1 line, in both totals). Tests, docs and the allowlist don't count.

Validation

After the rebase. At c935635d361, the rebased head before the fix for Copilot's last comment:

  • CI run 43254720952 passed with 33 jobs run and 9 skipped: Lint, the CLI on Linux and Windows, nvx-rs on ubuntu-latest and windows-latest, the musl and MSVC binaries, the guest artifacts, the microVM tests on KVM (guests and sandboxes, both shards), MSHV (the same and the debug kernel) and WHP (guests and sandboxes, both shards), the packages, the platform tests on all three backends, and the performance gate.
  • Windows: the table's Windows gates passed again: Rust tests 1226 / 732 / built / 190 / 222, the Python suites 1803 (73 skipped), 84 (11 skipped) and 4, check_matrix.py, and check_test_changes.py --base origin/feature/nvx-refactor.
  • Linux/MSHV: the table's MSHV gates passed again: the area's tests 228; Clippy for every feature set; all features 2205, the defaults 726, no default features 190, guest 1033, testing 225; the musl check-guest-binaries 1117 and the guest binaries' 33 tests; doc, the public API (no change), MSRV and macOS. --features bundled is gone with SL6.6a.

At this head, 23bf183219f, which only moves send_queued before Input::write in exec::run: each commit passed cargo fmt --check, the Clippy matrix below, now also for x86_64-unknown-linux-musl with guest, and check_test_layout.py; the MSHV gates passed again (the area's tests 229, all features 2205, the defaults 726, no default features 190, guest 1032, testing 225, the musl check-guest-binaries 1118 and the guest binaries' 33 tests, with every Clippy, doc, the public API, MSRV and macOS); and CI run 43260539653 passed the same 33 jobs, with the same 9 skipped.

Before the rebase, at beb8611de91 on eda49e156dc, SL4.4a's head then: the gates that the orchestration checkout's local_gates.py --files selects for these files (the adapter gates don't exist on feature/nvx-refactor), run from this branch's own action definitions, with Rust 1.95.0 and NVX_BUNDLE=skip. Each commit also passed alone cargo clippy --locked -- -D warnings for Windows (--all-targets --all-features, --lib --bins with testing, --lib --no-default-features) and for x86_64-unknown-linux-gnu (--all-targets with all features and with guest, testing and cli alone; --lib --bins with guest,testing and cli,guest), and check_test_layout.py.

Where Check Result
Windows cargo fmt --all --check passed
Windows cargo clippy --locked -- -D warnings: --all-targets --all-features, --lib, --lib --no-default-features, serde alone, --lib --bins --features cli, --lib --bins --no-default-features --features testing; --all-targets --all-features for x86_64-unknown-linux-gnu, and --all-targets --no-default-features --features guest for x86_64-unknown-linux-musl passed, no warnings
Windows cargo test --locked --all-features / cargo test --locked / cargo build --locked --lib --no-default-features / cargo test --locked --lib --no-default-features / --lib --no-default-features --features testing 1360 / 921 / built / 246 / 278 passed, 0 failed
Windows RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps --all-features; cargo +1.89 check --locked --all-targets --all-features passed
Windows check_test_layout.py, check_markdown_links.py, validate-nvx's python -m compileall -q scripts, python -m unittest scripts/test_*.py ..., test_adversarial.py, test_hosts.py, nvx.py test-microvm --help passed: 1825 tests (73 skipped), 84 (11 skipped) and 4
Windows SL5.2's check_matrix.py against 99d2022085f; check_test_changes.py --base eda49e156dc passed; see "Changes to existing tests"
Linux/MSHV (nvx-mshv-test, kernel 6.18) the area's tests first: cargo test --locked --all-features --lib -- guest::managed_agent testing::fake_openvmm protocol::app guest::sys::poll guest::init 244 passed, 0 failed
Linux/MSHV cargo fmt --all --check; cargo clippy --locked -- -D warnings with --all-targets --all-features, --lib, --lib --no-default-features, serde, --lib --bins with cli and with testing, and --all-targets with guest, guest,testing and cli,guest passed
Linux/MSHV cargo test --locked: --all-features, the defaults, --lib --no-default-features, --lib --no-default-features --features guest, --lib --no-default-features --features testing; the bundled-artifacts test passed, 0 failed: 2386 / 909 / 246 / 1132 / 281; 21
Linux/MSHV RUSTDOCFLAGS="-D warnings" cargo doc; python3 scripts/public_api.py; cargo +1.89 check --all-targets --all-features; cargo check --all-targets --all-features --target aarch64-apple-darwin passed; the public-API snapshots don't change
Linux/MSHV nvx.py check-guest-binaries --toolchain 1.95.0 (musl Clippy and tests), build-guest-binaries, test-guest-binaries; check_test_layout.py passed: 1219 musl tests; 33 binary tests
Linux/KVM (nvx-kvm-test, kernel 6.6) the same commands fmt, every Clippy, doc, public API, MSRV, macOS, the defaults (909), --no-default-features (246), testing (281), bundling (21), the guest binaries' build and their 33 tests, and the layout pass. --all-features fails 33 tests, guest alone 7, and the musl check-guest-binaries the same 7. The base, eda49e156dc, fails the same 28 and 7 on this host, where ending a process group fails with EINVAL on kernel 6.6 (nvx-init: cannot end ProcessGroup: Invalid argument); the other 5 are this PR's streams tests that start a child, which fail the same way there and pass on MSHV.
Linux/KVM and Linux/MSHV nvx.py test-microvm --backend kvm, and --backend mshv: the default suite, every scenario but smp-lapic, with 1, 2, 4 and 8 processors, v1 on every host path. Its Alpine initramfs came from nvx.py build-initramfs --guest alpine with this head's nvx-init and managed agent (the same 199b01e7… on both hosts), with a static musl nvx-rs built from this head, and CI run 43185976865's kernel, Ubuntu distro layer, scratch template and OpenVMM (14dc4116ec7, this base's pin), checked by SHA-256 passed on both: 58 runs, none skipped, with the guest boot and identity, workload identity, managed lifecycle and execution configuration, structured outcomes, the public sandbox scenarios (lifecycle, proxy-only networking, RAM-upper, GPT-layer), sandbox blocks, and every snapshot and restore scenario

A code-review subagent reviewed the whole diff. I folded its valid findings: a STOP counts only during a v2 execution, a RESET abandons it, the input that the pipe took while the queue was full is acknowledged before EXIT, a v2 execution reads one output chunk a turn, and the fake honors fake_ignore_stop and fake_drop_stop while a workload runs and refuses in the request's version. Its remaining finding, v2 input in the agent's sandbox mode, is a judgment call above. Copilot's review found three issues: I fixed two, the READYs of joined ACTIVATEs of mixed versions and a cat that ignored its timeout, and answered the third, a send queue in the fake, in its thread. Its review of the rebased head found one more, output that could starve STDIN_ACK while the queue was full, which I fixed.

I ran no microVM on WHP locally: nothing here is specific to Windows or a backend, and the guest code is the same on every backend; CI's runs cover WHP, as run 43254720952 did.

Changes to existing tests

python scripts/check_test_changes.py --base origin/feature/nvx-refactor (this PR's base, which holds SL4.4a's codec) reports 35 changes to existing tests, 41 added test items and none relocated unchanged, so this PR carries the tests-modified label:

  • The codec's tests compile where the codec does (20 items: protocol::app::v2::tests, its module, imports, helpers and tests): their code is unchanged, and they now compile where testing, or guest on Linux x86-64, does, rather than also with cli alone (Judgment calls, above).
  • Six tests and helpers take the session's request type: guest::managed_agent::session::tests's a_replacement_session_joins_one_activation_and_caches_ready_without_reapplying, changed_record_or_workload_bytes_during_application_fail_without_ready, failed_application_never_reports_ready_or_admits_root_dependent_work, restore: report the attribution of a restore that fails in its guest (SL5.1d) #1515's a_failed_restored_activation_gives_init_the_status_of_its_class and the helper queue, and dispatch::tests::runtime's helper prelaunch_termination, match Request::App where they unwrapped the frame, and prelaunch_termination passes exec::run's new stop argument. Their assertions don't change.
  • The fake OpenVMM serves v2 (9 test-double items in testing::fake_openvmm, which the Scope names): Session gains the session's version, the workload's standard input and a v2 STOP, and ignore_stop; send encodes v2 frames, request decodes either version, run answers in the request's version and acknowledges a STOP that ended a workload, interrupt takes STDIN, STDIN_END and STOP, exec_script runs cat, exit sends v2's outcome, forward caps only v1's output, and serve_client sets up the new fields.
  • Test policy: scripts/test-layout-allowlist.json tightens nvx-rs/src/protocol/app.rs from 1 seam to none, since the expect(dead_code) hook goes.
  • Added: guest::managed_agent::dispatch::tests::streams (Linux): version_2_serves_ping_and_the_mapping_table_beside_version_1, version_2_has_no_features_request, version_2_carries_capture_and_activation_with_their_payloads, a_version_2_execution_takes_its_input_until_its_end_and_acknowledges_what_it_wrote, a_version_2_execution_never_has_more_than_the_window_unacknowledged, a_version_2_execution_runs_alone_and_ends_on_cancel_or_stop, a_version_2_execution_forwards_output_past_the_old_cap and the_agent_reads_no_output_while_its_send_queue_is_full, with their helpers; guest::managed_agent::session::tests's joined_activations_get_ready_in_their_own_versions and standard_input_while_the_record_applies_fails_the_activation_as_the_protocols_failure, with the peer's send_v2; testing::fake_openvmm::tests (Unix, cli): version_2_workloads_read_their_input_until_its_end_under_the_window (which also times out a cat that waits for input), version_2_workloads_send_output_past_the_cap_of_version_1 and version_2_workloads_run_alone_and_end_on_cancel_or_stop, with their Host, Reply, script and timed helpers.

Every old test of the area is kept: the guest loop's (dispatch::tests and its runtime module, session::tests, the managed agent's loopback tests), the fake's and the codec's. No test is moved or retired. The new tests are unit tests, which the case matrix doesn't inventory; the one matrix row that names a test that this PR touches, failed_application_never_reports_ready_or_admits_root_dependent_work, still names it, and SL5.2's check_matrix.py passes.

@ppenna Pedro Henrique Penna (ppenna) added tests-modified Changes existing tests, test data, or test wiring feat-nvx-refactor labels Nov 12, 2026
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

This pull request changes existing tests

It modifies or removes 35 existing test asset(s), listed below. Review each with care: a change to a test can hide a regression that the test caught. See doc/contribute.md#changes-to-existing-tests.

Test code

Change What Before After
modified attributes the inner attributes of protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:1 nvx-rs/src/protocol/app/v2/tests.rs:1
modified helper fn prelaunch_termination in guest::managed_agent::dispatch::tests::runtime nvx-rs/src/guest/managed_agent/dispatch/tests/runtime.rs:211 nvx-rs/src/guest/managed_agent/dispatch/tests/runtime.rs:211
modified helper fn queue in guest::managed_agent::session::tests nvx-rs/src/guest/managed_agent/session/tests.rs:379 nvx-rs/src/guest/managed_agent/session/tests.rs:442
modified helper const ID in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:13 nvx-rs/src/protocol/app/v2/tests.rs:13
modified helper const FRAMES in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:17 nvx-rs/src/protocol/app/v2/tests.rs:17
modified helper fn ready in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:44 nvx-rs/src/protocol/app/v2/tests.rs:44
modified helper fn exit in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:52 nvx-rs/src/protocol/app/v2/tests.rs:52
modified helper fn error in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:57 nvx-rs/src/protocol/app/v2/tests.rs:57
modified helper fn wire in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:63 nvx-rs/src/protocol/app/v2/tests.rs:63
modified helper macro proptest in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:214 nvx-rs/src/protocol/app/v2/tests.rs:214
modified imports the imports of protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:6 nvx-rs/src/protocol/app/v2/tests.rs:6
modified module mod tests in protocol::app::v2 (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2.rs:334 nvx-rs/src/protocol/app/v2.rs:340
modified test fn a_replacement_session_joins_one_activation_and_caches_ready_without_reapplying in guest::managed_agent::session::tests nvx-rs/src/guest/managed_agent/session/tests.rs:139 nvx-rs/src/guest/managed_agent/session/tests.rs:152
modified test fn changed_record_or_workload_bytes_during_application_fail_without_ready in guest::managed_agent::session::tests nvx-rs/src/guest/managed_agent/session/tests.rs:215 nvx-rs/src/guest/managed_agent/session/tests.rs:259
modified test fn failed_application_never_reports_ready_or_admits_root_dependent_work in guest::managed_agent::session::tests nvx-rs/src/guest/managed_agent/session/tests.rs:252 nvx-rs/src/guest/managed_agent/session/tests.rs:296
modified test fn a_failed_restored_activation_gives_init_the_status_of_its_class in guest::managed_agent::session::tests nvx-rs/src/guest/managed_agent/session/tests.rs:280 nvx-rs/src/guest/managed_agent/session/tests.rs:324
modified test fn every_frame_round_trips_in_its_wire_form in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:77 nvx-rs/src/protocol/app/v2/tests.rs:77
modified test fn ready_round_trips_the_protocol_version in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:89 nvx-rs/src/protocol/app/v2/tests.rs:89
modified test fn stream_frames_keep_their_bounds in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:116 nvx-rs/src/protocol/app/v2/tests.rs:116
modified test fn malformed_frames_are_refused in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:158 nvx-rs/src/protocol/app/v2/tests.rs:158
modified test fn decoded_frames_encode_back_to_their_bytes in protocol::app::v2::tests, proptest!`` (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and `test` hold, not where `any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) )` and `test` did) nvx-rs/src/protocol/app/v2/tests.rs:218 nvx-rs/src/protocol/app/v2/tests.rs:218
modified test fn exec_requests_carry_their_workload_unchanged in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and feature = "cli" and feature = "testing" and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and feature = "cli" and feature = "testing" and test did) nvx-rs/src/protocol/app/v2/tests.rs:236 nvx-rs/src/protocol/app/v2/tests.rs:236
modified test fn exec_payloads_cross_unchanged in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:266 nvx-rs/src/protocol/app/v2/tests.rs:266
modified test fn maps_tables_cross_unchanged_in_order in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test did) nvx-rs/src/protocol/app/v2/tests.rs:280 nvx-rs/src/protocol/app/v2/tests.rs:280
modified test fn capture_requests_carry_their_flag_unchanged in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and feature = "cli" and feature = "guest" and target_arch = "x86_64" and target_os = "linux" and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and feature = "cli" and feature = "guest" and target_arch = "x86_64" and target_os = "linux" and test did) nvx-rs/src/protocol/app/v2/tests.rs:313 nvx-rs/src/protocol/app/v2/tests.rs:313
modified test fn the_activate_exchange_carries_its_payloads_unchanged in protocol::app::v2::tests (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and feature = "cli" and feature = "guest" and target_arch = "x86_64" and target_os = "linux" and test hold, not where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and feature = "cli" and feature = "guest" and target_arch = "x86_64" and target_os = "linux" and test did) nvx-rs/src/protocol/app/v2/tests.rs:336 nvx-rs/src/protocol/app/v2/tests.rs:336

Test doubles

Change What Before After
modified helper struct Session in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:2631 nvx-rs/src/testing/fake_openvmm.rs:2656
modified helper fn send in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2650 nvx-rs/src/testing/fake_openvmm.rs:2687
modified helper fn request in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2665 nvx-rs/src/testing/fake_openvmm.rs:2731
modified helper fn run in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2682 nvx-rs/src/testing/fake_openvmm.rs:2762
modified helper fn interrupt in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2720 nvx-rs/src/testing/fake_openvmm.rs:2814
modified helper fn exec_script in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2741 nvx-rs/src/testing/fake_openvmm.rs:2858
modified helper fn exit in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2944 nvx-rs/src/testing/fake_openvmm.rs:3085
modified helper fn forward in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2955 nvx-rs/src/testing/fake_openvmm.rs:3109
modified helper fn serve_client in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:2972 nvx-rs/src/testing/fake_openvmm.rs:3126

Relocated unchanged: 0; added: 41. Compared the merge base 992164142d85 with 23bf183219f9.

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 fake can block control requests under output backpressure and hang timed cat executions, while mixed-version activation retries can send READY responses using the wrong protocol version.

3 open issues
What changed in this PR

This PR adds NVXC v2 serving to the shared guest loop and fake OpenVMM, building on the codec from #1528. It implements the server-side portion of #1128 while retaining v1 for the later host migration.

Changes:

  • Adds streamed stdin, write acknowledgements, uncapped output, and guest-side output backpressure.
  • Adds v2 request handling, execution cancellation and stopping, plus fake workload input through cat.
  • Adds streaming tests and updates protocol feature gates and documentation.
File Description
scripts/​test-layout-allowlist.json Removes the obsolete codec allowance.
nvx-rs/​src/​testing/​fake_openvmm/​tests.rs Tests v2 input, output, and execution control.
nvx-rs/​src/​testing/​fake_openvmm.rs Adds v2 serving and the cat command.
nvx-rs/​src/​protocol/​mod.rs Gates v2 exports for server users.
nvx-rs/​src/​protocol/​app/​v2.rs Documents server-side streaming semantics.
nvx-rs/​src/​protocol/​app.rs Updates codec gates and removes dead-code suppression.
nvx-rs/​src/​guest/​sys/​poll.rs Adds writable-descriptor polling.
nvx-rs/​src/​guest/​sys/​mod.rs Re-exports the new polling helper.
nvx-rs/​src/​guest/​managed_agent/​session/​tests.rs Adapts tests to decoded request variants.
nvx-rs/​src/​guest/​managed_agent/​session.rs Adds version-aware framing and queued writes.
nvx-rs/​src/​guest/​managed_agent/​mod.rs Documents v2 guest behavior.
nvx-rs/​src/​guest/​managed_agent/​exec.rs Implements streamed input, backpressure, and v2 stopping.
nvx-rs/​src/​guest/​managed_agent/​dispatch/​tests/​streams.rs Adds guest streaming and compatibility tests.
nvx-rs/​src/​guest/​managed_agent/​dispatch/​tests/​runtime.rs Adapts runtime helpers to execution interfaces.
nvx-rs/​src/​guest/​managed_agent/​dispatch/​tests.rs Registers the streaming tests.
nvx-rs/​src/​guest/​managed_agent/​dispatch.rs Enables nonblocking control I/O and handles execution stops.
nvx-rs/​README.md Documents v2 servers and feature gates.

Review effort: Balanced

Comment thread nvx-rs/src/testing/fake_openvmm.rs Resolved
Comment thread nvx-rs/src/testing/fake_openvmm.rs Resolved
Base automatically changed from agents/slim-sl4-4a-nvxc-v2-codec to feature/nvx-refactor November 12, 2026 15:51
The managed agent's loop waits on its control console and on an
execution's pipes with readable_or_hung_up, which polls each descriptor
for input or hangup. Serving NVXC version 2 (SL4.4b, #1128) needs that
wait to cover descriptors that the agent writes too: the console while
records wait in its send queue, and an execution's standard input while
bytes wait to enter its pipe.

sys::ready polls each descriptor for input or hangup, or, where its
flag is set, for space to write, and returns whether each one woke, in
the caller's order. readable_or_hung_up is ready with every flag clear,
so its callers keep their behavior.

Part of #1128 (SL4.4b).

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.

🟡 Changes recommended

The current queue ordering can indefinitely starve STDIN_ACK under continuous output, exhausting the host’s stdin window even while it continues reading output.

1 open issue
3 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/src/guest/managed_agent/exec.rs Resolved
NVXC v2's codec (SL4.4a) has no serving peer, so no host can stream an
execution's input and output. SL4.4b (#1128) has the guest loop that
nvx-init's layered roots and the managed agent share serve v2 beside
v1, on nvx-init's plain-process core.

The session decodes a request of either version, and answers it in its
version. v2 keeps v1's header and the codes of the kinds that both
have, so PING, EXEC, MAPS through the MappingTable, CAPTURE, ACTIVATE,
CANCEL and STOP reach the existing handlers, in their existing order,
and only their framing changes: READY carries the protocol version, the
busy refusal is BUSY, and EXIT names v2's outcome. STDIN and STDIN_END
feed the running execution's standard input. FEATURES, which v2 lacks,
fails to decode, as any malformed frame does. ACTIVATE requests that
join one activation each get READY in their own version, and STDIN or
STDIN_END while the activation's record applies fails it, as any
other request does.

An execution of v2 runs alone: as in v1, a second EXEC gets BUSY, as
does any request but its CANCEL and nvx-init's CAPTURE and ACTIVATE,
which keep their handling. Its workload reads a pipe, into which each
turn of the loop writes what STDIN sent, as the pipe takes it, and
acknowledges with STDIN_ACK what it wrote, and which it closes once
STDIN_END came and every byte went in. Bytes beyond the 64 KiB window,
or after the end, end the session, as a malformed request does, and
bytes that a closed pipe never took stay unacknowledged. Its output has
no cap, and each turn reads at most one chunk of it, so that its output
cannot keep the loop from serving control requests. A STOP of v2
cancels it, so that its EXIT reports the cancellation before STOPPED,
and the session ends; a RESET abandons that STOP with the rest of the
earlier epoch's work, and a STOP of v1, or during an execution of v1,
still gets BUSY. STDIN and STDIN_END of an execution that ended get no
answer, as its CANCEL gets none.

While an execution of v2 runs, the session streams: its records go
through a send queue, which never waits for the console, so that the
agent keeps serving CANCEL, STOP and STDIN while the host does not
read, and the loop polls the console writable while the queue holds
bytes. While the queue holds 64 KiB, the loop reads none of the
workload's output, which then waits on its pipes, and holds back
STDIN_ACK, which acknowledges before EXIT what the pipe took meanwhile.
Each turn sends what the console takes of the queue before it
acknowledges input, and reads output only then, so that output cannot
starve the acknowledgements.
A RESET sends the queue before its ACK, and the EXIT, the first record
sent without streaming, waits until the console took the queue and
itself. The control descriptor is nonblocking in every session, as
nvx-init's was, so the session's reads and writes always poll.

v1 stays as it was, FEATURES and its output cap included. The codec's
expect(dead_code) now covers only the builds without the guest loop,
and names the fake OpenVMM and SL4.4c's host.

dispatch::tests::streams covers v2's PING, MAPS through the table,
STOP, capture and activation, beside v1 on one session; the absence of
FEATURES; BUSY, CANCEL and STOP during an execution; input that cat
reads until its explicit end, with acknowledgements that follow its
writes; a workload that never reads its input, which leaves exactly
the window unacknowledged, and a byte beyond it that ends the session;
output past v1's cap; and a host that reads nothing, while which the
workload waits on its pipes and a CANCEL still ends it. session::tests
covers ACTIVATE requests of both versions that join one activation,
and STDIN while the record applies; it and dispatch::tests::runtime
take the session's request type.

Part of #1128 (SL4.4b).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CLI's integration tests run against the fake OpenVMM, which served
only NVXC v1, so SL4.4c could not switch them to v2. SL4.4b (#1128)
has the fake serve v2 beside v1, with the guest loop's observable
behavior.

The fake decodes a request of either version and answers it in its
version: READY carries the protocol version, the busy refusal is BUSY,
and EXIT names v2's outcome, while PING, EXEC, MAPS and STOP keep their
handling. A workload of v2 runs alone, and another EXEC gets BUSY, as
other requests do while it runs, each refusal in the version of the
request that it refuses. Its output has no cap. Its standard input is
what STDIN frames sent, which the fake holds until the workload reads
it, and acknowledges as it does, until STDIN_END: the new script
command cat copies it to standard output until its end, or until the
workload's timeout. More than the 64 KiB window unacknowledged, or
input after its end, ends the session.
A STOP while the workload sleeps or reads its input ends it with the
cancelled outcome, and the fake acknowledges the STOP and stops, unless
fake_ignore_stop=1 ignores the STOP, as between workloads, or
fake_drop_stop=1 closes the connection after the EXIT without the
acknowledgement. STDIN and STDIN_END of a workload that ended get no
answer. A workload of v1 reads no input and keeps its 1 MiB cap, and
FEATURES and GuestFeatures stay v1's alone.

With both servers in place, the codec is gated by the features of its
users, testing and guest on Linux x86-64, so its expect(dead_code) and
its seam in the test-layout allowlist go, and cli builds it again once
SL4.4c's host speaks it. The codec's docs say what NVX's peers do with
an execution of v2, and the README which programs serve it.

The fake's tests, on Unix, drive its control session over a socket
pair: cat's input until its explicit end, with an acknowledgement of
each read, a cat that waits for input until its timeout, and a byte
beyond the window that ends the session; output past v1's cap, which
v1 still caps on the same session; and BUSY, CANCEL and STOP while a
workload runs.

Part of #1128 (SL4.4b).

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

No active review issues were identified for the NVXC v2 serving changes.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit 2216377 into feature/nvx-refactor Nov 12, 2026
47 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/slim-sl4-4b-nvxc-v2-serve branch November 12, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat-nvx-refactor tests-modified Changes existing tests, test data, or test wiring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants