Crux Daemon · 16. Known defects

Eight blocker-severity defects were verified against the source on 2026-07-27, at workspace version 0.5.52. Each is published here with its path:line evidence, the consequence for an operator, and the workaround. Three of them mean a shipped deployment artefact does not do what it says.

This chapter is reference. It is dated, and it will be dated again when the list changes. A defect leaves this page when the code changes, not when it becomes inconvenient.

16.1 Summary

#DefectSeverityAffects
B1CORECRUX_LOG_FORMAT is read by no code. JSON logging is silently off in every shipped manifestblockerDocker image, Helm chart, both compose files, quickstart
B2A stock helm install cannot startblockerKubernetes deployments
B3POST /session advertises an MCP URL that 404sblockerAny client following the session handshake
B4POST /v1/admin/append always returns the platform-upgrade responseblockerAnyone following the README's endpoint list
B5All 10 registered gRPC RPCs return unimplementedblockerAnyone planning a gRPC integration
B6CRUX_PASSPORT_REVOCATION fails openblockerPassport revocation enforcement
B7docs/error-catalogue.md documents a taxonomy the daemon does not emitblockerClient error handling
B8Route-auth middleware defaults to shadow: violations log, never blockblockerAnyone who believes routes are enforced

16.2 B1: CORECRUX_LOG_FORMAT is read by no code

The daemon selects its log format from LOG_FORMAT, unprefixed:

// crates/corecruxd/src/main.rs:2068
let log_format = std::env::var("LOG_FORMAT").unwrap_or_default();

Consumed at main.rs:2108 and main.rs:2129. A workspace-wide grep for LOG_FORMAT across crates/**/*.rs returns exactly three hits: that read and two test lines. No Rust code anywhere reads CORECRUX_LOG_FORMAT.

The wrong name is set or documented in six shipped artefacts:

ArtefactLine
DockerfileDockerfile:65, ENV CORECRUX_LOG_FORMAT=json
docker-compose.ymldocker-compose.yml:35
helm/corecrux/templates/deployment.yamldeployment.yaml:44
examples/quickstart/docker-compose.ymldocker-compose.yml:50
config.example.envconfig.example.env:153: CORECRUX_LOG_FORMAT=text
examples/quickstart/README.mdexamples/quickstart/README.md:39, documents "Logs: JSON on stdout (CORECRUX_LOG_FORMAT=json)"

Consequence. The official Docker image, the Helm chart and the quickstart emit human-formatted logs, not JSON, while documenting and configuring JSON. Anyone shipping these logs to a structured-log pipeline is getting unparseable output. The quickstart README statement is factually wrong.

Workaround. Set LOG_FORMAT=json. Do not remove CORECRUX_LOG_FORMAT if something else in your stack reads it, but do not expect the daemon to.

Independently confirmed by two audit passes.

16.3 B2: a stock helm install cannot start

The chart hard-codes CORECRUXD_HTTP_HOST=0.0.0.0 (deployment.yaml:40-41) and defaults auth.mode: "off" (values.yaml:34-35), with no CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND.

That combination trips the bind-posture rail. validate_network_auth_posture computes loopback_only = http_addr.is_loopback() && grpc_addr.is_loopback() (main.rs:2031), HTTP is 0.0.0.0, so that is false, and AuthMode::Off is a dev auth mode (main.rs:2030), so it aborts:

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

(main.rs:2032-2037.)

Consequence. A stock helm install with no value overrides CrashLoopBackOffs at startup. The daemon's behaviour is correct, the chart default is defective.

This is demonstrably an oversight in the chart, not an intended posture. Both compose files get it right and set the escape hatch alongside their non-loopback binds:

FileCORECRUXD_AUTH_MODE..._ALLOW_INSECURE_DEV_AUTH_BINDBinds
docker-compose.yml:27-32dev_scopes1HTTP, gRPC, MCP all 0.0.0.0
examples/quickstart/docker-compose.yml:42-48dev_scopes1HTTP, gRPC, MCP all 0.0.0.0
helm deployment.yaml:40-47 plus values.yaml:35offabsentHTTP 0.0.0.0, gRPC and MCP default to loopback

Workaround, in order of preference. Set auth.mode to jwt_hs256 or jwt_jwks and supply the corresponding secret through .Values.env. Failing that, inject CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1 through .Values.env, and understand that you are then running an unauthenticated daemon reachable inside the cluster.

