Skip to content

The Rust executable

The Rust executable of Majordomus.

Rendered from apps/majordomus-cli/README.md — the same Markdown GitHub shows. The command line, the capabilities, the MCP and HTTP surfaces and the benchmarks are not written there: they are generated from the registry, and their pages are the row of links above.

The Rust executable of Majordomus. It builds a binary named majordomus that reads the repository's provider-neutral AI layer under .ai/, composes one capability registry from its own executable capabilities and the layer's objects, and serves that registry through several interfaces, all read-only: MCP over stdio (mcp), which also starts, or attaches to, the repository's one shared server (a home page listing every surface it serves, this repository's documentation, an OpenAPI document, a Swagger UI shell, the Cockpit and MCP over HTTP on the loopback interface, the same server serve runs alone), introspection on the command line (capabilities), and generated reference files (generate). Every client attached to the shared server is a peer the others can see.

It is not a second implementation of the shell tool: init, start, check, finish, doctor, update and the task lifecycle live in bin/majordomus at the repository root, and this executable advertises only the commands it implements. The architecture is docs/CAPABILITIES.md; the MCP surface as a client sees it is docs/MCP.md; the decisions are ADR 1, ADR 2 and ADR 3 under .ai/repo/adrs/.

What belongs here, and what does not

Belongs: reading the layer as the repository declares it, validating what it reads against the schemas of the distribution and the repository, composing the registry, projecting it, and saying precisely what could not be read. Every capability is read-only and every start leaves the repository byte-identical.

Does not belong: any mutation of the repository, the task lifecycle, projections of the policy into provider files, hooks, a daemon, authentication, a database, model invocation, or anything Prismatic. The binary depends on no service and no other repository; it needs the tool distribution's share/ directory at run time and git for the default discovery.

Build, run, test

cargo build --manifest-path apps/majordomus-cli/Cargo.toml
B=apps/majordomus-cli/target/debug/majordomus
$B --help
$B --version
$B mcp --inspect                  # what would be served, every diagnostic; exit 10 when degraded
$B mcp                            # MCP on stdio until the client goes; the first one in a repository is the shared server (Swagger UI at the URL it logs), every later one attaches
$B mcp --standalone               # this client alone: no port, no lease, no peers
$B serve                          # the shared server alone: 127.0.0.1:8741, the home page /, /docs/, /swagger, /openapi.json, /mcp, /api/v1/...; exits 0 when one already runs
$B web list                       # every web surface: id, kind, mount, category, world, source — discovered, never registered
$B web explain docs               # one surface and where each of its values came from
$B web validate                   # the topology's invariants; exit 10 with every finding and its remedy
bin/majordomus-mcp                # builds $B when needed, then `mcp`: what .mcp.json, .gemini/settings.json and .codex/config.toml name
just                              # the recipes a person runs, routed to $B wherever it can serve them
$B capabilities list              # every capability with its projections
$B capabilities describe objects.get
$B capabilities schema objects.search --side input
$B capabilities validate          # the registry's invariants, every projection and the benchmark coverage; exit 10 with the list
$B generate                       # docs/generated/ (each document in every encoding it is committed in; artifacts.md indexes them) and share/allow/*.txt
$B generate --check               # exit 10 naming every stale file; writes nothing
$B bench coverage                 # every required benchmark target and whether it is covered; the denominator is the registry's
$B bench --profile quick          # time every operation: directly, over MCP (a real child), over HTTP (a real socket); slowest first
$B bench objects.search --transport mcp --profile full --format json
$B bench --check                  # against this platform's baseline under .ai/repo/benchmarks/rust/, under policy.yaml
$B bench baseline update          # record this platform's baseline (a tracked, reviewable file); refuses a dirty tree

Rust 1.85 or newer, git on PATH for the default discovery, and a share directory (below). From apps/majordomus-cli/:

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings   # missing_docs is an error here
cargo test                                                  # unit, integration, doctests
cargo doc --no-deps                                         # RUSTDOCFLAGS="-D warnings" in CI
cargo bench                                                 # criterion, benches/{projections,shared,scaling}.rs
cargo llvm-cov --all-targets --summary-only                 # coverage; CI enforces the threshold in scripts/rust-check

