rig brings up and manages every stack on a robot/vehicle computer or replay host —
shared infra (zenoh router, bag loggers, dashboard), sensors (cameras, GNSS/INS, lidar), and
autonomy (planners, SLAM, perception) — driven by config. It is "a loop + a manifest": it
reads a vehicle manifest and delegates the bring-up of each stack to that service's own launcher
(<service>-up), in tier order. It never reimplements per-stack logic. Stacks come from sibling
checkouts or from package registries (pinned services, profiles, overlays, suites, and — since
v0.2.17 — whole-vehicle plans), and a deployment bakes into a tagged artifact that runs on bare Docker.
The dependency is strictly one-way: rig depends on the service repos; a service never knows about
rig. rig learns each service only through its rigging.yaml descriptor + the launcher CLI, so
services evolve independently and new ones drop in by adding two files (a launcher + a rigging.yaml —
rig rigify generates both onto existing software).
registries (public / internal) vehicle.yaml services.yaml
services · profiles · overlays · which stacks, in 3 tiers; where each repo lives
suites · vehicle plans (pinned) identity, platform, ROS env (checkout or vendored)
│ rig pkg add / upgrade │ │
└──────────────────────────────────────► rig ◄──────────────────────┘
│ per stack: <launcher> <config> <verb>
│ + fleet env (ROS_DOMAIN_ID, RMW, VEHICLE_ID,
│ RIG_IMAGE_REGISTRY/TAG, RIG_TARGET_PLATFORM, RIG_DATA_DIR)
┌────────────────────────────────────────────────┼────────────────────────────────────────────────┐
▼ infra (up FIRST, down last) ▼ sensors (producers) ▼ autonomy (up LAST, down FIRST)
zenoh-router-up · dash-up · bag-logger-up cam-up · novatel-up · sbg-up · lidar-up planner-up · slam-up · perception-up
docker compose — one project per instance docker compose — one project per instance docker compose — one project per instance
│
rig bake ──► tagged artifact (configs + vendored launchers + compose-only
scripts + rig) ──► ./run.sh up on the vehicle, bare Docker
Each launcher is the service's own (cam-up is the exemplar; rig rigify scaffolds one onto existing
software); rig only sequences the tiers, exports the fleet env, and aggregates status.
Start with the documentation index for deployment, replay and simulation workflows, current validation status, and the historical design notes.
- Launcher (
<service>-up) owns everything per-stack: parse the config, derive the ROS namespace (/<name>), render driver params, select/parameterize its static compose, wire devices/network, rundocker compose. It honors the rig-providedCOMPOSE_PROJECT_NAME(never passes-p), falling back to its own<service>_<name>only when run standalone. - rig owns the cross-cutting concerns: which stacks run (the manifest, in three tiers),
globally-unique instance names, the compose project name (
<name>-vehicle-<vehicle_id>), bring-up order (infra → sensors → autonomy; reversed on the way down, so the decider dies before its eyes), the fleet env (ROS_DOMAIN_ID/RMW_IMPLEMENTATION,VEHICLE_ID,RIG_IMAGE_REGISTRY/RIG_IMAGE_TAG,RIG_TARGET_PLATFORM+ each service's declared platform override env,RIG_DATA_DIR), per-host identity and platform (vehicle.yaml + the machine tier written byrig provision), run directories (one session = one folder), status/health aggregation, and lifecycle: external-volume GC on final teardown (down --purge) and full decommission (rig cleanup— images + volumes off the host, never containers, never data).
- Ubuntu/Debian (incl. Jetson): download
rig_<v>_all.debfrom the latest release →sudo apt install ./rig_<v>_all.deb(requirespython3+python3-yaml). For offline installation, have those dependencies installed/cached first. - macOS:
brew install christomaszewski/rig/rig-cli(formularig-cli, binaryrig; the tap tracks releases automatically) - anywhere with pipx/uv:
pipx install git+https://github.com/christomaszewski/rig - from a checkout:
./rig …works as-is;rig setup --shellputs it on PATH
Then, per user: rig setup (creates ~/.rig + subscribes the default public registry). On a
vehicle, also provision its identity once: sudo rig provision --id 7 --name skiff-07
(writes /etc/rig/vehicle.local.yaml — every fleet artifact on the machine reads it).
TAB completion (verbs, instance names, package refs, registries): deb and brew installs ship
it for bash + zsh — new shells just have it. pipx/checkout installs: rig setup --shell wires it
into your rc (or eval rig completion bash|zsh yourself).
Upgrades ride the package manager (apt/brew upgrade) — rig never self-updates. Uninstall:
rig setup --purge (user state), then the package manager (the package never touches ~/.rig).
Releases are cut by tag: every vX.Y.Z tag publishes the deb/wheel/sdist and bumps the Homebrew
formula automatically.
# host needs Python 3 + PyYAML and Docker (compose v2). For local dev:
python3 -m venv .venv && .venv/bin/pip install pyyaml
# authoring — build a deployment (see docs/CHEATSHEET.md for the full flow)
./rig init my-vehicle --vehicle-id 1 --infra zenoh-router --discover # scaffold: wired infra + a menu
# (--vehicle-id pins a single-vehicle identity; without it the tree
# carries per-host MARKERS, supplied by `rig provision`/RIG_VEHICLE_ID)
./rig add ../novatel # wire ONE more service into an existing deployment (path or bare name)
./rig fetch # hand-wrote vehicle.yaml rows? materialize their configs from examples
./rig rigify ../my-sw # make EXISTING software rig-compatible (descriptor + launcher + example)
# the package registry — no checkouts needed (CHEATSHEET §1.5; public seed registry on GitHub)
./rig setup # once per machine: ~/.rig + the default `public` registry
./rig registry sync # then offline: pinned installs, vendored surfaces, rig.lock (a service's
# first install at a new pin fetches its repo into the local git mirror)
./rig init my-vehicle --infra public/zenoh-router --add public/sbg # scaffold straight from a registry
./rig add public/zenoh-router # infra from the registry, at an exact pin
./rig add sensor:zr30 # profile match -> service + editable config, hash-anchored
./rig config diff # git-status for configs
./rig pkg save zr30 # publish the edits into the package they CAME FROM (render identical);
# `pkg promote` is for NEW packages/forks; `pkg list` = the inventory
./rig pkg outdated # dependency drift across the registries (repair: pkg repin / rebase)
# fleet vehicles — one artifact, N vehicles (CHEATSHEET §1.6)
# reference per-vehicle values as {{vehicle_id}} etc. in configs; `rig bake` auto-detects the
# templates and defers rendering to the vehicle, which supplies identity via `rig provision`
# lifecycle — run the vehicle
./rig doctor # read-only preflight (unique names, one ROS distro, launchers present, ...)
./rig doctor --deep # + certify each service's launcher against the contract (runs `config`)
./rig certify # launcher-contract conformance under a poisoned fleet env (see below)
./rig up --dry-run # print the exact launcher invocations + fleet ROS env, run nothing
./rig up # infra → sensors → autonomy; equal order within a tier starts concurrently
./rig pull # pre-pull enabled stacks' images, NO container changes
./rig pull bag_player # explicitly pull a disabled replay player (use its actual instance name)
./rig status # one rolled-up row per sensor (state + health from compose ps; OP =
# observed operational state for services declaring the state verbs —
# read OP and HEALTH as a pair)
./rig status -v # expand per-container detail
./rig standby # park declared stacks (reverse order): ready but quiet — lifecycle idle,
# devices low-power (ouster: motor stopped, laser off); HEALTH unaffected.
# Services without the verbs are always-active, skipped. `up --standby`
# comes up parked (RIG_TARGET_STATE beats each config's initial_state)
./rig activate # wake declared stacks (producers first; launchers own the budgets —
# device spin-up can take a minute). Trigger-style stacks (SLAM) may
# instead be activated by the mission layer via ROS lifecycle directly
./rig logs cam_front -f # follow one sensor's logs
./rig config gnss_primary # render a sensor's merged compose (delegates to the launcher's `config`)
./rig graph # observed pub/sub/service topology from a run's graph epochs (the bag
# logger's `graph:` sidecar, rig-infra ≥ v1.7.0); --check compares vs the
# riggings' declared interface: (WARN-only), --contract <instance>
# scaffolds that block from observation, -o writes the union YAML
./rig run rm <id> / run import <path> # registry lifecycle: reclaim disk (sealed freely,
# interrupted --force, the OPEN run never) / adopt archived runs so
# id-based verbs + TAB cover them. The registry home: `rig setup
# --data-dir` (this user, no sudo — a first interactive `rig setup`
# asks) or `sudo rig provision --data-dir` (this machine); minted
# lazily. `rig setup --show` = where everything is, one screen;
# `--data-dir NEW --migrate` moves a registry (symlink left behind).
# A tree with its OWN data_dir (vehicle.local.yaml — a reconstruct
# workspace) sees the host registry READ-THROUGH too (runs/TAB/
# refs); writes stay in its own, and no tree sees another's
./rig run archive <id…> --to /Volumes/nas/runs # move a sealed run's BYTES off the registry
# (<to>/<vehicle>/<id>) and keep a LINKED entry: runs/replay/TAB still
# resolve it, `run rm` unlinks, an unmounted drive lists as dangling
./rig run tag <run> site:mojave event:demo-day # tags live in the run's own manifest and travel
# with it (run untag removes); `rig catalog --tag site:` finds them
./rig catalog [query] [--tag T --label L --vehicle V --since DATE] # EVERY run rig knows about:
# the machine registry, every registry it opened/imported into,
# fleet-sync trees, reconstruct workspaces (`catalog roots|add|remove`).
# `rig runs`/TAB stay scoped: this deployment's registry + the host's
./rig run export <run> --profile review # a SLIM copy of a sealed run under <run>/exports/review/
# made ON the vehicle: the profile's `omit` globs leave files out
# (video), services whose rigging declares `export:` re-write their
# own data smaller (the bag logger: zstd_small, topics dropped), the
# rest is hardlinked. `rig fleet sync --profile review` pulls it
./rig run export <run> --service camera-service # camera-owned default h264 profile: service-only
# proxies under RUN/exports/services/camera-service/h264/<export-id>/;
# originals/bags stay intact; --instance selects recorded names,
# --force regenerates without deleting previous generations
./rig run export <run> --in-place # RECLAIM disk instead: the same exporters REWRITE their data
# inside the run (bags re-written at zstd_small, per-topic counts
# verified against the original, then swapped in). Lossless with no
# --profile; a profile that drops topics needs --lossy; nothing is
# ever omitted/deleted; any run that holds the data works — old
# ones and other deployments' included. The manifest records it
./rig reconstruct <run-dir> # a run dir back into a runnable tree, anywhere: every opened run
# captures the deployment (surfaces + configs + rig, no image bytes)
# into .rig/artifact.tar.gz — extract, verify, overlay a config
# snapshot, localize. Pre-capture runs: `rig run retrofit` stamps them
# with the deploy artifact their manifest names. Opt-out: run_capture
# --registry HOST localizes images.registry too (a bench's mirror;
# machine-wide: `provision --registry`); --enable-replay <path|ref>
# wires the SIL player into a tree that flew without it (opt-in)
./rig replay <run> planner # SIL: NEW provenance-linked run; the named instances come up LIVE and
# the current ros2-bag-player plays broad recorded data inputs;
# optional ownership evidence excludes regenerated outputs.
# --selection recorded retains recorded-subscription selection.
# The bag-logger records the new outputs: source bag vs replay bag is
# the A/B pair. Refuses while stacks run; teardown = down --end-run
# --from S / --to S replay a SECTION (seconds from bag start — the
# call-script zero; latches restored; --auto-end composes for sweeps)
./rig run replay <run> --skip-service camera-service --auto-end
# replay without camera services/video; skips every camera instance
# and its bag topics. Repeat --skip-service for other services.
# `rig replay` and `rig run replay` are equivalent; --dry-run previews.
./rig down # reverse groups (autonomy FIRST); equal-order peers stop concurrently
# --purge also GCs external volumes after teardown
./rig replay check <run> # inspect a hand-built deployment's readiness, without writes
./rig replay prepare <run> --recipe field-test
# --player /path/to/rig-infra/ros2-bag-player adds a missing player row,
# route and config; kept disabled for ordinary up, selected by replay
./rig replay --recipe field-test
# Author replay/analysis.yaml using the recipe example in docs/REPLAY.md, then:
./rig run export <run> --recipe replay/analysis.yaml --dry-run
./rig run export <run> --recipe replay/analysis.yaml
./rig replay prepare <run> --recipe replay/analysis.yaml
./rig replay --recipe replay/analysis.yaml
# one recipe selects bags, per-camera originals/proxies and exact services;
# recipe inputs need no --use-proxies. Missing proxies fail explicitly
./rig sim check --recipe warehouse # inspect an already running Isaac ROS graph and /clock
./rig sim up --recipe warehouse # attach selected infra/autonomy using sessions/warehouse.yaml
./rig sim down --end-run # stop the saved Rig session; its recording replays normally
./rig fleet sync --into fleet-runs --include-service-exports
# include committed camera/service exports; default sync excludes exports.
# Bag proxies under exports/inputs/bags need a complete run copy or re-export
./rig swap <svc> <path|ref> # re-point an INSTALLED service at different code — a local checkout
# (routed live: edit, rebuild, re-up) or a registry ref (vendored +
# pinned). Rows, working configs and overlays are KEPT; config-key
# drift vs the new example is reported. The reconstructed-tree SIL
# swap: same config, same data, different code
./rig cleanup # decommission: remove this deployment's images + volumes from the host
# (after the final down, before deleting the tree; --dry-run to preview)
./rig up cam_front ins_main # operate on a subset by name./rig uses the system python3; on a host where PyYAML lives in a venv, run .venv/bin/python rig …
(and point each launcher's *_PYTHON env at that interpreter), or apt install python3-yaml on the robot.
Build images into a registry the vehicle can reach, bake a tagged artifact, ship it, run it — no driver
source or internet on the vehicle. bake auto-vendors each service's launch surface (no manual rig vendor step) and digest-pins images against the registry from vehicle.yaml (images.registry) — pass
--registry <ip:5000> only to override it:
rig build # all declared runtimes and CPU architectures, build/push
rig build --scope host --tag iteration-1 # this machine's provisioned runtime + Docker CPU
rig build --scope deployment,host # vehicle target plus this host's native runtime
rig build --scope deployment,replay # vehicle target plus supported replay companions
rig build --platform dev --arch amd64 --tag iteration-1 # faster targeted iteration
rig build --no-cache # disable instruction caching in every participating build
rig build sbg novatel # only these services; a failure prints the retry command
rig src sync # mirror every git source the builds need (~/.rig/cache/git)
rig build --offline # then build without remote git or upstream registries
rig image audit # inspect what the stacks will RUN: one ROS distro, the
# declared rmw installed, ros-* versions agree across
# images; warns on images built from local checkouts
rig bake --tag v1 # -> var/artifacts/v1.tar.gz (auto-vendored + digest-pinned)
# rig bake --tag v1 --include-replay # auto-wire a missing player + validate onboard replay
# combine --include-replay --bundle-images for offline startup AND replay images
# rig bake # same when vehicle.yaml has images.tag: v1
scp var/artifacts/v1.tar.gz vehicle:~/ # on the vehicle: `tar xzf v1.tar.gz && cd v1 && ./run.sh up`Portable builds and replay covers the build contract, Buildx setup,
companion digests, reconstruction onto another host, replay startup policy, and migration of old runs.
Onboard replay covers replaying local recordings from a shipped artifact,
with registry access by default and optional offline replay images.
Coordinated startup covers opt-in held replay, consumer
readiness, common release, startup timeouts and compatible companion images.
Replay preparation covers instance replay.start, local recipes, check/prepare,
renamed services, exact bag selection, and source-preserving legacy imports. No pub/sub declarations
are required; packaging recipes with suites is deferred.
Isaac simulation covers attaching selected services to an existing Isaac
ROS graph, shared version-2 session recipes, clock checks, recording and replay. Version-1 replay
recipes retain their existing behavior.
Offline Isaac walkthrough covers preparing a compatible Jazzy/Zenoh
deployment, local scene/assets and image caches, then camera capture and replay with a shared
recipe. Isaac attachment is experimental (since v0.2.73); live GPU acceptance is still pending.
Hardware runtimes use separate tags (v1-jp6m, v1-dev); multiarch indexes select the CPU within a
runtime. rig reconstruct RUN --platform dev selects the software runtime; --platform recorded
keeps the source runtime. Doodle's updated descriptor omits live radio startup during replay while
preserving recorded topics; rig replay RUN --include-service doodle opts back in.
--tag overrides the artifact name; when omitted, it defaults to the literal images.tag in
vehicle.yaml. A missing or templated image tag still needs --tag; host-local overrides do
not choose the artifact name. This does not change the images' tags.
Reusing an existing artifact tag prompts before replacement; non-interactive jobs must pass
--overwrite. Publication is atomic, so a failed archive write preserves the previous archive.
rig artifact ls # target, provenance and archive/staging/extracted sizes
rig artifact show v1 # target, bundled image IDs and complete release catalog
rig artifact show v1 --format json
rig artifact rm v1 --dry-run # preview removal of the archive and generated staging
rig artifact rm v1 # confirm interactively; --yes for automation
rig artifact prune --keep 3 --dry-run # preview removal of all but the 3 newest archivesRemoval preserves extracted trees by default; --include-unbaked explicitly includes matching
var/unbaked/<tag> trees, which may contain edits. Pruning orders archives by file modification time
and removes their matching staging trees. Orphaned staging trees are listed and can be removed by tag
with artifact rm. These commands leave Docker images and recorded runs alone; rig cleanup and
rig run rm manage those separately.
Bake uses deployment declarations and explicit flags; workspace/machine vehicle.local.yaml, shell
configuration overrides and generated fleet/replay state cannot leak into it. platform selects the
runtime and optional arch: arm64|amd64 selects the CPU. --platform/--arch override them for one bake;
missing values are inferred only when the service matrices have one common choice.
The artifact bundles the resolved configs + vendored surfaces + rig + a compose-only form that runs on
just Docker (graceful fallback when Python/PyYAML are absent). --bundle-images additionally docker-saves
the deployment runtime/CPU image set into the artifact (multi-GB) for zero-registry deploys;
with --include-replay, it also includes the selected replay image set
(Docker API 1.48+). The complete release catalog remains metadata. Content aliases preserve identity
across save/load. up.sh checks the target and compares local image
IDs with the bundle, reloads missing or different images, verifies the result, and starts Compose
without pulling. ./run.sh load forces a reload and the same identity verification.
Re-baking inside an extracted artifact (on the vehicle, after field edits) records the parent artifact in
metadata.yaml, so save-points form a lineage. Full offline / local-registry flow: docs/HOST_SETUP.md.
Fleet artifacts need no flag: if the deployment references {{var}} values (e.g.
rtsp://10.160.{{vehicle_id}}.80/…), rig bake stages the tree unresolved and each vehicle
renders it locally from its provisioned identity — one artifact serves the whole fleet
(sudo ./provision.sh --id 7 --name skiff-07 once per vehicle, then ./run.sh up forever).
CHEATSHEET §1.6 has the full lifecycle.
Bundled fleet artifacts require each enabled service's verbs.images planner so images can be selected
without resolving vehicle identity. The contract and scopes
describe the workflow and defaults.
Stacks install from package registries (a git repo or shared folder of manifests — the public seed
is rig-registry-public; rig registry init
scaffolds your own, with tools/validate + CI wrappers). Five kinds:
| kind | what it is | install shape |
|---|---|---|
service |
a code pointer (repo + full-SHA rev) and/or a digest-pinned image | rig add internal/lidarish — vendors the launch surface, routes it, instance from its example |
profile |
a complete nameless config for one service (requires.service), keyed service:short |
rig add sensor:zr30 / rig add camera-service:siyi-zr30 — the instance's editable working config, base pinned + hash-anchored |
overlay |
a versioned delta on top of a profile-based instance (ordered bindings, last wins; local still beats overlays) | rig overlay apply <instance> <ref> — bindings only |
suite |
references only: members at exact pins (services/profiles/overlays + at most one vehicle plan) | rig pkg add internal/gideon-boat — atomic, all-or-rollback |
vehicle |
a suite's instance plan (v0.2.17): a template vehicle.yaml whose rows carry YOUR instance names, order, enabled, tiers, per-row overlay bindings, fleet defaults (platform, images, data_dir…); per-host identity stays markers |
never standalone — captured with its suite (promote --all --suite S --vehicle V), installed through it into an empty rig init tree |
The capture/reproduce loop: tune a deployment → rig pkg promote --all --suite <s> --vehicle <plan> --to internal [--adopt] (dirty instances become overlays; hand-authored ones become adopted profiles
with --adopt) → on a fresh tree rig pkg add internal/<s> gives the vehicle back, identity supplied
per host. Exact pins are snapshots (v0.2.19): a member's later release never breaks the suites or
profiles that pin it — registry validate warns, installs serve the pinned version from the
registry's git history, rig pkg outdated reports drift (exit 1, CI-able) and rig pkg repin
refreshes. CHEATSHEET §1.5 is the daily loop; docs/DESIGN.md has the decision log.
doctor checks the vehicle (manifest composition); certify checks a service — it runs the
launcher's config verb under poison env values (certify.invalid:5000, certify-tag-x, instance
certifyname0) and asserts the contract held: project name honored (no -p), images pulled from
RIG_IMAGE_REGISTRY, built images pulled as :RIG_IMAGE_TAG (build/pull agreement; composed
<tag>-<platform> for services declaring a build.platforms matrix), the declared platform honored
(RIG_TARGET_PLATFORM/override env — every matrix entry must render on any host, never host-probe),
fleet ROS env reaching containers unmangled, deterministic output, identity fully derived from the config
name, clean stdout, parseable status. Run it in a deployment (rig certify [name…], or
rig doctor --deep) or in a service repo's CI with no deployment at all:
rig certify --repo . --config core-driver/config/usb-real.yaml # the service's own CI gate
rig certify --repo . --require-build-check # also require a completed isolated cache/platform check
rig certify cam_front --emit /tmp/mac.yaml # then the same on the vehicle...
rig certify --diff /tmp/mac.yaml /tmp/orin.yaml # identical = `config` output is host-independent, so a
# dev-box bake is provably correct for the targetvehicle.yaml # which stacks THIS machine runs + fleet-wide ROS settings (+ vars:/env:
# fleet defaults; {{var}} markers = supplied per vehicle)
vehicle.local.yaml # OPTIONAL, gitignored, never baked: THIS machine's identity/values for
# bench trees; vehicles use /etc/rig/vehicle.local.yaml (rig provision)
services.yaml # catalog: service routing key -> where its repo lives
config/sensors/*.yaml # one config per sensor (the single source of truth for that stack)
config/infra/*.yaml # one config per shared infra service (zenoh router, bag logger, …)
config/autonomy/*.yaml # one config per autonomy stack (planner, SLAM, perception, …)
replay/analysis.yaml # authored replay recipe; version 2 can select exact session.instances
sessions/*.yaml # optional location for recipes shared by replay and simulation
var/replay/<recipe>/ # generated local bindings/plans; re-prepare after changing a recipe
services/ # VENDORED launch surfaces (`rig vendor`; bake auto-vendors; `rig add
# public/<svc>` vendors at the registry pin) — for dev, point services.yaml
# at sibling checkouts instead. Never source/submodules.
config/.pins/ # GENERATED pristine base copies (registry installs) — what `config diff`,
# `pkg upgrade`, and `pkg promote` measure against. Commit them.
config/.overlays/ # GENERATED bound-overlay payload copies (deployment stays self-contained)
rig.lock # GENERATED pins: registries@commit, packages+hashes, instance anchors,
# image digests (bake). Commit it; `rig add <ref> --locked` reproduces.
rig, rig_cli/ # the CLI (thin shim + package: manifest/catalog/dispatch/status/doctor/certify/
# build/bake/init/rigify/runs/…)
docs/ # index · command reference · worked workflows · design/history
Lists active stacks (name, service, config, enabled, order) and the fleet ROS settings, in three
tiers: infra: (substrate — routers, loggers, dashboards; up FIRST, down last), sensors: (producers),
and autonomy: (graph consumers — planners, SLAM, perception; up after ALL sensors, down FIRST, so the
decider dies before its eyes). The tier partition is hard; per-entry order sorts within a tier only.
For up, entries with the same tier and order start concurrently, without a flag. Rig waits for
every launcher in that group to finish before starting the next order or tier. Assign independent
sensors the same order to overlap their startup waits; keep different orders for dependencies or
services sharing an exclusive resource. For example:
sensors:
- {name: lidar, service: ouster, config: config/sensors/lidar.yaml, order: 10}
- {name: camera, service: camera-service, config: config/sensors/camera.yaml, order: 10}
- {name: consumer, service: processor, config: config/sensors/consumer.yaml, order: 20}lidar and camera start together; consumer waits for both launchers to return. up --dry-run
shows concurrent groups in a stable order. down reverses the groups: autonomy → sensors → infra,
with higher orders first within each tier. Equal-order peers stop concurrently, and the next group
waits for all of them. down --dry-run shows that reverse schedule.
This also works in newly baked artifacts: the bundled rig, run.sh up / run.sh down, and
generated up.sh / down.sh preserve the same groups. Re-bake existing artifacts to include the
new scheduler. Compose-only scripts wait for Compose startup; launcher-based startup
also includes any sensor-state waits implemented by each launcher. Neither adds a universal sensor
health check, and consumers must still retry as discovery is dynamic.
Ordinary rig up / rig down attempt remaining groups after launcher failures and report failures at the
end. Compose-only artifact startup still stops on failure, after waiting for all peers already
started in the failing group; artifact teardown continues through all groups and reports any failure.
down --end-run captures logs before teardown and seals only after success; --purge runs after all
teardown groups finish. Ctrl-C stops in-flight launcher processes without rolling back completed
container operations. Use rig status to inspect the result. Other verbs, replay, and simulation
retain their existing ordering. Disable
a stack with enabled: false rather than deleting its config. name must be unique across the vehicle —
it keys the compose project, external volumes, and ROS namespace.
Per-vehicle values flow through {{var}} markers (never ${VAR} — that's compose's): configs and
manifest scalars may reference {{vehicle_id}}, vars: entries, etc., resolved at render from
shell (RIG_VAR_*) > vehicle.local.yaml > /etc/rig/vehicle.local.yaml > vehicle.yaml
defaults. A self-marker (vehicle_id: "{{vehicle_id}}") makes the value MANDATORY per vehicle —
and since v0.2.20 rig init scaffolds vehicle/vehicle_id as markers by default (nothing comes
up as "vehicle 1" by accident; --vehicle-id N pins a single-vehicle literal). platform: (e.g.
jp7) declares the deployment runtime → RIG_TARGET_PLATFORM; optional arch: arm64|amd64 declares
the CPU → RIG_TARGET_ARCH. Services with a build matrix
pull <image>:<tag>-<platform> and images.tag means VERSION only. Both identity and platform are
per-host facts the machine tier (sudo rig provision --id 7 --name skiff-07 --platform jp7) may
override during local execution. Build scopes and bake use the separate deployment/host selection
rules above; an artifact's recorded target is validated before startup.
An env: map exports extra (interpolated) variables to every launcher. One mapping form,
{{map <list_var> <template_var>}}, builds lists (e.g. zenoh peer endpoints from
{{fleet_peer_ids}} — the fleet minus THIS vehicle, derived). CHEATSHEET §1.6.
Maps each service routing key to its repo path (resolved relative to this repo). The key may differ
from the repo dir name (service sbg → repo sbg_driver).
Thin ROS 2 drivers share a generic schema — service, name, connection (tcp/udp/serial/file),
ros.namespace, and an opaque driver_params block the launcher renders into the driver's ROS 2
params. The rich camera-service camera uses its own service-specific schema (rig hands it to cam-up as-is).
A config can instead be a nameless profile reused across instances via a per-sensor overrides: patch
in vehicle.yaml — rig deep-merges the patch, stamps in name, and renders the result to
var/rendered/<name>.yaml before handing it to the launcher (a complete named config with no overrides is
passed through untouched). See config/sensors/camera.profile.yaml and docs/ROADMAP.md §1.
Shared infra services (rig-infra)
Ready-to-use shared (infra:) services live in their own repo — clone it beside your deployment,
point services.yaml at a service dir (../rig-infra/zenoh-router) and add an infra: entry, or let
rig init --infra zenoh-router (bare name — the workspace scan finds it; or the registry form
--infra public/zenoh-router, no checkout needed) wire it for you; in an
EXISTING deployment, rig add <name|path> does the same wiring after the fact (infra arrives enabled,
sensor/autonomy services get a commented menu row to uncomment):
zenoh-router/— the vehicle's sharedrmw_zenohrouter (order 0). Default: thefleet-rosbase image runningrmw_zenohd, so the router and the ROS sessions share one distro's zenoh packages by construction. Optional inlinerouter_config:renders to a mountedzenohd.json5.ros2-bag-logger//ros1-bag-logger/— record the ROS telemetry graph to${RIG_DATA_DIR}/current/bags/<name>(the open run — ROADMAP §3c; flatbags/<name>without a registry). Config:record.mode: all|allow|exclude(+exclude_images, default true — image streams are huge over ROS and already recorded compressed at the camera source) and anoutputblock (storage/compression/split). Place at order ~1 so it records from startup. The ros2 logger defaults tofleet-ros(rosbag2 + mcap + rmw_zenoh, ~1 GB — no camera image on camera-less vehicles).base/— thefleet-rosimage;rig buildbuilds + pushes it via the riggings'build:declaration, and certify enforces the composes pull the same tag. The zenoh-router and ros2-bag-logger riggings mark itprovides: base, making it the DEPLOYMENT's base image:rig buildbuilds it first and exports it to every other build (and to launchers) asRIG_BASE_IMAGE, so one image pins the fleet's distro+rmw packages;vehicle.yaml images.base(orrig build --base-image) overrides it with an external ref.msgs/— thefleet-ros-msgsoverlay: base + the union of the interface packages the fleet's services declare in their riggings'msgs:blocks. rosbag2 cannot record a topic whose message package isn't installed in the recorder's image (it logs "unknown type" and keeps going), so a fleet with custom types silently gets bags missing them. When services declaremsgs:and a base provider declaresbuild.msgs_overlay,rig buildrenders the union manifest (RIG_MSGS_MANIFEST), builds the overlay right after the base (FROMRIG_BASE_IMAGE— an externalimages.basegets an overlay too), and exports the ref asRIG_MSGS_IMAGE; the bag logger's compose prefers it over the bare base. No declarations → no overlay → the bare base, which is correct. Under rig, thesource:repos are exported on the host from rig's local git mirrors (no clone in the builder, sorig build --offlinebuilds the overlay too). An overlay built from local checkouts (rig-infra'sFLEET_MSGS_LOCAL) is labelledio.rig.build.local:rig image auditwarns about it andrig bakerefuses it without--allow-local.
Each is just a launcher + compose around a stock tool — the same contract any service meets; rig-infra's
CI runs rig certify against every one.
A repo is rig-compatible when its launcher exposes up/down/status/logs/config on one config, accepts a
config at any host path, honors fleet ROS env, observes stdout/stderr discipline (machine output on
stdout, human lines on stderr), and ships a rigging.yaml (the legacy name deploy.yaml is still accepted).
Optionally it also implements the operational-state trio — standby/activate/state, declared in
verbs: all-three-or-none (the declaration is the support claim; contract + reference adoption: the
service-state adoption prompt in ~/ws/infra, ouster ≥ v0.2.0) — and honors RIG_TARGET_STATE at up.
Start from rig rigify <dir>: it generates the descriptor + a contract-correct launcher skeleton +
an example config in an existing software dir, pre-wired from a read-only analysis (found composes get
-f-wired; ports/volumes/images/Dockerfiles become commented host_ports/external_volumes/mirror/
build: hints). The onboarding arc is rig rigify → rig certify --repo (until green) → rig add:
service: novatel
launcher: novatel-up # default: <service>-up
verbs: { status: ps } # adapt logical verbs -> launcher args (defaults shown in descriptor.py)
ros_distro: lyrical
# ros_namespace: "/robot/{name}" # optional: actual ROS NODE namespace; fixed /control/relay also works
# # identifies graph ownership; does not launch/remap nodes
tier: sensor # optional: "infra" = shared, up-first (routers, loggers, dashboards);
# "autonomy" = graph consumer, up-last / down-first (planners, SLAM)
examples: [sensors/novatel.example.yaml] # optional: example configs — init/add/fetch copy them;
# `rig certify --repo` uses the first as its default --config
launch_surface: # the minimal file set `rig vendor`/`bake` copy to LAUNCH
- novatel-up # this service (never its source)
- docker/compose/compose.deploy.yaml
# build: { command: tools/build-images.sh, images: [novatel] } # `rig build` runs `<cmd> <registry> [tag]`
# # (ROS_DISTRO exported from vehicle.yaml ros.distro)
# build: { command: ..., images: [...], platforms: [jp7, jp6] } # a build MATRIX: distinct image sets per
# # hardware/OS target — rig composes pulls as <tag>-<platform>
# build: { command: ../base/build.sh, images: [fleet-ros], provides: base } # this build produces the
# # deployment's BASE image: rig builds it FIRST and exports
# # <registry>/<images[0]>:<tag> as RIG_BASE_IMAGE to every
# # other build + launcher (vehicle.yaml images.base overrides).
# # Providers of one base must agree on build.platforms AND
# # the build script — rig refuses rather than pick by order.
# build: { command: ../base/build.sh, images: [fleet-ros], provides: base,
# msgs_overlay: { command: ../msgs/build-msgs.sh, image: fleet-ros-msgs } } # base providers may
# # also declare the msgs OVERLAY build: base + the union of
# # the riggings' `msgs:` blocks, exported as RIG_MSGS_IMAGE
# build: { command: ..., sources: [{name: sbgecom, repo: <git url>, ref: <tag|branch|sha>}] }
# # repos the build would fetch itself: rig mirrors them
# # and hands each over as $RIG_BUILD_SOURCES/<name> (pass
# # --build-context <name>=...), so `rig build --offline` works
# # (the bag logger prefers it over the bare base)
# msgs: # interface packages THIS service's topics use (beyond
# apt: [mavros_msgs] # ros-base/common_interfaces) — rosbag2 can't record a topic
# source: # whose message package isn't installed in the recorder's
# - repo: https://github.com/PX4/px4_msgs.git # image. ROS names (underscores) in apt:; source pins
# ref: v1.16.0 # are MANDATORY and must equal the pin the service builds
# packages: [px4_msgs] # against. rig unions the blocks fleet-wide; one repo at two
# # refs is refused ("align the riggings"), never guessed.
# export: { data: "bags/{name}" } # this service can SLIM the run data it wrote (under
# # <run>/<data>) for `rig run export`: rig calls the
# # launcher's `export` verb with RIG_EXPORT_SOURCE (run),
# # RIG_EXPORT_DEST (write <data> under it), RIG_EXPORT_OPTIONS
# # (the profile's block for this instance, a YAML file rig
# # never reads), RIG_EXPORT_PROFILE, RIG_EXPORT_FORCE
# replay: { sim_time: true, # the launcher wires use_sim_time from RIG_SIM_TIME (rig's
# # one clock token under `rig replay`; explicit config wins
# # both ways). Undeclared services under test WARN at replay
# service_introspection: true } # + CONTENTS-level service introspection on its servers/
# # clients: calls RECORD into bags (record.services) and
# # REPLAY at this service (RIG_REPLAY_SERVICES / --calls)
# interface: # the service's declared topic/service contract — checked
# publishes: # WARN-only against a run's OBSERVED graph epochs
# - {topic: fix, type: sensor_msgs/msg/NavSatFix} # relative = instance-namespace; absolute
# - /tf # (/tf) = shared-bus; bare string = name only. Bootstrap
# subscribes: [{topic: rtcm}] # from observation: `rig graph --contract <instance>`
# provides: [{service: reset, type: std_srvs/srv/Trigger}] # service servers
# requires: [] # service clients (what this service NEEDS)
# platform: { auto_detect: /etc/nv_tegra_release, override_env: CAM_PLATFORM } # the launcher's standalone
# # host probe + the env var it honors; rig mirrors the vehicle's
# # declared `platform:` into it (RIG_TARGET_PLATFORM sibling)
# mirror: [eclipse/zenoh:1.2.1] # third-party images `rig build` copies into the registry
external_volumes: ["novatel_{name}_data"] # optional: GC'd by `rig down --purge` (final teardown only)
host_ports: ["plugins[name=webrtc-bridge,enabled=true].params.port"] # optional: rig validates these don't clashThe optional top-level ros_namespace declares the service's actual ROS node namespace,
without a publisher/subscriber inventory. Use an absolute, non-root namespace such as
/control/relay, or a quoted template such as "/robot/{name}"; {name} is the only supported
placeholder. This is association metadata, not a launcher setting or a ROS remap.
rig graph expands {name} with the current instance name. Replay uses the recorded identity
(instances.NAME.recorded_as, otherwise the destination name), and associates observed topics
and service servers using that namespace. A recipe's instances.NAME.recorded_namespace takes
precedence for historical namespace changes; undeclared services keep the /instance fallback.
Distinct instances sharing a fixed namespace have ambiguous ownership: use a per-instance
template or explicit recipe namespaces. Graph evidence is still required to identify regenerated
outputs such as status topics. Recorded calls default to unambiguous servers started by replay,
excluding targets with a known selected client, after request-content inspection. Use recipe
playback.services: [] to disable recorded calls or a nonempty list to select targets manually.
cam-up, novatel-up, and sbg-up all satisfy this. See docs/DESIGN.md for the full rationale.