ROS2 Markers and Observability¶
The AutonomyOps ROS2 governance stack emits observable PASS markers to stdout at each governance checkpoint. These markers are simultaneously written to a WAL (Write-Ahead Log) for durable audit. CI harnesses, demo scripts, and operators all rely on these exact strings.
PASS marker format¶
PASS <label> context=<context>
label— kebab-case identifier for the checkpoint (stable across versions)context— execution path:container,native, orsim
Example (allow path, container, embedded ROS2 policy):
PASS ros2-telemetry-active context=container
PASS ros2-runtime-container-ready context=container
PASS ros2-launch-allowed context=container
PASS ros2-stack-start context=container
PASS ros2-policy-enforced context=container
PASS ros2-wal-recording context=container
Marker reference¶
Launcher-level markers (host-side runtime)¶
Marker label |
Context |
When emitted |
|---|---|---|
|
container / native |
WAL emitter initialised — durable recording is live |
|
container only |
Container execution path selected; Docker is reachable |
|
container / native |
Host-side launch-policy gate allowed |
|
container / native |
Host-side launch-policy gate denied |
|
container / native |
About to dispatch to |
|
container / native |
Policy evaluation passed AND execution returned nil |
|
container / native |
WAL has a record of this execution (post-exec) |
Node-level markers (Tier 4 — both entry points)¶
These markers are printed by the launched demo node itself when the
launcher provides an in-process tool server. Both
autonomy run ros2.launch … (CE) and autonomy ros2 run … (paid) set
RunOptions.AutoRuntime=true by default whenever the host-side gate
runs. The markers are absent only when AUTONOMY_RUNTIME_URL is not
injected into the subprocess env (rare — implies the host-side gate
was bypassed).
Marker label |
When emitted |
|---|---|
|
Node POSTed |
|
Node POSTed |
Each POST produces one further autonomy.decision frame in the WAL
(emitted by the in-process tool server, distinct from the launch-level
frame the host-side gate emits). The action kinds (tool.ros2.topic.list
allowed, tool.shell hard-denied) are stable across every ros2 policy
variant the demo can run under — the embedded ros2-demo allowlist AND
ros2.DefaultEvaluator — so the markers fire deterministically
regardless of which evaluator the launcher wired.
Container vs native context¶
ros2-runtime-container-ready is emitted only on the container path. It is
absent when Docker is unavailable and the native fallback is taken.
CI harnesses that assert all six markers must first confirm that Docker is
present. If running on a host without Docker, assert only the five markers
whose context is native:
# Container path — assert all six (allow path)
grep -F 'PASS ros2-telemetry-active context=container' output.txt
grep -F 'PASS ros2-runtime-container-ready context=container' output.txt
grep -F 'PASS ros2-launch-allowed context=container' output.txt
grep -F 'PASS ros2-stack-start context=container' output.txt
grep -F 'PASS ros2-policy-enforced context=container' output.txt
grep -F 'PASS ros2-wal-recording context=container' output.txt
# Native path — assert five (container-ready is absent)
grep -F 'PASS ros2-telemetry-active context=native' output.txt
grep -F 'PASS ros2-launch-allowed context=native' output.txt
grep -F 'PASS ros2-stack-start context=native' output.txt
grep -F 'PASS ros2-policy-enforced context=native' output.txt
grep -F 'PASS ros2-wal-recording context=native' output.txt
ros2-launch-allowed / ros2-launch-denied only fire when no --policy
override is set; otherwise the host-side gate is skipped and the runtime
layer (RunGoverned) applies the user-supplied policy directly.
When markers are NOT emitted¶
Situation |
Markers emitted |
|---|---|
Host-side gate denied |
|
Runtime-layer policy denied ( |
|
|
|
|
|
Execution succeeded (allow path) |
All six (container) / five (native) |
ros2-policy-enforced and ros2-wal-recording are post-execution markers —
they are never emitted for denied or failed executions.
WAL recording¶
Every PASS marker is backed by a WAL entry before the stdout line is written. The WAL:
Survives process crashes between marker emission and the final
ros2-wal-recordingIs stored in a temporary directory (
/tmp/adk-ros2-wal-<random>/) for the duration of the governance sessionIs drained to the orchestrator after execution completes
The WAL entry format (JSONL):
{"kind":"PASS","label":"ros2-telemetry-active","context":"container","ts":"2026-04-07T10:00:00.123456Z"}
Querying WAL entries after a run¶
With a running orchestrator:
# Fetch recent log entries from the orchestrator
autonomy logs --orchestrator-url http://localhost:8888 --limit 50
# Filter to this node only
autonomy logs --orchestrator-url http://localhost:8888 \
--node my-edge-01 --limit 50
# Stream new entries as they arrive
autonomy logs --orchestrator-url http://localhost:8888 --follow
WALPass — the marker primitive¶
PASS markers are emitted via markers.WALPass(label, context, emitter) in
cmd/autonomy/pkg/markers/:
// WALPass writes a PASS marker to both the WAL and stdout.
// It calls emitter.EmitPASS first (durable) then markers.Pass (stdout).
// Returns an error if either write fails.
func WALPass(label, context string, emitter PassEmitter) error
The PassEmitter interface is satisfied by *telemetry.Emitter. Tests can
inject a lightweight in-memory emitter to verify marker emission without a
real WAL directory.
CE-tier markers (autonomy run ros2.launch)¶
The CE dispatch path (autonomy run ros2.launch) emits the same five PASS
markers using the same WAL-backed mechanism. The execution context is resolved
by dualpath.Resolve at call time, so context annotation is accurate even when
Docker availability changes between calls.
Observability pipeline¶
PASS marker (stdout)
│
▼
markers.WALPass ──▶ telemetry.Emitter.EmitPASS ──▶ WAL (BoltDB)
│
orchestrator drain
│
GET /v1/logs
│
autonomy logs / SSE
The WAL acts as a durable buffer: even if the orchestrator is temporarily unreachable, markers are persisted locally and drained when connectivity is restored.
Demo script pattern¶
Demo scripts and CI harnesses that need to assert PASS markers can use:
#!/bin/bash
set -euo pipefail
output=$(autonomy ros2 run \
--image ghcr.io/autonomyops/adk-ros2-runtime:local \
launch demo_robot arm_demo.launch.py 2>&1) || true
for marker in \
"PASS ros2-telemetry-active context=container" \
"PASS ros2-runtime-container-ready context=container" \
"PASS ros2-stack-start context=container" \
"PASS ros2-policy-enforced context=container" \
"PASS ros2-wal-recording context=container"
do
if ! echo "$output" | grep -qF "$marker"; then
echo "FAIL: marker not found: $marker" >&2
exit 1
fi
done
echo "All PASS markers present"
Troubleshooting¶
ros2-policy-enforced missing
The governance stack executed but the policy denied the action (or the subprocess returned a non-zero exit code). Check:
The policy file (
demo/bundles/ros2/policies/ros2_safety.rego) allows the requested package and launch file.The
--imagematches the build in the bundleactivation.entrypoint.Run
autonomy bundle inspect demo/bundles/ros2.tar --local --show-policyto verify the policy text.
ros2-runtime-container-ready missing
Docker is not reachable. Confirm:
docker info
If Docker is absent, the native path is taken. The marker is not emitted on the native path — this is expected behaviour, not an error.
WAL entries not appearing in autonomy logs
Requires a running orchestrator:
autonomy-orchestrator serve --listen 0.0.0.0:8888 --data-dir /tmp/orch
autonomy logs --orchestrator-url http://localhost:8888 --follow
See also¶
ROS2 Governance — dual-path execution and policy gates
Robotics Quickstart — end-to-end run
cmd/autonomy/pkg/markers/— marker primitive packagetelemetry/— WAL emitter package