Developer guide
Crux Daemon · 1. Architecture
Crux Daemon developer guide
1. Architecture — what you are building against
1.1 One process, three listeners
corecruxd is a single Rust binary. It opens three sockets, all configured in load_config (crates/corecruxd/src/config.rs:793):
| Plane | Default | Host env | Port env | Notes |
|---|---|---|---|---|
| HTTP (axum) | 127.0.0.1:14800 | CORECRUXD_HTTP_HOST | CORECRUXD_HTTP_PORT | The surface this guide uses (config.rs:801) |
| MCP (HTTP transport) | 127.0.0.1:14801 | CORECRUXD_MCP_HOST | CORECRUXD_MCP_PORT | Toggled by CORECRUXD_MCP_ENABLED, default true (config.rs:817) |
| gRPC (tonic) | 127.0.0.1:4007 | CORECRUXD_GRPC_HOST | CORECRUXD_GRPC_PORT | (config.rs:812) |
Do not change the HTTP port default. It is a fixed contract for every client in the ecosystem.
1.2 The data directory
Resolution order (config.rs:834):
CORECRUXD_DATA_DIR(tilde-expanded)- file config
daemon.data_dir - file config
daemon.state_dir - default
../CoreCruxData/v1
Everything this guide touches lives under that root:
<data_dir>/
integrations/
index.json # installed pack index
packs/<pack_id>/<version>/manifest.json
grants/<passport_fpr>/<pack_id>.json
audit.jsonl # pack + extension audit trail
github/credentials.json # sealed PAT envelope
github/selected_repos.json
openai/credentials.json # sealed API-key envelope
extensions/
trusted-keys.json # operator Ed25519 keyring (both rails)
registry/index.json # cached curator-signed extension index
<extension_id>/extension.wasm # cached WASM module bytes
studio/
library/index.json # cached curator-signed Studio library index
Sources: crux-integrations/src/lib.rs:818, extension_registry.rs:76, http/extensions.rs:35, wasm_dispatcher.rs:61, integrations_github.rs:177, integrations_openai.rs:187, studio_library.rs:68.
Two things are deliberately not files:
- Installed extension records are facts under
__extension__::<id>keyrecord(extension_registry.rs:29). - Extension grants are facts under
__extension_grant__::<extension_id>::<passport_fpr>keyrecord(extension_grants.rs:37).
Both prefixes are on the daemon's born-private list, so those records are never push-eligible to a remote.
1.3 The fact store is the extension database
There is no separate extensions table. An extension install is a fact write, an extension grant is a fact write, and a scheduler job's health is a fact write. That has three practical consequences for you:
- Anything you can read with
GET /v1/factsyou can use to inspect extension state — no new endpoint needed. Scheduler status is deliberately surfaced this way (sync_scheduler.rs:24). - Deletion is a tombstone, not a row removal: an uninstall writes an empty-valued fact with confidence 0 (extension_registry.rs:231).
- Reads take the latest fact per
(entity, key)—dedup_latest(extension_registry.rs:208).
1.4 Authentication and scopes
CORECRUXD_AUTH_MODE has no default. The daemon refuses to start without it: CORECRUXD_AUTH_MODE must be set explicitly; see config.example.env (main.rs:307).
| Mode | Meaning |
|---|---|
off | require_http_scopes returns Ok(()) immediately — every scope check passes (auth.rs:1321) |
dev_scopes | Scopes come from the X-Corecrux-Scopes header, or from the bearer token parsed as a scope list (auth.rs:385) |
jwt_hs256 | HS256 JWT; secret in CORECRUXD_JWT_HS256_SECRET, issuer/audience via CORECRUXD_JWT_ISS / CORECRUXD_JWT_AUD (auth.rs:35) |
jwt_jwks | JWKS/OIDC; CORECRUXD_JWT_JWKS_URL, CORECRUXD_JWT_OIDC_DISCOVERY_URL, and friends (auth.rs:44) |
Mode strings are parsed leniently — dev, dev-scopes, jwt, jwks, oidc and several casings all resolve (auth.rs:58).
require_http_scopes(auth, headers, required) requires all of required (auth.rs:1320). Failure is a 403 whose body carries "code": "MISSING_SCOPE" and "missingScopes": [...] (auth.rs:1331). A request with no scopes at all in dev mode gets a 401 with "hint": "set X-Corecrux-Scopes or Authorization: Bearer <scopes>" (auth.rs:857).
Headers the daemon reads
| Header | Purpose |
|---|---|
Authorization: Bearer <token> | Token or, in dev_scopes, a literal scope list (auth.rs:373) |
X-Corecrux-Scopes | Scope list, comma- or whitespace-separated (auth.rs:363) |
X-Corecrux-Passport-Id | Acting passport. Trusted verbatim under off/dev_scopes; under JWT modes it must match the token's passport_id claim or you get 403 PASSPORT_HEADER_MISMATCH (auth.rs:1186) |
X-Corecrux-Tenant-Id | Tenant selector (auth.rs:1151) |
Scopes you will meet in this guide
There is no canonical scope enum in the codebase — scopes are string literals at each call site. The set the daemon actually uses:
admin:read, admin:write, events:read, events:write, exports:read, facts:read, facts:write, integrations:disable, integrations:grant, integrations:install, integrations:read, passport:impersonate, passport:read, provenance:write, query:read, receipts:read, replication:write, sessions:read, sessions:write, tenant:chunks:read, tenant:content:preview, tenant:metadata:read, tool:invoke:read.
The 14-item capability allowlist in a manifest is a different, smaller namespace that overlaps these strings. Do not conflate them — see chapter 3.
The route-auth middleware
Independent of the per-handler require_http_scopes calls, a middleware classifies routes by prefix. CORECRUXD_ROUTE_AUTH selects off, shadow (the default) or enforce (route_auth.rs:576).
| Prefix | Class | Any-of scopes |
|---|---|---|
/v1/studio/library/ POST | write | facts:write, admin:write (route_auth.rs:144) |
/v1/studio/ (everything else) | read | query:read, admin:read (route_auth.rs:157) |
/v1/extensions GET | read | admin:read, facts:read, query:read, sessions:read (route_auth.rs:386) |
/v1/extensions non-GET | write | admin:write, facts:write, integrations:install |
/v1/integrations/ | write | integrations:install, integrations:disable (route_auth.rs:326) |
In shadow the middleware logs; in enforce it rejects. The handler's own require_http_scopes always applies.
The Studio carve-out is worth noting as a pattern: the install route is the one /v1/studio/ route that mutates, so it is classified ahead of the read-class prefix rule to guarantee a read token can never authorise an install (route_auth.rs:144). If you add a mutating route under a read-class prefix, do the same.
1.5 Errors are RFC 7807
Every failure path in the routes this guide covers goes through problem_response (http/mod.rs:1788), which serialises ProblemDetails (corecrux-types/src/lib.rs:783) with Content-Type: application/problem+json (problem.rs:27).
{
"type": "https://errors.cuecrux.com/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "insufficient scopes",
"code": "MISSING_SCOPE",
"missingScopes": ["facts:write"]
}
type, title, status are always present. detail and instance are omitted when absent. Extension members (code, missingScopes) are flattened to the top level, not nested (corecrux-types/src/lib.rs:798).
Read detail — the daemon puts genuinely actionable text there, including which environment variable to flip (http/extensions.rs:210) and which CLI command to run (http/extensions.rs:240).
1.6 Four ways to extend the daemon
| Mechanism | Runs where | Trust anchor | Chapter |
|---|---|---|---|
| Integration pack (declarative) | Nowhere — a client reads the recipe | Manifest signature + operator grant | 2 |
| External tool | Your own HTTPS service | Manifest signature + registry sha + per-passport grant | 5 |
| WASM extension | In-process wasmtime sandbox | Module SHA-256 pin + grant + fuel/memory/epoch limits | 6 |
| File-watcher pack | In-process daemon runtime | Operator grant and an environment variable | 7 |
Three design rules run through all four, and they are worth internalising before you write anything:
- Install is not enable. Every path requires a second, explicit operator action naming exactly what the code may do.
- Artefacts are content-addressed and pinned. Nothing is fetched "latest"; the daemon verifies a hash the operator's trust chain endorsed before bytes are persisted.
- Capability surface is declared, then enforced. The manifest says what it wants; the grant says what it gets; the dispatcher enforces the intersection.
1.7 Reading the daemon's own route manifest
crates/corecruxd/src/http/openapi.rs holds a ROUTES table, and route_manifest_matches_router (tests/route_spec_drift.rs:757) asserts it is set-equal to the routes actually mounted on the router. If you need the authoritative list of endpoints, that table is it — it cannot drift without failing CI.
Ground truth
- crates/corecruxd/src/config.rs:793 —
load_config, ports, data dir - crates/corecruxd/src/auth.rs:24 —
AuthMode - crates/corecruxd/src/auth.rs:1320 —
require_http_scopes - crates/corecruxd/src/main.rs:307 — auth-mode-required abort
- crates/corecruxd/src/http/route_auth.rs:157 — studio route class
- crates/corecruxd/src/http/route_auth.rs:386 — extensions route class
- crates/corecruxd/src/http/mod.rs:1788 —
problem_response - crates/corecrux-types/src/lib.rs:783 —
ProblemDetails - crates/corecruxd/src/extension_registry.rs:29 — extension fact prefix
- crates/corecruxd/src/extension_grants.rs:37 — grant fact prefix
- crates/corecruxd/tests/route_spec_drift.rs:757 — route manifest parity test





