Skip to content

ROS2 Integration

Why this bridge exists

Kyvern produces cryptographically signed Decision objects tied to a tamper-evident hash chain. This bridge publishes each Decision to a ROS2 topic so any ROS2 node — controller, logger, HMI — can consume decisions natively without coupling to the Kyvern Python API.

Quick start

Prerequisites: ROS2 Humble installed and sourced (source /opt/ros/humble/setup.bash).

Publish decisions:

from services.integrations.ros2_bridge import KyvernDecisionPublisher

pub = KyvernDecisionPublisher(topic="/kyvern/decisions")
pub.start()
pub.publish(signed_decision)   # signed_decision is a dict from sign_decision()
pub.stop()

Subscribe and verify:

from services.integrations.ros2_subscriber_example import KyvernDecisionVerifier

verifier = KyvernDecisionVerifier(public_key_path="/tmp/kyvern-demo/signing.pub")
verifier.start()   # blocks; Ctrl-C to stop

Or run the subscriber standalone:

python -m services.integrations.ros2_subscriber_example \
    --pubkey /tmp/kyvern-demo/signing.pub

Message format

Topic type: std_msgs/String

Payload: canonical JSON produced by decision_to_ros2_json(decision). Field order is alphabetical (sort_keys=True) for determinism.

Key fields:

Field Type Description
action string Decision action (allow / alert / halt / handoff / engage)
chain_index int Position in the hash chain
payload_hash string SHA-256 of canonical payload (hex)
prev_hash string | null Hash of preceding decision; null for chain head
policy_version_id string SHA-256 of the policy file at decision time
roe_reference string Rule ID that fired
signature string Base64 Ed25519 signature
timestamp_iso string ISO 8601 UTC timestamp

Why not a custom message type? A custom kyvern_msgs/Decision.msg would require every subscriber to build and source the kyvern_msgs package. std_msgs/String with JSON means a Python, C++, or Rust subscriber can parse the payload with a single json.loads call, with no build-time Kyvern dependency.

QoS recommendations

Scenario Setting
Safety-critical decisions (halt, handoff, engage) qos_reliability="reliable" (default)
High-frequency telemetry / allow decisions qos_reliability="best_effort"
pub = KyvernDecisionPublisher(qos_reliability="best_effort")

End-to-end test (WSL + ROS2 Humble)

First generate demo files:

python scripts/generate_demo_chain.py

Terminal 1 — subscriber:

source /opt/ros/humble/setup.bash
python -m services.integrations.ros2_subscriber_example \
    --pubkey /tmp/kyvern-demo/signing.pub

Terminal 2 — publish one decision from the demo chain:

source /opt/ros/humble/setup.bash
python -c "
import json
from services.integrations.ros2_bridge import KyvernDecisionPublisher

chain = [json.loads(l) for l in open('/tmp/kyvern-demo/chain.jsonl')]
pub = KyvernDecisionPublisher()
pub.start()
pub.publish(chain[0])
pub.stop()
"

Terminal 1 should log: [VERIFIED] chain_index=0

Recording a robot's own decisions

The bridge publishes decisions Kyvern's engine made. To record the decisions your robot's own controller makes, call kyvern.record_decision() from your node; examples/ros2_safety_demo does this for a LaserScan-based safety controller.