Crux Daemon · 4. Startup and lifecycle
If the daemon will not start, §4.3 has your answer: all 25 conditions that abort boot, each with its message, its line of source, and the exact fix. The rest of this chapter is the ordered boot sequence, the background tasks it spawns, and how it shuts down.
This chapter is reference. Read §4.3 first when something is broken; read §4.2 when you need to know what happens before what.
4.0 In plain English
Starting the daemon is not one action. It is a fixed, ordered sequence of 67 steps: read the environment, check the auth posture, open the data directory, take the locks, replay what is on disk back into memory, bind the listeners, then spawn the background tasks that keep running for as long as the process lives. The nearest familiar thing is an aircraft pre-flight checklist. The order is fixed, each item is checked, and if a check fails the aircraft does not take off.
That refusal is the point of the design, not a rough edge. Twenty-five distinct conditions abort boot rather than letting the daemon come up half-configured, and §4.3 lists every one with its exact message, the line of source that emits it, and the fix. A daemon that refuses to start tells you what is wrong in one line. A daemon that starts anyway and is quietly missing its auth mode tells you nothing until something much worse happens.
The moment you will need this chapter is the unhappy one: it will not start, you have a message and no context, and you want the answer rather than a tour. Go straight to §4.3 and match the message. The second moment is quieter but just as real: something is behaving oddly and you need to know whether a given piece of state is rebuilt at boot or carried over, which is §4.7, or whether some background task is meant to be running at all, which is §4.6.
The thing people get wrong is assuming that because the daemon started, it is fully configured. It is not the same claim. §4.4 lists the silent degradations: the conditions that leave a feature switched off, an optional dependency unreachable, or a subsystem inert, while boot completes and the process reports itself healthy. If you are debugging "why is this feature doing nothing", read §4.4 before you read anything else, because a green start is not evidence that the thing you configured is running.
4.1 Before main: arguments, and the flag that is silently ignored
#[tokio::main] async fn main() begins at main.rs:270. Its first action is CLI dispatch, deliberately short-circuited before load_config() "so they never start the daemon, read env, or touch the filesystem" (main.rs:272).
parse_cli_arg (main.rs:231) is a hand-rolled matcher on only the first argument. Clap was deliberately not pulled in, "keeping the env-only design intact".
| Argument | Action |
|---|---|
--version, -V, version | Print version_line() to stdout, exit 0 (main.rs:276) |
--help, -h, help | Print help_text() to stdout, exit 0 (main.rs:284) |
mcp-stdio | Run the stdio-to-HTTP MCP bridge and exit, not the daemon (main.rs:291) |
self (e.g. self update, self update --check) | Run the self-updater and exit (main.rs:294) |
| anything else, or no arguments | Start the daemon (main.rs:297) |
corecruxd accepts no runtime configuration flags, and silently ignores unrecognised ones. help_text() states it verbatim: "It takes no runtime configuration flags, all configuration is supplied via environment variables." A flag such as corecruxd --data-dir /x starts the daemon normally and ignores the flag. There is no unknown-argument error. If you thought you set something on the command line, you did not.
version_line() is corecruxd <CARGO_PKG_VERSION> (<CORECRUX_GIT_SHA|unknown>) (main.rs:242). A Docker build without --build-arg GIT_SHA=... self-reports (unknown), because the build context excludes .git so the git fallback cannot fire.
The mcp-stdio bridge reads CRUX_MCP_URL (default http://127.0.0.1:14801/mcp) and an optional CRUX_AGENT_TOKEN (main.rs:260).
4.2 The 67-step boot sequence, in order
Every step is in crates/corecruxd/src/main.rs. "Fatal" means the daemon exits non-zero before serving.
| # | Step | Location | Fatal? |
|---|---|---|---|
| 1 | load_config(): parse the YAML file if any, then env vars, into Config | main.rs:300; impl config.rs:793 | no |
| 2 | config.validate_embedding_selection() | main.rs:301; impl config.rs:609 | yes |
| 3 | Assert auth_mode_explicitly_set | main.rs:304 | yes |
| 4 | Assert the auth mode parsed, fail closed on a typo | main.rs:313 | yes |
| 5 | Authz::from_env(config.auth_mode), load mode-specific secrets (JWT secret or JWKS) | main.rs:320 | yes |
| 6 | Resolve the MCP agent-token registry | main.rs:326; impl main.rs:1983 | yes unless overridden |
| 7 | validate_network_auth_posture(...) | main.rs:333; impl main.rs:2022 | yes |
| 8 | validate_mcp_bind_posture(...) | main.rs:342; impl main.rs:2050 | yes |
| 9 | A 70-field tuple binding that keeps config fields live on CPU-only builds | main.rs:349 | no |
| 10 | init_tracing(&config.log_level), the first point at which anything is logged | main.rs:421; impl main.rs:2065 | no |
| 11 | Enterprise trust-root validation, only if enterprise_trust_root is set | main.rs:422 | yes |
| 12 | Content-manifest load and optional signature verify, only if content_manifest_path is set | main.rs:446 | yes |
| 13 | Install the panic hook, logs panic.payload and panic.location via tracing::error! | main.rs:457 | no |
| 14 | create_dir_all(state_dir), then create_dir_all(data_dir) | main.rs:475 | yes |
| 15 | acquire_lock(&data_dir), exclusive flock on <data_dir>/LOCK | main.rs:477; impl main.rs:2156 | yes |
| 16 | ControlHandle::load_or_init(<data_dir>/CONTROL.json) | main.rs:479 | yes |
| 17 | Build BuildInfo { version, commit } | main.rs:483 | no |
| 18 | Metrics::new(...); register redaction metrics; seed gauges | main.rs:488 | no |
| 19 | load_or_init_node_meta(<data_dir>/meta/node.json) → node_id | main.rs:500 | yes |
| 20 | LocalPassportKey::from_path(&config.passport_key_path) | main.rs:511 | yes |
| 21 | Mint the RCX free-local capability token (self-signed, 366-day validity) and build RcxRouter | main.rs:512 | no |
| 22 | Optionally spawn the anonymous passport claim (network call) if passport_claim_on_startup | main.rs:535 | no (background) |
| 23 | ShardMapStore::new(&data_dir).load_or_init(...) → RoutingTable::new(...) | main.rs:545 | yes |
| 24 | Initialise Readiness::default() | main.rs:562 | no |
| 25 | let dataplane_pool: Option<DataPlanePool> = None;, hard-coded | main.rs:564 | - |
| 26 | reconcile_control_checkpoint_with_evidence(...), seeds the control-evidence readiness fields | main.rs:567 | no |
| 27 | Create the shutdown broadcast channel (capacity 1) before any task spawns | main.rs:581 | no |
| 28 | spawn_routing_reloader(...), background task 1 | main.rs:588 | no |
| 29 | Measure data-dir space, build CapacityState | main.rs:600 | no |
| 30 | spawn_capacity_guard(...) if enabled, background task 2 | main.rs:620 | no |
| 31 | update::initial_status(&config) | main.rs:634 | no |
| 32 | spawn_shutdown_signal(...), SIGINT and SIGTERM handler | main.rs:636 | no |
| 33 | update::spawn_update_checker(...), background task 3 | main.rs:637 | no |
| 34 | Open CreditMeterStore at <data_dir>/credit-meter.jsonl if enabled | main.rs:644 | yes if enabled and the open fails |
| 35 | Open FactStore: persistent if fact_persistence_enabled, else in-memory | main.rs:652 | yes if the persistent open fails |
| 36 | cost::init_persistence(&data_dir), replays journalled cost reports; no-op unless the cost lens is on | main.rs:658 | no |
| 37 | Build ProjectionState; replay relations.jsonl into it | main.rs:663 | no (warn, start empty) |
| 38 | RepoWatchService::maybe_new(...) | main.rs:671 | no |
| 39 | Warn if sync_mutual_auth is on but no sync_peer_trust_root is set | main.rs:677 | no |
| 40 | Construct AppState: ~90 fields, including opening SessionStore, EntityStore, EdgeStore and WitnessProofStore, loading .ccxi retrieval indexes, and building SessionServices | main.rs:683 | yes for the store opens |
| 41 | Wire the shared EventBus into fact_store and session_store for SSE | main.rs:869 | no |
| 42 | repo_registry::fail_incomplete_scans(...), mark scans in flight at last shutdown as failed | main.rs:872 | no |
| 43 | repo_watch.start_existing_repos() | main.rs:888 | no |
| 44 | Dense-embedder selection: three-way precedence, see §4.5 | main.rs:892 | yes for a misconfigured delegation |
| 45 | Semantic near-duplicate threshold wiring | main.rs:981 | no (warns if no embedder) |
| 46 | Bootstrap seed: BootstrapSeeder.seed(), "always seed agent-facing documentation on startup (idempotent)" | main.rs:992 | no |
| 47 | Optional default-passport seeding (CORECRUXD_SEED_DEFAULT_PASSPORTS, default off) plus unconditional default-project seeding | main.rs:1004 | no |
| 48 | Lens-kind registration: crux_lens_features::bootstrap_kinds and agentgraph_kinds::bootstrap | main.rs:1037 | no (warn and continue) |
| 49 | spawn_ephemeral_gc(...): background task 4, gated by CORECRUXD_EPHEMERAL_GC, default off | main.rs:1054 | no |
| 50 | spawn_consolidation_scheduler(...): background task 5, gated by CORECRUXD_CONSOLIDATION_SCHEDULER, default off | main.rs:1061 | no |
| 51 | Near-duplicate router sweep (15s interval), background task 6, only if a dedup threshold is set | main.rs:1073 | no |
| 52 | Build the shared McpContext if config.mcp_enabled, shared with the HTTP OpenAI shim | main.rs:1113 | no |
| 53 | mcp_app = mcp_context.map(crux_mcp::server::router) | main.rs:1176 | no |
| 54 | Open CaseStore: passed to the router via an Extension layer, not via AppState | main.rs:1184 | yes if the persistent open fails |
| 55 | Build the HTTP router: http::router(...) → apply_ingress_limits(...) → .layer(TraceLayer::new_for_http()) | main.rs:1195 | no |
| 56 | Session TTL reaper (60s), background task 7 | main.rs:1199 | no |
| 57 | Observation retention (hourly, 30s initial delay), background task 8, only if CORECRUXD_OBS_RETENTION_DAYS parses above 0 | main.rs:1220 | no |
| 58 | Background sync loop: background task 9, only if sync is enabled with a non-empty remote URL; 5s initial delay | main.rs:1258 | no |
| 59 | Background witness submission: background task 10, only if witness_enabled; 5s initial delay | main.rs:1325 | no |
| 60 | Log the corecruxd starting banner: HTTP/gRPC/MCP addresses, data dir, commit level, append lane, tenant stamp mode | main.rs:1424 | no |
| 61 | Spawn the HTTP server task, serve_http(http_addr, app, rx, drain_cap) | main.rs:1442 | - |
| 62 | Emit the once-per-boot, consent-gated daemon_start usage ping on the blocking pool | main.rs:1448 | no |
| 63 | Spawn the gRPC server task, builds DataPlaneService and ExportService, calls grpc::serve(...) | main.rs:1459 | - |
| 64 | Register periodic integration jobs on the SyncScheduler: github-sync (registered unconditionally, self-skipping) and the vault watcher (double-gated), background task 11 | main.rs:1486 | no |
| 65 | Spawn the MCP server task if mcp_app is present, same ingress limits as the API plane | main.rs:1583 | - |
| 66 | tokio::try_join! on the HTTP, gRPC and MCP runners, main blocks here until shutdown | main.rs:1591 | - |
| 67 | drop(lock_file), release the LOCK flock; return Ok(()) | main.rs:1601 | - |
Two consequences of the ordering are worth internalising.
Nothing is logged before step 10. Steps 1 to 9 include the config parse and every auth-posture rail. If the daemon dies in that window you get a message on stderr from main returning an error, and no log line at all, including no indication of which config file was read, because config-file failures are silent (see chapter 5 §5.3).
The shutdown channel is created before any task spawns (main.rs:581). The comment explains why: the routing reloader and capacity guard "would otherwise outlive SIGTERM and hold the runtime open past graceful_shutdown_on_sigterm's 5s budget."
4.3 Everything that can refuse to start
The complete fail-closed list. Each aborts main with a non-zero exit and a message on stderr.
| # | Condition | Message or behaviour | Fix | Location |
|---|---|---|---|---|
| 1 | CORECRUXD_AUTH_MODE unset, and daemon.auth_mode absent from the YAML file | CORECRUXD_AUTH_MODE must be set explicitly; see config.example.env | Set CORECRUXD_AUTH_MODE to one of off, dev_scopes, jwt_hs256, jwt_jwks, or set daemon.auth_mode in the config file. Either satisfies it. | main.rs:304 |
| 2 | CORECRUXD_AUTH_MODE set to an unrecognised value | `unknown CORECRUXD_AUTH_MODE <bad>; valid values: off, dev_scopes, jwt_hs256, jwt_jwks` | Fix the spelling. Parsing is case-sensitive per arm: off/OFF work, Off does not. This is deliberate, "an unknown or typo'd auth mode must abort, never degrade to dev scopes". | main.rs:313 |
| 3 | Authz::from_env fails, e.g. jwt_hs256 selected but CORECRUXD_JWT_HS256_SECRET missing | Propagated as InvalidInput | Set the secret for the mode you chose. HS256 needs CORECRUXD_JWT_HS256_SECRET; JWKS needs one of CORECRUXD_JWT_JWKS_JSON, _PATH, _URL or CORECRUXD_JWT_OIDC_DISCOVERY_URL. | main.rs:320 |
| 4 | An MCP agent-token env var is present but fails the strength policy | <err>. Fix the agent token to enable MCP auth, or set CRUX_MCP_ALLOW_EMPTY_AGENT_REGISTRY=1 to run with no MCP auth (local dev/tests only). | Make each token 32 to 256 bytes from [A-Za-z0-9._~-]. For local dev only, set CRUX_MCP_ALLOW_EMPTY_AGENT_REGISTRY=1. | main.rs:326, main.rs:1996 |
| 5 | Auth mode is off or dev_scopes and HTTP or gRPC binds a non-loopback address | auth mode {:?} may not bind to non-loopback addresses (http=…, grpc=…) without CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1 | Either switch to jwt_hs256/jwt_jwks, or keep the binds on loopback and publish the port through a proxy, or accept the risk with CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1. | main.rs:2032 |
| 6 | A JWT auth mode and commit_level == ReplicatedCommit and CORECRUXD_REPLICATION_AUTH_BEARER unset or blank | ReplicatedCommit with JWT auth requires CORECRUXD_REPLICATION_AUTH_BEARER for follower replication | Set CORECRUXD_REPLICATION_AUTH_BEARER to a non-blank value, or leave CORECRUXD_COMMIT_LEVEL at its default local_commit. In this edition replicated commit cannot become ready anyway, see chapter 9 gate 3. | main.rs:2040 |
| 7 | MCP enabled and binding non-loopback and the agent registry is empty and no override | MCP may not bind to non-loopback address (<addr>) without CRUX_AGENT_TOKEN/CRUX_AGENT_TOKENS or CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1 | Set CRUX_AGENT_TOKEN or CRUX_AGENT_TOKENS, or bind MCP to loopback, or set CORECRUXD_MCP_ENABLED=0. | main.rs:2056 |
| 8 | CORECRUXD_COMPUTE_PROVIDER and CORECRUXD_EMBED_DELEGATE_URL both set | …are mutually exclusive to prevent delegation cycles | Choose one. A node either serves embedding work for peers or delegates it, not both. | config.rs:616 |
| 9 | Embedding delegation partially configured, _TOKEN or _DIMENSIONS set without _URL | CORECRUXD_EMBED_DELEGATE_URL is required when embedding delegation is configured | Set CORECRUXD_EMBED_DELEGATE_URL, or unset the other two. | config.rs:621 |
| 10 | CORECRUXD_EMBED_DELEGATE_URL and CORECRUXD_EMBEDDING_URL both set | …are mutually exclusive | Choose one embedding source. | config.rs:624 |
| 11 | Delegation configured without CORECRUXD_EMBED_DELEGATE_TOKEN | CORECRUXD_EMBED_DELEGATE_TOKEN is required when embedding delegation is configured | Set the token. This is re-checked a second time at main.rs:898. | config.rs:627 |
| 12 | Delegation configured with CORECRUXD_EMBED_DELEGATE_DIMENSIONS unset or 0 | …must be a positive integer… | Set it to the delegate's real vector dimensionality. An unparseable value deliberately parses to 0 so it fails closed rather than reading as unset. | config.rs:630 |
| 13 | Delegation configured with an empty CORECRUXD_EMBEDDING_MODEL | CORECRUXD_EMBEDDING_MODEL must be non-empty when embedding delegation is configured | Set the model name the delegate expects. | config.rs:635 |
| 14 | DelegatingEmbedder::new rejects the configuration | embedding delegation configuration is invalid: <err> | Read the wrapped error; it names the field. | main.rs:918 |
| 15 | Enterprise trust root present but invalid | invalid enterprise trust root: <comma-separated issue codes> | Fix enterprise.customer_id, backend_id, trust_root_kid or trusted_issuer_kids per the issue codes, or set enterprise.enabled: false. | main.rs:422 |
| 16 | The content manifest fails to load or verify | Propagated error from load_content_manifest | Fix or remove content.manifest_path; if the failure is a signature, either supply the right manifest or set content.verify_signatures: false. | main.rs:446 |
| 17 | create_dir_all on state_dir or data_dir fails, permissions, read-only filesystem | I/O error | Check ownership. The container image runs as UID 65532; a bind-mounted host directory must be chowned by you. | main.rs:475 |
| 18 | <data_dir>/LOCK is already flocked, another corecruxd is running on the same data dir | try_lock_exclusive error | Stop the other daemon. Note the lock is per resolved path: two daemons started from different working directories against the default relative data_dir will not collide, and will quietly diverge. | main.rs:477, main.rs:2156 |
| 19 | CONTROL.json exists but is not valid ControlV1 JSON | serde_json error | Repair the JSON, or move it aside, the daemon writes a fresh one with defaults. Moving it aside clears any operator valves, including read_only and emergency_brake. | main.rs:480, control.rs:138 |
| 20 | meta/node.json unreadable or unwritable | I/O error | Check permissions on <data_dir>/meta/. Deleting the file gives the daemon a new node_id, which stales shard-map entries. | main.rs:501 |
| 21 | The passport key at passport_key_path is unreadable or malformed | Error from LocalPassportKey::from_path | Restore the key from backup. Do not delete it to "fix" the error: the passport key encrypts stored integration credentials via a derived subkey, so losing it loses those credentials, and every receipt signed by the old key stops verifying. | main.rs:511 |
| 22 | The shard map fails to load or initialise, or RoutingTable::new rejects it | Error | Inspect <data_dir>/meta/routing/. current holds the active version; the shardmap.v*.json files are immutable and can be hand-restored. | main.rs:548 |
| 23 | Credit meter enabled but credit-meter.jsonl cannot be opened | I/O error | Check permissions, or set CORECRUXD_CREDIT_METER=0. | main.rs:644 |
| 24 | A fact, session, entity, edge or case store fails to open for persistence | I/O error | Check permissions and free space on the data dir. As a diagnostic only, CORECRUXD_FACT_PERSISTENCE=0 runs the fact and session stores in memory, all writes are then lost on restart. | main.rs:653, main.rs:804, main.rs:1187 |
| 25 | Any of the three listeners fails to bind, port in use, permission denied | TcpListener::bind error surfaced through the try_join! | Free the port or change it. Remember all three planes share a fate: a gRPC bind failure on 4007 kills the HTTP plane too. On Linux, ports below 1024 need a capability the non-root container user does not have. | main.rs:2217, main.rs:1591 |
4.4 The silent degradations: what does not stop it
These log a warning and continue. Each is a case where the daemon is running but is not doing what you configured, so they are worth an alert rule.
| Situation | What actually happens | Location |
|---|---|---|
| Durable session wiring fails | Falls back to ephemeral in-memory sessions | main.rs:836 |
relations.jsonl replay fails | Starts with an empty ProjectionState | main.rs:666 |
WitnessProofStore replay fails | Starts empty | main.rs:702 |
| Console or onboarding settings unreadable | Defaults | main.rs:848 |
| WASM engine init fails (feature builds only) | kind: wasm extension requests return 503; kind: external_tool extensions keep working | main.rs:158 |
| Lens-kind bootstrap errors | Warn and continue | main.rs:1042 |
CORECRUXD_SYNC_PEER_SIGNING_KEY is not valid 32-byte hex, or CORECRUXD_SYNC_PEER_TOKEN is not valid capability-token JSON | Sync peer auth is silently disabled and falls back to bearer | main.rs:174 |
sync_mutual_auth on without a valid CORECRUXD_SYNC_PEER_TRUST_ROOT | Warn: "tenant sync requests will fail closed" | main.rs:677 |
CORECRUXD_DENSE_MODEL=fastembed on a binary built without the dense-embed-model feature | Warns and uses LocalHashEmbedder | main.rs:971 |
CORECRUXD_SEMANTIC_DEDUP set with no dense embedder | Warn; dedup inactive | main.rs:983 |
| A malformed or unreadable YAML config file | No log line at all. Every value silently falls back to its default. See chapter 5 §5.3 | config.rs:709 |
An unparseable CORECRUXD_*_PORT or _HOST | Silently binds the default | config.rs:800 |
4.5 Dense-embedder selection
Three-way, first match wins (main.rs:892). The in-code comment states the intent: "Startup validation rejects an ambiguous or incomplete delegation configuration, so this branch never silently falls through to a different semantic space."
CORECRUXD_EMBED_DELEGATE_URLset →DelegatingEmbedder, authenticated daemon-to-daemon delegation. Requires_TOKENand_DIMENSIONS; fatal if missing (main.rs:897).- Else
CORECRUXD_EMBEDDING_URLset →EmbeddingClientagainst an Ollama-compatible service.dimensions: 0means auto-detect (main.rs:930). - Else
CORECRUXD_LOCAL_EMBEDDER(default on) → an in-process CPU embedder (main.rs:942). With thedense-embed-modelfeature andCORECRUXD_DENSE_MODEL=fastembedthis isFastEmbedEmbedder, which downloads its model into the data dir on first use; on init failure it falls back toLocalHashEmbedder. Otherwise it isLocalHashEmbedder, pure Rust, always available, offline. - If none apply, no embedder is configured and dense retrieval is inert.
4.6 The 14 background tasks
All subscribe to the same broadcast::Sender<()> shutdown channel created at main.rs:586.
| # | Task | Spawn gate | Interval | Location |
|---|---|---|---|---|
| 1 | Routing reloader | always | CORECRUXD_ROUTING_RELOAD_INTERVAL_MS, default 1000 | main.rs:588 |
| 2 | Capacity guard | CORECRUXD_CAPACITY_GUARD_ENABLED, default on | CORECRUXD_CAPACITY_GUARD_INTERVAL_SECS, default 30, floored at 10s | main.rs:620 |
| 3 | Update checker | inside spawn_update_checker | CORECRUXD_UPDATE_CHECK_INTERVAL_SECS, default 3600 | main.rs:637 |
| 4 | Ephemeral reserved-fact GC | CORECRUXD_EPHEMERAL_GC, default off, read once at boot, toggling requires a restart | hourly | main.rs:1054 |
| 5 | Consolidation review scheduler | CORECRUXD_CONSOLIDATION_SCHEDULER, default off | CORECRUXD_CONSOLIDATION_SCHEDULER_INTERVAL_SECS, default 3600 | main.rs:1061 |
| 6 | Near-duplicate router sweep | a semantic dedup threshold is set | 15s | main.rs:1080 |
| 7 | Session TTL reaper | always | 60s | main.rs:1199 |
| 8 | Observation retention | CORECRUXD_OBS_RETENTION_DAYS above 0 | hourly, after a 30s initial delay | main.rs:1223 |
| 9 | Background fact sync (pull then push) | sync enabled with a non-empty remote URL | CORECRUXD_SYNC_INTERVAL_SECS, default 300, after a 5s initial delay | main.rs:1258 |
| 10 | Witness submission drain | CORECRUXD_WITNESS_ENABLED | CORECRUXD_WITNESS_INTERVAL_SECS, default 300, floored at 1 | main.rs:1329 |
| 11 | SyncScheduler driver, hosts github-sync and the vault watcher | always spawned; jobs self-skip | github-sync: CORECRUXD_GITHUB_SYNC_INTERVAL_SECS, default 900. Vault watcher: its own interval | main.rs:1490 |
| 12 | Shutdown signal handler | always | - | main.rs:636 |
| 13 | Anonymous passport claim (one-shot, network) | CORECRUXD_PASSPORT_CLAIM_ON_STARTUP, default on | once | main.rs:535 |
| 14 | daemon_start usage ping (one-shot, blocking pool) | a three-way consent gate | once per boot | main.rs:1448 |
SyncScheduler job status is written as a fact under __sync__::<job_id> key status, readable through GET /v1/facts (main.rs:1487).
Task 13 is the one outbound call in a near-default configuration. CORECRUXD_PASSPORT_CLAIM_ON_STARTUP defaults to true (config.rs:890) and posts to https://passport.vaultcrux.com/v1/claim-anonymous (config.rs:16). It writes a passport.claimed marker under state_dir so it never retries. Set CORECRUXD_PASSPORT_CLAIM_ON_STARTUP=0 for an air-gapped or privacy-sensitive deployment. config.example.yaml:16 sets it to true, so an operator who copies the example gets outbound traffic.
4.7 Restart-recovery behaviour
Six things happen on a restart that are not obvious from the boot table.
| Behaviour | What it does | Location | ||
|---|---|---|---|---|
| Incomplete repo scans | Any scan still marked in progress from the previous run is marked failed with reason "daemon restarted before scan completed" | main.rs:872 | ||
.ccxi index reload | Sealed retrieval-index companions are rescanned from shards/*/segments/. Guarded on `config.build_ccxi \ | \ | config.local_ingest_enabled`: without this leg, local-ingest segments would not be served after a restart | main.rs:774 |
| Relations replay | relations.jsonl is replayed into ProjectionState | main.rs:663 | ||
| Cost-report replay | Journalled POST /v1/cost/report posts replay into the in-memory cost store, so attribution survives a restart. No-op unless the cost lens is on | main.rs:658 | ||
| Witness proofs replay | witness_proofs.jsonl is replayed | main.rs:700 | ||
| Repo watchers | repo_watch.start_existing_repos() | main.rs:888 |
One thing that does not survive a restart: device-authorization grants. Pending grants and refresh credentials live in a process-local registry, and the source states so (auth_device.rs:29).
4.8 Shutdown
spawn_shutdown_signal(main.rs:2168) selects onctrl_c()and, on Unix only,SIGTERM. SIGTERM registration failure is a deliberateexpect, justified in the source: "SIGTERM registration failure is fatal, daemon cannot shut down gracefully." On non-Unix only SIGINT is handled.- With the
otelfeature, the batch span exporter is flushed before the broadcast fires (main.rs:2186). tx.send(())broadcasts to every subscriber (main.rs:2189).- Each HTTP listener runs
axum::serve(...).with_graceful_shutdown(...)and arms a drain cap timer that starts only once draining begins (main.rs:2246). If the cap elapses first, the serve future is dropped and a warning is logged:graceful-shutdown drain cap exceeded; abandoning remaining connections to process exit. The cap comes fromCORECRUXD_SHUTDOWN_DRAIN_SECS, default 30;0means drain forever. drop(lock_file)releases theLOCKflock (main.rs:1601).
The drain cap bounds how long shutdown blocks, not how long connections live. Connection tasks already spawned by axum keep running until process exit closes their sockets (main.rs:2226).
Every accepted connection on both HTTP listeners gets TCP_NODELAY (main.rs:2238); failure is logged at trace only.
Sources
- crates/corecruxd/src/main.rs:270,
main - crates/corecruxd/src/main.rs:231,
parse_cli_arg - 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:2156,
acquire_lock - crates/corecruxd/src/main.rs:2168,
spawn_shutdown_signal - crates/corecruxd/src/config.rs:609,
validate_embedding_selection - crates/corecruxd/src/control.rs:138,
CONTROL.jsonload - crates/corecruxd/src/http/auth_device.rs:29, process-local device grants