scripts/rust-check at the repository root (just rust-check) runs all of it in the order CI does; the coverage floor is the one integer in scripts/rust-coverage-threshold, read by CI, by the script and by just coverage. The tests build a disposable repository each and never read this checkout; test/cases/72_rust_mcp.sh, 76_capabilities_projections.sh and 90_mcp_shared_server.sh at the repository root run the built binary against a repository the shell tool's own init wrote, which is the cross-check that the two executables read the same layer, and 77_rust_evidence.sh proves the gates above are wired the same way in the script, the workflow and the justfile (rule project.rust-cli-evidence, claims rust-evidence-gates, rust-coverage-floor, rust-hot-path-benchmarks).

The binary shares its name with the shell tool on purpose. Put one or the other on PATH, or call this one by path. An MCP client is configured once, at the repository root, and this repository ships those files: .mcp.json for Claude Code, .gemini/settings.json for Gemini CLI and .codex/config.toml for Codex all start bin/majordomus-mcp, which builds this crate when the executable is missing or older than its sources, sets MAJORDOMUS_SHARE, and execs majordomus mcp. To name the executable directly:

{ "mcpServers": { "majordomus": { "command": "/path/to/apps/majordomus-cli/target/debug/majordomus", "args": ["mcp"], "env": { "MAJORDOMUS_SHARE": "/path/to/prismatic-majordomus/share" } } } }

The share directory: kinds and schemas at run time

Nothing about kinds or keys is compiled in. On every start the executable locates a share directory and reads kinds.yaml and schemas/<name>.schema.json from it, in this order:

sourcewhen
--share <DIR>always wins; a directory without kinds.yaml is an error
MAJORDOMUS_SHAREsame
<repository root>/sharewhen it holds kinds.yaml: this repository supervising itself
<directory of the executable>/../sharean installation laid out as bin/ beside share/

None found: exit 12, every directory tried named. A repository adds kinds in its own kinds.yaml under .ai/repo/knowledge/ and schemas under .ai/repo/knowledge/schemas/; a name the distribution already declares is an error naming both files.

Command reference

The command line is declared once, in src/cli.rs: clap for the structure, and typed Rust metadata beside it (EXAMPLES) for the examples clap cannot carry. Every reference is generated from that one declaration and never written by hand — majordomus --help at a terminal, docs/generated/cli.md for a reader on GitHub, docs/generated/cli.json and its YAML sibling for the website's generator and for a reader of configuration, the site's reference under /docs/cli/ with one page per command, and the command line page of the registry. majordomus generate writes the files and majordomus generate --check refuses a stale one in CI. Every artifact it writes declares the document it projects, its encoding, the schema its content satisfies and its source, and a structured document is written in every encoding it is committed in from one value; docs/generated/artifacts.md is the generated index of the whole set, and artifacts.list reads it back reconciled with the working tree. Every command starts from the same options (--repo, --discovery, --strict, --share); the reference lists them on each command that accepts them.

A command is not finished when it parses. cli::validate — run by majordomus capabilities validate, by tests/cli_docs.rs and therefore by CI — refuses a command with no summary, an argument with no help, an enumerated value that says nothing, and any command a person can run that carries no example. tests/cli_examples.rs then runs every documented example against the built executable in a disposable repository: the argument vectors it executes are read from the same declaration the pages render, so a documented example that stopped working is a failing test rather than a stale page. Adding a command therefore means: declare it in clap, declare at least one example beside it, implement it, just derive.

Exit codes

The same contract as docs/CLI.md:

codemeaningexamples
0okserved until EOF; --inspect or validate with nothing wrong
2usageunknown command or option (clap)
10contract unmetmanifest, sources.yaml, kinds or a schema invalid; the registry does not build; --strict with errors; --inspect with errors; stale artifacts under --check
12missing artifactno .ai/manifest.yaml in any ancestor; the pre-.ai layout; no share directory; an unknown capability id
13internalI/O failure, git unusable under --discovery vcs, a port that cannot be bound, transport failure

Side-effect table

