Skip to content

host: speak NVXC v2, and stream execs in the CLI (SL4.4c) - #1541

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

Pedro Henrique Penna (ppenna) merged 2 commits into
feature/nvx-refactorfrom
agents/slim-sl4-4c-nvxc-v2-host

Conversation

@ppenna

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

Copy link
Copy Markdown
Contributor

Part of #1128 (SL4.4c, "NVX's host speaks v2, and the CLI streams execs"). Parent #1089, umbrella #1076.

Rebased on feature/nvx-refactor at c0cf126efa2, into which SL4.4a (#1528) and SL4.4b (#1531) merged; it was stacked on #1531 before.

What and why

SL4.4a's codec and SL4.4b's servers carry NVXC v2, but every host path still spoke v1, which buffers an execution's input and caps its output at 1 MiB, and asked the guest for FEATURES. As SL4.4c's Scope says, this PR moves every host path onto v2, and has sandbox exec and sandbox run stream through it.

The host's control session (host/session.rs) speaks v2 alone. Every READY must carry the protocol version: one of another version fails with the guest agent speaks NVXC version N, not version 2, and a frame of another protocol fails to decode. No host path sends FEATURES. ping, capture, activate and map_host_paths keep their payloads, order and messages on v2's frames. An execution streams: start_exec starts it with an empty window; send_stdin sends what the 64 KiB window takes, at most one frame, and next_exec_event gives the window back on STDIN_ACK (an acknowledgement of bytes never sent fails closed); close_stdin ends the input; STDOUT and STDERR arrive without a total cap; and stop works during an execution, skipping that execution's frames, its refusal included, until STOPPED. SL6.12's expect(dead_code) markers on ping, start_exec, cancel_exec and stop go, since the CLI calls all four, and features goes with its marker, so their five seams leave the test-layout allowlist.

The restore driver (host/restore_driver.rs) keeps its order: ACTIVATE on each v2 session until Done, then MAPS and the Start, now start_exec followed by close_stdin, since the restored workload reads no input, as v1's null device gave it none. It returns the live v2 session, in which the CLI reads the workload's events.

The CLI (host/cli/session.rs) delegates ping, map_host_paths and stop to the host session, with Python's messages (the guest agent … reads managed guest …). exec streams: a thread reads the command's standard input in frame-sized chunks, which the loop sends as the window allows, then STDIN_END once the input ends; the workload's output reaches the command's standard output and error as it arrives; and Ctrl-C sends CANCEL, so that the workload does not outlive the session, then ends the command with status 130 without waiting for the EXIT. A read of the standard input that fails cancels the workload and fails the command, rather than ending the input as if it were whole. While a workload streams, the transport leaves Ctrl-C to the loop, which needs the session to send CANCEL. --timeout still bounds the whole exec. sandbox exec and sandbox run (host/cli/sandbox/mod.rs) no longer print buffered output, and an error after the workload's output still starts a line of its own, as do sandbox run's own warnings after it (lifecycle/start.rs). start (lifecycle/start.rs) pings without asking for features, and REQUIRED_FEATURES goes with that last caller.

Version 1 stays for SL4.4d to delete, with its servers: its frames lose the cli gate, which no host code uses now, and protocol::app's v2 gets it back. ExitCategory::parse and GuestFeatures's host side lose their last production callers, so they get dead-code markers that name SL4.4d: an expect on parse, which takes a seam in the test-layout allowlist (protocol/app.rs: 1), and a module-level allow in protocol/features.rs, since which of its items the tests use differs between builds, so that no expectation holds in all of them.

Docs. nvx-rs/README.md (the nvx::protocol and control-session paragraphs, sandbox run and exec, "Guest protocol", which replaces "Guest features", the differences from nvx.py, and the tests), doc/usage.md and doc/run.md describe the streams and Ctrl-C, and doc/design/4.4-host-guest-control-interface.md says what version 2 changes: standard input, no output cap, no feature negotiation, and STOP during an execution.

Not in this PR, as the Scope says: deleting v1, FEATURES or GuestFeatures (SL4.4d), payload changes, a network-policy frame, concurrent executions, signal forwarding beyond CANCEL and STOP, negotiation or a FEATURES fallback, and a separate streaming subsystem. 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 host's FEATURES request goes; GuestFeatures stays. With FEATURES sent on no host path, ControlSession::features, the CLI's Session::features and REQUIRED_FEATURES have no caller, so they go, with their tests; the type, its constants and codec, and ExitCategory, stay, marked, for SL4.4d. The public ErrorCode::GuestFeaturesMissing is no longer reported; it stays, since removing it changes the public API, for SL4.4d or a later decision.
  • The restored workload's input ends at once, as the null device ended v1's. Since STDIN_END follows the Start, an endpoint that closes right after the Start can reset the stream before it read the end, so the start treats a reset or broken pipe of the restore's Start or events as a closed endpoint, and gives OpenVMM its grace to exit, as it already did for an end of the stream. a_failed_restore_ends_its_vm_and_its_launch_before_its_snapshot_goes_free caught this on Linux.
  • Ctrl-C cancels without waiting for the EXIT: the command ends with status 130 at once, and the guest ends the workload.
  • Streaming shows the guest's diagnostic of a refusal, such as a working directory that the workload cannot enter, before the CLI's error, where the buffered output was dropped; the scenarios' parser already finds the error after other output.
  • A refusal keeps the CLI's session open. Protocol errors of the host session, such as a refused mapping, close nothing, so that start can still stop the guest after a refused mapping, as before; transfer failures still close it.
  • BUSY during an execution is an invalid exec response: the exec lock keeps a second exec from reaching a guest.
  • The output's backpressure is the reader's. A stalled reader of the command's output stalls the stream, as it would stall the buffered write before; on Unix, Ctrl-C still ends such a write.

Size

Measured: 342 core lines by #1076's rule (366 counting regate-only lines), in 7 counted files (estimate 300 and 5; stop at 600): host/cli/session.rs 164, host/session.rs 141, host/cli/sandbox/lifecycle/start.rs 19, protocol/app.rs 7 (the expect on ExitCategory::parse; its 9 other lines only regate), protocol/features.rs 7 (the allow), host/restore_driver.rs 3 and host/cli/sandbox/mod.rs 1. protocol/mod.rs and protocol/exec.rs only gate items by the features that use them (15 lines), and host/cli/sandbox/lifecycle/mod.rs only loses code, so they don't count; tests, docs, the scenario and the allowlist don't either. Seven files rather than five: two of them hold only the dead-code markers that removing the host's last v1 callers forces, which no other PR can take before this one.

Validation

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. Each row names the head that it ran at: 6cd2f1944d0 and 5999e4facab, on SL4.4b's beb8611de91, the first before the CLI's read-failure and reset fixes (host/cli/session.rs and lifecycle/start.rs only); d35423505fa, which adds only the fixes of Copilot's two threads (stop skips a refused execution's ERROR, and sandbox run's warnings start lines of their own) and their tests; 0c20546b7e8, the same commits rebased on 22163771b23; and this head, d66994591e9, their identical patches (by git range-diff) on c0cf126efa2.

Where Check Result
Windows (workstation) at 5999e4facab: 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 for x86_64-unknown-linux-gnu --all-targets --all-features, --lib --bins with cli, testing and guest, and --lib --tests with cli and with guest passed
Windows at 5999e4facab: cargo test --locked: --all-features, the defaults, --lib --no-default-features; also --lib with cli alone and with testing alone passed, 0 failed: lib 1179, tests/cli.rs 113 and the rest of the targets; the defaults' lib 853; 246; 1144 and 278
Windows at 5999e4facab: RUSTDOCFLAGS="-D warnings" cargo doc --all-features; cargo +1.89 check --all-targets --all-features passed
Windows at 5999e4facab: check_test_layout.py; check_markdown_links.py; ruff check and ruff format --check; pyright for Linux and for Windows; python -m unittest scripts/test_microvm_tests.py; the case-matrix check (check_matrix.py … 99d2022085f) passed; 354 tests, 9 skipped; the matrix check passed
Linux/MSHV (nvx-mshv-test, kernel 6.18) at 6cd2f1944d0: the same fmt, Clippy (also --all-targets with guest, guest,testing and cli,guest), doc, MSRV, aarch64-apple-darwin check, bundling test, public_api.py, check_test_layout.py, nvx.py check-guest-binaries --toolchain 1.95.0, build-guest-binaries and test-guest-binaries, and cargo test with --all-features, the defaults, --lib without default features, with guest, with testing and with cli all passed but one test: the restore's hangup case got ECONNRESET, which this head treats as a closed endpoint (second judgment call)
Linux/MSHV at 5999e4facab: the Clippy matrix above, fmt, cargo test --all-features, cargo test --lib --features cli, the layout and public-API checks; the restore tests five times passed, 0 failed: lib 2060, tests/cli.rs 123 (with the Ctrl-C test), the rest; 1129; 13 × 5
Linux/KVM (nvx-kvm-test, kernel 6.6) at 6cd2f1944d0: the same gates; at 5999e4facab: Clippy (--all-targets --all-features, cli), cargo test --all-features, the restore tests five times every gate but the tests passed. cargo test --all-features fails the same 33 guest:: tests as the base, beb8611de91, which I ran in a worktree of its own there and compared by name (kernel 6.6 refuses to end a process group by pidfd); every other target passes, tests/cli.rs 123 with the Ctrl-C test. guest alone and the musl check-guest-binaries fail 7 of those 33
Linux/KVM and Linux/MSHV at 5999e4facab: nvx.py test-microvm --backend kvm, and --backend mshv: the default suite, every scenario but smp-lapic, with 1, 2, 4 and 8 processors. Its Alpine initramfs came from nvx.py build-initramfs --guest alpine at 5999e4facab (df66f452… on both hosts; this PR changes no guest code), with CI run 43185976865's kernel, Ubuntu distro layer, scratch template and musl OpenVMM (14dc4116ec7, this base's pin), checked by SHA-256 against their provenance passed on both: 58 runs, none skipped, with the public sandbox scenarios (sandbox-lifecycle, whose exec streamed all 2 MiB with an exit outcome, managed-exec-config, proxy-only, RAM-upper, GPT-layer), managed-lifecycle (the host session on v2 against the real managed agent), vm-lifecycle, structured outcomes, workload identity, and every snapshot and restore scenario, the public snapshot-core and snapshot-tiers among them
Windows/WHP (nvx-whp-test) at 5999e4facab: nvx.py test-microvm --backend whp: the default suite, with that initramfs (copied from MSHV), the same kernel, distro layer and scratch template, and CI run 43185976865's Windows OpenVMM, checked by SHA-256 passed: 59 runs, with the same public sandbox, snapshot, managed-lifecycle and vm-lifecycle scenarios; sandbox-lifecycle's exec streamed all 2 MiB
Windows, Linux/MSHV and Linux/KVM at d35423505fa: fmt; Clippy --all-targets --all-features and --lib --bins --features cli (on Windows also for x86_64-unknown-linux-gnu); cargo test --all-features --test cli and --lib host::; the new tests fail without their fixes passed: tests/cli.rs 114 on Windows and 124 on Linux (with the Ctrl-C test), host:: 898 and 884
Windows at 0c20546b7e8: cargo fmt --all --check; cargo clippy --locked -- -D warnings with --all-targets --all-features, --lib, --lib --no-default-features, serde, and --lib --bins with cli and with testing, --lib --tests with cli; for x86_64-unknown-linux-gnu, those and --all-targets with guest, guest,testing and cli,guest; for x86_64-unknown-linux-musl, the guest binaries' --all-targets with guest; cargo test --all-features and cargo test --lib --features cli; check_test_layout.py, check_markdown_links.py, the case-matrix check, ruff, and python -m unittest scripts/test_microvm_tests.py passed: lib 1067, tests/cli.rs 114, the rest; 1033; the unit tests 362, after one error that three reruns didn't reproduce
Linux/MSHV at 0c20546b7e8: fmt; the same Clippy matrix, natively; nvx.py check-guest-binaries --toolchain 1.95.0; cargo test --all-features --lib -- host:: protocol:: guest::managed_agent::tests::loopback and --test cli passed: 981 musl tests; 949; tests/cli.rs 124, with the Ctrl-C test
Linux/KVM at 0c20546b7e8: cargo test --all-features --lib -- host:: protocol:: and --test cli passed: 947; tests/cli.rs 124, with the Ctrl-C test
Linux/KVM, Linux/MSHV and Windows/WHP at 0c20546b7e8: nvx.py test-microvm with --backend kvm, mshv and whp, the default suite, with an Alpine initramfs built at that head (caa26244… on both Linux hosts, copied to WHP), nvx-rs built from that head (its provenance names that head's sources), and CI run 43263930955's kernel, Ubuntu distro layer and scratch template, at 22163771b23, and OpenVMM (14dc4116ec7, unchanged), checked by SHA-256 passed: 58, 58 and 59 runs; sandbox-lifecycle's exec streamed all 2 MiB on each. Two earlier runs don't count: the scenarios run an existing nvx-rs/target/release/nvx-rs without rebuilding it, so a run at d35423505fa (the public sandbox scenarios, managed-lifecycle and snapshot-core on MSHV) and a first run at 0c20546b7e8 ran the nvx-rs that the 5999e4facab suite had built; the second failed untiered-network-policy on each host, for the --net of snapshot capture that SL3.7c added to the base
Windows at this head: 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 --lib --tests with cli; for x86_64-unknown-linux-gnu, those and --all-targets with guest, guest,testing and cli,guest; for x86_64-unknown-linux-musl, --all-targets with guest; cargo test --all-features; python -m unittest scripts/test_nvx_tools.py, the README's module-table test among them; check_test_layout.py, check_markdown_links.py and the case-matrix check passed: lib 1067, tests/cli.rs 114, the rest; 536 tests, 44 skipped

Left to CI: its full matrix on this head, with the Ubuntu guest, the debug kernel and benchmarks.

Changes to existing tests

check_test_changes.py --base beb8611de91 lists 131 changes: 127 to test code, 3 to an end-to-end test and 1 to test policy. Each kept test checks what it checked, through v2, unless this list says otherwise.

  • v1 → v2 in the host's tests. The scripted guests and helpers that answered the host in v1 answer in v2, and the tests that use them keep their names and checks: host::session::tests (Responder, attached_recording, acknowledgement; exec_streams_events_in_order, exec_sends_the_working_directory_and_reports_its_refusal, records_that_the_protocol_does_not_allow_fail_closed, timed_out_polls_keep_partial_records, frames_travel_in_one_record_each, pings_and_stops_need_an_empty_acknowledgement, the capture, activate, mapping and mismatch tests), host::session::tests::managed_lifecycle (the exec helper ends the input after the Start), host::cli::session::tests (the Guest helper, Recording, exec_payload; the exec, refusal, deadline, closed-endpoint and transfer tests), host::cli::snapshots::capture::tests::respond, host::restore_driver::tests (its Python peer) and the CLI's restore tests (serve).
  • The restore's input end. The restore tests trace a STDIN_END after the Start, so their request kinds gain it and their request counts grow by one per restore (2 → 3, 4 → 6). Their guest (serve) keeps the behaviors that a_restore_that_fails_before_its_guest_is_ready_reports_its_attribution added on the new base, unusable-root and unreadable-report, on v2's frames.
  • Removed: the FEATURES tests features_report_what_the_guest_provides, guests_that_predate_the_features_request_provide_none, malformed_features_responses_fail_closed (host::session::tests) and features_of_a_guest_that_predates_them_are_none (host::cli::session::tests), with the methods that they tested; and SL6.12's five test hooks on ControlSession.
  • Renamed, inverted by the Scope: output_beyond_the_guest_limit_breaks_the_protocol is exec_streams_output_past_the_old_cap, since v2 has no cap; start_requires_the_guest_features_of_layered_sandboxes (tests/cli.rs) is starts_ask_the_guest_for_no_features, since no host path sends FEATURES.
  • The guest's diagnostic now shows. tests/cli/lifecycle.rs (managed_execs_exit_with_the_workload_status and its executions helper) and tests/cli/sandbox_run.rs (a_run_whose_workload_the_guest_refuses_stops_its_vm) expect the streamed diagnostic of a refused working directory or workload before the error (fourth judgment call).
  • The loopback test (guest::managed_agent::tests::loopback) drives the real agent through the v2 session: control_session_broker_and_real_agent_loop_back asks for no features, streams output past 1 MiB and standard input past the window, and stops during an execution; output_limit_forwards_nothing_after_the_cap keeps checking v1's cap, on raw v1 frames, until SL4.4d.
  • Regated, unchanged: v1's tests (protocol::tests' frames_match_their_vectors, decoders_reject_malformed_input, record_definitions_keep_their_wire_values, frame_kind, the GuestFeatures helpers, and host::restore::tests::live_init) lose the cli gate with the items that they test, and protocol::app::v2::tests gains it with mod v2; protocol_violations_keep_the_messages_of_python loses its gate, since it needs no v1 code.
  • End to end: sandbox-lifecycle's check_bounded_results expects an exec that writes 2 MiB to stream all of it with status 0 and the exit category, where it expected v1's output-limit cut at 1 MiB (OUTPUT_LIMIT_BYTES and OUTPUT_CHUNK_BYTES give way to STREAMED_OUTPUT_BYTES); its harness test's fake and regressions follow (capped-output, short-output), and the case matrix's row says what changed.
  • Test policy: the allowlist's new seam for protocol/app.rs, the expect on ExitCategory::parse above; host/session.rs's five seams go.

