Depending on HORUS

horus new, horus build and horus run write your Cargo manifest for you. If you are not using them — an existing crate, a member of a workspace you already have, a build script, CI that runs plain cargo test — you write it yourself, and no cargo add invocation produces a manifest that resolves. This page gives you one that does, says where each line came from, and names what will break it.

horus = "0.4" is somebody else's crate

Start here, because this is the failure that costs the most time.

The name horus on crates.io is taken, by an unrelated project. Queried on 2026-09-03, https://crates.io/api/v1/crates/horus returns the description "A small, modular Rust framework for building coding agents", the repository https://github.com/citizenhicks/horus, and 45 published versions with 0.9.0 as the newest. One of those versions is 0.4.0. So horus = "0.4" does not fail to resolve. It resolves and downloads, and then your first use horus::prelude::*; fails, because that crate has no such module: its published 0.4.0 declares agent, backend, middleware and protocol and nothing else. A missing prelude reads like a mistake in your own code rather than a mistake in your manifest.

None of HORUS's own crates are on crates.io at all. Also checked on 2026-09-03:

Crate nameOn crates.io
horusYes — a different project's coding-agent framework
horus_core"crate horus_core does not exist"
horus_types"crate horus_types does not exist"
horus_macros"crate horus_macros does not exist"
horus_net"crate horus_net does not exist"
horus_sys"crate horus_sys does not exist"
horus_manager"crate horus_manager does not exist"

They cannot be published as they stand. The horus crate depends on two git repositories — horus-robotics through the workspace (horus/Cargo.toml:21, resolved at Cargo.toml:40) and horus-tf directly (horus/Cargo.toml:25) — and cargo refuses to package a crate whose dependency carries no version requirement. Reproduced on 2026-09-03 with cargo package --no-verify on a crate carrying nothing but the horus-tf git dependency:

error: failed to verify manifest at `.../Cargo.toml`

Caused by:
  all dependencies must have a version requirement specified when packaging.
  dependency `horus-tf` does not specify a version
  Note: The packaged dependency will use the version from crates.io,
  the `git` specification will be removed from the dependency declaration.

The project README states this in one paragraph (README.md:70-73), and Using Prebuilt Nodes carries a callout saying the same. Take the manifest from this page rather than from that callout: the snippet at using-prebuilt-nodes.mdx:153-159 gives the path dependency on its own, without the two [patch] tables, and that does not resolve. The next two sections say why.

Where the source tree is

The installer clones HORUS and keeps the tree, because the whole build model depends on it: horus run compiles user projects against horus as path dependencies, "so the source must outlive the install or no Rust project can ever be built" (install.sh:256-260).

It lands at <state root>/cache/horus@<version>. The state root is $HORUS_PREFIX when that is set and non-empty, and ~/.horus otherwise (install.sh:245-255, mirrored on the CLI side by state_root and cache_root in horus_manager/src/version.rs:47-87). The version in the directory name is read out of horus_core/Cargo.toml in the tree that was just cloned, not from the tag you asked for (install.sh:458-462). On a default Linux install that gives you:

~/.horus/cache/horus@0.4.0/

holding the entire workspace — horus/, horus_core/, horus_macros/, horus_types/, horus_sys/, horus_net/, horus_cpp/, horus_py/, examples/, tests/ and the rest — not a packaged subset.

Get the version from the directory listing, not from the CLI:

ls ~/.horus/cache

horus --version prints the version of the binary on your PATH, which is not required to be the version of any tree in the cache. The machine this page was checked on had a 0.2.2 CLI and two cached trees, horus@0.2.2 and horus@0.4.0 — nothing prunes the old one, and nothing repoints your manifest at the new one.

The CLI does not go straight to a fixed path either. find_horus_source_dir (horus_manager/src/commands/run/run_rust.rs:1046-1117) tries $HORUS_SOURCE first, then the fixed development locations /horus, ~/softmata/horus, ~/horus, /opt/horus, /usr/local/horus, and only then the cache roots — up to three of them: a HORUS_PREFIX root when it differs from the legacy one, the XDG cache, and ~/.horus/cache (run_rust.rs:1023-1043). Within those it prefers horus@<CLI version> and otherwise accepts any horus@* tree it finds. Every candidate has to contain horus/Cargo.toml to be accepted.