A second chart limitation, not a defect but worth knowing: the chart exposes only the HTTP plane. The container port list is 14800 alone (deployment.yaml:33-36), and the service has a single port. MCP on 14801 and gRPC on 4007 bind inside the pod but are unreachable.

16.4 B3: POST /session advertises an MCP URL that 404s

AppState.session is built with:

// crates/corecruxd/src/main.rs:830
let mcp_url = format!("http://{}/mcp", config.http_addr);

That is the HTTP address, default 127.0.0.1:14800, not the MCP address, default 127.0.0.1:14801. The value is stored on SessionServices.mcp_url (session.rs:66) and returned to clients in the POST /session handshake as channels.mcp (session.rs:405).

There is no /mcp route on the HTTP router. A workspace grep for route("/mcp across crates/corecruxd/src/http/*.rs returns zero registrations; the only hit is a literal JSON string in a console payload. The MCP router is crux_mcp::server::router served on its own listener (main.rs:1176, server.rs:48).

The hard-coded default is wrong the same way: SessionServices defaults mcp_url to http://localhost:14800/mcp (session.rs:90).

Consequence. Any client that follows channels.mcp from the session handshake gets a 404, or falls through to the console handler, instead of reaching MCP. The daemon contradicts its own --help, which documents the correct URL: CRUX_MCP_URL (default http://127.0.0.1:14801/mcp) at main.rs:261.

Workaround. Do not follow channels.mcp. Use CORECRUXD_MCP_PORT, default 14801, and the path /mcp. Under default configuration the correct endpoint is http://127.0.0.1:14801/mcp.

16.5 B4: POST /v1/admin/append always returns platform-upgrade

AppState.http_dataplane is built from the dataplane pool (main.rs:756), and that pool is hard-coded:

// crates/corecruxd/src/main.rs:565
let dataplane_pool: Option<crate::pool::DataPlanePool> = None;

So http_dataplane.enabled() is false, and the handler short-circuits (append.rs:41-47):

if let Err(problem) = require_http_scopes(&state.auth, &headers, &["admin:write"]) {
    return problem.into_response();
}
if !state.http_dataplane.enabled() {
    return platform_upgrade_response("admin_append");
}

The scope check runs first, so an unauthenticated caller gets a 401 or 403 and an authorised caller gets the platform-upgrade response. The body-validation branches immediately below are unreachable in this build.

Consequence. The endpoint never accepts a write in any build produced from this repository. README.md:461 lists it as a working endpoint. docs/getting-started.md:220 attributes 501 responses to "Pro required", pointing users at a subscription for a route that no subscription enables in this binary. Only docs/architecture.md:64 admits the truth, and it does so after describing the append path in six confident steps.

Workaround. Use the fact and memory surfaces, PUT /v1/facts and the local ingest door, for writes. Do not plan an ingestion path around /v1/admin/append or its compatibility alias /v1/append.

16.6 B5: every registered gRPC RPC returns unimplemented

Port 4007 binds unconditionally (main.rs:1459) and accepts connections. Nothing behind it works.

ServiceDeclaredRegisteredImplemented
CoreCruxDataPlaneV1990
CoreCruxExportV1110
CoreCruxObserveV1500

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 others are at grpc.rs:788, :798, :808, :818, :828, :840, :850 and :944.

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, only two services are registered (grpc.rs:979-980). Calling one gets a transport-level Unimplemented for an unknown service.

Two RPCs check a scope before returning unimplemented; the other eight do not check anything.

Consequence. A reader must not plan an integration against gRPC. The port answers, which makes the failure look like a permissions or wiring problem rather than an absent implementation. crates/corecrux-proto/src/lib.rs:13-15 calls dataplane_v1 "the primary data plane used by corecruxd for high-throughput event ingestion", which is stale for this edition and a likely source of integrator confusion.

Workaround. Use HTTP. The Observe service's functionality is available at /v1/ops/facts, /v1/ops/errors, /v1/ops/health, /v1/bootstrap/pull and /v1/bootstrap/status (http/mod.rs:819). This is stated in chapter 1 §1.4 so a reader meets it before planning anything.

Related, and worth stating in the same breath: the gRPC plane has no TLS, no mTLS and no server reflection (grpc.rs:962).

16.7 B6: CRUX_PASSPORT_REVOCATION fails open

// crates/crux-mcp/src/dispatch.rs:115
std::env::var("CRUX_PASSPORT_REVOCATION")
    .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
    .unwrap_or(true)

Unset means enforced. But once the variable is set, anything that is not exactly 1 or a case-insensitive true evaluates to false and silently disables revocation enforcement. There is no trim.

CRUX_PASSPORT_REVOCATION=yes, =on, =enabled and =TRUE with a trailing space all turn revocation off, while reading to an operator as if they turned it on.

A security control that fails open. That is the plainest sentence available.

Contrast with env_default_on (config.rs:744), the workspace's other default-on helper, which behaves the opposite way: only an explicit 0 or false disables. Two default-on flags, two incompatible semantics, and the failure direction on the security-relevant one is open.

Consequence. An operator who "turns revocation on" with =yes has turned it off, and nothing tells them.

Workaround. Leave the variable unset, the default is enforced, or set it to exactly 1. Verify with CRUX_CONTEXT_CUSTODY_AUDIT or by testing a revoked passport against a write tool.

A related, separate posture that must not be conflated with this one. crux-router never consults revocation at all, and says so in its own source (crux-router/src/lib.rs:120-125): CruxModeStamp.revocation_checked, carried on the stamp field of a RouterDecision, not on RouterDecision itself (lib.rs:126), is "currently always false", so "a revoked-but-unexpired token is still authorised". That is honestly disclosed in code and in the repository's threat model, which makes it a documentation obligation rather than a hidden flaw, but it must be stated plainly: RCX capability-token revocation is not enforced at the router. Revoke by expiry, or by MCP-plane passport revocation.

16.8 B7: the shipped error catalogue documents a different taxonomy

docs/error-catalogue.md is 48 lines and predates the HTTP surface. It presents 11 codes in an "Error Codes" table with HTTP and gRPC status columns, implying an API contract that does not exist.

All 11 rows are the structured-logging and CLI taxonomy (structured_log.rs:18-48, corecrux-types/src/lib.rs:37-63). No CORE_ERROR_* code is ever emitted as an HTTP problem code member anywhere in the daemon. A client matching code == "IO_READ_FAILED" will never match.

Thirteen numbered discrepancies are enumerated in chapter 8 §8.7. The headline four:

  • Roughly 40 real HTTP problem codes are entirely absent from the catalogue.
  • SHARD_NOT_OWNER, EPOCH_MISMATCH, BACKPRESSURE and TIMEOUT are documented as HTTP codes and never emitted as such.
  • The document never mentions application/problem+json, the RFC 9457 body shape, or the flattened-extensions convention; it describes a code list, not an error shape.
  • The single most client-relevant contract, 401 versus 403, and the missingScopes and missingAnyScope arrays, is absent.

Consequence. Integrators cannot handle errors from the published catalogue.

Workaround. Use chapter 8, which reflects the code. Treat docs/error-catalogue.md as superseded.

16.9 B8: route-auth defaults to shadow mode

CORECRUXD_ROUTE_AUTH selects the enforcement posture, read once at router build time. Anything other than off or enforce, including unset, resolves to Shadow (route_auth.rs:578-589).

In shadow mode the middleware evaluates the route contract, logs a route_auth_shadow_mismatch line, and continues (route_auth.rs:685-696).

Consequence. Out of the box, the route-auth layer blocks nothing. Only the per-handler require_http_* calls actually enforce. An operator reading the route-auth contract table can reasonably believe routes are gated when they are not.

Workaround. Set CORECRUXD_ROUTE_AUTH=enforce. Two things change when you do, and both are expected:

  • Unclassified routes and requests with no matched route template fail closed with 403.
  • The console asset routes return 403, because they are registered after .with_state and are absent from classify_route (console.rs:294). If you need the console, you need shadow mode or a proxy in front.

Run in shadow first, watch for route_auth_shadow_mismatch, then switch.

16.10 Documentation drift found in the same pass

These are not code defects. They are places where the repository's own documentation states a number or a fact that the code contradicts. They are published here because a reader who checks this set against those documents deserves to know which one is wrong.

ClaimWhereReality
crux-mcp has "28 token-filtered tools"docs/architecture.md:7118, or 119 with the mint-request flag (tools/mod.rs:3108, tools/mod.rs:3360). Off by a factor of about 4.2. The figure 28 is also the correct crate count used everywhere else, which is very likely how the error survived
corecruxd is 83.8k LOCdocs/agent/CODEMAP.md151,433 lines across src/**/*.rs, understated by about 68,000 lines. An agent budgeting a read from that number will mis-plan. The same column understates corecrux-memory by 60%, corecrux-receipts by 47%, corecruxctl by 41% and crux-claude-hooks by 100%
dev_scopes is the "launch-default" auth modedocs/developer-portal.md:15There is no default. Startup aborts if unset. The README and getting-started are both correct; the developer portal contradicts them
Workspace version 0.5.37AGENTS.md:560.5.52 (Cargo.toml:45)
docs/architecture.md presents its crate graph as the graphdocs/architecture.md:5-49It covers 16 of 28 crates. All of routing, identity and session is invisible
Cargo features5 of 7 audited documents never mention themAll four corecruxd features are off by default. hosted-surfaces removes routes and their handler code at compile time while the .route() lines stay visible in source, a reading trap
corecrux-index provides "GPU-native retrieval"crate module docThis repository is CPU-only, and the crate is a dev-dependency only of corecruxd
corecrux-storage and corecrux-segment module docs name six key typesthose module docsNone of the six exist
crux-contrib is a live componentdocs/architecture.md:123Orphan crate. 165 lines, zero reverse dependencies, linked into nothing, yet CI-tested with a 99% coverage floor
corecruxctl ops append is a commandcrates/corecruxctl/src/ops.rs:6No such subcommand exists. The 444-line module is a gRPC client for the unimplemented AppendBatch
crux-claude-hooks has "three subcommands"crate module docIt ships seven
corecruxctl is the "CoreCrux v3 control tool (Phase 0)"crates/corecruxctl/src/main.rs:29Stale branding against version 0.5.52. Cosmetic, but it is the first line corecruxctl --help prints
getting-started.md:99 links README.md#quickstart-That anchor does not exist

Undocumented shipped capability, for balance. Nine additional code-map languages, Go, Java, C, C++, C#, Ruby, Swift, PHP, Svelte, plus JavaScript and JSX, ship in every stock binary behind CORECRUXD_POLYGLOT_V2 and _V3 (workspace_scan_polyglot.rs:346-370). The tree-sitter grammars are unconditional dependencies. No document mentions them.

16.11 What the same pass verified as correct

A disclosure page that lists only faults is not honest either. These claims were checked and hold.

ClaimVerified
28 workspace cratesCargo.toml:2-30, exactly 28 members, and docs/agent/CODEMAP.md's 28 rows match with zero missing and zero extra
Ports 14800 / 14801 / 4007config.rs:805, :826, :816
MSRV 1.88.0Cargo.toml:47, rust-toolchain.toml:2
CPU-only, with no CUDA anywhereNo cuda feature in any manifest; ADR present at docs/adr/003-cpu-only-crux-daemon.md
unsafe_code = "forbid" workspace-wideCargo.toml:115, compiler-enforced, not a policy document
Licence headers on 100% of .rs filesgrep -rL "Licensed under" --include=*.rs crates/ returns nothing
No proprietary crates in the treecorecrux-analytics, corecrux-decision and corecrux-coordinator are absent from every manifest
Zero production todo!() / unimplemented!() call sitesOutside the deliberate gRPC Status::unimplemented returns. Workspace clippy denies both macros
The daemon dials nothing by defaultPaired with a CI gate, scripts/assert-no-phone-home.sh, referenced at main.rs:1451. The one exception is the anonymous passport claim, which is on by default and documented in chapter 4 §4.6
All 34 spot-checked public symbols in CODEMAP resolve exactlySymbol coverage in that file is accurate; only its LOC column drifted
Per-crate AGENTS.md: 28 of 28 present, all under 60 lines-

The last three rows in the first half of that table, CPU-only, unsafe_code = "forbid", and universal licence headers, are genuine trust assets. Each is mechanically checkable by a reader in under a minute, which is what makes them worth more than any assurance in prose.

16.12 How to read this page

Three things it is, and three it is not.

It is a dated snapshot against one commit at one workspace version. It is limited to what a source audit could verify with a path:line. It is the first page to update when any of these are fixed.

It is not a security advisory feed, a defect here may or may not be exploitable in your deployment. It is not exhaustive: it is what one audit pass found, and absence from this list is not evidence of correctness. It is not a roadmap: nothing here carries a fix date, because this set does not publish dates it cannot keep.

If you find something this page should carry, the evidence standard is the same one used above: a path:line on the public repository, and a statement of consequence that an operator can act on.

Sources