diff --git a/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md b/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md index 13748ce..0a33618 100644 --- a/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md +++ b/docs/12_SIMULATION_POLYGON_PRODUCT_AND_SRS.md @@ -51,11 +51,14 @@ accepted navigation behavior, safety behavior or real actuator control. Polygon is now a parallel product branch. As of this document: -- P0 architecture and SRS are being committed; +- P0 architecture and SRS are committed on the Polygon branch; - SIM S0 has a strict candidate qualification profile and read-only doctor; -- no target-worker S0 evidence has been admitted; -- PX4/Gazebo/ROS 2/Nav2 have not been installed or accepted by this repository; -- no stock rover run has been accepted; +- the dedicated D-only worker distribution has been created and the candidate + PX4/Gazebo/ROS 2/Nav2 stack has been installed without using Docker; +- PX4 v1.17.0 and the stock Ackermann rover have completed repeated factual + smoke runs with ROS 2 vehicle status, Gazebo clock and clean shutdown; +- the complete digest-bound S0 evidence manifest has not been admitted; +- pause/step/speed and accepted 1×/2× resource evidence remain open; - `actuator_authority=false`; - `navigation_or_safety_accepted=false`. @@ -369,7 +372,8 @@ incompatible. The S0 candidate line is: - Windows 11 AI worker; -- WSL2 Ubuntu 24.04; +- dedicated `MissionCore-Sim` WSL2 Ubuntu 24.04 distribution physically on D; +- no Docker dependency in S0, so active shared Docker co-tenants remain untouched; - ROS 2 Jazzy; - Gazebo Harmonic; - PX4 Autopilot v1.17.0; @@ -378,9 +382,10 @@ The S0 candidate line is: - Nav2 Jazzy; - PX4 Gazebo stock rover models. -The exact source commits already resolved in the candidate profile do not become -accepted merely because they are present. ROS/Gazebo package versions and every -candidate build remain pending target-worker qualification. +The exact source commits and package versions are resolved in the candidate +profile. Target installation, PX4 build, DDS telemetry and repeated stock-rover +smoke evidence now exist, but components do not become `accepted` until the +remaining S0 behavior/resource cases and digest-bound evidence generation pass. S1 uses direct exact `px4_msgs` integration. The experimental `px4-ros2-interface-lib` is evaluated later and is not on the S1 critical path. @@ -390,8 +395,9 @@ S1 uses direct exact `px4_msgs` integration. The experimental All mutable worker bytes must be physically placed under `D:\NDC_MISSIONCORE`, including: -- WSL distribution/VHD; -- Docker data and images; +- the dedicated `MissionCore-Sim` WSL distribution/VHD; +- Docker data and images if a later profile elects to use containers; the S0 + profile explicitly does not; - sources and build products; - package/model caches; - runtime and logs; @@ -402,6 +408,13 @@ The WSL view is `/mnt/d/NDC_MISSIONCORE`. C-drive placement is a blocking failure. S0 requires at least 300 GiB free before installation and a stop-below threshold of 200 GiB. Full external datasets are out of S0 scope. +S0 runtime processes execute in a new loopback-only Linux network namespace. +Micro XRCE-DDS Agent v2.4.3 has no address-selection flag and therefore binds +UDP wildcard inside that namespace; the namespace contains only `lo`, so the +effective exposure remains loopback-only. ROS domain 0 is isolated by the same +boundary. This distinction is part of the port evidence and must not be +misreported as a host-wide `127.0.0.1` bind. + ## 16. Delivery plan | Phase | Estimate | Exit condition | @@ -435,7 +448,10 @@ Implementation cards follow this order: SIM S0 requires all of the following factual evidence: - worker inventory; -- physical D-only placement, including WSL and Docker VHD; +- physical D-only placement of the dedicated WSL VHD, sources, builds, caches + and artifacts; +- confirmation that S0 uses the dedicated D-only WSL and does not use the + existing C-backed Docker Desktop runtime; - exact component/package pins and licenses; - one stock PX4 rover launch; - ROS 2 telemetry; @@ -444,7 +460,7 @@ SIM S0 requires all of the following factual evidence: - clean stop with no orphan processes; - 1× resource baseline; - 2× resource baseline; -- explicit `GO`, `PAUSE` or `BLOCKED` report. +- explicit `GO`, `INCOMPLETE` or `BLOCKED` report. The repository profile and doctor implement preflight and evidence admission. They do not install packages, start processes, open ports or write a target diff --git a/docs/adr/0015-simulation-polygon-qualification-boundary.md b/docs/adr/0015-simulation-polygon-qualification-boundary.md index 33f4b81..9c65a3b 100644 --- a/docs/adr/0015-simulation-polygon-qualification-boundary.md +++ b/docs/adr/0015-simulation-polygon-qualification-boundary.md @@ -44,13 +44,20 @@ reports easy to misuse. 9. Source-of-record, derived artifacts and presentation state remain separate. Reset creates a new immutable run/episode. 10. Ground truth is evaluator-only and is not visible to perception or planning. -11. SIM S0 targets the reviewed Windows 11/WSL2 AI worker with all mutable bytes - physically under `D:\NDC_MISSIONCORE`. -12. S1 uses exact `px4_msgs` directly. The experimental +11. SIM S0 targets the reviewed Windows 11 worker through a dedicated + `MissionCore-Sim` WSL2 Ubuntu 24.04 distribution physically under + `D:\NDC_MISSIONCORE`. +12. SIM S0 does not depend on the existing Docker Desktop runtime. Its WSL + backend is C-backed and currently serves unrelated co-tenants. +13. S1 uses exact `px4_msgs` directly. The experimental `px4-ros2-interface-lib` is not a mandatory S1 dependency. -13. A top-level Polygon UI is gated on accepted contracts and a proven S1 +14. A top-level Polygon UI is gated on accepted contracts and a proven S1 backend lifecycle with persisted run history. -14. Real actuator authority requires a new decision and physical safety gate. +15. Real actuator authority requires a new decision and physical safety gate. +16. S0 processes run inside an ephemeral loopback-only Linux network namespace. + Micro XRCE-DDS Agent's UDP wildcard bind is acceptable only inside that + boundary; no S0 listener may be reachable from the worker LAN or Windows + host network. ## Consequences @@ -67,7 +74,7 @@ reports easy to misuse. ## First implementation -The first implementation is deliberately read-only: +The qualification doctor is deliberately read-only: - `simulation/s0/qualification-profile.yaml` is the candidate stack, storage, clock, authority, port and process contract. @@ -77,6 +84,12 @@ The first implementation is deliberately read-only: starting or modifying anything. - MISSIONCOR-40 tracks factual worker qualification. +The first target bootstrap also includes `simulation/s0/worker-smoke.sh`. It +creates the reviewed loopback-only namespace, starts only the pinned stock-rover +providers, observes ROS 2 vehicle status and Gazebo clock, requests PX4 shutdown, +checks for owned-process residue and writes D-only factual artifacts. It never +arms the vehicle or sends an actuator command. + The doctor reports `INCOMPLETE` until it is running on the reviewed target with resolved/accepted pins and all required evidence. It cannot infer `GO` from the repository state. diff --git a/docs/lab/006_SIM_S0_WORKER_BOOTSTRAP_20260724.redacted.md b/docs/lab/006_SIM_S0_WORKER_BOOTSTRAP_20260724.redacted.md new file mode 100644 index 0000000..63ce1e9 --- /dev/null +++ b/docs/lab/006_SIM_S0_WORKER_BOOTSTRAP_20260724.redacted.md @@ -0,0 +1,126 @@ +# Lab 006: SIM S0 worker bootstrap and stock-rover smoke + +Date: 2026-07-24 (Europe/Moscow). + +## Scope + +The reviewed Windows 11 AI worker received a dedicated Docker-free +`MissionCore-Sim` WSL2 Ubuntu 24.04 distribution physically stored at +`D:\NDC_MISSIONCORE\simulation\wsl\MissionCore-Sim`. Source, build, cache, +runtime, artifacts and datasets are all under the same D-only root. Existing +Docker Desktop co-tenants were inventoried but not stopped, reconfigured or +used. + +This lab grants no real or simulated actuator authority. Every smoke run kept +`actuator_authority=false`; it observed the stock rover while disarmed. + +## Redacted worker inventory + +- Windows 11 Pro, build 26200; +- WSL2 kernel `6.18.33.1-microsoft-standard-WSL2`; +- Ubuntu 24.04.4 LTS, x86_64; +- 13th Gen Intel Core i9-13900KF, 32 WSL-visible logical CPUs; +- 50,427,760,640 WSL-visible RAM bytes and 12,884,901,888 swap bytes; +- NVIDIA GeForce RTX 4090, driver 610.47, 24,564 MiB VRAM; +- D capacity 2,000,381,014,016 bytes; +- D free after bootstrap and builds: 392,758,685,696 bytes. + +The remaining D capacity is above the 300 GiB S0 start threshold and 200 GiB +stop threshold. This does not waive continuous disk monitoring. + +## Installed candidate generation + +- ROS 2 Jazzy `ros-base=0.11.0-1noble.20260616.084325`; +- `ros-gz=1.0.22-1noble.20260616.074726`; +- Gazebo Harmonic `1.0.0-1~noble`, Gazebo Sim `8.14.0-1~noble`, + SDFormat `14.9.0-1~noble`; +- PX4 Autopilot v1.17.0 commit + `d6f12ad1c4f70ad3230afd7d86e971421e02fef4`; +- `px4_msgs` v1.17.0 commit + `86d8239e962f6939e05c3737784f60c02fa884db`; +- PX4 Gazebo models commit + `b6127f4ec20de867e215fb5f78ae88b80f371909`; +- Micro XRCE-DDS Agent v2.4.3 commit + `73622810d984349b80bbac0ef55fc0b694d62222`; +- Nav2 `navigation2=1.3.12-1noble.20260615.181551` and + `nav2-bringup=1.3.12-1noble.20260616.082701`. + +PX4 SITL and `px4_msgs` were built successfully from the pinned D-only source. +The Micro XRCE-DDS Agent was built from source and installed under the D-only +runtime root. Nav2 is installed but has not yet passed an S2 navigation case. + +## Observed smoke result + +Two consecutive canonical runs and one preliminary resource run passed: + +- `20260724T134400Z-stock-rover`; +- `20260724T134600Z-stock-rover-repeat`; +- `20260724T135000Z-stock-rover-baseline`. + +The final smoke-script generation also passed as +`20260724T135800Z-stock-rover-final` and added a runtime listener inventory plus +the point resource snapshot. + +Each run started PX4 v1.17.0 with Gazebo world `rover` and model +`rover_ackermann_0`, established a Micro XRCE-DDS session, observed +`/fmu/out/vehicle_status_v1`, observed Gazebo `/clock`, requested PX4 shutdown +and found no owned runtime residue. The captured vehicle state was disarmed, +`failsafe=false`, with pre-flight checks passing. + +Each provider set ran inside a new Linux network namespace containing only +`127.0.0.1` and `::1`. The Agent's UDP `0.0.0.0:8888` bind was therefore +effective only inside that loopback boundary. No PX4, Gazebo or Agent service +was exposed to the LAN. + +The two canonical artifact directories are 68 KiB each. The preliminary +resource run completed in 38.75 seconds. One point sample observed approximately +202 MiB RSS for headless Gazebo, 19 MiB for PX4 and 17 MiB for the Agent. The +worker GPU was already occupied by unrelated co-tenants; no S0 GPU allocation +was attributable in this headless run. These point observations are not the +accepted 1× or 2× resource baselines. + +## Diagnostics retained + +Earlier bootstrap attempts remain outside Git under the D-only artifact root: + +- one check raced the asynchronous DDS connection even though the session + subsequently established; +- one used an unsupported ROS 2 Jazzy long option; +- one used ROS domain 42 while PX4 v1.17.0 published on domain 0. + +The smoke contract now waits for the Agent session plus the PX4 +`vehicle_status_v1` writer, uses Jazzy `topic list -t`, and relies on the +loopback-only namespace to isolate domain 0. + +An initial malformed interactive log repeatedly captured PX4 terminal repaint +output and reached approximately 1.1 GiB. That generated diagnostic artifact +was removed, then the case was rerun non-interactively; the valid standalone +PX4 log was approximately 8 KiB. + +## Target doctor + +The exact candidate profile was executed through the read-only doctor inside +`MissionCore-Sim`. Profile SHA-256: +`70bf0df44c85dd57eb3cedb98a474a46ba498c6ffc60ce0d8a06829e28587df1`. + +Target identity, authority, clock, port/process registries, all seven D-only +mutable roots, the disk threshold and all eight required executables passed. +Version acceptance and the nine-item digest-bound evidence set remain +incomplete, so the doctor correctly returned `INCOMPLETE`. + +## Verdict + +`INCOMPLETE`. + +Worker compatibility, D-only placement, exact candidate pins, PX4 build, +stock-rover launch, DDS telemetry, authoritative clock observation and repeated +clean lifecycle are factually proven. S0 still requires: + +- pause/resume and deterministic single-step evidence; +- 1× speed/real-time-factor acceptance; +- isolated 2× resource/coexistence baseline; +- a complete digest-bound evidence manifest; +- final `GO`, `INCOMPLETE` or `BLOCKED` decision. + +This lab does not accept Nav2 behavior, navigation safety, real-time guarantees, +or real actuator control. diff --git a/docs/runbooks/SIM_S0_AI_WORKER.md b/docs/runbooks/SIM_S0_AI_WORKER.md index 7d08dab..0f3426a 100644 --- a/docs/runbooks/SIM_S0_AI_WORKER.md +++ b/docs/runbooks/SIM_S0_AI_WORKER.md @@ -5,11 +5,17 @@ real vehicle control or a `GO` verdict by itself. ## Safety and placement -- Target: reviewed Windows 11 worker with WSL2 Ubuntu 24.04. +- Target: reviewed Windows 11 worker with the dedicated `MissionCore-Sim` WSL2 + Ubuntu 24.04 distribution physically installed on D. - All mutable bytes must be under `D:\NDC_MISSIONCORE`. -- Do not place a WSL distribution, Docker VHD, source, build, cache, log, bag, - ULog, Gazebo artifact or dataset on C. +- Do not place a WSL distribution, source, build, cache, log, bag, ULog, + Gazebo artifact or dataset on C. +- Do not use or reconfigure the existing Docker Desktop runtime in S0. Its + backend is C-backed and serves unrelated co-tenants. - Do not expose S0 services beyond loopback. +- Run providers inside the ephemeral loopback-only network namespace created + by `simulation/s0/worker-smoke.sh`. Micro XRCE-DDS Agent binds UDP wildcard + inside that namespace because v2.4.3 has no address-selection option. - Do not download full external datasets. - Keep `actuator_authority=false`. @@ -24,13 +30,34 @@ uv run missioncore-sim s0 doctor --json Before target evidence exists, the expected verdict is `INCOMPLETE`. +## Stock-rover smoke + +Copy the reviewed script to the D-only worker source root, then invoke it as +root inside `MissionCore-Sim` with a unique immutable run ID: + +```bash +simulation/s0/worker-smoke.sh -stock-rover +``` + +The script: + +- creates a new network namespace containing only loopback; +- starts Micro XRCE-DDS Agent, PX4 SITL and the stock Gazebo Ackermann world; +- verifies the DDS session, `/fmu/out/vehicle_status_v1` and Gazebo `/clock`; +- records listeners and a non-acceptance resource point sample; +- sends the PX4 `shutdown` command and rejects owned-process residue. + +Do not reuse a run ID. A failed run remains diagnostic evidence; rerun with a +new ID after correction. + ## Required target inventory Record a redacted artifact for each of: 1. Windows release, WSL version, Ubuntu version and architecture. 2. GPU/driver, CPU, RAM and VRAM. -3. Physical locations of the WSL distribution and Docker data/VHD. +3. Physical location of the dedicated WSL distribution and confirmation that + the Docker-free S0 profile is active. 4. D total/free bytes before installation. 5. Existing Docker images, WSL distributions and simulation-related processes. 6. Existing listeners and the candidate port registry. diff --git a/simulation/s0/qualification-profile.yaml b/simulation/s0/qualification-profile.yaml index 31ab7f7..49ab091 100644 --- a/simulation/s0/qualification-profile.yaml +++ b/simulation/s0/qualification-profile.yaml @@ -4,8 +4,10 @@ profile_id: ai-worker-px4-gazebo-v1 target_host: windows_release: windows-11 virtualization: wsl2 + wsl_distribution: MissionCore-Sim linux_distribution: ubuntu-24.04 architecture: x86_64 + container_runtime: none storage: windows_root: 'D:\NDC_MISSIONCORE' @@ -15,11 +17,8 @@ storage: forbid_windows_system_drive: true mutable_paths: - id: wsl - windows_path: 'D:\NDC_MISSIONCORE\wsl' - wsl_path: /mnt/d/NDC_MISSIONCORE/wsl - - id: docker - windows_path: 'D:\NDC_MISSIONCORE\docker' - wsl_path: /mnt/d/NDC_MISSIONCORE/docker + windows_path: 'D:\NDC_MISSIONCORE\simulation\wsl' + wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/wsl - id: source windows_path: 'D:\NDC_MISSIONCORE\simulation\source' wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/source @@ -56,24 +55,24 @@ authority: - rover-speed-yaw-rate/v1 ros: - domain_id: 42 + domain_id: 0 namespace: /missioncore/polygon/s0 components: - id: ros2 source_url: https://docs.ros.org/en/jazzy/ requested_ref: jazzy - resolved_version: + resolved_version: ros-base=0.11.0-1noble.20260616.084325;ros-gz=1.0.22-1noble.20260616.074726 resolved_commit: license: mixed-apache-2.0-bsd - qualification_state: pending-worker-resolution + qualification_state: candidate - id: gazebo source_url: https://gazebosim.org/docs/harmonic/ requested_ref: harmonic - resolved_version: + resolved_version: gz-harmonic=1.0.0-1~noble;gz-sim=8.14.0-1~noble;sdformat=14.9.0-1~noble resolved_commit: license: apache-2.0 - qualification_state: pending-worker-resolution + qualification_state: candidate - id: px4-autopilot source_url: https://github.com/PX4/PX4-Autopilot requested_ref: v1.17.0 @@ -90,9 +89,9 @@ components: qualification_state: candidate - id: px4-gazebo-models source_url: https://github.com/PX4/PX4-gazebo-models - requested_ref: main@2026-07-24 - resolved_version: - resolved_commit: e00d3b9cde682dbcb3bf6f30a2f2b8ef4325dae8 + requested_ref: PX4-Autopilot/v1.17.0-submodule + resolved_version: px4-v1.17.0-submodule + resolved_commit: b6127f4ec20de867e215fb5f78ae88b80f371909 license: bsd-3-clause qualification_state: candidate - id: micro-xrce-dds-agent @@ -105,38 +104,38 @@ components: - id: nav2 source_url: https://github.com/ros-navigation/navigation2 requested_ref: jazzy@2026-07-24 - resolved_version: + resolved_version: navigation2=1.3.12-1noble.20260615.181551;nav2-bringup=1.3.12-1noble.20260616.082701 resolved_commit: c92ea2fd9008f50c0ec8447610800214b2a0dafb license: apache-2.0 qualification_state: candidate ports: - id: micro-xrce-dds - host_scope: worker-wsl + host_scope: loopback-only-netns protocol: udp - bind: 127.0.0.1 + bind: 0.0.0.0 port: 8888 owner: micro-xrce-dds-agent - - id: px4-mavlink-sitl - host_scope: worker-wsl + - id: px4-mavlink-onboard + host_scope: loopback-only-netns protocol: udp - bind: 127.0.0.1 - port: 14540 + bind: 0.0.0.0 + port: 14580 owner: px4-sitl - - id: qgroundcontrol-reference - host_scope: worker-wsl + - id: px4-mavlink-normal + host_scope: loopback-only-netns protocol: udp - bind: 127.0.0.1 - port: 14550 + bind: 0.0.0.0 + port: 18570 owner: px4-sitl - id: simulation-orchestrator-api - host_scope: worker-wsl + host_scope: loopback-only-netns protocol: tcp bind: 127.0.0.1 port: 18080 owner: simulation-orchestrator - id: simulation-telemetry - host_scope: worker-wsl + host_scope: loopback-only-netns protocol: tcp bind: 127.0.0.1 port: 18081 @@ -164,9 +163,10 @@ processes: required_tools: - git - - docker - gz - make + - cmake + - ninja - colcon - ros2 - MicroXRCEAgent diff --git a/simulation/s0/worker-smoke.sh b/simulation/s0/worker-smoke.sh new file mode 100755 index 0000000..ca5d13c --- /dev/null +++ b/simulation/s0/worker-smoke.sh @@ -0,0 +1,259 @@ +#!/usr/bin/env bash +# +# Run the SIM S0 stock-rover smoke case inside a loopback-only network +# namespace. The script writes factual runtime artifacts to D and never grants +# actuator authority. + +set -Eeuo pipefail + +readonly EXPECTED_DISTRO="MissionCore-Sim" +readonly D_ROOT="${MISSIONCORE_SIM_D_ROOT:-/mnt/d/NDC_MISSIONCORE/simulation}" +readonly PX4_ROOT="${MISSIONCORE_SIM_PX4_ROOT:-${D_ROOT}/source/PX4-Autopilot}" +readonly AGENT_PREFIX="${MISSIONCORE_SIM_AGENT_PREFIX:-${D_ROOT}/runtime/micro-xrce-dds-agent/2.4.3}" +readonly PX4_MSGS_SETUP="${MISSIONCORE_SIM_PX4_MSGS_SETUP:-${D_ROOT}/build/ros2/px4_msgs/install/setup.bash}" +readonly ARTIFACT_ROOT="${MISSIONCORE_SIM_ARTIFACT_ROOT:-${D_ROOT}/artifacts/s0}" +readonly RUN_ID="${1:-$(date -u +%Y%m%dT%H%M%SZ)}" +readonly RUN_DIR="${ARTIFACT_ROOT}/${RUN_ID}" + +if [[ "${MISSIONCORE_S0_NETNS:-0}" != "1" ]]; then + if [[ "${EUID}" -ne 0 ]]; then + echo "error: initial worker-smoke invocation must run as root to create the network namespace" >&2 + exit 2 + fi + if [[ "${WSL_DISTRO_NAME:-}" != "${EXPECTED_DISTRO}" ]]; then + echo "error: expected WSL distro ${EXPECTED_DISTRO}, observed ${WSL_DISTRO_NAME:-unset}" >&2 + exit 2 + fi + + exec unshare --net -- bash -c ' + set -e + ip link set lo up + exec runuser -u missioncore -- env MISSIONCORE_S0_NETNS=1 "$@" + ' bash "$0" "$@" +fi + +if [[ "${WSL_DISTRO_NAME:-}" != "${EXPECTED_DISTRO}" ]]; then + echo "error: smoke case escaped the reviewed WSL distribution" >&2 + exit 2 +fi + +for required_path in "${PX4_ROOT}" "${AGENT_PREFIX}" "${PX4_MSGS_SETUP}" "${ARTIFACT_ROOT}"; do + if [[ "${required_path}" != /mnt/d/NDC_MISSIONCORE/* ]]; then + echo "error: mutable or executable path escaped D-only root: ${required_path}" >&2 + exit 2 + fi +done + +readonly AGENT_BIN="${AGENT_PREFIX}/bin/MicroXRCEAgent" +readonly AGENT_LOG="${RUN_DIR}/micro-xrce-dds-agent.log" +readonly PX4_LOG="${RUN_DIR}/px4-rover.log" +readonly BRIDGE_LOG="${RUN_DIR}/ros-gz-clock-bridge.log" +readonly TOPICS_LOG="${RUN_DIR}/ros-topics.txt" +readonly VEHICLE_STATUS_LOG="${RUN_DIR}/vehicle-status.yaml" +readonly CLOCK_LOG="${RUN_DIR}/clock.yaml" +readonly NETWORK_LOG="${RUN_DIR}/network.txt" +readonly RESOURCE_LOG="${RUN_DIR}/resource-snapshot.txt" +readonly RESULT_LOG="${RUN_DIR}/result.env" + +for required_file in \ + "${PX4_ROOT}/build/px4_sitl_default/bin/px4" \ + "${PX4_ROOT}/Tools/simulation/gz/worlds/rover.sdf" \ + "${AGENT_BIN}" \ + "/opt/ros/jazzy/setup.bash" \ + "${PX4_MSGS_SETUP}"; do + if [[ ! -e "${required_file}" ]]; then + echo "error: required S0 runtime input is missing: ${required_file}" >&2 + exit 2 + fi +done + +mkdir -p "${RUN_DIR}" + +agent_pid="" +px4_pgid="" +bridge_pgid="" + +stop_process_group() { + local pgid="${1:-}" + if [[ -z "${pgid}" ]]; then + return + fi + if kill -0 -- "-${pgid}" 2>/dev/null; then + kill -INT -- "-${pgid}" 2>/dev/null || true + for _ in {1..20}; do + if ! kill -0 -- "-${pgid}" 2>/dev/null; then + return + fi + sleep 0.25 + done + kill -TERM -- "-${pgid}" 2>/dev/null || true + sleep 1 + kill -KILL -- "-${pgid}" 2>/dev/null || true + fi +} + +cleanup() { + set +e + exec 3>&- + stop_process_group "${bridge_pgid}" + stop_process_group "${px4_pgid}" + if [[ -n "${agent_pid}" ]] && kill -0 "${agent_pid}" 2>/dev/null; then + kill -TERM "${agent_pid}" 2>/dev/null + wait "${agent_pid}" 2>/dev/null + fi +} +trap cleanup EXIT INT TERM + +wait_for_pattern() { + local file="$1" + local pattern="$2" + local attempts="$3" + for ((attempt = 0; attempt < attempts; attempt += 1)); do + if grep -Fq "${pattern}" "${file}" 2>/dev/null; then + return 0 + fi + sleep 0.5 + done + return 1 +} + +env \ + LD_LIBRARY_PATH="${AGENT_PREFIX}/lib" \ + "${AGENT_BIN}" udp4 -p 8888 -v 4 >"${AGENT_LOG}" 2>&1 & +agent_pid="$!" +sleep 1 +if ! kill -0 "${agent_pid}" 2>/dev/null; then + echo "error: Micro XRCE-DDS Agent exited during startup" >&2 + exit 1 +fi + +{ + echo "network_namespace=loopback-only" + ip -brief address + ss -lunp +} >"${NETWORK_LOG}" + +coproc PX4_PROCESS { + exec setsid env HEADLESS=1 make -C "${PX4_ROOT}" \ + px4_sitl gz_rover_ackermann >"${PX4_LOG}" 2>&1 +} +px4_pgid="${PX4_PROCESS_PID}" +exec 3>&"${PX4_PROCESS[1]}" + +if ! wait_for_pattern "${PX4_LOG}" "Startup script returned successfully" 120; then + echo "error: PX4 stock rover did not reach startup completion" >&2 + exit 1 +fi + +sleep 3 +printf '%s\n' \ + "ver all" \ + "commander status" >&3 + +if ! wait_for_pattern "${AGENT_LOG}" "session established" 60 \ + || ! wait_for_pattern \ + "${PX4_LOG}" \ + "successfully created rt/fmu/out/vehicle_status_v1 data writer" \ + 60; then + echo "error: PX4 uXRCE-DDS client did not connect to the loopback agent" >&2 + exit 1 +fi +printf '%s\n' "uxrce_dds_client status" >&3 +if ! wait_for_pattern "${PX4_LOG}" "Running, connected" 20; then + echo "error: PX4 did not report the established uXRCE-DDS session" >&2 + exit 1 +fi +{ + echo + echo "runtime_listeners=loopback-only-netns" + ss -lunp +} >>"${NETWORK_LOG}" + +set +u +# shellcheck source=/dev/null +source /opt/ros/jazzy/setup.bash +# shellcheck source=/dev/null +source "${PX4_MSGS_SETUP}" +set -u +export ROS_DOMAIN_ID=0 +export ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST +unset ROS_LOCALHOST_ONLY + +setsid ros2 run ros_gz_bridge parameter_bridge \ + '/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock' >"${BRIDGE_LOG}" 2>&1 & +bridge_pgid="$!" + +timeout 20s ros2 topic list -t >"${TOPICS_LOG}" +vehicle_status_topic="/fmu/out/vehicle_status" +if grep -Fq "/fmu/out/vehicle_status_v1" "${TOPICS_LOG}"; then + vehicle_status_topic="/fmu/out/vehicle_status_v1" +fi +timeout 20s ros2 topic echo --once \ + "${vehicle_status_topic}" >"${VEHICLE_STATUS_LOG}" +timeout 20s ros2 topic echo --once \ + /clock rosgraph_msgs/msg/Clock >"${CLOCK_LOG}" + +{ + echo "sample=non-acceptance-point-snapshot" + free -b + ps -g "${px4_pgid}" -o pid,ppid,pgid,rss,vsz,pcpu,comm,args + ps -p "${agent_pid}" -o pid,ppid,pgid,rss,vsz,pcpu,comm,args + ps -g "${bridge_pgid}" -o pid,ppid,pgid,rss,vsz,pcpu,comm,args + if command -v nvidia-smi >/dev/null; then + nvidia-smi \ + --query-gpu=memory.used,utilization.gpu \ + --format=csv,noheader,nounits + fi +} >"${RESOURCE_LOG}" + +printf '%s\n' "shutdown" >&3 +if ! wait_for_pattern "${PX4_LOG}" "Exiting NOW." 20; then + echo "error: PX4 did not acknowledge the shutdown command" >&2 + exit 1 +fi + +cleanup +trap - EXIT INT TERM + +px4_status="fail" +dds_status="fail" +vehicle_status="fail" +clock_status="fail" +lifecycle_status="fail" + +grep -Fq "world: rover, model: rover_ackermann_0" "${PX4_LOG}" && px4_status="pass" +if grep -Fq "session established" "${AGENT_LOG}" \ + && grep -Fq \ + "successfully created rt/fmu/out/vehicle_status_v1 data writer" \ + "${PX4_LOG}"; then + dds_status="pass" +fi +grep -Fq "arming_state:" "${VEHICLE_STATUS_LOG}" && vehicle_status="pass" +grep -Fq "clock:" "${CLOCK_LOG}" && clock_status="pass" +if ! pgrep -f "${PX4_ROOT}/build/px4_sitl_default/bin/px4" >/dev/null \ + && ! pgrep -f "gz sim.*rover.sdf" >/dev/null \ + && ! pgrep -f "${AGENT_BIN}" >/dev/null; then + lifecycle_status="pass" +fi + +{ + echo "run_id=${RUN_ID}" + echo "network_scope=loopback-only-netns" + echo "actuator_authority=false" + echo "px4_stock_rover=${px4_status}" + echo "uxrce_dds=${dds_status}" + echo "ros2_vehicle_status=${vehicle_status}" + echo "gazebo_clock=${clock_status}" + echo "clean_lifecycle=${lifecycle_status}" +} >"${RESULT_LOG}" + +if [[ "${px4_status}" != "pass" \ + || "${dds_status}" != "pass" \ + || "${vehicle_status}" != "pass" \ + || "${clock_status}" != "pass" \ + || "${lifecycle_status}" != "pass" ]]; then + echo "error: one or more S0 smoke checks failed; inspect ${RUN_DIR}" >&2 + exit 1 +fi + +echo "SIM S0 smoke PASS: ${RUN_DIR}" diff --git a/src/k1link/simulation/s0.py b/src/k1link/simulation/s0.py index 73dd7f4..fbaab6e 100644 --- a/src/k1link/simulation/s0.py +++ b/src/k1link/simulation/s0.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import os import platform import re import shutil @@ -42,8 +43,10 @@ class DoctorVerdict(StrEnum): class TargetHost: windows_release: str virtualization: str + wsl_distribution: str linux_distribution: str architecture: str + container_runtime: str @dataclass(frozen=True, slots=True) @@ -308,22 +311,34 @@ def _parse_target_host(value: object) -> TargetHost: required={ "windows_release", "virtualization", + "wsl_distribution", "linux_distribution", "architecture", + "container_runtime", }, context="target_host", ) target = TargetHost( windows_release=_string(data["windows_release"], "target_host.windows_release"), virtualization=_string(data["virtualization"], "target_host.virtualization"), + wsl_distribution=_string( + data["wsl_distribution"], + "target_host.wsl_distribution", + ), linux_distribution=_string( data["linux_distribution"], "target_host.linux_distribution", ), architecture=_string(data["architecture"], "target_host.architecture"), + container_runtime=_string( + data["container_runtime"], + "target_host.container_runtime", + ), ) if target.windows_release != "windows-11" or target.virtualization != "wsl2": raise S0ProfileError("S0 target must remain the reviewed Windows 11 WSL2 worker") + if target.wsl_distribution != "MissionCore-Sim" or target.container_runtime != "none": + raise S0ProfileError("S0 must use the dedicated Docker-free MissionCore-Sim distro") if target.linux_distribution != "ubuntu-24.04" or target.architecture != "x86_64": raise S0ProfileError("S0 target Linux distribution or architecture drifted") return target @@ -583,7 +598,12 @@ def _parse_ports(value: object) -> tuple[PortBinding, ...]: owner = _safe_id(data["owner"], f"ports[{index}].owner") if protocol not in {"tcp", "udp"}: raise S0ProfileError(f"port {identifier!r} has unsupported protocol") - if bind != "127.0.0.1": + if bind == "0.0.0.0": + if host_scope != "loopback-only-netns": + raise S0ProfileError( + f"port {identifier!r} wildcard bind requires loopback-only-netns scope" + ) + elif bind != "127.0.0.1": raise S0ProfileError(f"port {identifier!r} must remain loopback-only in S0") if not 1 <= port <= 65535: raise S0ProfileError(f"port {identifier!r} is outside 1..65535") @@ -666,15 +686,17 @@ def _target_host_match(target: TargetHost) -> tuple[bool, str]: os_release.get("id") == "ubuntu" and os_release.get("version_id") == "24.04" ) architecture_match = machine in {"x86_64", "amd64"} - matched = is_wsl2 and distribution_match and architecture_match + distro_name_match = os.environ.get("WSL_DISTRO_NAME") == target.wsl_distribution + matched = is_wsl2 and distribution_match and architecture_match and distro_name_match if matched: return True, ( f"matched {target.windows_release}/{target.virtualization}/" - f"{target.linux_distribution}/{target.architecture}" + f"{target.wsl_distribution}/{target.linux_distribution}/{target.architecture}" ) return False, ( "doctor is not running inside the reviewed Windows 11 WSL2 Ubuntu 24.04 " - f"x86_64 target (observed system={system!r}, machine={machine!r})" + f"x86_64 MissionCore-Sim target (observed system={system!r}, machine={machine!r}, " + f"wsl_distribution={os.environ.get('WSL_DISTRO_NAME')!r})" ) @@ -822,10 +844,7 @@ def _evidence_check( if artifact_candidate.is_symlink(): raise S0ProfileError(f"evidence artifact {identifier!r} cannot be a symlink") artifact = artifact_candidate.resolve(strict=True) - if ( - not artifact.is_relative_to(evidence_root) - or not artifact.is_file() - ): + if not artifact.is_relative_to(evidence_root) or not artifact.is_file(): raise S0ProfileError(f"evidence artifact {identifier!r} is not a regular file") expected_sha256 = _string( entry["sha256"], @@ -966,8 +985,7 @@ def _tool_list(value: object, context: str) -> tuple[str, ...]: def _string_list(value: object, context: str) -> tuple[str, ...]: result = tuple( - _string(item, f"{context}[{index}]") - for index, item in enumerate(_sequence(value, context)) + _string(item, f"{context}[{index}]") for index, item in enumerate(_sequence(value, context)) ) if len(set(result)) != len(result): raise S0ProfileError(f"{context} values must be unique") diff --git a/tests/test_simulation_s0.py b/tests/test_simulation_s0.py index f4a3bd9..5af0ecb 100644 --- a/tests/test_simulation_s0.py +++ b/tests/test_simulation_s0.py @@ -37,6 +37,8 @@ def test_canonical_s0_profile_is_strict_and_safe() -> None: profile = load_s0_profile(PROFILE) assert profile.profile_id == "ai-worker-px4-gazebo-v1" + assert profile.target_host.wsl_distribution == "MissionCore-Sim" + assert profile.target_host.container_runtime == "none" assert profile.storage.windows_root.drive.upper() == "D:" assert profile.storage.wsl_root.as_posix() == "/mnt/d/NDC_MISSIONCORE" assert profile.clock.authority == "gazebo:/clock" @@ -46,13 +48,23 @@ def test_canonical_s0_profile_is_strict_and_safe() -> None: assert profile.authority.actuator_authority is False assert profile.authority.navigation_or_safety_accepted is False assert profile.authority.direct_actuator_setpoints_allowed is False - assert profile.ros.domain_id == 42 + assert profile.ros.domain_id == 0 + assert "docker" not in profile.required_tools assert {item.identifier for item in profile.components} >= { "px4-autopilot", "px4-msgs", "gazebo", "nav2", } + components = {item.identifier: item for item in profile.components} + assert components["px4-gazebo-models"].resolved_commit == ( + "b6127f4ec20de867e215fb5f78ae88b80f371909" + ) + assert components["ros2"].qualification_state == "candidate" + assert components["gazebo"].qualification_state == "candidate" + micro_xrce = next(item for item in profile.ports if item.identifier == "micro-xrce-dds") + assert micro_xrce.host_scope == "loopback-only-netns" + assert micro_xrce.bind == "0.0.0.0" assert {item.identifier for item in profile.processes} == { "simulation-orchestrator", "gazebo-server", @@ -100,6 +112,20 @@ def test_profile_rejects_duplicate_scoped_port(tmp_path: Path) -> None: load_s0_profile(_write_profile(tmp_path, document)) +def test_profile_rejects_wildcard_bind_outside_loopback_namespace( + tmp_path: Path, +) -> None: + document = _profile_document() + ports = document["ports"] + assert isinstance(ports, list) + first = ports[0] + assert isinstance(first, dict) + first["host_scope"] = "worker-wsl" + + with pytest.raises(S0ProfileError, match="wildcard bind requires loopback-only-netns"): + load_s0_profile(_write_profile(tmp_path, document)) + + def test_doctor_never_claims_go_without_target_evidence(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( "k1link.simulation.s0._target_host_match",