None of that search applies to you. It runs inside the CLI, to build the manifest in .horus/. A hand-written Cargo.toml is read by cargo, which resolves nothing on your behalf: the path you write is the path used. HORUS_SOURCE will not redirect it, and a developer checkout at ~/softmata/horus will not be picked up. Most of the fragility described below follows from that one fact.

What horus new generates

horus new my_robot --rust writes horus.toml (new.rs:120), .gitignore (new.rs:110) and src/main.rs (new.rs:686-688). Use the long flag: -r is still accepted on horus new as a deprecated alias and prints a notice, because the same letter is --release on horus build, horus run and horus test (main.rs:164-166, main.rs:2052-2070). Add --yes in a script — the bare command prompts (main.rs:194-199).

It writes no root Cargo.toml. Native build files "are generated automatically from horus.toml by the build pipeline (cargo_gen/pyproject_gen)" (new.rs:123-124), and a test asserts the absence: "horus new should NOT create Cargo.toml in project root" (new.rs:2009-2010). The Cargo manifest is .horus/Cargo.toml, written by cargo_gen::generate (horus_manager/src/cargo_gen.rs:35-166) and gitignored as a build artefact (new.rs:349). horus new pre-generates it once, best-effort, so an editor has a project to load on first open (new.rs:143-153, cargo_gen::ensure at cargo_gen.rs:1612-1659); horus build, horus run and horus test regenerate it.

To read one, use a generated project — a project that has a root Cargo.toml takes a different path entirely, described further down:

horus build
cat .horus/Cargo.toml

What generate emits, in the order it emits it:

EmittedWritten byWhy it is there
[workspace], emptycargo_gen.rs:64Stops the generated manifest being adopted by a parent workspace
A [[bin]] per entry pointcargo_gen.rs:67-97The sources live outside .horus/, so each target needs an explicit path
horus, horus_core, horus_macros as path dependenciescargo_gen.rs:1062-1093Without them the section is emitted empty and the build fails with a wall of "cannot find type Topic/Scheduler" instead of an actionable message (cargo_gen.rs:108-113)
serde = { version = "1", features = ["derive"] }cargo_gen.rs:970-988, literal at :984The message! macro expands to serde::Serialize, so serde has to be a direct dependency of your crate; transitive is not enough
[patch."…horus-robotics.git"] and [patch."…horus-tf.git"]cargo_gen.rs:1100-1109 and :1260-1325, called last at :146The next section

Two details worth knowing. The path-dependency loop at cargo_gen.rs:1066 tries four names — horus, horus_core, horus_library, horus_macros — and emits only those that have a Cargo.toml on disk (:1068). There is no horus_library directory in the workspace or in a cached tree, so three lines come out, not four. And on Windows the path is written with forward slashes, because a backslash in a TOML string is an escape and C:\Users\… made the generated manifest unparseable before cargo could read it (cargo_gen.rs:941-959).

The two [patch] tables, and why nothing resolves without them

horus depends on horus-robotics and horus-tf as git dependencies. Both of those repositories reference horus_core and its siblings by relative path, which resolves inside a HORUS checkout and not at all in a bare clone of either repo. The HORUS workspace root compensates with two [patch] tables pointing at the local crates (Cargo.toml:104-115, comment at :105-107).

Cargo honours [patch] only from the workspace root of the build actually running, and never inherits it through a path dependency (cargo_gen.rs:1239-1243). Your project is its own workspace root, so those tables do not reach it. Verified on 2026-09-03: a project whose only dependency is horus = { path = "/home/you/.horus/cache/horus@0.4.0/horus" } and which declares no patches fails cargo metadata outright, exit 101, before any compilation:

error: no matching package named `horus_core` found
location searched: Git repository https://github.com/softmata/horus-robotics.git?rev=631483a2...
required by package `horus-robotics v0.2.0 (https://github.com/softmata/horus-robotics.git?rev=631483a2...)`
    ... which satisfies git dependency `horus-robotics` of package `horus v0.4.0 (/home/you/.horus/cache/horus@0.4.0/horus)`
    ... which satisfies path dependency `horus` of package `nopatch v0.1.0 (...)`

This is not a corner case you might avoid. It is the first thing that happens to any project that depends on horus by path and does nothing else. Re-emitting the tables is what keeps generated projects buildable, and horus_cpp/fuzz/Cargo.toml:26-35 solves the identical problem by hand for the same reason.

