autonomy ros2 run

Run a ros2 subcommand under policy governance (dual-path)

Synopsis

Run any ros2 subcommand under AutonomyOps policy governance.

The dual-path executor selects the execution mode automatically:

  Container path: requires --image pointing to a ROS2-capable OCI image.
    docker run --rm <image> ros2 <subcommand> [args...]

  Native path: used when Docker is unavailable and ros2 is in PATH.
    ros2 <subcommand> [args...]
    REDUCED-GOVERNANCE and FAIL-CLOSED by default — refused unless
    --allow-reduced-governance is passed; a warning is printed to stderr and
    the acceptance is audited.

PASS markers are emitted to stdout at each governance checkpoint so that
demo scripts and CI harnesses can assert end-to-end correctness:

  PASS ros2-telemetry-active   context=<container|native>
  PASS ros2-runtime-container-ready  context=container  (container path only)
  PASS ros2-stack-start        context=<container|native>
  PASS ros2-policy-enforced    context=<container|native>  (on success only)
  PASS ros2-wal-recording      context=<container|native>  (on success only)

Every invocation also persists one autonomy.decision frame to the WAL
(tool=tool.ros2.<kind>, outcome=allow|deny, policy_ref=<provenance>).
Inspect with: autonomy wal inspect.

Examples:

  # List topics using the container path (pin to a specific release)
  autonomy ros2 run --image ghcr.io/autonomyops/adk-ros2-runtime:v1.0.0 \
      topic list

  # Launch a node using the container path with a custom policy
  autonomy ros2 run \
      --image ghcr.io/autonomyops/adk-ros2-runtime:v1.0.0 \
      --policy policies/arm_safety.rego \
      launch arm_control arm.launch.py

  # Native path (no Docker): ros2 must be in PATH
  autonomy ros2 run topic echo /cmd_vel

Usage

autonomy ros2 run <subcommand> [args...] [flags]