commandfilesystem mutationgit mutationnetworkstdout
--help, --version, capabilities …, mcp --inspect, generate --check, bench coveragenononotext or JSON
bench.ai/local/benchmarks/<run>.json (the local half, not tracked) unless --no-write; baseline update writes .ai/repo/benchmarks/rust/baseline.<platform>.jsonnoa loopback socket and a majordomus mcp child of its owntext or JSON
mcpthe lease under .ai/local/state/mcp/ (not tracked), removed on exit; none with --standalonenolistens on loopback (the server) or connects to it (a bridge); the browser fetches Swagger UI assetsMCP protocol
servethe lease, as abovenolistens on loopback; the browser fetches Swagger UI assetsHTTP on the socket, nothing on stdout
generate (without --check)writes docs/generated/ and share/allow/nonothe paths written

tests/mcp_stdio.rs::serving_mutates_nothing, tests/http_serve.rs::serving_from_a_nested_directory_finds_the_same_root_and_writes_nothing and test/cases/90_mcp_shared_server.sh compare git status and every tracked blob before and after a session; tests/mcp_shared.rs checks that the lease is gone after the server.

Architecture

main.rs               parse, init stderr logging, run, map the error to an exit code
cli.rs                clap declarations; RepoArgs shared by every command
app.rs                the one composition point: repository -> share -> kinds and schemas -> index -> registry
commands/             mcp, serve, capabilities, generate, bench: each a function from its arguments to an exit code;
                      mcp holds the session that is answered locally (this process is the server) or bridged
perf.rs               the process-wide counters and phase timings behind perf.counters and the structural tests
bench/                projection.rs (targets from the registry), coverage.rs, runner.rs (direct, HTTP, MCP),
                      stats.rs, results.rs (the versioned document), baseline.rs (per-platform baseline, the policy), system.rs
synthetic.rs          a generated repository of any size, for the property tests and the scaling benchmarks
lease.rs              one shared server per repository: the lease file, the election, the stale-lease takeover
shared.rs             the shared server: bind, publish the URL, serve until the last peer leaves, release
peers.rs              the peer board: sessions named by their clients, announcements, in memory only
repository.rs         root discovery (nearest .ai/manifest.yaml), the typed manifest
share.rs              locating the distribution's share directory; reading its schemas
discovery/            sources.yaml, the DiscoverySource trait, its two implementations, :(glob) matching
metadata/             kinds.yaml, the schema set (jsonschema), the YAML subset, front matter
index.rs              read every discovered file into an Object or a Diagnostic; dedupe; sort
model.rs              Object, Provenance, Diagnostic, Severity: the domain of the layer, no I/O
capability/           the canonical model: model.rs (descriptor, kinds, cache and benchmark policy), schema.rs
                      (canonical schemas and their MCP and OpenAPI projections), handler.rs (typed handlers, Context,
                      the capability! macro), module.rs (module! and compose_modules!), benchmark.rs (BenchmarkCases),
                      executor.rs (the one execution path and the cache), registry.rs (the registry, its modules and
                      invariants, the fingerprint), builtin/<module>.rs (the executables, one file per module;
                      mod.rs composes the application), declarative.rs