The manifest

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

[dependencies]
horus = { path = "/home/you/.horus/cache/horus@0.4.0/horus" }
serde = { version = "1", features = ["derive"] }

[patch."https://github.com/softmata/horus-robotics.git"]
horus_core = { path = "/home/you/.horus/cache/horus@0.4.0/horus_core" }
horus_types = { path = "/home/you/.horus/cache/horus@0.4.0/horus_types" }
horus_macros = { path = "/home/you/.horus/cache/horus@0.4.0/horus_macros" }

[patch."https://github.com/softmata/horus-tf.git"]
horus_core = { path = "/home/you/.horus/cache/horus@0.4.0/horus_core" }
horus_macros = { path = "/home/you/.horus/cache/horus@0.4.0/horus_macros" }

The crate lists in the two tables are not interchangeable and are not guesses: they mirror GIT_PATCH_TARGETS (cargo_gen.rs:1100-1109) and the workspace root (Cargo.toml:104-115). horus-tf needs two crates redirected, horus-robotics three.

Substitute your own home directory and the version your installer actually cached. Every path is absolute; see the fragility section below for why, and what to do about it.

Verified on 2026-09-03 against ~/.horus/cache/horus@0.4.0: a crate with exactly that manifest and this src/main.rs passes cargo check, and cargo build links a binary.

use horus::prelude::*;

message! {
    Ping { seq: u64 }
}

fn main() {
    let _ = Scheduler::new();
    let _ = Ping { seq: 0 };
}

Three notes on it.

horus_core and horus_macros are not in the stanza. cargo_gen emits them into every generated project because it cannot know what you will import; a hand-written manifest needs them only if you name those crates directly. Add them the same way, as path dependencies into the same tree, if you do — both forms were checked and both compile.

The toolchain floor is rust-version = "1.90" (Cargo.toml:28), inherited by every workspace member that writes rust-version.workspace = true.

No C++ toolchain or CMake is needed for the Rust dependency. horus_cpp is not in the resolved graph. From cargo metadata and the build artefacts of the check above, the HORUS-side crates that get built are horus, horus_core, horus_types, horus_macros, horus_sys, horus-robotics, horus-tf and softmata-core, plus their crates.io dependencies. horus_macros is there even though you did not name it: the macros feature is on by default. horus_net is not. The C++ and Python routes into the same cached tree are separate and are described in README.md:61-68.

Turning on networking

horus ships five features — default, macros, telemetry, blackbox and net — where default = ["macros", "telemetry", "blackbox"] (horus/Cargo.toml:32-56; the same list is written out in a comment at cargo_gen.rs:1008). LAN replication is the opt-in one:

horus = { path = "/home/you/.horus/cache/horus@0.4.0/horus", features = ["net"] }

It has to go on the horus dependency, not on your own crate. Asking for --features net on your own crate is rejected — on cargo 1.97.1, with error: the package 'my-robot' does not contain this feature: net — which is the reasoning that put the flag inside the generated dependency rather than on the command line (cargo_gen.rs:999-1001). If you want it for one build without editing the manifest, cargo check --features horus/net works; that was checked and it pulls horus_net into the graph.

What is fragile about this

The path is absolute and belongs to one machine. Nothing in cargo resolves it, and nothing in HORUS rewrites it. A teammate with a different home directory, or a CI runner with no ~/.horus, gets a resolution failure before any compilation:

error: failed to get `horus` as a dependency of package `missing v0.1.0 (...)`

Caused by:
  failed to read `/home/you/.horus/cache/horus@0.4.0/horus/Cargo.toml`

The generated manifest has the same property and gets away with it because it is regenerated per machine and gitignored; yours is committed. The CLI at least notices when a generated manifest has gone stale — cargo_gen::is_stale reads back the path = "…" values and reports true when an absolute one no longer exists (cargo_gen.rs:1687-1709). Nothing performs that check on a manifest you wrote.

cargo add does not save you here either. Run on 2026-09-03, cargo add --path <tree>/horus wrote a dependency carrying a relative path and version = "0.4.0" — a requirement that the unrelated crates.io crate happens to have a release for — and added no patch tables, so the command finished by failing with the same "no matching package named horus_core" error as above.

