Crux Daemon · 1. Architecture
The Crux Daemon is a single Rust binary that opens three TCP listeners and writes everything it knows into one directory. Understand those two sentences and the rest of this set is detail. This chapter is explanation: it draws the model, then fills it in.
For the routes themselves see the API reference set. For extending the daemon see the developer guide.
1.0 In plain English
The Crux Daemon is a background program you run on a machine you control. It holds what your agents have learned, answers their questions about it, and keeps a record of what it stored and handed back. In shape it is much closer to a database server than to a modern microservice deployment: one executable, started once, keeping its own state in one directory on local disk. There is no cluster to stand up and no orchestrator needed to make it work.
The "three planes" are three doors into that one process, not three services. HTTP on :14800 is the full API that the console, the SDKs and any HTTP client use. MCP on :14801 is the JSON-RPC surface that agent tools connect to, and it is a genuinely separate listener rather than a route on the HTTP port. gRPC on :4007 binds and accepts connections but answers unimplemented on every call, so it is a door that opens onto a wall. They are separate ports so that you can expose one without exposing the others, which is the entire reason the split exists.
Why does it exist at all? Because the alternative is that every agent framework you use invents its own memory, in its own format, and nothing you run can see what anything else knows. Putting it in one process over one directory means there is exactly one thing to back up, one thing to secure, and one place to look when an agent's answer does not match what you expected.
You will actually touch this chapter at three moments. When you decide where to run the daemon and what to put on a network: §1.5 covers the bind rails, two validators that run before anything binds and refuse a development auth mode on a non-loopback address, so you cannot expose an unauthenticated daemon by accident. When you reason about blast radius: there is no service boundary inside the process, and one AppState value roughly 90 fields wide is cloned into every handler, so a fault is not contained by anything structural. And when you read the source and need to know which crate holds the answer, which is §1.6.
The thing people get wrong is reading "three planes" as "three independently deployable services" and designing a topology around it. It is one process; if it stops, all three stop together. The close second is planning an integration against gRPC because the port is open and the protobuf files are in the tree. Read §1.4 before you write a line of that code.
1.1 The shape of the thing
corecruxd is not a thin binary over library crates. At 151,433 lines across 155 files it holds about 43% of the workspace's Rust and contains the large majority of the domain logic. Its HTTP module alone, crates/corecruxd/src/http/, is 78,029 lines across 73 files. The library crates are, with two exceptions, comparatively thin.
That matters for two reasons. If you are reading the source, crates/corecruxd/src/ is where the answers are. And if you are reasoning about blast radius, there is no service boundary inside the daemon to contain a fault: one AppState value, roughly 90 fields wide, is cloned into every HTTP handler (main.rs:683).
| Measure | Value |
|---|---|
| Workspace crates | 28 (Cargo.toml:2-30) |
| Out-of-workspace crates in-tree | 3 (see chapter 3) |
| Total workspace Rust | ~349,000 LOC |
corecruxd | 151,433 LOC, 155 files, binary only, no lib.rs |
| Workspace version | 0.5.52 (Cargo.toml:45) |
| MSRV | 1.88.0 (Cargo.toml:47) |
unsafe blocks | zero: unsafe_code = "forbid" (Cargo.toml:115) |
1.2 Three listeners
All three addresses are resolved in load_config (config.rs:793) and stored on Config as SocketAddr values.
| Plane | Default bind | Host env | Port env | YAML key | Can it be turned off? |
|---|---|---|---|---|---|
| HTTP (axum) | 127.0.0.1:14800 | CORECRUXD_HTTP_HOST | CORECRUXD_HTTP_PORT | daemon.listen_addr / daemon.http_port | No |
| MCP (axum) | 127.0.0.1:14801 | CORECRUXD_MCP_HOST | CORECRUXD_MCP_PORT | daemon.listen_addr / daemon.mcp_port | Yes: CORECRUXD_MCP_ENABLED, default true |
| gRPC (tonic) | 127.0.0.1:4007 | CORECRUXD_GRPC_HOST | CORECRUXD_GRPC_PORT | daemon.listen_addr / daemon.grpc_port | No |
Line references: HTTP host config.rs:796, port config.rs:801 with default 14800 at config.rs:805; gRPC host config.rs:807, port default 4007 at config.rs:816; MCP host config.rs:817, port default 14801 at config.rs:826; mcp_enabled defaults true at config.rs:827.
Precedence for each address is: CORECRUXD_<PLANE>_HOST env → daemon.listen_addr from the YAML file → 127.0.0.1. The YAML file has one shared listen_addr for all three planes but three separate port keys.
Two things that trip people up on the first attempt:
- An unparseable host or port falls back to the default with no warning.
CORECRUXD_HTTP_PORT=not-a-numbersilently binds 14800; this is asserted in-tree at config.rs:2441. There is no log line. If the daemon is on the wrong port, check the value you actually set. - MCP is a second, separate listener, not a route on 14800.
crux_mcp::server::routerbuilds its own axum router (server.rs:48) and is served onconfig.mcp_addr(main.rs:1583). There is no/mcproute on the HTTP router. See §1.7.
Do not change the HTTP port default. It is a fixed contract for every client in the ecosystem, but note it is configurable, so treat 14800 as policy rather than as a constraint.
1.3 What each plane is actually for
| Plane | Status | What it serves |
|---|---|---|
HTTP :14800 | SHIPPED | The full API: facts, entities, edges, sessions, query, receipts, admin, console, work board, integrations, extensions. 327 distinct .route() registrations in http/mod.rs, plus 16 from merged sub-routers and 6 console routes. |
MCP :14801 | SHIPPED | JSON-RPC 2.0 over Streamable HTTP, protocol version 2024-11-05. A catalogue of 118 tools (tools/mod.rs:3108), 119 when CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS is on. Methods handled: initialize, notifications/initialized, tools/list, tools/call; there is no resources/* or prompts/* surface. |
gRPC :4007 | STUBBED | Nothing. See §1.4. |
The MCP context is built once and shared between the MCP listener and the HTTP OpenAI tools shim at /v1/openai/* (main.rs:1113), one source for the tool surface.
1.4 The gRPC plane is declared, not implemented
Do not plan an integration against gRPC. Port 4007 binds, accepts connections, and answers, with unimplemented on every call.
| Service | RPCs declared | Registered on the listener | Functionally implemented |
|---|---|---|---|
CoreCruxDataPlaneV1 | 9 | 9 | 0 |
CoreCruxExportV1 | 1 | 1 | 0 |
CoreCruxObserveV1 | 5 | 0 | 0 |
All 10 registered RPCs return Status::unimplemented("requires the proprietary edition"), including the primary write RPC AppendBatch (grpc.rs:758) and ReadStream (grpc.rs:771). The five CoreCruxObserveV1 RPCs have Rust types generated but no implementation anywhere in the workspace, and the service is never added to the tonic server (grpc.rs:979).
The equivalent functionality is on HTTP: /v1/ops/facts, /v1/ops/errors, /v1/ops/health, /v1/bootstrap/pull, /v1/bootstrap/status (http/mod.rs:819).
The root cause is one line: let dataplane_pool: Option<DataPlanePool> = None; (main.rs:565), with the comment "Crux Daemon: no dataplane pool (requires a dataplane-enabled distribution)". The same None disables POST /v1/admin/append over HTTP (append.rs:45).
Also absent from the gRPC plane: TLS, mTLS and server reflection. grpc::serve builds a plain tonic::transport::Server (grpc.rs:962). grpcurl needs -proto proto/corecrux_dataplane_v1.proto rather than -plaintext list.
This is defect B5 in chapter 16.
1.5 The bind rails: why you cannot expose this by accident
Two validators run before anything binds, and both fail closed. This is the best-designed part of the daemon's operational surface and it is worth knowing before you fight it.
validate_network_auth_posture (main.rs:2022):
- Computes
dev_auth_mode = matches!(auth_mode, Off | DevScopes)(main.rs:2030). - Computes
loopback_only = http_addr.ip().is_loopback() && grpc_addr.ip().is_loopback()(main.rs:2031). MCP is deliberately not considered here; it has its own validator. - Refuses a dev auth mode on a non-loopback bind unless
CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BINDis set (main.rs:2032). - Refuses
ReplicatedCommitplus a JWT auth mode withoutCORECRUXD_REPLICATION_AUTH_BEARER(main.rs:2039).
validate_mcp_bind_posture (main.rs:2050) returns early if any of these hold: MCP disabled, MCP binds loopback, the agent registry is non-empty, or the insecure-dev override is set. Otherwise it refuses.
Net effect: a stock CORECRUXD_AUTH_MODE=dev_scopes daemon cannot be exposed on 0.0.0.0 by accident. The error message names the escape hatch:
auth mode Off may not bind to non-loopback addresses
(http=0.0.0.0:14800, grpc=127.0.0.1:4007)
without CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1
The shipped Helm chart trips this rail on a stock install, defect B2 in chapter 16. Both compose files get it right.
1.6 The crate map at a glance
Twenty-eight workspace members, layered with no cycles. Full detail, including public surface and reverse dependencies, is in chapter 3.
| Layer | Crates | Character |
|---|---|---|
| L0, zero workspace deps | corecrux-types, corecrux-frame, corecrux-proto, corecrux-segment, crux-session, crux-contrib, crux-cost, crux-observe-api, vaultcrux-local, crux-config-wizard | Leaf types and formats. The best place to start reading. |
| L1 | corecrux-index, corecrux-receipts, rcx-capability-token | Index format, receipt signing, capability tokens |
| L2 | corecrux-storage, crux-router, crux-enterprise-shim, crux-sync, crux-integrations, crux-integration-tests | Shard store, routing decisions, trust contracts |
| L3 | corecrux-projections, corecrux-memory | Living Objects projections; the fact/session/entity/edge/case stores |
| L4 | corecrux-retrieval, crux-observe, crux-lens-features | BM25 + graph + dense-cosine fusion; self-observation; lenses |
| L5 | crux-mcp, crux-claude-hooks | The 118-tool MCP surface; the Claude Code hook binaries |
| L6 | corecruxd, corecruxctl | The daemon and the operator CLI |
The five biggest crates account for most of the code: corecruxd (151,433), corecruxctl (46,615), crux-mcp (41,878), corecrux-memory (18,727), corecrux-receipts (17,639).
Two members are not runtime code. crux-integration-tests is a test harness. crux-contrib is an orphan: 165 lines, zero reverse dependencies, compiled and CI-tested under a 99% coverage floor, and linked into nothing.
1.7 How a request moves through the system
Take a PUT /v1/facts on the HTTP plane. This is the path, outside-in.
1. Socket. axum::serve over a TcpListener (main.rs:2217). Every accepted connection gets TCP_NODELAY (main.rs:2238) and a ConnectInfo<SocketAddr> extension so the rate limiter can see the peer IP.
2. Ingress hardening, applied by apply_ingress_limits to both the API and MCP routers (main.rs:1195, main.rs:1587). Request-path order is documented at ingress.rs:19:
| Step | Mechanism | Default | Rejection |
|---|---|---|---|
| a | Passport-header validation | always on | 400, type …/invalid-passport-header |
| b | IP-keyed rate limit | 300 rps, 600 burst | 429 + numeric Retry-After |
| c | Load-shed / concurrency gate | 1024 in flight | 503 …/overloaded + Retry-After: 1 |
| d | Inflight gauge | on when metrics present | - |
| e | 413 decorator + body limit | 16 MiB (64 MiB on four large routes) | 413 …/payload-too-large |
Every mechanism reads 0 as "disabled", so an emergency rollback needs no redeploy (ingress.rs:63).
3. Router layers, innermost to outermost (http/mod.rs:1541): request_id_middleware mints or echoes x-request-id and emits the structured op-log line; traceparent_middleware; a 30-second TimeoutLayer; CatchPanicLayer; then route_auth_middleware, quota_middleware, presence_middleware, and the Extension(case_store) layer.
4. Route-auth middleware classifies the request by its axum route template and checks an any-of scope set. Its default mode is shadow: it evaluates the contract, logs a mismatch, and continues. It blocks nothing unless CORECRUXD_ROUTE_AUTH=enforce. See chapter 7.
5. The handler takes headers: HeaderMap and calls require_http_scopes or a sibling imperatively; there is no axum extractor for auth. This is the check that actually bites out of the box.
6. AppState carries the stores. state.fact_store is an Arc<RwLock<FactStore>>; the write appends a JSON line to facts.jsonl and updates the in-memory index. Facts are the substrate for far more than "facts", coordination announces, punchcards, extension grants, session bindings and legal holds are all fact writes under reserved entity prefixes. See chapter 6 and chapter 10.
7. The response is either the handler's JSON or an RFC 9457 problem body with Content-Type: application/problem+json. See chapter 8.
The MCP plane on 14801 takes the same ingress hardening and then an additional gate: every tools/call passes an RCX capability check (dispatch.rs:641), and a revoked passport is refused every tool outside a small read-only allowlist.
1.8 One directory, and what is deliberately not a file
Resolution order for data_dir (config.rs:834):
CORECRUXD_DATA_DIR, tilde- and XDG-expanded- file config
daemon.data_dir - file config
daemon.state_dir - default
../CoreCruxData/v1
That default is a relative path. It resolves against the daemon's working directory, so running corecruxd from two different directories silently gives you two different data directories, and the LOCK single-instance guard cannot catch it, because they are different LOCKs. The Docker image avoids the problem by setting CORECRUXD_DATA_DIR=/data (Dockerfile:66). Set it explicitly everywhere else too.
Three things are deliberately not separate files, and looking for them wastes an afternoon:
- Coordination state, announces, presence, punchcards and leases are facts under the
__coord__::entity prefix (coord.rs:105). - Extension grants, facts under
__extension_grant__::(extension_grants.rs:12). - ExecPlan work items, a read-time projection over external
.mdfiles under$CRUX_EXECPLANS_ROOT, never written to the data dir (work_execplans.rs:1130).
The console SPA is served from embedded assets, not extracted to disk (console.rs).
1.9 What a stock build actually contains
Four optional cargo features exist on corecruxd, and all four are off by default (crates/corecruxd/Cargo.toml:10).
| Feature | Status | What turning it on adds |
|---|---|---|
otel | FLAG, off by default | The OTLP span exporter in init_tracing |
wasm-extensions | FLAG, off by default | The wasmtime host for kind: wasm extensions. Documented cost: ~30s cold build, ~60MB binary growth |
dense-embed-model | FLAG, off by default | The fastembed/ONNX embedder, activated at runtime with CORECRUXD_DENSE_MODEL=fastembed |
hosted-surfaces | FLAG, off by default | /v1/gpu1/* and GET /v1/cloud/access-contract |
hosted-surfaces is the one that catches people. The .route() lines for those endpoints are visible in the source but wrapped in a #[cfg(feature = "hosted-surfaces")] block (http/mod.rs:1528). Reading the router and concluding the route exists is a reading trap. In a stock binary the routes and their handler code are both absent, and the endpoint 404s.
A stock cargo build --release therefore has: no OpenTelemetry export, no WASM extension host, no ONNX embedder (the pure-Rust LocalHashEmbedder is used instead), and no hosted-surface routes.
1.10 What this architecture does not give you
Stated as plainly as the rest:
- No TLS anywhere in
corecruxd. Not on HTTP, not on gRPC. Every credential rides plaintext unless a proxy terminates TLS (main.rs:2217). - No partial availability.
try_join!on the three planes means a gRPC bind failure kills the HTTP plane too (main.rs:1594). - No tenant isolation in the dev auth modes.
OffandDevScopesboth yieldTenantAllow::Any(auth.rs:853). - No route-auth enforcement by default. Shadow mode logs; it does not block.
- No automatic compaction of the fact journal, and no GC at all for
quarantine/orobservations/*.jsonl. See chapter 6 §6.6. - No clustering in this build.
ReplicatedCommitselects a code path whose dataplane isNone, so thereplicated_commit_dataplanereadiness gate fails permanently.
Sources
- crates/corecruxd/src/config.rs:793,
load_config - crates/corecruxd/src/main.rs:1591, the three-plane
try_join! - crates/corecruxd/src/main.rs:2022,
validate_network_auth_posture - crates/corecruxd/src/main.rs:2050,
validate_mcp_bind_posture - crates/corecruxd/src/main.rs:565,
dataplane_pool = None - crates/corecruxd/src/grpc.rs:758,
AppendBatchreturns unimplemented - crates/corecruxd/src/grpc.rs:979, only two services registered
- crates/corecruxd/src/http/mod.rs:455,
router - crates/corecruxd/src/http/ingress.rs:67,
apply_ingress_limits - crates/crux-mcp/src/server.rs:48, the MCP router
- crates/crux-mcp/src/tools/mod.rs:3108,
TOOL_COUNT = 118 - crates/corecruxd/Cargo.toml:10,
default = []

