Capabilities · 12. Operating it
The daemon is built to refuse rather than degrade, and almost every operating capability in this chapter is a consequence of that choice. Twenty-five named conditions abort the boot instead of starting a half-configured process, readiness has nine ordered gates instead of one, and the pieces that cannot fail closed say so out loud.
This chapter is explanation. It says why you would want each operating capability and what it will not do for you. It is not a runbook and it is not a configuration reference: the Operations set is the runbook, daemon/05 is the full variable surface, and every explainer here hands off to one of them rather than restating it.
12.0 In plain English
Running a daemon in earnest is two different jobs. The first is getting it started correctly, which is a job you do once and then forget. The second is knowing, at three in the morning, whether the thing is healthy, and if it is not, what specifically is wrong. Most of this family exists for the second job.
The design commitment underneath it is worth stating before you meet any of the parts. A process that starts with a bad configuration and quietly does something reasonable is worse than a process that will not start at all, because the first one fails weeks later in a way nobody connects to the change. So the daemon checks a long list of conditions at boot and aborts on any of them, naming the condition. If your daemon will not start and it printed a sentence at you, that sentence is the whole answer.
The same instinct shows up in readiness. /healthz is a liveness probe that cannot fail: it tells you the process is running and nothing else. /readyz is nine separate ordered checks, and it will take an otherwise-perfectly-healthy daemon out of load-balancer rotation because your disk is 91% full. That looks aggressive until the first time you watch a full disk corrupt something.
Two things reliably surprise people. The first is that /metrics is public and unauthenticated, on purpose, so you have to restrict it at the network layer yourself. The second is that configuration failures are much quieter than boot failures: a typo in your YAML file looks exactly like having no YAML file, and a boolean flag set to yes is on for some flags and off for others. Sections 12.2 and 12.7 give both in full, because they are the two places where "it looks fine" and "it is fine" come apart.
12.1 One process, three listeners, and a boot that aborts
Status SHIPPED · Reached through
CORECRUXD_HTTP_HOSTand_PORT,CORECRUXD_MCP_HOSTand_PORT,CORECRUXD_GRPC_HOSTand_PORT,CORECRUXD_MCP_ENABLED,CORECRUXD_SHUTDOWN_DRAIN_SECS· Who it is for human
What it does. One binary opens three independent TCP listeners over one data directory: HTTP on 14800, MCP on 14801 and gRPC on 4007, all bound to loopback by default. They share one shutdown broadcast with a drain cap, so a stop is a drain and not a severing. Twenty-five named conditions abort the boot rather than starting a partially configured daemon.
Why it works this way. One process over one data directory removes the entire class of problems where two components disagree about the state of the same files, and it is why the daemon has no coordination story to get wrong internally. Three listeners rather than one keeps the protocol surfaces independently addressable, so you can expose HTTP to a network and keep MCP on loopback without a proxy in front. The abort list is the important part: each of the twenty-five conditions is a case where continuing would produce a daemon that works today and is wrong later. Naming the condition in the abort message is what turns a failed start into a two-minute fix.
What changes for you.
- As an operator: a failed start is self-describing. The daemon tells you which condition it refused on, and you do not have to bisect a configuration to find out.
- As an agent: the three ports are three protocols over one state, so a fact written over HTTP is visible over MCP immediately.
What it does not do.
- gRPC on 4007 binds and answers, and every RPC on it returns
unimplementedin this edition. See §13.4 before planning any integration against it. - The three listeners are joined, so a bind failure on any one of them takes the whole process down. A gRPC port collision kills your HTTP plane too.
- Loopback-by-default is a default, not a lock. Binding a non-loopback address in a development auth mode requires an explicit override, but nothing stops you setting it.
Where the detail lives. Ports, hosts and the shutdown path: daemon/01 Architecture. The full abort list and boot order: daemon/04 Startup and lifecycle.
12.2 Configuration that resolves predictably
Status SHIPPED · Reached through environment variables and an optional
config.yaml· Who it is for human
What it does. 393 environment variables plus a YAML file resolve in a documented precedence, with the startup-fatal variables named and the truthiness rules published. The publication includes the uncomfortable parts: nine mutually incompatible boolean-parsing dialects exist in the workspace, and one documented variable is read by no code at all.
Why it works this way. Environment-first configuration was chosen because it is the shape every container platform, systemd unit and Helm chart already speaks, and a YAML file was added for the small set of keys that are genuinely structural. The nine truthiness dialects are not a design; they are accumulated history, and the decision that matters is that they were counted and published rather than smoothed over. A configuration surface whose hazards are documented is one you can defend against. The practical consequence is a rule worth memorising: 1 is the only boolean value that works under every rule in this codebase. Use 1 or leave the variable unset, and nothing else.
What changes for you.
- As an operator: precedence is stated, so you can predict which of two settings wins without experimenting.
- As an agent: nothing directly. Configuration is an operator surface.
What it does not do.
- It does not parse booleans consistently.
yes,onandTrueenable some flags and silently disable others, and six flags are matched without trimming, so a trailing newline fromdocker --env-fileor a systemdEnvironmentFile=reads as off. This is documented per variable in api/15, not fixed. - It does not report a config-file problem. A read failure or a YAML parse error is swallowed and the daemon continues with defaults, logging nothing (config.rs:716). A typo in
config.yamlis indistinguishable from having noconfig.yaml, so verify a file was applied by observing a value it should have changed. - It does not validate unknown variables. A misspelled variable name is not an error; it is a variable nobody reads.
Where the detail lives. Every variable, its default and its parsing rule: daemon/05 Configuration reference. The nine dialects and the known drift: api/15 Flag parsing and known drift.
12.3 The agent config wizard
Status SHIPPED · Reached through
crux-config-wizard init|regenerate|check|list|add|remove|diff,.crux/agent-profile.toml· Who it is for both
What it does. The wizard composes CLAUDE.md and AGENTS.md out of ten versioned rule fragments, so the operating rules your agents follow are reproducible, diffable and checkable in CI. It refuses to regenerate over hand edits. It configures your agents, not the daemon.
Why it works this way. Agent operating rules are the highest-leverage configuration in an agent fleet and they are normally a single hand-maintained file that drifts per developer. Making them a composition of versioned fragments turns "what rules is this team's agent running under" into a question with an answer, and puts the answer in a pull-request diff. Refusing to overwrite hand edits is the concession to reality: people will edit the generated file, and a tool that silently discards that work gets uninstalled.
What changes for you.
- As an operator:
checkis a CI gate. A repository whose agent rules have drifted from the declared profile fails a build instead of surprising a reviewer. - As an agent: the rules you boot with come from a named profile version rather than from whatever was in the file.
What it does not do.
- It does not configure the daemon. Nothing the wizard writes changes daemon behaviour; §12.2 is that surface.
- It does not merge. When it detects hand edits it refuses and tells you, leaving reconciliation to you.
- Fragments are versioned, not validated for sense. Two fragments can contradict each other and the wizard will compose both.
Where the detail lives. Fragments, the profile file and the CI check: daemon/02 The config wizard.
12.4 Agent config audit and drift warning
Status SHIPPED, advisory only · Reached through MCP
audit_config,check_config_audit, SessionStart hook · Who it is for both
What it does. The audit hashes the eight files that define an agent's behaviour and records a sign-off against those hashes. At the start of a later session, if one of the hashes no longer matches, the agent is warned. It is a warning, never a block.
Why it works this way. The problem is narrow and real: someone changes an agent's rules and nobody notices for a fortnight. Hashing the defining files makes the change detectable without anyone having to remember to look. The choice to warn rather than block is deliberate and worth understanding, because it sets the honest ceiling on what this capability is: an agent that cannot start because a rule file changed is an agent that will have its audit disabled by the end of the week.
What changes for you.
- As an operator: a drifted rule set announces itself at the next session start rather than at the next incident.
- As an agent: you receive the warning at boot, in the same place as the rest of your session context.
What it does not do.
- The auditor field is unvalidated free text bound to nothing. It records who said they signed off, and there is no mechanism that checks the claim.
- There is no revocation. A recorded sign-off cannot be withdrawn.
- It fails open. When the daemon is unreachable the hook proceeds without a warning, so an absent warning is not evidence of an unchanged configuration.
Turn it on. It runs from the SessionStart hook once installed. The opt-out is the literal string CRUX_HOOK_CONFIG_AUDIT=off.
Where the detail lives. The eight files, the sign-off record and the hook: daemon/15 Coordination and cost. Tool contracts: api/12 MCP tool reference.
12.5 Ingress hardening
Status SHIPPED · Reached through
CORECRUXD_MAX_REQUEST_BODY_BYTES(16 MiB),CORECRUXD_MAX_INFLIGHT(1024),CORECRUXD_RATE_LIMIT_RPS(300),CORECRUXD_RATE_LIMIT_BURST(600),CORECRUXD_RATE_LIMIT_EXEMPT_CIDRS,CORECRUXD_TRUSTED_PROXY_CIDRS· Who it is for human
What it does. Four caps apply identically to the HTTP and MCP listeners, outside every other layer: a body-size cap, an in-flight concurrency cap that sheds load rather than queueing it, per-client-IP rate limiting with loopback exempt by default, and forwarded-header trust that stays off until an operator opts in. Every one of the four accepts 0 to disable it.
Why it works this way. These caps sit outside authentication because the requests you most want to stop are the ones that would exhaust you before authentication runs. Load shedding was chosen over queueing because a queue converts an overload into latency that propagates to every caller, whereas a shed request is a caller who can retry. Forwarded-header trust is off by default because trusting X-Forwarded-For from an untrusted network turns your rate limiter into a rate limiter for whatever IP the client felt like claiming. The 0-disables convention exists so an emergency rollback of a limit needs no redeploy.
What changes for you.
- As an operator: the defaults are real limits, not placeholders, and you will meet them under bulk load before you meet anything else.
- As an agent: a shed request returns promptly. It is a signal to back off, not a symptom of a broken daemon.
What it does not do.
- Ingress hardening is not authorisation. The layer that authorises each route against a declared contract defaults to
shadow, meaning a violation is logged and the request continues. OnlyCORECRUXD_ROUTE_AUTH=enforceblocks anything, and it is not the default. See daemon/07 before assuming route-level denial is in force. - Rate limiting is per daemon and per client IP. It is not per passport, not per tenant, and not shared between daemons.
- Loopback is exempt by default, so anything on the box is unlimited.
Where the detail lives. The four knobs, their defaults and the exemption syntax: daemon/05 Configuration reference. Where ingress sits in the request path: api/00 API index.
12.6 Health, readiness and the capacity guard
Status SHIPPED · Reached through
GET /healthz,GET /readyz,GET /v1/version· Who it is for human
What it does. /healthz is a pure liveness probe that cannot fail. /readyz is nine ordered gates, all of which must pass, and one of them is a data-directory capacity gate that takes an otherwise-healthy daemon out of rotation when free space drops below 10%. Four capacity thresholds and three metrics exist so you get warning before the gate flips.
Why it works this way. Splitting liveness from readiness is what lets an orchestrator do the right thing in two different situations: restart a dead process, and stop sending traffic to a live one that cannot serve correctly. The capacity gate is the opinionated part. Taking a healthy daemon out of rotation for a disk that is merely nearly full looks like an overreaction, and it is there because the failure it prevents, writing into a full data partition, is the expensive kind. The warning metrics exist so the gate is the last thing that happens rather than the first thing you learn.
What changes for you.
- As an operator: a red
/readyznames the failing gate. Read the gate name first; it is almost always the answer, and on shared CI runners it is almost always the capacity gate. - As an agent: nothing directly, though a daemon that never becomes ready presents to a client as connection failures with an empty error log.
What it does not do.
/healthzproves nothing about correctness. It proves the process is running.- The corruption gate never clears itself.
corruption_detectedstarts false and is only ever set true (health.rs:228); no runtime path sets it back. Clearing it means restarting the daemon, and a restart discards the finding rather than repairing anything, so a daemon that came back ready after a corruption report has not been fixed by coming back. - The capacity gate measures space available to the daemon's user, not raw free space. On a filesystem with reserved blocks the two differ.
Where the detail lives. All nine gates, their thresholds and their exact failure strings: daemon/09 Observability. What to do when one is red: operations/07 Daily operation.
12.7 Prometheus metrics
Status SHIPPED · Reached through
GET /metrics· Who it is for human
What it does. 142 metrics are registered across four sites onto one registry and served at /metrics in the usual text format. They cover request paths, retrieval, tool dispatch, truncation, capacity and valve state.
Why it works this way. One registry rather than several keeps a single scrape target and a single naming convention, which is what makes a dashboard portable between deployments. Truncation is instrumented as a counter rather than returned in tool responses, because the party who needs to know that responses are being trimmed is the operator watching a trend, not the agent handling one call.
What changes for you.
- As an operator: the useful early-warning signals, capacity ratios and truncation counters, exist before anything goes red.
- As an agent: nothing. Metrics are not a tool surface.
What it does not do.
/metricsis unauthenticated and public. It is not behind a scope, and no configuration puts it behind one. Restrict it at the network layer or accept that anyone who can reach the port can read it.- Its labels are not neutral. They expose shard ids, node topology, tenant-id hashes and valve state, which is a topology disclosure to anyone scraping it.
- Metrics are process-local and reset on restart. They are telemetry, never evidence.
Where the detail lives. The metric inventory and naming: daemon/09 Observability. Route classification for /metrics: route_auth.rs:79.
12.8 Logging, redaction and tracing
Status Redaction SHIPPED; OpenTelemetry export is FEATURE
otel, compiled out by default · Reached throughRUST_LOG,OTEL_EXPORTER_OTLP_ENDPOINT· Who it is for human
What it does. One redactor is applied at every sink boundary: stderr, JSON log output, the ops-fact path and the MCP parse-error echo, with a leak-canary test guarding it. A structured operations log carries x-request-id and traceparent so requests can be correlated across services without an OpenTelemetry deployment. OTLP export exists behind the compile-time otel feature, which is off in the default build.
Why it works this way. Redaction is applied at the sink rather than at the call site because there is exactly one place you can be sure covers everything, and it is the last one. The leak-canary test exists because a redactor without a test that tries to defeat it is a hope. Carrying traceparent in a structured log, rather than requiring OpenTelemetry, means correlation works in the deployment most people actually have, which is one daemon and a log file.
What changes for you.
- As an operator: you can correlate a user-visible failure to a daemon log line by request id without deploying a tracing stack.
- As an agent: parse errors echoed back over MCP are redacted, so a malformed payload does not reflect your secrets back to you.
What it does not do.
CORECRUX_LOG_FORMATis read by no code. The variable the daemon actually reads isLOG_FORMAT, unprefixed, and the wrong name appears in several shipped manifests, so JSON logging is silently not enabled wherever those are used. This is logged as a known defect, not a subtlety.- A mistyped OTLP endpoint produces no diagnostic. Export simply does not happen.
- Redaction covers known patterns at known sinks. It is a strong default, not a guarantee that no secret can ever reach a log.
Where the detail lives. Redaction scope and the structured log fields: daemon/09 Observability. The LOG_FORMAT naming defect: daemon/16 Known defects.
12.9 Admin actions, valves and the operator queue
Status SHIPPED · Reached through
POST /v1/admin/actions,GET /v1/admin/actions/{id},POST /v1/admin/valves,POST /v1/admin/restart,GET /v1/admin/control,GET /v1/admin/ops-log, Console System › Settings · Who it is for human
What it does. Operator actions are typed, queued, given an id and recorded in an audit trail, so an action is a thing you can look up afterwards rather than a command that happened. Alongside them sit emergency valves: pause ingest, pause compaction, throttle, read-only and an emergency brake. A restart action exists because the container restarts cleanly, so the console can drive it.
Why it works this way. Emergency levers are usually implemented as flags you set and a redeploy, which is precisely the wrong ergonomics for an emergency. Making them valves on a live process means the mitigation takes seconds. Making every action typed and queued with an id means the post-incident question, "who paused compaction and when", has an answer that is not somebody's memory. The queue is bounded on purpose; an unbounded operator queue under an incident is a second incident.
What changes for you.
- As an operator: mitigation and investigation use the same surface. You pull a valve, and the action is already recorded.
- As an agent: a daemon in read-only or braked state refuses writes with an explicit error, not a hang.
What it does not do.
verify-storeandscrubrequire a dataplane pool (admin.rs:508), and this edition hard-wires that pool toNone(main.rs:568). Both return "dataplane disabled" here, which is also why the corruption gate in §12.6 cannot be tripped in this edition.- A valve is a mitigation, not a fix. Pausing compaction stops growth from being reclaimed; it does not stop growth.
- The queue is bounded by
CORECRUXD_OPERATOR_ACTION_MAX_PENDINGand a timeout. Actions beyond the bound are refused, not deferred.
Where the detail lives. Every action type, valve and response shape: api/08 Admin and operations.
12.10 Update and drift posture
Status SHIPPED · Reached through MCP
update_status,GET /v1/versionupdate block, Console System › Settings · Who it is for human
What it does. The daemon reports whether its own checkout is ahead of, behind or diverged from its origin, and whether a newer release exists. "Should I upgrade" is therefore answerable from a version endpoint instead of from someone's memory of what was deployed.
Why it works this way. Deployed version drift is invisible until it bites, and the usual answer, a deployment manifest, describes what was intended rather than what is running. Asking the running process is the only reading that cannot be stale. Reporting diverged as a distinct state from ahead and behind matters because a diverged checkout is the one case where upgrading is not the right next action.
What changes for you.
- As an operator: an upgrade decision is a read, and the answer comes from the running binary.
- As an agent:
update_statusreturningbehindis a reason to pull current documentation before acting on deploy guidance.
What it does not do.
- The git posture is meaningless for container deployments, which have no checkout. Containers should set
CORECRUXD_UPDATE_CHECK_ENABLED=0. - Reporting
behinddoes not upgrade anything. It is a report. - The check is one of the daemon's few outbound behaviours. Air-gapped deployments should disable it deliberately rather than rely on it failing.
Where the detail lives. How to act on each posture: operations/10 Upgrade and rollback. The update block in the version response: daemon/09 Observability.
12.11 Live event stream
Status SHIPPED · Reached through
GET /v1/events/stream· Who it is for both
What it does. A server-sent event stream of daemon control events, so a console or a watcher reacts to what happened instead of polling for it.
Why it works this way. The console needs to reflect state changes promptly, and the two ways to do that are polling and pushing. Polling at a rate that feels live costs more than the events do, and polling at a rate that is cheap does not feel live. Server-sent events were chosen over WebSockets because the traffic is one-directional and SSE survives ordinary HTTP infrastructure without an upgrade negotiation.
What changes for you.
- As an operator: a watcher script is a
curlon a stream, not a polling loop with a sleep in it. - As an agent: an open stream is also how a stateless MCP client learns that the tool list changed.
What it does not do.
- It carries control events, not data. It is not a change feed for facts and it will not tell you a fact was written.
- There is no replay. A consumer that disconnects misses what it missed; reconnecting starts from now.
- It is not a durable queue. Nothing is buffered for an absent consumer.
Where the detail lives. Event shapes and the stream contract: api/03 Query and retrieval.
12.12 The operator console
Status FLAG
CORECRUXD_CONSOLE_ENABLED, default on · Reached through/console,/console-assets/*,/activate,/v1/console/*, all served by the daemon · Who it is for human
What it does. Behind CORECRUXD_CONSOLE_ENABLED, which is on by default, the daemon serves its own single-page console over 40 dedicated API routes: rings, work, memory, trust, meters, system, studio and explorer, including a review queue, chunk previews, tenant category management, onboarding, and read-only proxies to upstream CoreCrux and Engine deployments when those base URLs are configured.
Why it works this way. The console ships inside the daemon rather than as a separate deployment because a control plane you have to deploy separately is a control plane that is not there during the incident it was built for. One binary means the console version can never drift from the daemon version, which removes a whole category of confusing bug reports. The proxies are read-only by construction: the console is allowed to show you an upstream, and never to act on one.
What changes for you.
- As an operator: the console is available the moment the daemon is, with no extra service, no separate auth and no version matrix.
- As an agent: nothing. The console is a human surface, and the work it shows is the same work the tools show.
What it does not do.
- The console HTML routes are outside the route-authorisation matrix. They are unclassified, which means they are open under the default
shadowposture and would return 403 underenforce. Treat console exposure as a network decision. - The upstream proxies are unset by default and read-only when set. The console cannot mutate an upstream deployment.
- Turning the console off does not turn off the
/v1/console/*API routes' underlying capabilities; it removes the served application.
Where the detail lives. Every console route and its response shape: api/09 Console and surfaces. A guided tour of what each destination is for: operations/02 The console tour.
12.13 Context injection bundle
Status FLAG
CORECRUXD_CONTEXT_SURFACE, default off; the memoisation cache is FLAGCORECRUXD_ASSEMBLY_CACHE, default off · Reached throughGET /v1/context,POST /v1/context· Who it is for agent
What it does. Behind CORECRUXD_CONTEXT_SURFACE, default off, the daemon assembles a provider-agnostic versioned context_bundle/v1 of facts plus session state that any harness can inject at session start. CORECRUXD_ASSEMBLY_CACHE, also default off, memoises the assembly so a repeated request does not rebuild it.
Why it works this way. Not every harness speaks MCP. OpenAI-style loops, Codex, editor rule files and LangChain chains all want the same thing at session start, which is a blob of relevant context, and asking each of them to orchestrate several tool calls to produce it is asking for four subtly different implementations. One versioned bundle format makes the daemon the assembler. It is versioned because a context format that changes shape without a version breaks every harness at once, silently.
What changes for you.
- As an agent: a single request produces the context you would otherwise assemble from several, in a shape you can parse without knowing the daemon's internals.
- As an operator: it is off by default, so enabling it is a deliberate decision to expose an assembled context view.
What it does not do.
- It is off by default and the cache is a separate flag, also off. Enabling the surface does not enable memoisation.
- The bundle is facts and session state. It is not a corpus export and it does not carry document content.
- Caching trades freshness for cost. A memoised bundle can be behind the store, which matters if you enable it for a fast-moving session.
Where the detail lives. The bundle schema and both routes: api/02 Sessions and handoffs.
12.14 Storage hygiene
Status Ephemeral GC is FLAG
CORECRUXD_EPHEMERAL_GC, default off and read once at boot; observation retention is unset by default, meaning retain forever; the session TTL reaper is always on and runs every 60 seconds · Reached throughCORECRUXD_EPHEMERAL_GC,CORECRUXD_OBS_RETENTION_DAYS· Who it is for human
What it does. Three mechanisms bound growth without an operator having to go digging: a garbage collector for ephemeral reserved facts behind CORECRUXD_EPHEMERAL_GC (default off, and the value is read once at boot), observation retention by age via CORECRUXD_OBS_RETENTION_DAYS (unset by default, which means retain forever), and a session TTL reaper that is always on. What grows without bound anyway is documented rather than left to be discovered.
Why it works this way. Every one of these defaults to the conservative choice, which is "keep it", because the alternative default deletes somebody's evidence on their behalf. Retention is opt-in for the same reason a retention policy is a decision a person makes, not one a daemon guesses. The session reaper is the exception and is always on, because a session TTL is an explicit statement by the writer that the state expires. Publishing the list of things that grow without bound is the honest counterpart: hygiene here is partial by design, and you need to know which part is yours.
What changes for you.
- As an operator: disk growth is predictable if you read the unbounded list, and unpredictable if you assume the defaults clean up. They mostly do not.
- As an agent: session state you wrote with a TTL will actually go away, so a TTL is not advisory.
What it does not do.
- Nothing here reclaims disk from the fact journal. Facts leave disk only through soft-delete followed by compaction, which is a different capability entirely.
- The ephemeral GC flag is read once at boot. Changing it on a running daemon has no effect.
- Unset retention means retain forever, not retain sensibly. Observations accumulate until you set a value.
Turn it on. CORECRUXD_EPHEMERAL_GC=1 and a restart; CORECRUXD_OBS_RETENTION_DAYS=<days> for observation retention.
Where the detail lives. What each on-disk artefact is and what grows: daemon/06 The data directory. Where the reapers are spawned in the boot sequence: daemon/04 Startup and lifecycle.
Each explainer above carries its own grounding in its final block. Statuses in this chapter reconcile against the daemon feature inventory taken at 93b41a7d9735c4f7d1186c7a57b861d746273366; the complete status table for all 119 capabilities is chapter 15.