The workable answer is a stable absolute path that every machine provides: a symlink at a fixed location pointing at whichever tree that machine has. Verified on 2026-09-03 — a manifest whose paths run through a symlink compiles exactly as one naming the real directory does. /opt/horus is a reasonable choice because it is also in the CLI's own search list (run_rust.rs:1063), so horus run and your plain cargo build then agree about which tree they are using. It is a convention you enforce; HORUS neither creates nor checks it.

horus@<version> is a directory name, not a version requirement. Cargo checks that the directory exists and stops there. Reinstalling the same version replaces the tree in place — install.sh:481 and install_source_cache (horus_manager/src/commands/upgrade.rs:681-692) both remove the destination before moving the new tree in — so the same path can hold different code tomorrow under the same name. Upgrading to a new version leaves your pin untouched, because the cache is versioned by directory "so multiple installs coexist" (install.sh:458-459): your project keeps compiling against 0.4.0 while the CLI on your PATH is newer, and nothing says so.

The crate version is not the identity that matters. The number that decides whether your binary can attach to the shared memory another HORUS process wrote is the topic-header version, mirrored on the CLI side as CLI_TOPIC_VERSION = 4 (horus_manager/src/version.rs:15-24). Two trees that both called themselves 0.4.0 were once 93 commits apart with different topic ABIs, and the first symptom was a node that could not attach to the shared memory its own libraries wrote (run_rust.rs:1119-1129). The CLI prints a warning when its source search falls back to a tree whose version is not its own (run_rust.rs:1133-1160). A plain cargo build never runs that search, so you get no warning at all. install_manifest.toml in the state root records the version, tag, commit and the tree's topic_version when the installer could read them (install.sh:790-839, fields at version.rs:115-135) — treat it as optional evidence rather than a guarantee: readers "must tolerate its absence (older installs) and the absence of individual keys" (install.sh:787-789), and it was absent on the machine this page was checked on.

[patch] must live in the workspace root. If you are adding HORUS to a member of a workspace you already have, both tables go in the root Cargo.toml, not the member's. Cargo warns and then fails anyway (reproduced 2026-09-03):

warning: patch for the non root package will be ignored, specify patch at the workspace root:
package:   .../app/Cargo.toml
workspace: .../Cargo.toml
error: no matching package named `horus_core` found

The cache path and the tool that clears it disagree. clean_horus_cache prints "Removing ~/.horus/cache/" but resolves the directory through paths::cache_dir()horus_sys::platform::cache_dir(), which on Linux is $XDG_CACHE_HOME/horus or ~/.cache/horus (horus_manager/src/commands/clean.rs:432-466, horus_manager/src/paths.rs:25-27, horus_sys/src/platform/mod.rs:243-262). The installer writes to ~/.horus/cache (install.sh:260, :475). On a default Linux install those are two different directories, which platform/mod.rs:235-242 says in as many words. Trust neither the message nor the command about which tree is at risk; check the path your own manifest names.

Keeping a horus.toml beside your Cargo.toml

If your project has a root Cargo.toml, horus build and horus run build from it directly with cargo build, and never generate .horus/Cargo.toml (run_rust.rs:238-248). Artefacts still go to .horus/target, because every cargo spawn gets CARGO_TARGET_DIR set (build_dirs.rs:51-61, :73-85). Two consequences.

A [rust] section in horus.toml is then parsed, accepted by horus check, and ignored. The CLI warns about exactly this — "horus.toml has a [rust] section, but this project has its own Cargo.toml, so HORUS builds from that and the section has no effect" (run_rust.rs:1173-1196). Your profile, lints and features belong in the Cargo manifest you now own.

horus build also passes --features to your crate, computed from [drivers] and enable in horus.toml (run_rust.rs:257-258, features::get_all_cargo_features at features.rs:323-348). An unrecognised capability is passed through verbatim (features.rs:278-279), so a name your crate does not declare as a feature fails the build. net and network are mapped to nothing on purpose, with the reason in the source: passing it here yields "the package does not contain this feature: net" (features.rs:264-268).

Both paragraphs of this section are read out of the 0.4.0 source and were not exercised against a running CLI; the binary on the machine this page was checked on was 0.2.2, which predates them.

enable = ["net"] in a generated multi-package workspace does nothing

This concerns generated projects rather than your hand-written manifest, and is here because it is the same feature reaching the same crate by a different route.

