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
| Source | TOML value | When to use | Generated 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
| Field | Type | Default | Description |
|---|---|---|---|
version | String | None | Version requirement ("1.0", ">=1.24", "^2.0") |
source | String | "registry" | Dependency source (see table above) |
features | Array | [] | Cargo/Python features to enable |
optional | Boolean | false | Whether this dependency is optional |
path | String | None | Local path (for source = "path") |
git | String | None | Git repository URL (for source = "git") |
branch | String | None | Git branch |
tag | String | None | Git tag |
rev | String | None | Git revision hash |
apt | String | None | Apt package name (for source = "system") |
cmake_package | String | None | CMake find_package() name (for source = "system") |
lang | String | None | Language hint: "cpp", "rust", "python" |
workspace | Boolean | false | Inherit 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.
| Field | Type | Default | Description |
|---|---|---|---|
members | Array of strings | [] | Glob patterns for workspace members (e.g., ["crates/*"]) |
exclude | Array of strings | [] | Glob patterns to exclude from membership |
dependencies | Table | {} | 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:
- Local project nodes — a node defined in the current project
- Installed registry packages — a driver package installed via
horus install - 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
| Key | Description |
|---|---|
use | Required. Node registry name (or exec: prefixed path for subprocess drivers) |
sim | Boolean. Marks device as simulation-only (true) |
args | Array 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
| Field | Description |
|---|---|
topic | Sensor data output topic name |
topic_state | State/feedback output topic name |
topic_command | Command 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
| Name | Hardware |
|---|---|
dynamixel | Dynamixel servos |
rplidar | SLAMTEC RPLiDAR |
realsense | Intel RealSense cameras |
webcam | V4L2 cameras |
mpu6050, bno055 | I2C IMU sensors |
vesc | VESC motor controllers |
i2c, spi, serial, can, gpio, pwm | Raw 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.
| Field | Type | Default | Description |
|---|---|---|---|
pre_run | Array of strings | [] | Run before horus run |
pre_build | Array of strings | [] | Run before horus build |
pre_test | Array of strings | [] | Run before horus test |
post_test | Array 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:
| Name | Runs |
|---|---|
fmt | horus fmt (rustfmt + ruff format) |
lint | horus lint (clippy + ruff check) |
check | horus 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:
| Capability | Description |
|---|---|
cuda, gpu | CUDA GPU acceleration |
editor | Scene editor UI |
python, py | Python bindings |
headless | No rendering (for training/CI) |
gpio, i2c, spi, can, serial | Hardware interface support |
opencv | OpenCV backend |
realsense | Intel RealSense support |
full | Enable all features |
[cpp]
C++ build configuration. Only needed for projects with C++ code.
| Field | Type | Default | Description |
|---|---|---|---|
compiler | String | None | Override C++ compiler (e.g., "clang++") |
cmake_args | Array of strings | [] | Additional CMake arguments |
toolchain | String | None | Cross-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.
| Field | Type | Default | Description |
|---|---|---|---|
name | String | None | Robot name, used as a namespace prefix in topic names (e.g., turtlebot.imu) |
description | String | None | Path to the robot's URDF file, relative to the project root |
simulator | String | "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
| Rule | Details |
|---|---|
| Name | 2-64 chars, lowercase alphanumeric + -_@/. 14 reserved names rejected. |
| Version | Must be valid semver: MAJOR.MINOR.PATCH (e.g., "1.0.0", not "1.0") |
| Edition | Only "1" is recognized. Unknown editions produce a warning, not an error. |
| Sources | Must be one of: registry, crates.io, pypi, system, path, git |
| Hardware | Each device entry must have a use field. Legacy source keys (terra, package, node, crate, pip, exec) are accepted for backward compatibility. |
| Workspace | Virtual 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
| Error | Fix |
|---|---|
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 N | Check TOML syntax: keys use =, strings are quoted, tables use [brackets] |
Package name 'horus' is reserved | Choose a different name (14 names are reserved) |
Virtual workspace must have members | Add 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
| Gain | Cost |
|---|---|
| Single manifest — one file for all languages | Must specify source for non-default registries |
| Generated build files — native tooling works unchanged | Cannot use Cargo.toml features not exposed by horus.toml |
horus add/remove — no need to know per-language syntax | cargo 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 deps | Members still need their own horus.toml |
See Also
- horus.toml Concept — Why a single manifest
- Package Management — Install, search, publish
- CLI Reference —
horus add,horus build,horus run - Multi-Crate Workspaces — Workspace setup guide
- Native Tool Integration —
horus cargo,horus pipproxy