Configuration Reference

horus.toml is the single source of truth for a HORUS project. It contains project metadata, dependencies (all languages), hardware devices, scripts, hooks, and build configuration. Native build files (Cargo.toml, pyproject.toml, CMakeLists.txt) are generated into .horus/ automatically — you never edit them directly.


Quick Reference

Minimal horus.toml:

[package]
name = "my-robot"
version = "0.1.0"

Full horus.toml:

[package]
name = "my-robot"
version = "1.2.3"
description = "Autonomous mobile robot with SLAM navigation"
authors = ["Robotics Team <team@example.com>"]
license = "Apache-2.0"
repository = "https://github.com/team/my-robot"
package-type = "app"
categories = ["navigation", "perception"]
type = "bin"

[dependencies]
# HORUS registry (default source)
pid-controller = "1.0"

# Rust crates
serde = { version = "1.0", source = "crates.io", features = ["derive"] }
tokio = { version = "1", source = "crates.io", features = ["full"] }

# Python packages
numpy = { version = ">=1.24", source = "pypi" }
opencv-python = { version = ">=4.8", source = "pypi" }

# System libraries
opencv = { source = "system", apt = "libopencv-dev", cmake_package = "OpenCV" }

# Local path
my-lib = { path = "../my-lib" }

# Git
my-driver = { git = "https://github.com/team/driver.git", branch = "main" }

[dev-dependencies]
criterion = { version = "0.5", source = "crates.io" }

[robot]
name = "turtlebot"
description = "robot.urdf"
simulator = "sim3d"

[hardware]
arm = { use = "dynamixel", port = "/dev/ttyUSB0", baudrate = 1000000 }
lidar = { use = "rplidar", port = "/dev/ttyUSB1" }
camera = { use = "opencv" }
imu = { use = "bno055", bus = 1 }
realsense = { use = "exec:./realsense_bridge", args = ["--serial", "12345"] }

# Simulation-only device
sim_lidar = { use = "rplidar", sim = true, noise = 0.01 }

[scripts]
sim = "horus sim start --world warehouse"
deploy-pi = "horus deploy robot@192.168.1.5 --release"

[hooks]
pre_run = ["fmt", "lint"]
pre_test = ["lint"]

[ignore]
files = ["debug_*.py"]
directories = ["experiments/"]

enable = ["cuda"]

[cpp]
compiler = "clang++"
cmake_args = ["-DCMAKE_BUILD_TYPE=Release"]
toolchain = "aarch64"

[package]

Project metadata. Required for all projects except virtual workspaces.

name (Required)

Type: String Validation: 2-64 characters, lowercase alphanumeric + hyphens + underscores + @ + / (for scoped names like @org/package)

Reserved names (will be rejected): horus, core, std, lib, test, main, admin, api, root, system, internal, config, setup, install

name = "my-robot"

version (Required)

Type: String (semantic versioning) Format: MAJOR.MINOR.PATCH

version = "0.1.0"

description (Optional)

Type: String Default: None

description = "Autonomous mobile robot with SLAM navigation"

authors (Optional)

Type: Array of strings Default: []

authors = ["Jane Doe <jane@example.com>", "Robotics Team"]

license (Optional)

Type: String (SPDX identifier) Default: None Common values: "Apache-2.0", "MIT", "GPL-3.0", "BSD-3-Clause"

license = "Apache-2.0"

edition (Optional)

Type: String Default: "1"

Manifest schema version. Controls which fields and features are available.

edition = "1"

repository (Optional)

Type: String (URL) Default: None

repository = "https://github.com/team/my-robot"

package-type (Optional)

Type: String Default: None Values: "node", "driver", "tool", "algorithm", "model", "message", "app"

Used for registry classification and discovery.

package-type = "app"

categories (Optional)

Type: Array of strings Default: []

categories = ["navigation", "perception", "control"]

type (Optional)

Type: String Default: "bin" Values: "bin", "lib", "both"

Crate target type. "lib" for shared libraries, "bin" for executables, "both" for crates that are both.

type = "lib"

standard (Optional)

Type: String Default: None

C++ language standard for C++ projects.

standard = "c++20"

rust_edition (Optional)

Type: String Default: None

Rust edition override for the generated .horus/Cargo.toml. If not set, defaults to "2021".

rust_edition = "2024"

[dependencies]

All project dependencies — Rust, Python, system, local, and git — declared in one place. HORUS generates the appropriate native build files from these declarations.

Simple Form

A version string defaults to the HORUS registry:

[dependencies]
pid-controller = "1.0"
sensor-fusion = "0.5"

Detailed Form