For a generated single-package project it works. horus_dep_features (cargo_gen.rs:1002-1040) reads enable from the manifest and HORUS_ENABLE from the environment — net or network, case-insensitively (:1025-1034) — and write_horus_path_deps puts features = ["net"] on the horus dependency (:1064, :1069-1080).

For a generated multi-package workspace nothing does. The root's [workspace.dependencies] writer emits name = { path = "…" } with no features branch (cargo_gen.rs:1808-1819), and every member writes horus.workspace = true (:1979-1981). horus_dep_features has exactly one non-test call site, :1064, on the single-package path. So enable = ["net"] in a workspace horus.toml produces a binary containing no horus_net, silently — the same failure the function's own comment records for the older horus run --net flag: "the built binary contained no horus_net symbols at all. A developer wiring two robots saw a normal run with zero networking output and concluded multicast was blocked" (cargo_gen.rs:992-997).

In a hand-written manifest you write features = ["net"] yourself, so the hole is not yours. If you are converting a workspace project and wondering why networking never worked, this is why.

panic = "abort" deletes the per-node fault isolation

The [rust.profile.release] example on the Configuration page carries panic = "abort" (configuration.mdx:1150-1171, the line at :1161) with nothing said against it there or in the table below it. If you are writing your own profile, decide this one deliberately.

Every node tick runs inside std::panic::catch_unwind: NodeRunner::run_tick (horus_core/src/scheduling/primitives.rs:226-238, the catch_unwind at :229) holds the only call to a node's tick() in horus_core outside test modules. A node that panics is caught, and the rest of the schedule keeps running. With panic = "abort" there is nothing to catch, and the first panic in any node takes the whole process with it.

Seven comments in the shipped runtime name that assumption (grep -rn 'panic = "abort"' horus_core/src horus_types/src horus_net/src; an eighth is in a horus_net test). Three of them:

  • A panic inside a compute node's tick "killed only that executor thread (there is no panic = "abort" in the release profile)" (horus_core/src/scheduling/compute_executor.rs:570-573; the async I/O executor says the same at async_executor.rs:71-74).
  • A duplicate node name is recorded and returned as an error from run() rather than panicked, because panicking "in a robot process, in a release profile with no panic = "abort", would be a worse failure than the one being fixed" (horus_core/src/scheduling/scheduler/mod.rs:2064-2070).
  • A malformed topic header indexes past the end of a mapping, which "with no panic = "abort" … silently kills just that thread" (horus_core/src/communication/topic/header.rs:1497-1499).

horus_net relies on it twice more, and there the consequence is stated plainly: an unwinding panic leaves "the process alive with networking permanently dead", including networked e-stop, "while ReplicatorHandle::is_running() kept returning true" (horus_net/src/wire.rs:434-441, horus_net/src/heartbeat.rs:582-586).

Set panic = "abort" only if you have decided that a robot should stop dead on any node fault. That is a legitimate choice for some machines. It is not a build-time optimisation, and the example does not say so.

What is not checked

Nothing verifies the manifest on this page. It cannot be verified as it stands: the docs extractor treats toml as a reference language, extracted and never compiled or resolved, while rust and python are the verifiable ones (horus-docs/scripts/extract-code-blocks.mjs:43-46, :239-249). The Rust block above is compiled against a real HORUS checkout on every documentation pull request (horus-docs/.github/workflows/verify-docs.yml:85-162). The TOML block beside it is read by nobody but you.

The nearest guard is one step removed. horus_manager/tests/docs_examples.rs:820-863 builds a scratch crate with the same shape of manifest — the three path deps, serde, both patch tables — and project_deps_match_cargo_gen (:1266-1300) keeps that list in step with the generator; CI runs it at .github/workflows/docs-contract.yml:138. Read what it asserts before trusting it: for each of horus, horus_core and horus_macros it checks that the name still appears in write_horus_path_deps's literal list and in the test's own PROJECT_DEPS, and that write_implicit_deps still mentions serde. It does not assert the converse. The literal list already carries a fourth name, horus_library, that matches nothing on disk, and a fifth would pass unnoticed. So that test protects the test harness against a removal. It protects neither the harness nor this page against an addition, and it never reads this page at all.

The authority is therefore the generator, not this document. In a throwaway project:

horus new scratch --rust --yes && cd scratch
horus build
cat .horus/Cargo.toml

Whatever that prints is what your own manifest should mirror, with the version and the home directory changed to match your machine.