feat(simulation): qualify D-only stock rover smoke

This commit is contained in:
DCCONSTRUCTIONS 2026-07-24 16:51:39 +03:00
parent 81761067e9
commit 6290fcb2ce
8 changed files with 545 additions and 60 deletions

View File

@ -51,11 +51,14 @@ accepted navigation behavior, safety behavior or real actuator control.
Polygon is now a parallel product branch. As of this document: 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; - SIM S0 has a strict candidate qualification profile and read-only doctor;
- no target-worker S0 evidence has been admitted; - the dedicated D-only worker distribution has been created and the candidate
- PX4/Gazebo/ROS 2/Nav2 have not been installed or accepted by this repository; PX4/Gazebo/ROS 2/Nav2 stack has been installed without using Docker;
- no stock rover run has been accepted; - 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`; - `actuator_authority=false`;
- `navigation_or_safety_accepted=false`. - `navigation_or_safety_accepted=false`.
@ -369,7 +372,8 @@ incompatible.
The S0 candidate line is: The S0 candidate line is:
- Windows 11 AI worker; - 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; - ROS 2 Jazzy;
- Gazebo Harmonic; - Gazebo Harmonic;
- PX4 Autopilot v1.17.0; - PX4 Autopilot v1.17.0;
@ -378,9 +382,10 @@ The S0 candidate line is:
- Nav2 Jazzy; - Nav2 Jazzy;
- PX4 Gazebo stock rover models. - PX4 Gazebo stock rover models.
The exact source commits already resolved in the candidate profile do not become The exact source commits and package versions are resolved in the candidate
accepted merely because they are present. ROS/Gazebo package versions and every profile. Target installation, PX4 build, DDS telemetry and repeated stock-rover
candidate build remain pending target-worker qualification. 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 S1 uses direct exact `px4_msgs` integration. The experimental
`px4-ros2-interface-lib` is evaluated later and is not on the S1 critical path. `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 All mutable worker bytes must be physically placed under
`D:\NDC_MISSIONCORE`, including: `D:\NDC_MISSIONCORE`, including:
- WSL distribution/VHD; - the dedicated `MissionCore-Sim` WSL distribution/VHD;
- Docker data and images; - Docker data and images if a later profile elects to use containers; the S0
profile explicitly does not;
- sources and build products; - sources and build products;
- package/model caches; - package/model caches;
- runtime and logs; - 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 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. 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 ## 16. Delivery plan
| Phase | Estimate | Exit condition | | Phase | Estimate | Exit condition |
@ -435,7 +448,10 @@ Implementation cards follow this order:
SIM S0 requires all of the following factual evidence: SIM S0 requires all of the following factual evidence:
- worker inventory; - 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; - exact component/package pins and licenses;
- one stock PX4 rover launch; - one stock PX4 rover launch;
- ROS 2 telemetry; - ROS 2 telemetry;
@ -444,7 +460,7 @@ SIM S0 requires all of the following factual evidence:
- clean stop with no orphan processes; - clean stop with no orphan processes;
- 1× resource baseline; - 1× resource baseline;
- 2× 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. The repository profile and doctor implement preflight and evidence admission.
They do not install packages, start processes, open ports or write a target They do not install packages, start processes, open ports or write a target

View File

@ -44,13 +44,20 @@ reports easy to misuse.
9. Source-of-record, derived artifacts and presentation state remain separate. 9. Source-of-record, derived artifacts and presentation state remain separate.
Reset creates a new immutable run/episode. Reset creates a new immutable run/episode.
10. Ground truth is evaluator-only and is not visible to perception or planning. 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 11. SIM S0 targets the reviewed Windows 11 worker through a dedicated
physically under `D:\NDC_MISSIONCORE`. `MissionCore-Sim` WSL2 Ubuntu 24.04 distribution physically under
12. S1 uses exact `px4_msgs` directly. The experimental `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. `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. 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 ## Consequences
@ -67,7 +74,7 @@ reports easy to misuse.
## First implementation ## 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, - `simulation/s0/qualification-profile.yaml` is the candidate stack,
storage, clock, authority, port and process contract. storage, clock, authority, port and process contract.
@ -77,6 +84,12 @@ The first implementation is deliberately read-only:
starting or modifying anything. starting or modifying anything.
- MISSIONCOR-40 tracks factual worker qualification. - 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 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 resolved/accepted pins and all required evidence. It cannot infer `GO` from the
repository state. repository state.