New tests: ready_must_carry_the_protocol_version, standard_input_keeps_at_most_the_window_unacknowledged and a_stop_ends_the_execution_that_runs_and_skips_its_frames (host::session::tests); exec_streams_its_standard_input_under_the_window_until_its_end, ctrl_c_cancels_the_running_workload and a_failed_read_of_the_standard_input_cancels_the_workload (host::cli::session::tests); managed_execs_stream_their_standard_input_and_output (tests/cli/lifecycle.rs), a_run_streams_its_standard_input_and_output (tests/cli/sandbox_run.rs) a_runs_warnings_start_lines_of_their_own_after_its_workloads_output (tests/cli/sandbox_run.rs) and, on Unix, interrupts::interrupting_an_exec_cancels_the_workload_that_streams (tests/cli.rs), against the fake OpenVMM.

@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 131 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 host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:1 nvx-rs/src/host/restore/tests/live_init.rs:1
modified attributes the inner attributes of protocol::app::v2::tests (it now compiles where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 run_exec in guest::managed_agent::tests::loopback::tests nvx-rs/src/guest/managed_agent/tests/loopback/tests.rs:19 nvx-rs/src/guest/managed_agent/tests/loopback/tests.rs:52
modified helper fn serve in host::cli::sandbox::lifecycle::start::tests::restore nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:961 nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:971
modified helper fn from_connection in host::cli::session::tests, impl Session`` nvx-rs/src/host/cli/session/tests.rs:24 nvx-rs/src/host/cli/session/tests.rs:39
modified helper fn request in host::cli::session::tests, impl < T : Transport > Guest < T >`` nvx-rs/src/host/cli/session/tests.rs:163 nvx-rs/src/host/cli/session/tests.rs:218
modified helper fn send in host::cli::session::tests, impl < T : Transport > Guest < T >`` nvx-rs/src/host/cli/session/tests.rs:170 nvx-rs/src/host/cli/session/tests.rs:226
modified helper fn exec_payload in host::cli::session::tests nvx-rs/src/host/cli/session/tests.rs:255 nvx-rs/src/host/cli/session/tests.rs:315
modified helper fn write_all in host::cli::session::tests, impl Transport for Recording`` nvx-rs/src/host/cli/session/tests.rs:636 nvx-rs/src/host/cli/session/tests.rs:830
modified helper fn respond in host::cli::snapshots::capture::tests nvx-rs/src/host/cli/snapshots/capture/tests.rs:828 nvx-rs/src/host/cli/snapshots/capture/tests.rs:829
modified helper type AnyError in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:25 nvx-rs/src/host/restore/tests/live_init.rs:25
modified helper const ACTIVATE in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:26 nvx-rs/src/host/restore/tests/live_init.rs:26
modified helper const CAPTURE in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:27 nvx-rs/src/host/restore/tests/live_init.rs:27
modified helper struct OwnedVm in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:29 nvx-rs/src/host/restore/tests/live_init.rs:29
modified helper impl Drop for OwnedVm in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:30 nvx-rs/src/host/restore/tests/live_init.rs:30
modified helper fn drop in host::restore::tests::live_init, impl Drop for OwnedVm`` (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and `any ( target_os = "linux" , windows )` and `feature = "cli"` and `feature = "host"` and `test` hold, not where `any ( target_os = "linux" , windows )` and `feature = "cli"` and `feature = "host"` and `test` did) nvx-rs/src/host/restore/tests/live_init.rs:31 nvx-rs/src/host/restore/tests/live_init.rs:31
modified helper fn require in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:46 nvx-rs/src/host/restore/tests/live_init.rs:46
modified helper fn backend in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:54 nvx-rs/src/host/restore/tests/live_init.rs:54
modified helper fn private_directory in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:62 nvx-rs/src/host/restore/tests/live_init.rs:62
modified helper fn create_export in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:74 nvx-rs/src/host/restore/tests/live_init.rs:74
modified helper fn ready in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:88 nvx-rs/src/host/restore/tests/live_init.rs:88
modified helper fn workload in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:102 nvx-rs/src/host/restore/tests/live_init.rs:102
modified helper fn execute in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:111 nvx-rs/src/host/restore/tests/live_init.rs:111
modified helper fn check_restored_console in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:145 nvx-rs/src/host/restore/tests/live_init.rs:145
modified helper fn run in host::restore::tests::live_init (it now compiles where any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test hold, not where any ( target_os = "linux" , windows ) and feature = "cli" and feature = "host" and test did) nvx-rs/src/host/restore/tests/live_init.rs:160 nvx-rs/src/host/restore/tests/live_init.rs:160
modified helper const PEER in host::restore_driver::tests nvx-rs/src/host/restore_driver/tests.rs:20 nvx-rs/src/host/restore_driver/tests.rs:21
modified helper struct Responder in host::session::tests nvx-rs/src/host/session/tests.rs:123 nvx-rs/src/host/session/tests.rs:174
modified helper fn write_all in host::session::tests, impl Transport for Responder`` nvx-rs/src/host/session/tests.rs:142 nvx-rs/src/host/session/tests.rs:193
modified helper fn attached_recording in host::session::tests nvx-rs/src/host/session/tests.rs:166 nvx-rs/src/host/session/tests.rs:215
modified helper fn acknowledgement in host::session::tests nvx-rs/src/host/session/tests.rs:562 nvx-rs/src/host/session/tests.rs:673
modified helper struct Outcome in host::session::tests::managed_lifecycle nvx-rs/src/host/session/tests/managed_lifecycle.rs:92 nvx-rs/src/host/session/tests/managed_lifecycle.rs:91
modified helper fn printed in host::session::tests::managed_lifecycle, impl Outcome`` (only the paths that it names changed) nvx-rs/src/host/session/tests/managed_lifecycle.rs:105 nvx-rs/src/host/session/tests/managed_lifecycle.rs:104
modified helper fn exec in host::session::tests::managed_lifecycle, impl Guest`` nvx-rs/src/host/session/tests/managed_lifecycle.rs:137 nvx-rs/src/host/session/tests/managed_lifecycle.rs:142
modified helper fn layered_mapping_session in host::session::tests::managed_lifecycle nvx-rs/src/host/session/tests/managed_lifecycle.rs:379 nvx-rs/src/host/session/tests/managed_lifecycle.rs:388
modified helper const ID in protocol::app::v2::tests (it now compiles where any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and test hold, not where any ( 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 helper impl GuestFeatures in protocol::tests (moved, and 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/tests.rs:94 nvx-rs/src/protocol/tests.rs:98
modified helper const REQUIRED in protocol::tests, impl GuestFeatures`` (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/tests.rs:103 nvx-rs/src/protocol/tests.rs:107
modified helper fn contains in protocol::tests, impl GuestFeatures`` (it now compiles where any ( all ( feature = "cli" , feature = "testing" ) , all ( any ( feature = "cli" , feature = "testing" ) , feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) ) and `any ( feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) )` and `test` hold, not where `any ( all ( feature = "cli" , feature = "testing" ) , all ( any ( feature = "cli" , feature = "testing" ) , feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) )` and `any ( feature = "cli" , feature = "testing" , all ( feature = "guest" , target_os = "linux" , target_arch = "x86_64" ) )` and `test` did) nvx-rs/src/protocol/tests.rs:115 nvx-rs/src/protocol/tests.rs:119
modified helper fn frame_kind in protocol::tests (moved, and 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/tests.rs:150 nvx-rs/src/protocol/tests.rs:153
modified helper fn executions in cli/lifecycle (tests) nvx-rs/tests/cli/lifecycle.rs:816 nvx-rs/tests/cli/lifecycle.rs:816
modified helper def _FakeSandboxCli._exec in scripts/test_microvm_tests.py scripts/test_microvm_tests.py:1260 scripts/test_microvm_tests.py:1260
modified imports the imports of guest::managed_agent::tests::loopback::tests nvx-rs/src/guest/managed_agent/tests/loopback/tests.rs:1 nvx-rs/src/guest/managed_agent/tests/loopback/tests.rs:1
modified imports the imports of host::cli::sandbox::lifecycle::start::tests::restore nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:1 nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:1
modified imports the imports of host::cli::session::tests nvx-rs/src/host/cli/session/tests.rs:4 nvx-rs/src/host/cli/session/tests.rs:4

... and 77 more, which the job's summary lists.

End-to-end tests

Change What Before After
modified helper assignment OUTPUT_CHUNK_BYTES in scripts/nvx_tools/sandbox_lifecycle_tests.py (renamed to STREAMED_OUTPUT_BYTES) scripts/nvx_tools/sandbox_lifecycle_tests.py:59 scripts/nvx_tools/sandbox_lifecycle_tests.py:56
modified helper def LifecycleAcceptance.check_bounded_results in scripts/nvx_tools/sandbox_lifecycle_tests.py scripts/nvx_tools/sandbox_lifecycle_tests.py:833 scripts/nvx_tools/sandbox_lifecycle_tests.py:830
removed helper assignment OUTPUT_LIMIT_BYTES in scripts/nvx_tools/sandbox_lifecycle_tests.py scripts/nvx_tools/sandbox_lifecycle_tests.py:58

Test policy

Change What Before After
loosened allowlist entry nvx-rs/src/protocol/app.rs (the seams allow 1 test hooks) scripts/test-layout-allowlist.json

Relocated unchanged: 0; added: 46. Compared the merge base c0cf126efa29 with d66994591e99.

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

Outstanding issues can append cleanup diagnostics to unterminated stderr and cause shutdown to fail spuriously when it encounters an unconsumed execution ERROR frame.

2 open issues
What changed in this PR

Moves NVX’s host control paths to NVXC v2 and enables streaming CLI executions, building on the codec and servers from SL4.4a–b. Version 1 remains for removal in SL4.4d.

Changes:

  • Migrates host sessions, capture, and restore to v2, replacing feature queries with protocol-version checks.
  • Streams CLI stdin, stdout, and stderr, with windowed input, uncapped output, and Ctrl-C cancellation.
  • Updates tests, feature gates, and documentation for the new behavior.
File Description
scripts/​test-layout-allowlist.json Updates dead-code seam allowances.
scripts/​test_microvm_tests.py Updates streamed-output regression expectations.
scripts/​nvx_tools/​sandbox_lifecycle_tests.py Checks complete 2 MiB output and successful status.
nvx-rs/​tests/​cli/​sandbox_run.rs Covers streaming runs and refusal diagnostics.
nvx-rs/​tests/​cli/​lifecycle.rs Tests streamed input, output, and diagnostics.
nvx-rs/​tests/​cli/​harness.rs Adds commands with piped input.
nvx-rs/​tests/​cli.rs Tests feature-free startup and Ctrl-C cancellation.
nvx-rs/​src/​protocol/​tests.rs Adjusts v1 test feature gates.
nvx-rs/​src/​protocol/​mod.rs Gates protocol exports by their remaining users.
nvx-rs/​src/​protocol/​features.rs Marks legacy feature handling for later removal.
nvx-rs/​src/​protocol/​exec.rs Restricts the legacy output-limit constant’s gate.
nvx-rs/​src/​protocol/​app.rs Enables v2 for CLI and regates v1 items.
nvx-rs/​src/​host/​session/​tests/​managed_lifecycle.rs Migrates real-guest session tests to v2.
nvx-rs/​src/​host/​session/​tests.rs Tests version checks, input windows, and active-exec stop.
nvx-rs/​src/​host/​session.rs Implements v2 host requests and execution streams.
nvx-rs/​src/​host/​restore/​tests.rs Regates the retained v1 restore test.
nvx-rs/​src/​host/​restore_driver/​tests.rs Migrates the scripted restore peer to v2.
nvx-rs/​src/​host/​restore_driver.rs Starts restored workloads with explicit stdin end.
nvx-rs/​src/​host/​cli/​snapshots/​capture/​tests.rs Migrates capture fixtures to v2.
nvx-rs/​src/​host/​cli/​session/​tests.rs Covers streaming, cancellation, and input failures.
nvx-rs/​src/​host/​cli/​session.rs Streams CLI executions through the host session.
nvx-rs/​src/​host/​cli/​sandbox/​mod.rs Removes buffered output writes and tracks stderr endings.
nvx-rs/​src/​host/​cli/​sandbox/​lifecycle/​start/​tests/​restore.rs Checks v2 restore sequencing and stdin end.
nvx-rs/​src/​host/​cli/​sandbox/​lifecycle/​start.rs Removes feature queries and handles v2 restore events.
nvx-rs/​src/​host/​cli/​sandbox/​lifecycle/​mod.rs Removes the required-features constant.
nvx-rs/​src/​guest/​managed_agent/​tests/​loopback/​tests.rs Exercises v2 streams while retaining v1 cap coverage.
nvx-rs/​README.md Documents v2 sessions and streaming CLI behavior.
doc/​usage.md Explains streams and cancellation.
doc/​run.md Updates run behavior and acceptance expectations.
doc/​design/​4.4-host-guest-control-interface.md Describes v2 streaming, readiness, and shutdown.
doc/​acceptance-case-matrix.md Records the uncapped-output acceptance change.

Review effort: Balanced

Comment thread nvx-rs/src/host/cli/session.rs Resolved
Comment thread nvx-rs/src/host/session.rs Resolved
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the agents/slim-sl4-4b-nvxc-v2-serve branch 2 times, most recently from c935635 to 23bf183 Compare November 12, 2026 16:36
Base automatically changed from agents/slim-sl4-4b-nvxc-v2-serve to feature/nvx-refactor November 12, 2026 16:55
NVX's host spoke NVXC v1, which buffers an execution's input and caps
its output, and asked the guest for FEATURES. SL4.4c (#1128) moves
every host path onto v2, which SL4.4a's codec and SL4.4b's servers
provide.

The host's control session speaks v2 alone: every READY must carry the
protocol version, and no host path sends FEATURES. PING, MAPS, capture
and the restore driver's activate exchange use v2's frames, with their
payloads and order unchanged. The driver ends the restored workload's
standard input after its Start, and returns the live v2 session. The
session streams an execution: send_stdin keeps at most the 64 KiB
window unacknowledged, close_stdin ends the input, the output arrives
without the old total cap, and a STOP ends an execution that runs,
skipping its frames until STOPPED.

`sandbox exec` and `sandbox run` stream through the session: the
workload reads the command's standard input until its end, and its
output reaches the command's as it arrives. Ctrl-C sends CANCEL, so
that the workload does not outlive the session, and a guest's
diagnostic of a refused request now precedes the CLI's error. The CLI
delegates PING, MAPS and STOP to the host session, with Python's
messages, and a refusal keeps the session open, so that a refused
mapping still lets the start stop the guest.

SL6.12's dead-code expectations on ControlSession::{ping, start_exec,
cancel_exec, stop} go with their seams, since the CLI calls them.
Version 1's frames, which only the servers use now, lose the cli gate.
The host's FEATURES request and REQUIRED_FEATURES go with their only
caller, while GuestFeatures and ExitCategory::parse stay, marked dead,
for SL4.4d to delete with the rest of version 1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The sandbox-lifecycle scenario checked that an exec which writes 2 MiB
ends with status 125 and the output-limit category after at most the
1 MiB that version 1 of NVXC forwarded. NVX's host speaks version 2
now, which streams the output without a total cap (#1128), so the
scenario expects all 2 MiB, status 0 and the exit category instead.

Its harness test's regressions follow: a guest that still caps the
output fails the exec's status, and one that loses a chunk fails the
count. The case matrix's row of the scenario says what changed.

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 block the NVXC v2 host migration and CLI streaming changes.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

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