A table with source and other fields:

[dependencies]
serde = { version = "1.0", source = "crates.io", features = ["derive"] }
numpy = { version = ">=1.24", source = "pypi" }

Dependency Sources

SourceTOML valueWhen to useGenerated into
HORUS Registry"registry" (default)HORUS packages.horus/Cargo.toml or .horus/pyproject.toml
crates.io"crates.io"Rust crates.horus/Cargo.toml
PyPI"pypi"Python packages.horus/pyproject.toml
System"system"OS packages (apt/brew).horus/CMakeLists.txt
Path"path"Local workspace deps.horus/Cargo.toml
Git"git"Git repositories.horus/Cargo.toml

Detailed Dependency Fields

FieldTypeDefaultDescription
versionStringNoneVersion requirement ("1.0", ">=1.24", "^2.0")
sourceString"registry"Dependency source (see table above)
featuresArray[]Cargo/Python features to enable
optionalBooleanfalseWhether this dependency is optional
pathStringNoneLocal path (for source = "path")
gitStringNoneGit repository URL (for source = "git")
branchStringNoneGit branch
tagStringNoneGit tag
revStringNoneGit revision hash
aptStringNoneApt package name (for source = "system")
cmake_packageStringNoneCMake find_package() name (for source = "system")
langStringNoneLanguage hint: "cpp", "rust", "python"
workspaceBooleanfalseInherit from [workspace.dependencies]

Examples by Source

crates.io (Rust):

[dependencies]
serde = { version = "1.0", source = "crates.io", features = ["derive"] }
tokio = { version = "1", source = "crates.io", features = ["full"] }

PyPI (Python):

[dependencies]
numpy = { version = ">=1.24", source = "pypi" }
torch = { version = ">=2.0", source = "pypi" }

System (OS packages):

[dependencies]
opencv = { source = "system", apt = "libopencv-dev", cmake_package = "OpenCV" }
eigen3 = { source = "system", apt = "libeigen3-dev", cmake_package = "Eigen3" }

Path (local):

[dependencies]
my-messages = { path = "../my-messages" }

Git:

[dependencies]
my-driver = { git = "https://github.com/team/driver.git", branch = "main" }
my-algo = { git = "https://github.com/team/algo.git", tag = "v1.0" }

Adding dependencies via CLI:

horus add serde --source crates.io --features derive
horus add numpy --source pypi
horus add pid-controller                    # auto-detects registry
horus add ../my-lib --source path
horus remove numpy

[dev-dependencies]

Same format as [dependencies]. Dev-only dependencies are included in horus test and horus bench builds but excluded from horus publish and horus deploy.

[dev-dependencies]
criterion = { version = "0.5", source = "crates.io" }
pytest = { version = ">=7.0", source = "pypi" }

Add via CLI:

horus add criterion --dev --source crates.io

[workspace]

Multi-crate workspace configuration. When present, this manifest is a workspace root.

FieldTypeDefaultDescription
membersArray of strings[]Glob patterns for workspace members (e.g., ["crates/*"])
excludeArray of strings[]Glob patterns to exclude from membership
dependenciesTable{}Shared dependencies inherited by members via workspace = true
[workspace]
members = ["crates/*"]
exclude = ["crates/experimental"]

[workspace.dependencies]
serde = { version = "1.0", source = "crates.io", features = ["derive"] }
horus_library = "0.1.9"

Members inherit shared dependencies:

# crates/my-node/horus.toml
[package]
name = "my-node"
version = "0.1.0"

[dependencies]
serde = { workspace = true }     # inherits version + features from root
horus_library = { workspace = true }

Create a workspace:

horus new my-robot --workspace -r

Virtual Workspaces

A workspace without [package] is a virtual workspace — it organizes members but is not a package itself:

# Root horus.toml — no [package] section
[workspace]
members = ["sensor-node", "controller", "planner"]

[workspace.dependencies]
horus_library = "0.1.9"

Virtual workspaces must have at least one member. Use for monorepos where the root directory is just an organizer.


[hardware]

Hardware device configuration. Each entry declares a device by name, specifies which node drives it via the use field, and passes device-specific parameters.

[hardware] is the primary section name. The legacy [drivers] name is still accepted and parsed identically, but new projects should use [hardware].

The use field

Every device entry requires a use field that identifies the node responsible for the device. The value is looked up in the node registry — this includes Terra HAL drivers, registry packages, and local project nodes.

[hardware]
arm = { use = "dynamixel", port = "/dev/ttyUSB0", baudrate = 1000000 }
lidar = { use = "rplidar", port = "/dev/ttyUSB1" }
camera = { use = "opencv" }
imu = { use = "bno055", bus = "i2c-1", address = 0x28 }