View File

@ -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.

View File

@ -5,11 +5,17 @@ real vehicle control or a `GO` verdict by itself.
## Safety and placement ## 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`. - All mutable bytes must be under `D:\NDC_MISSIONCORE`.
- Do not place a WSL distribution, Docker VHD, source, build, cache, log, bag, - Do not place a WSL distribution, source, build, cache, log, bag, ULog,
ULog, Gazebo artifact or dataset on C. 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. - 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. - Do not download full external datasets.
- Keep `actuator_authority=false`. - Keep `actuator_authority=false`.
@ -24,13 +30,34 @@ uv run missioncore-sim s0 doctor --json
Before target evidence exists, the expected verdict is `INCOMPLETE`. 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 <UTC-run-id>-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 ## Required target inventory
Record a redacted artifact for each of: Record a redacted artifact for each of:
1. Windows release, WSL version, Ubuntu version and architecture. 1. Windows release, WSL version, Ubuntu version and architecture.
2. GPU/driver, CPU, RAM and VRAM. 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. 4. D total/free bytes before installation.
5. Existing Docker images, WSL distributions and simulation-related processes. 5. Existing Docker images, WSL distributions and simulation-related processes.
6. Existing listeners and the candidate port registry. 6. Existing listeners and the candidate port registry.

View File

@ -4,8 +4,10 @@ profile_id: ai-worker-px4-gazebo-v1
target_host: target_host:
windows_release: windows-11 windows_release: windows-11
virtualization: wsl2 virtualization: wsl2
wsl_distribution: MissionCore-Sim
linux_distribution: ubuntu-24.04 linux_distribution: ubuntu-24.04
architecture: x86_64 architecture: x86_64
container_runtime: none
storage: storage:
windows_root: 'D:\NDC_MISSIONCORE' windows_root: 'D:\NDC_MISSIONCORE'
@ -15,11 +17,8 @@ storage:
forbid_windows_system_drive: true forbid_windows_system_drive: true
mutable_paths: mutable_paths:
- id: wsl - id: wsl
windows_path: 'D:\NDC_MISSIONCORE\wsl' windows_path: 'D:\NDC_MISSIONCORE\simulation\wsl'
wsl_path: /mnt/d/NDC_MISSIONCORE/wsl wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/wsl
- id: docker
windows_path: 'D:\NDC_MISSIONCORE\docker'
wsl_path: /mnt/d/NDC_MISSIONCORE/docker
- id: source - id: source
windows_path: 'D:\NDC_MISSIONCORE\simulation\source' windows_path: 'D:\NDC_MISSIONCORE\simulation\source'
wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/source wsl_path: /mnt/d/NDC_MISSIONCORE/simulation/source
@ -56,24 +55,24 @@ authority:
- rover-speed-yaw-rate/v1 - rover-speed-yaw-rate/v1
ros: ros:
domain_id: 42 domain_id: 0
namespace: /missioncore/polygon/s0 namespace: /missioncore/polygon/s0
components: components:
- id: ros2 - id: ros2
source_url: https://docs.ros.org/en/jazzy/ source_url: https://docs.ros.org/en/jazzy/
requested_ref: 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: resolved_commit:
license: mixed-apache-2.0-bsd license: mixed-apache-2.0-bsd
qualification_state: pending-worker-resolution qualification_state: candidate
- id: gazebo - id: gazebo
source_url: https://gazebosim.org/docs/harmonic/ source_url: https://gazebosim.org/docs/harmonic/
requested_ref: 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: resolved_commit:
license: apache-2.0 license: apache-2.0
qualification_state: pending-worker-resolution qualification_state: candidate
- id: px4-autopilot - id: px4-autopilot
source_url: https://github.com/PX4/PX4-Autopilot source_url: https://github.com/PX4/PX4-Autopilot
requested_ref: v1.17.0 requested_ref: v1.17.0
@ -90,9 +89,9 @@ components:
qualification_state: candidate qualification_state: candidate
- id: px4-gazebo-models - id: px4-gazebo-models
source_url: https://github.com/PX4/PX4-gazebo-models source_url: https://github.com/PX4/PX4-gazebo-models
requested_ref: main@2026-07-24 requested_ref: PX4-Autopilot/v1.17.0-submodule
resolved_version: resolved_version: px4-v1.17.0-submodule
resolved_commit: e00d3b9cde682dbcb3bf6f30a2f2b8ef4325dae8 resolved_commit: b6127f4ec20de867e215fb5f78ae88b80f371909
license: bsd-3-clause license: bsd-3-clause
qualification_state: candidate qualification_state: candidate
- id: micro-xrce-dds-agent - id: micro-xrce-dds-agent
@ -105,38 +104,38 @@ components:
- id: nav2 - id: nav2
source_url: https://github.com/ros-navigation/navigation2 source_url: https://github.com/ros-navigation/navigation2
requested_ref: jazzy@2026-07-24 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 resolved_commit: c92ea2fd9008f50c0ec8447610800214b2a0dafb
license: apache-2.0 license: apache-2.0
qualification_state: candidate qualification_state: candidate
ports: ports:
- id: micro-xrce-dds - id: micro-xrce-dds
host_scope: worker-wsl host_scope: loopback-only-netns
protocol: udp protocol: udp
bind: 127.0.0.1 bind: 0.0.0.0
port: 8888 port: 8888
owner: micro-xrce-dds-agent owner: micro-xrce-dds-agent
- id: px4-mavlink-sitl - id: px4-mavlink-onboard
host_scope: worker-wsl host_scope: loopback-only-netns
protocol: udp protocol: udp
bind: 127.0.0.1 bind: 0.0.0.0
port: 14540 port: 14580
owner: px4-sitl owner: px4-sitl
- id: qgroundcontrol-reference - id: px4-mavlink-normal
host_scope: worker-wsl host_scope: loopback-only-netns
protocol: udp protocol: udp
bind: 127.0.0.1 bind: 0.0.0.0
port: 14550 port: 18570
owner: px4-sitl owner: px4-sitl
- id: simulation-orchestrator-api - id: simulation-orchestrator-api
host_scope: worker-wsl host_scope: loopback-only-netns
protocol: tcp protocol: tcp
bind: 127.0.0.1 bind: 127.0.0.1
port: 18080 port: 18080
owner: simulation-orchestrator owner: simulation-orchestrator
- id: simulation-telemetry - id: simulation-telemetry
host_scope: worker-wsl host_scope: loopback-only-netns
protocol: tcp protocol: tcp
bind: 127.0.0.1 bind: 127.0.0.1
port: 18081 port: 18081
@ -164,9 +163,10 @@ processes:
required_tools: required_tools:
- git - git
- docker
- gz - gz
- make - make
- cmake
- ninja
- colcon - colcon
- ros2 - ros2
- MicroXRCEAgent - MicroXRCEAgent