Options

      --agent-domain int                                       ROS_DOMAIN_ID the bridge subscribes on and the launched subprocess publishes on. Honored only with --governed-bridge. 0 → runtime default (99). Must differ from --real-domain or the bridge refuses to start (isolation contract).
      --allow-reduced-governance                               explicitly accept the REDUCED-GOVERNANCE native path (no container isolation or message interception). Required to run natively; without it a native launch fails closed. The acceptance is logged and audited.
      --bridge-enclave name                                    name SROS 2 enclave the bridge participant adopts on the real DDS domain (must start with '/'; e.g. /governed_ros2_bridge_real). Must exist under <bridge-keystore>/enclaves/<name>/ — mint with 'autonomy ros2 keystore mint'. All-or-nothing with --bridge-keystore + --workload-enclave.
      --bridge-keystore dir                                    dir absolute path to the SROS 2 keystore (created by 'autonomy ros2 keystore init'). #938 Phase 3-B. Enables DDS-Security defense-in-depth: the bridge container + launched workload both get ROS_SECURITY_* env injected and the keystore bind-mounted read-only. REQUIRES --governed-bridge — passing this flag without it errors out (SROS 2 is defense-in-depth on the bridge, not a standalone substitute). Requires --bridge-enclave AND --workload-enclave to be set together (all-or-nothing — any partial set errors out before any side effect).
      --bridge-topics strings                                  #939 4-A: the (topic, type) set the governed bridge subscribes on. Comma-separated 'topic:type' pairs OR repeated --bridge-topics flags. Example: --bridge-topics '/cmd_vel:geometry_msgs/msg/Twist,/disable_safety:std_msgs/msg/Bool'. Honored only with --governed-bridge. EMPTY (the default) makes the bridge fall back to its compiled-in '/agent_chat' typed std_msgs/msg/String — which means workload publishes on any other topic are silently ungoverned (stderr warning surfaces this). Always pass the workload's topics in production.
      --cap-drop cap                                           Linux cap to drop from the workload container, rendered as --cap-drop <name> on the docker spawn. Repeat the flag or comma-separate (e.g. --cap-drop NET_RAW,SYS_PTRACE). Each entry is a capability name like NET_RAW, SYS_PTRACE, SYS_ADMIN, or the magic value ALL to drop every capability docker normally grants. Unknown names are rejected at the autonomy layer (docker would silently accept typos, leaving the cap set wider than intended). Container path only — RunGoverned REFUSES this option when the resolved mode is native (--force-native, or docker unavailable + no --image): Linux caps are a process-spawn property and the native dispatcher inherits the autonomy CLI's own set with no per-spawn narrowing available (ErrHardeningRequiresContainer). (#960 Phase 5a)
      --dlopen-allowlist-from-bundle path                      path to a workload bundle (.tar or directory containing manifest.json) whose v1.5 manifest declares a dlopen_allowlist block. The CLI loads the manifest, validates it under Phase 2a's rules, joins the declared paths with `:`, and sets the result as AUTONOMY_PRELOAD_DLOPEN_ALLOWLIST on the workload container so the LD_PRELOAD shim's dlopen wrapper (Phase 2b-1) enforces it. Empty manifest field or no block declared → env unset on the workload → shim's opt-in default-allow pass-through. REQUIRES --ld-preload <path-to-libautonomy_preload.so> alongside — the env var enforces nothing if no shim is loaded to read it, and RunGoverned REFUSES this flag without --ld-preload via ErrDlopenAllowlistRequiresShim (the canonical 'looks enforced, isn't' trap, same shape as #962/#963/#964 reviewer fixes). Mirrors the --from-bundle precedent from #955. Container path only — REFUSED on native fallback via ErrHardeningRequiresContainer. When unset AND --policy is supplied, the CLI auto-sources the allowlist from the same v1.5 manifest the policy gate already loaded — operators using --policy don't need to repeat the bundle reference here (#960 Phase 2b-3). The explicit --dlopen-allowlist-from-bundle flag still wins when set: it lets the operator pick a different bundle's allowlist than --policy uses (e.g. dev / paranoid override). (#960 Phase 2b-2)
      --force-native                                           bypass Docker availability probe and run ros2 as a native subprocess (ros2 must be in PATH; selects the REDUCED-GOVERNANCE path)
      --governed-bridge                                        #913 Phase 2: spawn the long-lived governed_ros2_bridge process and run the launched ros2 subprocess on the agent DDS domain. The bridge subscribes there, POSTs every message to /v1/tool, and on allow republishes on the real DDS domain. Requires the in-process tool server (always wired by this command). Default off → existing AutoRuntime + ExecBridge behavior.
      --image string                                           OCI image for container-path execution (required when Docker is available; must include the ROS2 toolchain)
      --ld-preload path                                        absolute IN-CONTAINER path to a shared library the dynamic linker preloads on workload start (sets LD_PRELOAD on the docker spawn). Interposes on libc functions (execve/connect/open/dlopen) for syscall-adjacent mediation seccomp can't statically discriminate. Phase 4a ships the PLUMBING only — operators can make the .so reachable in the container either by baking it into a custom image, or by passing --writable-mount <host>:<container>:ro pointing at the directory holding the .so (--writable-mount honors --read-only-rootfs alongside; #1005). The runtime-shipped canonical shim lands in Phase 4b. The path is container-side; the autonomy layer validates absoluteness but CANNOT validate existence (no visibility into the container's filesystem) — a missing .so surfaces at workload startup as a dynamic-linker error. Container path only — RunGoverned REFUSES this option when the resolved mode is native (--force-native, or docker unavailable + no --image): on the native path LD_PRELOAD would run the operator's .so with HOST privileges, a different + more dangerous threat model than container-isolated workload interposition (ErrHardeningRequiresContainer). (#960 Phase 4a)
      --no-seccomp                                             opt out of the default-on seccomp profile for the workload container. By default this CLI applies the embedded starter profile (#960 acceptance criterion); --no-seccomp skips it entirely. Emits a loud stderr warning (containing the literal 'Audit event: seccomp-opt-out') AND a WAL marker so operator log scrapers + the audit pipeline both record the opt-out. Mutually exclusive with --seccomp-profile (the runtime errors before any side effect). REFUSED on native dispatch (--force-native, or docker unavailable + no --image): the warn+audit contract can't be honored without a container path, and silently no-oping would create a 'looks audited, isn't' trap — drop --no-seccomp on native (the native path is already running without seccomp). Use only for development or workloads that legitimately need denied syscalls (debuggers via ptrace, profilers via perf_event_open, namespace-managing tools via setns/unshare). (#960 Phase 1)
      --policy string                                          policy bundle OCI reference or local path (default: dev-mode ros2 policy — permissive on tool.ros2.*, hard-deny on tool.shell, fail-closed on unknown kinds; each invocation persists one autonomy.decision frame to the WAL)
      --preload-connect-allowlist destination                  destination the workload is permitted to connect() to. Five accepted forms: <ipv4>:<port> (exact), <ipv4>/<prefix>:<port> (IPv4 CIDR), [<ipv6>]:<port> (exact IPv6, bracket form), [<ipv6>/<prefix>]:<port> (IPv6 CIDR — prefix INSIDE brackets), or any of the above with `:*` for any-port. Repeat or comma-separate (e.g. --preload-connect-allowlist 10.0.0.5:443,10.42.0.0/16:8080,[fe80::1]:443,[2001:db8::/32]:*). Joined with `,` (NO_PROXY-style) and set as AUTONOMY_PRELOAD_CONNECT_ALLOWLIST on the workload container for the LD_PRELOAD shim's connect() wrapper to enforce. Empty (default) leaves the env unset → shim's opt-in default-allow pass-through. LOOPBACK always allowed by the shim regardless (IPv4 127.0.0.0/8, IPv6 ::1, IPv4-mapped ::ffff:127.0.0.0/96) — the autonomy runtime's /v1/tool endpoint lives there. AF_UNIX (local IPC sockets) pass through unconditionally; AF_INET6 is NOW GATED (post-#960 — was pass-through in Phase 3; existing IPv4-only configs are unaffected but operators upgrading should add IPv6 destinations explicitly). Hostnames NOT supported (DNS attack surface; operators pre-resolve). REQUIRES --ld-preload <path-to-libautonomy_preload.so> alongside; RunGoverned REFUSES this flag without --ld-preload via ErrPreloadConnectAllowlistRequiresShim. Operator-supplied list (egress policy is per-deployment, not per-bundle). Container path only; REFUSED on native fallback via ErrHardeningRequiresContainer. (#960 Phase 3 + post-#960 CIDR/IPv6)
      --preload-exec-allowlist path                            absolute binary path the workload is permitted to execve(). Repeat or comma-separate (e.g. --preload-exec-allowlist /bin/echo,/usr/bin/ros2). Joined with `:` and set as AUTONOMY_PRELOAD_EXEC_ALLOWLIST on the workload container for the LD_PRELOAD shim's execve wrapper (Phase 4b-1) to enforce. Empty (default) leaves the env unset → shim's opt-in default-allow pass-through. REQUIRES --ld-preload <path-to-libautonomy_preload.so> alongside — the env var enforces nothing without a shim to read it, and RunGoverned REFUSES this flag without --ld-preload via ErrPreloadExecAllowlistRequiresShim (same 'looks enforced, isn't' trap shape as #962/#963/#964/#969 reviewer fixes; caught proactively this slice). Operator-supplied list (no bundle-manifest auto-source yet because no v1.5 analogue for execve exists). Container path only; REFUSED on native fallback via ErrHardeningRequiresContainer. (#960 Phase 4b-2)
      --read-only-rootfs                                       mount the workload container's root filesystem read-only via docker's --read-only flag. Workloads needing writable in-container scratch declare it explicitly via --tmpfs <path> (ephemeral, in-memory) or --writable-mount <host>:<container>[:ro|:rw] (persistent, backed by the host filesystem). Both work alongside --read-only-rootfs. False (default) preserves Docker's default read-write rootfs. Container path only — RunGoverned REFUSES this option when the resolved mode is native (--force-native, or docker unavailable + no --image): there is no container rootfs to mount read-only on the native path (ErrHardeningRequiresContainer). (#960 Phase 5b)
      --real-domain int                                        ROS_DOMAIN_ID the bridge republishes allowed messages onto. Honored only with --governed-bridge. 0 → runtime default (42, matching the adk-ros2-runtime image's own ROS_DOMAIN_ID=42 default). Must differ from --agent-domain.
      --runtime-url string                                     AutonomyOps runtime HTTP address for governance callbacks (e.g. http://localhost:8080; default: local-only evaluation)
      --seccomp-profile path                                   absolute path to a seccomp profile JSON applied to the workload container via `docker --security-opt seccomp=<path>`. When set, overrides the bundled starter profile that this CLI applies by default (#960 acceptance criterion: default-on seccomp). The shipped starter (runtime/seccomp/runtime-starter.seccomp.json, embedded in the binary) narrows Docker default by denying container-escape primitives (mount, setns, unshare, kexec_load, ptrace, etc.); use --no-seccomp to skip seccomp entirely (emits a stderr warning + WAL seccomp-opt-out audit marker). Container path only — RunGoverned REFUSES this option when the resolved mode is native (--force-native, or docker unavailable + no --image): there is no native-path analog, so silently dropping it would be a 'looks hardened, isn't' trap (ErrHardeningRequiresContainer). #960 Phase 4 (LD_PRELOAD) is the planned native-path syscall mediation. (#960 Phase 1)
      --tmpfs path                                             absolute container path to mount as an in-memory tmpfs via docker --tmpfs. Repeat or comma-separate (e.g. --tmpfs /tmp,/run). The supported escape hatch for read-only-rootfs workloads that need ephemeral writable scratch (the common case: /tmp). Each path must be absolute; relative or empty entries are rejected at the autonomy layer (docker --tmpfs accepts them but silently ignores). Empty (default) preserves Docker's default rootfs layout. Container path only — RunGoverned REFUSES this option when the resolved mode is native: no container layer to host an isolated tmpfs, and silently letting writes hit the host /tmp would be a 'looks isolated, isn't' trap (ErrHardeningRequiresContainer). For PERSISTENT (not ephemeral) writable storage backed by the host filesystem, use --writable-mount instead. (#960 Phase 5b)
      --workload-enclave name                                  name SROS 2 enclave the launched workload subprocess adopts (must start with '/'; e.g. /demo_robot/arm_controller). DELIBERATELY separate from --bridge-enclave so a compromised workload can't impersonate the bridge to publish on real. All-or-nothing with --bridge-keystore + --bridge-enclave.
      --workspace string                                       ROS2 workspace directory forwarded as AUTONOMY_ROS2_WORKSPACE (native path only)
      --writable-mount <host-path>:<container-path>[:ro|:rw]   host→container bind mount declared as <host-path>:<container-path>[:ro|:rw], rendered as docker --volume on the container spawn. Repeat or comma-separate (e.g. --writable-mount /var/log/myapp:/var/log/myapp,/srv/data:/data:ro). The supported escape hatch for read-only-rootfs workloads that need PERSISTENT writable surface (logs, caches, fleet-shared state) rather than the ephemeral tmpfs --tmpfs provides. Both host and container paths must be absolute, the host path must exist at parse time, and `..` segments in the container path are rejected (escape-attempt guard; specify the intended path directly). Mode token is optional: `:rw` (default) for read-write; `:ro` for operator-supplied material the workload should NOT modify (signed certs, immutable config). Container path only — RunGoverned REFUSES this option when the resolved mode is native: no container layer to mount into (ErrHardeningRequiresContainer). Each declared mount emits a WAL audit marker (kind=writable_mount_declared, attrs={host, container, mode}) BEFORE dispatch so the writable surface is durably recorded even if the workload crashes on startup. (The CAVEAT noted on the CE `autonomy run python.run` surface — host-side audit skipped under --runtime-url — does NOT apply here: `autonomy ros2 run` always owns its WAL emitter.) (#1005)

See also

  • autonomy ros2 — Execute governed ROS2 commands under AutonomyOps policy