The use value resolves in order:

  1. Local project nodes — a node defined in the current project
  2. Installed registry packages — a driver package installed via horus install
  3. Terra HAL drivers — a built-in hardware abstraction driver

Subprocess drivers (exec: prefix)

For vendor-provided binaries, custom bridge programs, or drivers written in languages without native HORUS bindings, prefix the use value with exec: to run an external binary as a child process:

[hardware.camera]
use = "exec:./realsense_bridge"
args = ["--serial", "12345"]

[hardware.lidar]
use = "exec:/opt/velodyne/vlp16_driver"
args = ["--port", "2368", "--model", "VLP-16"]

The args field is an array of strings passed as command-line arguments to the binary. The binary must publish and subscribe to HORUS topics using shared memory (any language with HORUS bindings works).

Simulation devices (sim flag)

Mark a device as simulation-only by setting sim = true. Simulation devices are loaded only when running with --sim (horus run --sim or horus test --sim) and skipped during real-hardware runs.

[hardware]
# Real hardware — always loaded (skipped in --sim mode if a sim variant exists)
front_lidar = { use = "rplidar", port = "/dev/ttyUSB0" }
imu = { use = "bno055", bus = 1 }

# Simulation overrides — loaded only with --sim
front_lidar_sim = { use = "rplidar", sim = true, noise = 0.01, range_max = 12.0, num_rays = 360 }
imu_sim = { use = "bno055", sim = true, drift_rate = 0.001 }
camera_sim = { use = "opencv", sim = true, width = 640, height = 480, fps = 30 }

When horus run --sim is active, devices with sim = true replace their non-sim counterparts (matched by the device name prefix before _sim). Your node code does not change between simulation and real hardware — the same topic names and message types are used in both modes.

The separate [sim-drivers] section from earlier versions is no longer needed. Use sim = true inline on any device entry instead.

Device parameters

All keys other than the reserved keys are captured as a HashMap<String, toml::Value> and passed to the device node at runtime via NodeParams. Any TOML value type works — strings, integers, floats, booleans, arrays, and inline tables.

[hardware.arm]
use = "dynamixel"
port = "/dev/ttyUSB0"
baudrate = 1000000
servo_ids = [1, 2, 3, 4, 5, 6]   # array parameter
torque_limit = 0.8                 # float parameter

Reserved keys

KeyDescription
useRequired. Node registry name (or exec: prefixed path for subprocess drivers)
simBoolean. Marks device as simulation-only (true)
argsArray of strings. Command-line arguments for exec: subprocess drivers

Legacy keys (still accepted, mapped internally): terra, package, node, crate, source, pip, exec, simulated

Disabling a device

[hardware]
imu = false    # Disabled — not loaded

Topic mapping (optional)

Three reserved fields configure auto-bridging to HORUS topics:

[hardware.imu]
use = "mpu6050"
bus = "i2c-1"
topic = "sensors.imu"              # Sensor data output topic

[hardware.arm]
use = "dynamixel"
port = "/dev/ttyUSB0"
topic_state = "arm.joint_states"   # Joint state output topic
topic_command = "arm.joint_cmd"    # Command input topic
FieldDescription
topicSensor data output topic name
topic_stateState/feedback output topic name
topic_commandCommand input topic name

These fields are not included in NodeParams — they are bridge configuration used by the terra-horus layer to auto-create sensor/actuator forwarding. Access via hw.topic_mapping("name") in code.

Common use values

NameHardware
dynamixelDynamixel servos
rplidarSLAMTEC RPLiDAR
realsenseIntel RealSense cameras
webcamV4L2 cameras
mpu6050, bno055I2C IMU sensors
vescVESC motor controllers
i2c, spi, serial, can, gpio, pwmRaw bus access

See the Driver API reference for the runtime API.

Migration from [drivers] / [sim-drivers]

If you have an existing project using the old syntax, the mapping is straightforward:

# Old syntax                                          # New syntax
[drivers]                                              [hardware]
arm = { terra = "dynamixel", port = "/dev/ttyUSB0" }   arm = { use = "dynamixel", port = "/dev/ttyUSB0" }
lidar = { package = "horus-driver-rplidar" }            lidar = { use = "horus-driver-rplidar" }
conveyor = { node = "ConveyorDriver" }                  conveyor = { use = "ConveyorDriver" }
front_lidar = { crate = "rplidar-driver" }              front_lidar = { use = "rplidar-driver" }
imu = { pip = "adafruit-bno055", bus = 1 }              imu = { use = "adafruit-bno055", bus = 1 }
camera = { exec = "./realsense_bridge" }                camera = { use = "exec:./realsense_bridge" }

