ROS 2 Protected Execution

Audience: operators bringing up a protected actuator path on ROS 2 — #1310 Phase 4. This is the ROS 2 concrete instance of the domain-neutral Protected Execution Boundary model: it gives the robot execution exclusivity on top of the logical authority + faithfulness the governed bridge and enactment receipts already provide.

Untrusted autonomy nodes          Governed ROS bridge          Motor controller
  (proposal DDS domain)   ──▶   (the ONLY dual-domain member)  ──▶  (execution DDS domain)

The bridge subscribes proposals on the untrusted domain, consults ADK, and republishes only authorized commands on the execution domain. The boundary makes that the only path: no autonomy node can publish the actuator topic on the execution domain directly.

The gap it closes

The SROS 2 bridge already gives you the mechanism — distinct DDS domains and signed per-enclave permissions. But nothing validates that you assembled it correctly: two enclaves could both hold --publish /cmd_vel, or governance could allow unauthenticated participants, and the graph would still come up. The builtin.sros2 boundary provider turns the intended exclusivity into a checked, fail-closed contract: a bundle declaring the boundary refuses to activate unless exactly one enclave (the enforcer) can publish the actuator topic on the execution domain.

Step 1 — Declare the boundary (deployment manifest, schema v1.8)

schema_version: "1.8"
execution_boundaries:
  - id: rover_motion
    action_kinds: [tool.ros2.topic.publish]
    adapter:
      id: governed_ros2_bridge
      identity: /governed_ros2_bridge_real      # the enforcer SROS 2 enclave
    target:
      kind: ros2.dds_domain
      resource: /cmd_vel                          # the actuator topic
      schema: geometry_msgs/msg/Twist
    protection:
      provider: builtin.sros2
      provider_version: "1"
      configuration:
        mode: dds_domain_plus_sros2               # the strongest reference posture
        proposal_domain: 99
        execution_domain: 42
        keystore_path: /etc/adk/keystore
        enforcer_enclave: /governed_ros2_bridge_real
    requirements:
      exclusive_enforcement_principal: true
      proposer_has_no_direct_access: true
      preflight_validation: required              # fail closed at activation

Modes: dds_domain_isolation (distinct domains only), sros2_permissions (exclusive publish grant + no insecure fallback), or dds_domain_plus_sros2 (both — recommended). required preflight means a bundle whose boundary can’t be validated is refused and the prior bundle stays live.

Step 2 — Provision the keystore so ONLY the enforcer publishes

Mint identities + permissions so the actuator publish grant on the execution domain (42) is held by the bridge enclave and nobody else:

autonomy ros2 keystore init --keystore /etc/adk/keystore --domain 42 --domain 99
autonomy ros2 keystore permissions --keystore /etc/adk/keystore \
  --enclave /governed_ros2_bridge_real --domain 42 --domain 99 \
  --publish /cmd_vel --subscribe /cmd_vel,/disable_safety

Every workload/robot enclave gets, at most, a subscribe grant on /cmd_vel on domain 42 and its own publish grants on the proposal domain (99) — never a publish grant on the actuator topic on the execution domain. That asymmetry is exactly what the boundary validates. Governance for the execution domain must positively protect it — a domain rule that covers domain 42 with allow_unauthenticated_participants=false and enable_join_access_control=true (the defaults from keystore init); an ungoverned domain or disabled join access control is treated as an insecure fallback. Run participants under ROS_SECURITY_STRATEGY=Enforce.

Step 3 — Verify (activation is the gate)

Install the manifest at runtime start (or via a verified release):

autonomy runtime start --policy /etc/adk/policy --deployment-manifest /etc/adk/deploy.yaml

If the keystore satisfies the boundary, startup proceeds. If it does not — a second enclave can publish /cmd_vel on domain 42, the enforcer holds no grant, the domains collide, or governance allows unauthenticated participants — startup refuses fail-closed with an execution-boundary: reason naming the finding, e.g.:

runtime start: execution boundary "rover_motion" [execution-boundary: bypass_detected]:
  enforcement principal is not exclusive (bypass_publisher: non-enforcer enclave(s)
  can publish the actuator topic "/cmd_vel" on domain 42: CN=/planner_node)

The bundle does not activate; the prior (valid) bundle keeps running. Each decision on the protected kind then records execution_boundary_id + expected_adapter_identity on the audit frame, and an enactment receipt from any enforcer other than /governed_ros2_bridge_real is a deviation.

Failure modes

Symptom

Cause

Fix

execution-boundary: bypass_detected (bypass_publisher)

A non-enforcer enclave holds a /cmd_vel publish grant on the execution domain.

Revoke that enclave’s actuator publish grant.

execution-boundary: identity_mismatch (adapter_not_authorized)

The enforcer enclave has no /cmd_vel publish grant.

keystore permissions --enclave <enforcer> --publish /cmd_vel.

execution-boundary: invalid (dds_domain_not_isolated)

proposal_domain == execution_domain.

Assign distinct ROS_DOMAIN_IDs.

execution-boundary: invalid (insecure_fallback)

Governance allows unauthenticated participants.

allow_unauthenticated_participants=false + enable_join_access_control=true.

execution-boundary: provider_unavailable

Manifest pins a provider/version the runtime doesn’t register.

Use builtin.sros2 version 1.

Hardware validation

The end-to-end on-Orin scenario — the two-domain graph under Enforce, plus an on-hardware rogue publisher that fails to reach the execution domain — is a HIL procedure (the boundary provider is validated by host tests + keystore fixtures in CI; live DDS-Security is not CI-gated). Run it on the Jetson Orin target under harness/hil/ when validating a physical deployment.