259
simulation/s0/worker-smoke.sh Executable file
View File

@ -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}"

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import os
import platform import platform
import re import re
import shutil import shutil
@ -42,8 +43,10 @@ class DoctorVerdict(StrEnum):
class TargetHost: class TargetHost:
windows_release: str windows_release: str
virtualization: str virtualization: str
wsl_distribution: str
linux_distribution: str linux_distribution: str
architecture: str architecture: str
container_runtime: str
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@ -308,22 +311,34 @@ def _parse_target_host(value: object) -> TargetHost:
required={ required={
"windows_release", "windows_release",
"virtualization", "virtualization",
"wsl_distribution",
"linux_distribution", "linux_distribution",
"architecture", "architecture",
"container_runtime",
}, },
context="target_host", context="target_host",
) )
target = TargetHost( target = TargetHost(
windows_release=_string(data["windows_release"], "target_host.windows_release"), windows_release=_string(data["windows_release"], "target_host.windows_release"),
virtualization=_string(data["virtualization"], "target_host.virtualization"), virtualization=_string(data["virtualization"], "target_host.virtualization"),
wsl_distribution=_string(
data["wsl_distribution"],
"target_host.wsl_distribution",
),
linux_distribution=_string( linux_distribution=_string(
data["linux_distribution"], data["linux_distribution"],
"target_host.linux_distribution", "target_host.linux_distribution",
), ),
architecture=_string(data["architecture"], "target_host.architecture"), 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": if target.windows_release != "windows-11" or target.virtualization != "wsl2":
raise S0ProfileError("S0 target must remain the reviewed Windows 11 WSL2 worker") 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": if target.linux_distribution != "ubuntu-24.04" or target.architecture != "x86_64":
raise S0ProfileError("S0 target Linux distribution or architecture drifted") raise S0ProfileError("S0 target Linux distribution or architecture drifted")
return target return target
@ -583,7 +598,12 @@ def _parse_ports(value: object) -> tuple[PortBinding, ...]:
owner = _safe_id(data["owner"], f"ports[{index}].owner") owner = _safe_id(data["owner"], f"ports[{index}].owner")
if protocol not in {"tcp", "udp"}: if protocol not in {"tcp", "udp"}:
raise S0ProfileError(f"port {identifier!r} has unsupported protocol") 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") raise S0ProfileError(f"port {identifier!r} must remain loopback-only in S0")
if not 1 <= port <= 65535: if not 1 <= port <= 65535:
raise S0ProfileError(f"port {identifier!r} is outside 1..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" os_release.get("id") == "ubuntu" and os_release.get("version_id") == "24.04"
) )
architecture_match = machine in {"x86_64", "amd64"} 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: if matched:
return True, ( return True, (
f"matched {target.windows_release}/{target.virtualization}/" f"matched {target.windows_release}/{target.virtualization}/"
f"{target.linux_distribution}/{target.architecture}" f"{target.wsl_distribution}/{target.linux_distribution}/{target.architecture}"
) )
return False, ( return False, (
"doctor is not running inside the reviewed Windows 11 WSL2 Ubuntu 24.04 " "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(): if artifact_candidate.is_symlink():
raise S0ProfileError(f"evidence artifact {identifier!r} cannot be a symlink") raise S0ProfileError(f"evidence artifact {identifier!r} cannot be a symlink")
artifact = artifact_candidate.resolve(strict=True) artifact = artifact_candidate.resolve(strict=True)
if ( if not artifact.is_relative_to(evidence_root) or not artifact.is_file():
not artifact.is_relative_to(evidence_root)
or not artifact.is_file()
):
raise S0ProfileError(f"evidence artifact {identifier!r} is not a regular file") raise S0ProfileError(f"evidence artifact {identifier!r} is not a regular file")
expected_sha256 = _string( expected_sha256 = _string(
entry["sha256"], entry["sha256"],
@ -966,8 +985,7 @@ def _tool_list(value: object, context: str) -> tuple[str, ...]:
def _string_list(value: object, context: str) -> tuple[str, ...]: def _string_list(value: object, context: str) -> tuple[str, ...]:
result = tuple( result = tuple(
_string(item, f"{context}[{index}]") _string(item, f"{context}[{index}]") for index, item in enumerate(_sequence(value, context))
for index, item in enumerate(_sequence(value, context))
) )
if len(set(result)) != len(result): if len(set(result)) != len(result):
raise S0ProfileError(f"{context} values must be unique") raise S0ProfileError(f"{context} values must be unique")

View File

@ -37,6 +37,8 @@ def test_canonical_s0_profile_is_strict_and_safe() -> None:
profile = load_s0_profile(PROFILE) profile = load_s0_profile(PROFILE)
assert profile.profile_id == "ai-worker-px4-gazebo-v1" 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.windows_root.drive.upper() == "D:"
assert profile.storage.wsl_root.as_posix() == "/mnt/d/NDC_MISSIONCORE" assert profile.storage.wsl_root.as_posix() == "/mnt/d/NDC_MISSIONCORE"
assert profile.clock.authority == "gazebo:/clock" 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.actuator_authority is False
assert profile.authority.navigation_or_safety_accepted is False assert profile.authority.navigation_or_safety_accepted is False
assert profile.authority.direct_actuator_setpoints_allowed 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} >= { assert {item.identifier for item in profile.components} >= {
"px4-autopilot", "px4-autopilot",
"px4-msgs", "px4-msgs",
"gazebo", "gazebo",
"nav2", "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} == { assert {item.identifier for item in profile.processes} == {
"simulation-orchestrator", "simulation-orchestrator",
"gazebo-server", "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)) 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: def test_doctor_never_claims_go_without_target_evidence(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr( monkeypatch.setattr(
"k1link.simulation.s0._target_host_match", "k1link.simulation.s0._target_host_match",