[sim-drivers]                                          # Inline sim = true instead
front_lidar = { simulated = true, noise = 0.01 }       front_lidar_sim = { use = "rplidar", sim = true, noise = 0.01 }

The old [drivers] section name and the six source keys (terra, package, node, crate, pip, exec) are still parsed for backward compatibility. HORUS maps them to the use field internally. Likewise, [sim-drivers] entries with simulated = true are mapped to sim = true. No migration is required for existing projects, but new projects should use the [hardware] + use syntax.


[scripts]

Custom project commands. Like npm scripts or Justfiles.

Type: Table of name = "command" string pairs

[scripts]
sim = "horus sim start --world warehouse"
deploy-pi = "horus deploy robot@192.168.1.5 --release"
test-hw = "cargo test --features hardware -- --ignored"

Running scripts:

horus run sim              # checks [scripts] before looking for files
horus scripts sim          # explicit script execution
horus scripts              # list all available scripts
horus scripts sim -- -v    # pass extra args after --

Hook integration: Custom hook names in [hooks] that aren't fmt, lint, or check are looked up in [scripts]. For example, post_test = ["clean-shm"] runs the clean-shm script after tests.


[hooks]

Pre/post action hooks that run automatically before or after horus run, horus build, and horus test.

FieldTypeDefaultDescription
pre_runArray of strings[]Run before horus run
pre_buildArray of strings[]Run before horus build
pre_testArray of strings[]Run before horus test
post_testArray of strings[]Run after horus test

Built-in hook names: fmt, lint, check. Any other name is looked up in [scripts].

[hooks]
pre_run = ["fmt", "lint"]       # auto-format and lint before every run
pre_test = ["lint"]             # lint before testing
post_test = ["clean-shm"]      # custom script (defined in [scripts])

Skipping hooks:

horus run --no-hooks      # skip all hooks for this run
horus test --no-hooks     # skip hooks for this test

If any hook fails (non-zero exit), the command is aborted and the error is shown.

Built-in hook names and what they do:

NameRuns
fmthorus fmt (rustfmt + ruff format)
linthorus lint (clippy + ruff check)
checkhorus check (validate horus.toml + source)

Any other name is looked up in [scripts]. If not found in scripts, it's executed as a shell command.

Note: Only pre_run, pre_build, pre_test, and post_test exist. There are no post_run or post_build hooks — the process exits after run/build completes.


[ignore]

Patterns to exclude from HORUS file scanning and processing.

files

Type: Array of glob patterns

[ignore]
files = ["debug_*.py", "test_*.rs", "**/experiments/**"]

directories

Type: Array of directory names

[ignore]
directories = ["old/", "experiments/", "benchmarks/"]

packages

Type: Array of package name strings

Skip specific packages during auto-install.

[ignore]
packages = ["ipython", "debugpy"]

enable

Top-level array of capability flags to enable.

Type: Array of strings (at the top level, outside any table)

enable = ["cuda", "editor"]

Capabilities are passed to the build system as feature flags. Use horus run --enable cuda for one-off activation without editing horus.toml.

Available capabilities:

CapabilityDescription
cuda, gpuCUDA GPU acceleration
editorScene editor UI
python, pyPython bindings
headlessNo rendering (for training/CI)
gpio, i2c, spi, can, serialHardware interface support
opencvOpenCV backend
realsenseIntel RealSense support
fullEnable all features

[cpp]

C++ build configuration. Only needed for projects with C++ code.

FieldTypeDefaultDescription
compilerStringNoneOverride C++ compiler (e.g., "clang++")
cmake_argsArray of strings[]Additional CMake arguments
toolchainStringNoneCross-compilation target (e.g., "aarch64", "armv7")
[cpp]
compiler = "clang++"
cmake_args = ["-DCMAKE_BUILD_TYPE=Release", "-DBUILD_TESTS=ON"]
toolchain = "aarch64"

[robot]

Robot-specific metadata. Used by the simulator, URDF loading, and topic namespacing.

FieldTypeDefaultDescription
nameStringNoneRobot name, used as a namespace prefix in topic names (e.g., turtlebot.imu)
descriptionStringNonePath to the robot's URDF file, relative to the project root
simulatorString"sim3d"Simulator plugin to use when running in simulation mode
[robot]
name = "turtlebot"
description = "robot.urdf"
simulator = "sim3d"

