crates/tinybus/— the bus. One responsibility per module, core types in a module-localtypes.rswhere a module grows one, and every port insrc/ports/is one trait in one file.crates/tinybus-macros/—#[interface], and nothing else. A proc-macro crate compiles for the host, so anything that does not have to live there does not.crates/tinybus/src/bin/tinybus.rs— the CLI. Every subcommand is declared even when its milestone has not landed, so scripts and runbooks can be written against a stable surface.crates/tinybus/examples/— declared explicitly inCargo.tomlwithrequired-features, so an example can never be built by a job that has no transport compiled in.docs/modules/<module>/README.md— one document persrc/module.
Integrations are not members of this workspace. An integration is a separate process in its own repository with its own dependency graph — that is the entire point. A crate added here that pulls in a media codec has recreated the problem tinybus exists to solve.
cargo fmt --all -- --check
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo test --locked --all-features
cargo check --locked --no-default-features # the slim kernel buildThe default build links a Unix socket and a proc-macro. The
--no-default-features build links neither, and it must keep compiling: it is
what an embedded OpenHuman build uses, and it is the configuration that proves
the ports are real seams rather than decoration.
- rustfmt output, Rust 2024 idioms.
snake_casemodules and files,PascalCasetypes. - Return
Result<T>using the crate error type fromsrc/error.rs. - Every file opens with a
//!module doc describing its role and any feature gating. - Comments explain the decision, not the code. When an ordering, a bound, or a lock choice is load-bearing, say so and say why. "Bounded so a slow service cannot grow kernel memory" is a comment; "bounded channel" is not.
- Public items carry doc comments. Clap fields use
///— it becomes the help text.
- Tests live in-crate, in a sibling
<module>_tests.rsdeclared at the bottom of the module (#[cfg(test)] #[path = "<module>_tests.rs"] mod tests;), never an inlinemod testsblock. There is notests/directory. - Tests run on the in-memory transport. A test that needs a socket is testing
the socket, and belongs in
transport::unix. - Concurrency behaviour is asserted, not assumed: the timeout, the wedged peer, the dropped service and the forged sender each have a named test, because each is a property the kernel is relying on rather than an implementation detail.
- Test names state the property, not the function under test.
a_service_dying_releases_its_name_and_announces_it, nottest_detach. - No
sleepas synchronisation. Use the in-memory transport and await the thing you are waiting for; atokio::time::timeoutaround a receive is a deadline, not a sleep. - Maintain at least 80% coverage for meaningful library behaviour.
Keep every Markdown file, including this one, at 500 lines or fewer. When a
topic grows past that limit, split it into focused files and link them from the
module's README.md.
The wire format is a contract between processes built at different times, from different repositories, by different people. Treat it accordingly:
- Adding an optional header field or a struct field is compatible. Removing one, renaming one, or changing its type is not.
- A breaking change to an interface ships as a new interface name
(
Voice2), never as a redefinition. A kernel and a service built months apart must either speak or fail loudly at the first call. PROTOCOL_VERSIONis bumped only when an older peer cannot parse the new format at all.
These are invariants, not preferences. Changing any of them needs an explicit discussion in the pull request:
- The broker never parses a body. It reads the header, routes, and forwards. A broker that inspected bodies would be a process that has seen every credential, mail body and recovery phrase on the bus.
senderis stamped by the broker and overwritten on ingress. A peer that could set it could impersonate the kernel to every service on the bus. Every authorisation decision anywhere depends on this one line.- Errors never carry the value that caused them.
Error::bad_argumentsredacts backtick-quoted spans out of serde's messages for exactly this reason; new error paths must not reintroduce the leak. - The socket lives under the user's runtime directory, never
/tmp, and the bus never binds a TCP port. Filesystem permissions are the current access control story in full. - Every call has a deadline. It cannot be disabled. A call with no deadline reintroduces the hang that motivated the project.
- A confidential call goes to a loaded, hash-verified module, an explicitly
scoped SDK-created local host callback, or nobody. Only a module whose artifact the host hashed against a digest an
operator asserted — a
modules.tomlbeside the file, or one compiled into the host and checked against the release manifest before extraction — may receive one. The sole host exception isModuleHost::prepare_host_callbacksplusload_file_with_host_callbacks: a private SDK-created MemoryTransport connection, exact verified module unique sender, exact host unique destination, path, interface and member, all bound before initialization. This capability is not artifact attestation; aliases, other instances, ordinary connections and custom/remote transports never inherit it. Revocation and detach prevent new route and guarded user-dispatch admission, not cessation of already running provider work. Generic module loads grant no host callback authority. The message is never fanned out to a subscriber, never printed bymonitor, and never carried by a signal. This is admission control, not isolation — a loaded module is already inside the trust boundary and could read host memory directly; what the rule buys is that the bus will not be the delivery mechanism for unverified code.confidentialis the one header field the broker does not overwrite on ingress, because it can only ever restrict the sender's own traffic. Seedocs/modules/attest/README.md. - A misbehaving peer must not affect another peer. Bounded per-peer queues, best-effort signal delivery, and an accept loop that survives a bad client are all this invariant. Any change that lets one peer's slowness reach another's latency is a bug, not a tuning question.
- In-process modules are inside the trust boundary, not outside it. The
misbehaving-peer invariant applies to peers reached across a real transport
boundary. A module loaded with
dlopenshares the host address space and is trusted exactly as much as the host's own code. Bounded queues, deadlines and caught panics bound misbehaviour; they cannot contain a segfault, abort, heap corruption, OOM, or deliberate memory access. An integration whose crash or compromise must not reach the kernel belongs in a separate process.
-
Unit tests are never inline. Do not write a
#[cfg(test)] mod tests { ... }block in a source file. Put the tests in a sibling<module>_tests.rs(mod_tests.rsbeside amod.rs,lib_tests.rsbesidelib.rs) and declare it at the bottom of the module:#[cfg(test)] #[path = "foo_tests.rs"] mod tests;
-
The test file starts with
use super::*;and carries no#[cfg(test)]of its own. It is still a child module, so it reaches private items exactly as an inline module did. -
Name test files
<module>_tests.rs; a second group for the same module is<module>_<topic>_tests.rs. Nevertest.rs,tests.rsor<module>_test.rs. -
Integration tests stay in the crate's
tests/directory. -
OpenHuman's
scripts/externalize-inline-tests.mjs <repo-root> --writemoves inline test modules out mechanically; without--writeit only reports.