mcp/                  surface.rs (the registry as resources and tools), protocol.rs (JSON-RPC and MCP), stdio.rs,
                      bridge.rs (a stdio session forwarded to another process's shared server; its own HTTP client)
http/                 surfaces.rs (the resolved topology narrowed to this process, one handler bound per surface;
                      a native surface with no handler refuses the router), router.rs (asks the topology who owns a
                      path, and binds a capability's input from the registry), openapi.rs, swagger.rs,
                      server.rs (tiny_http, a few worker threads), mcp.rs (MCP over HTTP: sessions, expiry)
web/                  the web surfaces as one model: model.rs (Surface, Mount, Topology, and the two worlds a mount
                      can be claimed in), discover.rs (the executable's own declarations, the site's configuration,
                      a producer's surface.json), validate.rs, files.rs (serving a static surface safely),
                      home.rs (the page at /), html.rs, compose.rs, manifest.rs, report/
generate.rs           the one generator pipeline and the allow-list derivation
git/                  read-only git: toplevel, head, branch, dirty state, ls-files
logging.rs, error.rs  tracing to stderr; the typed errors and their exit codes

Dependencies flow inward: commands knows clap and everything below; mcp and http know the registry and nothing about clap or files; capability knows the index's model; index, discovery, metadata, repository, share and git know the model and the errors; model knows nothing. No MCP library and no HTTP framework: the read-only subset of MCP is eight methods in one file, a handful of loopback routes need a few synchronous worker threads over one immutable registry, not an async runtime, and a bridge's request is thirty lines over TcpStream. The one trait, DiscoverySource, exists because two enumerations ship. Dependencies: clap, serde, serde_json, schemars, jsonschema, tiny_http, thiserror, tracing, tracing-subscriber; dev: tempfile, criterion.

The web surface

Everything this executable exposes over HTTP is a surface, and no list of them exists. They are discovered — from capability! declarations and the constants in web::discover::native_all(), from site/config.toml, and from a surface.json a producer writes beside its output under target/web/<id>/. One resolution, held on Context::web and resolved once per process, feeds the router, the home page at /, the web.surfaces capability, the validator, the publication, the startup log and docs/generated/web.json.

If you are changing anything under src/http/, src/web/ or src/cockpit/, the invariant is: an HTTP surface is declared once, where its producer is, and every other appearance of it is derived. Do not add a path to the home page, to the OpenAPI document's infrastructure list, to a benchmark inventory, to the Cockpit's tables or to a documentation table — none of those is written by hand, and all of them read the same resolution. Adding a static surface is a surface.json and no Rust at all; adding a route this executable answers is one Surface in web::discover::native_all() and one arm in http::surfaces::Native::of, which the router checks at construction.

/docs is this repository's documentation and /swagger is Swagger UI. Neither name may be repurposed for the other: project.web-surface-declared-once states it, majordomus web validate enforces it in scripts/rust-check, and test/cases/89_web_surface.sh proves it over a real socket. The whole design, the metadata a surface carries, how /docs is built for its mount and what is enforced where, is docs/WEB.md; the decision is ADR 13.

Repository discovery

Walk from the start directory upward. The first ancestor carrying .ai/manifest.yaml is the root. A manifest that does not parse, carries a key nothing reads, or declares a schema other than ai-repository/v1 stops the search with that error: a nearer broken layer is never skipped for a farther working one. .git is not a marker, so an ordinary git repository is not a Majordomus repository. .majordomus/ is not a marker either: with bin/majordomus inside it is an installation of the tool and ignored; without one it is the pre-.ai layout and refused by name, pointing at majordomus migrate.

Data-driven model

Nothing in the Rust code names a repository file except the two bootstrap conventions the layer itself documents, .ai/manifest.yaml and sources.yaml under the manifest's knowledge section, and the file names of the distribution (kinds.yaml, schemas/*.schema.json). Everything else is read from data at run time:

decidesread fromowner
where the layer is.ai/manifest.yaml existsrepository
which sections existsections: in the manifestrepository
which file names must carry the context contractcontext.documents: in the manifestrepository
which files carry which kind.ai/repo/knowledge/sources.yamlrepository
how a kind is readshare/kinds.yaml, plus a kinds.yaml under .ai/repo/knowledge/distribution, repository
which keys and values a kind may carryshare/schemas/<kind>.schema.json, plus .ai/repo/knowledge/schemas/distribution, repository

The YAML reader implements the layer's documented subset (docs/SCHEMAS.md), not general YAML, so this executable and the shell tool accept the same files and refuse the same ones (one deliberate divergence: this reader trims trailing whitespace before testing quotes; the shell keeps the quotes and refuses such a prompt downstream). Failure policy, decided once in index.rs: the manifest, sources.yaml, the kinds files and the schemas are errors, because without them nothing can be discovered; every other file that cannot become an object is excluded with an error diagnostic naming its path and code, and the index is degraded. A degraded index still serves; --strict refuses it.

Diagnostic codes: malformed_front_matter, missing_front_matter, malformed_yaml, unknown_key, schema_violation, kind_mismatch, unsupported_version, missing_field, missing_context_contract, duplicate_identity, unknown_kind, required_source_empty, claimed_twice (warning), invalid_utf8, oversized, symlink, not_a_file, unreadable.

Security boundaries

Repository content is untrusted input: symlinks are never followed or read; a file over 4 MiB or a front matter over 64 KiB is refused; invalid UTF-8 is refused; only paths the declared pathspecs match are read, and only from the repository root; resources/read and objects.get answer from the in-memory index and the registry (one resolution of a URI, shared by both; majordomus://repository executes repository.info), never from a path a client names; the HTTP server binds loopback by default, reads at most 1 MiB of body, and trusts no header beyond the session id it issued itself, which names a session and grants nothing (every session sees the same read-only registry). The lease is trusted only after the server it names has answered for this root; a peer's announcement is text other peers read, never a path the server opens. Not defended against: a hostile sources.yaml naming a huge tree (the walk is bounded by the repository, not by a budget), and memory, since every object's content is held for the session.

Stability

status
--help, --version, mcp --help, exit codes, the new commands' help and codesbehaviourally verified (tests/cli.rs)
root discovery, nearest-wins, legacy refusal, --repobehaviourally verified (tests/repository_discovery.rs)
the kind schema, run-time schemas, every diagnostic code, repository-defined kindsbehaviourally verified (tests/metadata_contract.rs, tests/external_extension.rs)
determinism across enumerations, provenance, classificationbehaviourally verified (tests/index_behavior.rs)
MCP: handshake, listing, reads, tools, errors, clean EOF, protocol-only stdout, no mutationbehaviourally verified (tests/mcp_stdio.rs)
the registry's invariants; every projection present and none orphan; a change reaches every projectionbehaviourally verified (tests/registry.rs, tests/projections.rs)
HTTP over a socket, OpenAPI, Swagger shell, typed errors, HEAD, /dev/null stdin, MCP/HTTP paritybehaviourally verified (tests/http_serve.rs)
one shared server per repository: lease, bridge, /mcp sessions, peers, fallback port, serve deferring, --standalone, takeover, re-attachment, refusalbehaviourally verified (tests/mcp_shared.rs, tests/shared_units.rs, test/cases/90_mcp_shared_server.sh)
modules compose capabilities, the root composes modules; the module, cache and benchmark invariantsbehaviourally verified (tests/registry.rs, test/cases/91_canonical_architecture.sh)
one executor; cache equivalence off/cold/warm, no handler on a hit, errors and commands never cached, the bound, the fingerprint; key order irrelevantbehaviourally verified (tests/executor.rs, tests/properties.rs with proptest)
no request rebuilds canonical state; OpenAPI built once; listings prepared oncebehaviourally verified (tests/hot_path.rs, test/cases/91_canonical_architecture.sh)
every operation a benchmark target with a generated denominator; exposure and policy propagation; direct, HTTP and MCP runners over real transports; the baseline checkbehaviourally verified (tests/bench.rs, tests/bench_units.rs, test/cases/91_canonical_architecture.sh)
generated reference per module, benchmark matrix, registry manifest: deterministic, reconciledbehaviourally verified (tests/projections.rs)
generate, byte-identical regeneration, --checkbehaviourally verified (tests/generate_check.rs)
the layer init writes, served with state ok; one capability through every interfacebehaviourally verified (test/cases/72_rust_mcp.sh, 76_capabilities_projections.sh)
CLI names, URIs, tool names, routes, diagnostic codes, kinds.yaml, the schema filesimplemented; pre-1.0, changes are documented, never silent
--discovery filesystemimplemented; a convenience outside the layer's contract

Deferred, deliberately: MCP prompts (prompt assets render {{CONTEXT}} from local state the shell tool owns), mutation of the repository over any interface, subscriptions and list-change notifications, a server-initiated stream on /mcp, path parameters, /openapi.yaml, Swagger UI assets offline, hot reload (a shared server keeps the index it built at start until its last client leaves), the .ai/local/ half as served content, a content hash per object, and persistent coordination (the peer board is one process's memory; the task record and scope of the shell tool are the durable form).

Claims implemented in the executable

Attached by the file that implements each claim; every one links to its page with the implementation and the test that proves it.