Twenty-five crates is a lot of Cargo.toml files for a system whose entire demo fits in memory. The obvious question is whether the split is genuine architecture or mere tidiness. In the Valhalla workspace the honest answer is: the crates exist because each boundary is load-bearing. Every vhalla-* directory marks a place where the compiler, not a code review, is expected to stop a specific kind of mistake.
The workspace is a peer-to-peer coordination protocol for agents and humans: signed chat, owner-controlled policy, a private validator set for room state. The root Cargo.toml declares twenty-five members, resolver = "2", edition = "2021", publish = false, and workspace lints that forbid unsafe_code and warn on missing_docs. Nothing here ships to crates.io. The crates are not packages for the world; they are walls inside one repository.
what one crate cannot say
A single crate can model all of Valhalla’s stages (parsing, signature verification, policy, host effects) as modules. What it cannot do is make the dependency direction between those stages enforceable. Inside one crate, a mod wire can use crate::host and nothing compiles differently. The architecture exists on a diagram and nowhere else.
The design plan in kb/plans/valhalla-security-first-design.md states the rule that produced the split: dependency arrows are permitted flow. vhalla-core knows nothing about the network or tools; vhalla-transport knows nothing about host authority; vhalla-host does not get to reinterpret untrusted wire data. Written as modules, those sentences are conventions. Written as crates, they are checked on every build: vhalla-wire literally cannot name vhalla_host::MemoryHost because the dependency edge does not exist in its Cargo.toml.
That is the thing one crate cannot say: this code may not know that code exists. Every crate in the workspace earns its boundary by making one such sentence true.
the actual map
The workspace is easier to read as strata than as a flat list.
vhalla-core pure protocol values; #![no_std], no I/O of any kind
vhalla-wire canonical envelope, bounded decode; #![no_std]
vhalla-crypto Ed25519/SHA-256 evidence, replay admission; #![no_std]
vhalla-policy local grants, move-only capabilities; #![no_std]
vhalla-host in-memory effect execution; #![no_std]
vhalla-session paired-chat handshake; no net, storage, clock, or entropy
vhalla-ledger derived roots over retained history
vhalla-social signed social records; no_std + alloc
vhalla-discovery bounded search over admitted records; no_std
vhalla-attention private reader state; no_std
vhalla-rooms room registry types
Those are the portable cores. Around them sit the effectful adapters, each confined to exactly the platform it touches:
vhalla-identity Unix key custody (0700 dirs, 0600 files)
vhalla-journal durable commit ordering; unix-gated Store trait
vhalla-social-store private-file social persistence; unix only
vhalla-rooms-store registry snapshots; unix only
vhalla-discovery-store private reader persistence; unix only
vhalla-native libp2p/QUIC paired chat adapter
vhalla-rooms-node Malachite engine wiring; cfg(unix)
vhalla-rooms-consensus journal-gated decided-batch adapter
vhalla-rooms-app renderer-agnostic read replica
vhalla-rooms-tui terminal UI over the replica
vhalla-transport in-memory Endpoint trait + InMemoryRelay
vhalla-retrieval bounded candidate retrieval
vhalla-cli the `vhalla` binary
vhalla-steel-thread the end-to-end oracle crate
Three structural choices are worth noticing. First, vhalla-steel-thread exists as its own crate rather than a test module, because its job is to depend on everything and prove the stages compose: it is the one place allowed to import the whole map. Second, the store crates are separate from the pure crates they persist: vhalla-social runs on wasm32-unknown-unknown while vhalla-social-store openly admits a local filesystem with exclusive locks and atomic rename. Third, prototypes/* is excluded from the workspace entirely. Experiments stay outside the member list so a spike can never quietly become a dependency of the maintained code.
The smallness is the point: most of these crates are a few hundred lines. vhalla-core is 117, vhalla-transport is 137, vhalla-discovery is 35 lines of lib.rs plus modules. A small crate is cheap to audit: vhalla-transport’s entire public surface is Frame, Path, TransportError, Endpoint, and InMemoryRelay, and you can read it in one sitting. The crate boundary makes the audit surface the same size as the authority it holds.
the rules that survived
The workspace has accumulated rules that look fussy until you see what they prevent:
publish = falsefor the whole workspace. None of these crates are artifacts. Renamingvhalla-wirecosts aCargo.tomledit, not a versioned migration for downstream consumers. The boundary can move because nobody outside the repo can hold it fixed.unsafe_code = "forbid"at the workspace lint level. Every member inherits it. The onlyunsafein the checked-in tree is a singlelibc::flockcall indesktop/menubar, which lives in a different workspace entirely, precisely so Tauri’s build dependency tree never enters the protocol crates’ Linux or WASM checks.#![no_std]where feasible.vhalla-core,vhalla-wire,vhalla-crypto,vhalla-policy,vhalla-host,vhalla-session,vhalla-social,vhalla-discovery, andvhalla-attentionall compile without a standard library: nine crates, eachno_stdat the crate level withextern crate allocwhere collections are needed. That is not an aesthetic preference: it is the mechanism that guarantees a signed-record verifier can never open a socket, becausestd::netis not in its universe. It is also the mechanism that lets the same verification code compile towasm32-unknown-unknown. CI checks the portable crates for the WASM target, andno_stdis what makes that a build flag rather than a port.cfg(unix)gates for every filesystem adapter. The store crates andvhalla-identitycompile only on Unix, not because other platforms are unwanted, but because the custody guarantees (mode bits, exclusive locks, atomic rename, directory fsync) are Unix facts the code refuses to fake.- Committed
Cargo.lock. Reproducible builds are a security property here: a dependency must be justified by the boundary it serves before it is admitted. codegen-units = 1,lto = "thin"in release. A deliberate choice to pay compile time for a smaller, more predictable artifact.
None of these rules protect against a hostile OS or a compromised process. The plan says so itself: these are fences, not sandboxes. What they do is make whole categories of accident unrepresentable.
what a boundary buys
The payoff shows up in the type system. vhalla-crypto produces a VerifiedEnvelope, a move-only value with no Clone, no decoder, and no public constructor. vhalla-policy consumes it into a RemoteRequest and can mint an AuthorizedEffect. vhalla-host executes exactly one of those and returns a Receipt. The crate boundaries mean each step’s only API is the next step’s input type. A compile-fail doctest in vhalla-steel-thread demonstrates that a vhalla_social::VerifiedRecord (fully authenticated social evidence) cannot be passed to RemoteRequest::from_verified. Different crate, different evidence type, no conversion exists, and none can be written inside the closed set.
The same pattern repeats at the system edge. vhalla-rooms-node is split so that context and cert compile portably (a WASM replica can decode and verify a canonical VC2 commit certificate against a trusted validator set) while the engine wiring, durable adapter, and qualification harness sit behind cfg(unix) in unix.rs. The boundary between “what a light consumer may do” and “what only a hosted node may do” is a module gate the compiler evaluates, not a deployment diagram.
the costs are real
This shape is not free, and the workspace does not pretend otherwise. Twenty-five crates mean twenty-five manifests to keep honest, feature flags that must be negotiated across boundaries (vhalla-identity gates its social and rooms signing behind social and rooms features), and refactors that cross directories instead of modules. cargo doc output is fragmented. New contributors bounce between crates to follow one message end to end: parse in vhalla-wire, verify in vhalla-crypto, pair in vhalla-session, admit in vhalla-policy, execute in vhalla-host.
The justification is that the places where boundaries hurt are exactly the places they should. If crossing two crates feels heavy, that is the cost of an authority transition being visible. The workspace would rather make you edit two manifests than let a signature check quietly grow a filesystem dependency. A crate earns its boundary or it does not get one, and the rules above are how you tell which is which.
sources
- vhalla: the public repository; workspace layout in
Cargo.toml crates/vhalla-core/src/lib.rs,crates/vhalla-wire/src/lib.rs,crates/vhalla-crypto/src/lib.rs: the portable no_std corescrates/vhalla-steel-thread/src/lib.rs: the composition oracle and its compile-fail doctestscrates/vhalla-rooms-node/src/lib.rs: the portable/unix crate split- valhalla-security-first-design.md: the dependency-direction rules and testing contract