A C++ mission execution runtime built around a typed, auditable authority boundary between high-level autonomy and a trusted low-level controller.
MAF sits between your autonomy stack and your vehicle controller. Every command crossing that boundary — whether from a hand-coded behavior tree, a classical planner, or a learned policy — passes through a runtime monitor that enforces structural invariants before it reaches the actuators.
┌─────────────────────────────────────┐
│ Mission layer │
│ Hand-coded behaviors / AI nodes │ ← untrusted, swappable
└────────────────┬────────────────────┘
│ command stream
▼
┌─────────────────────────────────────┐
│ Runtime monitor │ ← trusted, always runs
│ bounds · rate-of-change · staleness│
└────────────────┬────────────────────┘
│ approved commands only
▼
┌─────────────────────────────────────┐
│ Adapter → ArduPilot / controller │ ← single writer
└─────────────────────────────────────┘
The boundary doesn't depend on what sits above it. A hand-coded navigate node and a 10B-parameter VLA satisfy the same interface. The monitor, adapter, and session log stay the same regardless.
Most robot autonomy stacks mix mission logic, sensor fusion, command generation, and controller communication in ways that make it hard to reason about what can go wrong and harder to detect it when it does.
MAF's thesis is that a small trusted surface — a monitor and a single-writer adapter — can provide meaningful safety guarantees even when the autonomy above it is learned, opaque, or partially trusted, as long as the boundary contract is specified precisely and logged completely.
The empirically interesting question: for any given autonomy stack, what fraction of failures does the architectural monitor catch alone, the behavioral monitor catch alone, both, or neither? That partition shifts as the command distribution changes. Measuring it across policy types is what this prototype is built to do.
Only maf_ardupilot_adapter writes to the controller. No other code path has access to the transport. Commands from the mission layer are evaluated by the RuntimeMonitor before the adapter sees them. If the monitor rejects a command, it substitutes a fallback or triggers a halt — the adapter never sees the original.
Three invariant classes cover most of what's catchable at the command interface:
- Per-sample bounds — NaN/Inf, out-of-range values, stale timestamps
- Rate-of-change limits — per-step deltas exceeding what the platform can execute
- Temporal-window invariants — individually valid commands that are collectively dangerous (oscillation, jerk, geofence displacement) (in progress)
Every monitor decision records the full active invariant set alongside the outcome. Post-mortem attribution is decidable from logs without operator memory.
Missions are sequences of tasks. Each task has a goal, a behavior node that executes it, and transition rules for what happens on success or failure. The mission executor is the state machine. The behavior node is just the current executor — it ticks every 20 Hz and returns Success, Failure, or Running.
Hand-coded and learned nodes satisfy the same interface. Substituting one for the other changes nothing below the boundary.
Phase 1 — Monitor contract in SITL
-
BTNodecontract: symmetric for hand-coded and learned nodes -
WorldState: telemetry thread writes, BT nodes read on tick -
ArdupilotAdapter: MAVLink telemetry → live pose inWorldState -
RuntimeMonitor: per-sample bounds, rate-of-change, staleness -
MonitorDecision: Passed / Fallback / Halt + full active invariant set - Single-writer adapter enforced at the module boundary
- Session log: frame, timestamp, outcome, triggered invariant, throttle, steering
-
NavigateNode: navigates using real EKF pose; rejects stale observations - ArduPilot stream rate configuration on connect
- Temporal-window invariants (oscillation, geofence, jerk)
- Wire MAVLink submodule and test against ArduPilot Rover SITL
- MCAP structured logging
Phase 2 — Learned node injection
Replace NavigateNode with a learned ONNX node above the boundary; run the same monitor unchanged. Measure catch fractions: hand-coded vs. learned, conservative vs. distribution-shifted.
Phase 3 — Behavioral monitor
Add a behavioral monitor above the architectural boundary. Measure the four-way failure partition: architectural-only, behavioral-only, both, neither.
MAF/
├── shared/contracts/ # BTNode, GoalContext, CommandStream,
│ # MonitorDecision, WorldState
└── maf_rover/
├── main.cpp # tick thread (20 Hz)
├── monitor/ # RuntimeMonitor
├── mission/ # BehaviorTree, NavigateNode
└── adapter/ # ArdupilotAdapter
Requires CMake 3.16+, a C++20 compiler, and OpenSSL.
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build buildOptional flags:
| Flag | Default | Effect |
|---|---|---|
MAF_ENABLE_MAVLINK |
OFF | Enable real MAVLink transport (requires third_party/mavlink submodule) |
MAF_ENABLE_ONNXRUNTIME |
OFF | Enable ONNX learned node support (Phase 2) |
Without MAF_ENABLE_MAVLINK, the adapter runs in stub mode: a fixed pose is injected into WorldState and command writes are no-ops. The monitor and mission logic run normally.
(MAVLink submodule wiring in progress — see plan)
# Start ArduPilot Rover SITL
sim_vehicle.py -v Rover --console
# Run MAF
./build/maf_rover/maf_rover
# Session log written to /tmp/maf_<session_id>.log- Architecture — authority boundary, data flow, monitor design
- Plan — research goal, phase breakdown, exit criteria
- Mission Design — task sequencing, state machine, user roles
- High-Level Behaviors — behavior composition, multi-actuator output, coverage planning
The trusted surface stays small. Monitor + adapter + session log. Everything above is untrusted and swappable.
The contract is symmetric. Hand-coded and learned nodes satisfy the same interface. No runtime path differs between them below the boundary.
Every decision is attributable. The active invariant set is logged alongside every monitor outcome. The key post-mortem question — did no applicable invariant exist, or did one exist and the monitor miss it — is always answerable from the log.
The boundary outlasts the backend. maf_ardupilot_adapter is the first of a pluggable adapter family. The monitor contract doesn't care what controller is downstream.
MIT. See LICENSE.