The name field sets the robot identity for the session. When present, some drivers and tools use it to namespace topics (e.g., a lidar driver might publish to turtlebot.scan instead of scan).

The description field points to a URDF file that describes the robot's kinematic structure. The simulator and transform frame system both read this file to build the robot model. The path is relative to the project root:

my_project/
├── horus.toml
├── robot.urdf         ← description = "robot.urdf"
├── models/
│   └── arm.urdf       ← description = "models/arm.urdf"
└── src/
    └── main.rs

The simulator field selects which simulator plugin to launch when you run horus run --sim. The default is "sim3d" (the built-in Bevy 3D physics simulator). To use a different simulator plugin, install it from the registry and set its name here:

horus install my-custom-sim
[robot]
simulator = "my-custom-sim"

The .horus/ Directory

The .horus/ directory is automatically generated by HORUS. You should never edit files inside it.

my_project/
├── horus.toml              ← You edit this (single source of truth)
├── src/
│   ├── main.rs
│   └── main.py
└── .horus/                 ← Generated (don't edit)
    ├── Cargo.toml          ← Generated from horus.toml [dependencies] (Rust deps)
    ├── pyproject.toml      ← Generated from horus.toml [dependencies] (Python deps)
    ├── CMakeLists.txt      ← Generated from horus.toml [dependencies] (C++ deps)
    ├── target/             ← Rust build artifacts
    ├── cpp-build/          ← CMake build artifacts
    └── packages/           ← Cached registry packages

Git: Always add .horus/ to .gitignore. The horus new command does this automatically.

Cleaning: If .horus/ gets corrupted:

horus clean --all    # delete everything, regenerated on next build

When .horus/ is created: Automatically on horus run, horus build, or horus install.


Validation

HORUS validates horus.toml on every command (horus run, horus build, horus check, etc.).

Validation Rules

RuleDetails
Name2-64 chars, lowercase alphanumeric + -_@/. 14 reserved names rejected.
VersionMust be valid semver: MAJOR.MINOR.PATCH (e.g., "1.0.0", not "1.0")
EditionOnly "1" is recognized. Unknown editions produce a warning, not an error.
SourcesMust be one of: registry, crates.io, pypi, system, path, git
HardwareEach device entry must have a use field. Legacy source keys (terra, package, node, crate, pip, exec) are accepted for backward compatibility.
WorkspaceVirtual workspace (no [package]) must have at least one member

Source auto-inference

You can omit source when path or git is present — HORUS infers the source:

# No source needed — inferred from path
my-lib = { path = "../my-lib" }         # → DepSource::Path

# No source needed — inferred from git
my-driver = { git = "https://..." }     # → DepSource::Git

Common Errors

ErrorFix
Missing required field 'name'Add name and version under [package]
Invalid version format '1.0'Use MAJOR.MINOR.PATCH format: "1.0.0" (3 segments required)
Unknown dependency source 'npm'Use: registry, crates.io, pypi, system, path, git
Invalid TOML syntax at line NCheck TOML syntax: keys use =, strings are quoted, tables use [brackets]
Package name 'horus' is reservedChoose a different name (14 names are reserved)
Virtual workspace must have membersAdd at least one path to [workspace] members = [...]

Design Decisions

Why one file instead of per-language configs? A robot using Rust for control and Python for ML traditionally needs Cargo.toml, pyproject.toml, and possibly requirements.txt. When a team member adds a dependency, they need to know which file to edit. horus.toml is the single source of truth: all dependencies are declared once with an explicit source field. HORUS generates the native build files into .horus/ automatically. One file to learn, one file to review in PRs, one file to validate in CI.

Why generated build files in .horus/? Native build tools (cargo, pip, cmake) need their own config files to function. Rather than forcing users to maintain both horus.toml and Cargo.toml in sync, HORUS generates the native files into .horus/. Users never edit these files. cargo build and pip install still work under the hood with their standard tooling — HORUS generates their input, it does not replace build systems.

Why TOML? YAML has implicit type coercion (3.10 becomes 3.1, yes becomes true). TOML has an unambiguous grammar — every value has an explicit type. HORUS still uses YAML for launch configs and parameter files where flexibility is useful, but the manifest uses TOML because dependency versions must be parsed unambiguously.

Trade-offs

GainCost
Single manifest — one file for all languagesMust specify source for non-default registries
Generated build files — native tooling works unchangedCannot use Cargo.toml features not exposed by horus.toml
horus add/remove — no need to know per-language syntaxcargo add and pip install don't update horus.toml (use horus cargo add or horus pip install for auto-sync)
Workspace support — multi-crate with shared depsMembers still need their own horus.toml

See Also