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):

PlaneDefaultHost envPort envNotes
HTTP (axum)127.0.0.1:14800CORECRUXD_HTTP_HOSTCORECRUXD_HTTP_PORTThe surface this guide uses (config.rs:801)
MCP (HTTP transport)127.0.0.1:14801CORECRUXD_MCP_HOSTCORECRUXD_MCP_PORTToggled by CORECRUXD_MCP_ENABLED, default true (config.rs:817)
gRPC (tonic)127.0.0.1:4007CORECRUXD_GRPC_HOSTCORECRUXD_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):

  1. CORECRUXD_DATA_DIR (tilde-expanded)
  2. file config daemon.data_dir
  3. file config daemon.state_dir
  4. 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:

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:

  1. Anything you can read with GET /v1/facts you can use to inspect extension state — no new endpoint needed. Scheduler status is deliberately surfaced this way (sync_scheduler.rs:24).
  2. Deletion is a tombstone, not a row removal: an uninstall writes an empty-valued fact with confidence 0 (extension_registry.rs:231).
  3. 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).

ModeMeaning
offrequire_http_scopes returns Ok(()) immediately — every scope check passes (auth.rs:1321)
dev_scopesScopes come from the X-Corecrux-Scopes header, or from the bearer token parsed as a scope list (auth.rs:385)
jwt_hs256HS256 JWT; secret in CORECRUXD_JWT_HS256_SECRET, issuer/audience via CORECRUXD_JWT_ISS / CORECRUXD_JWT_AUD (auth.rs:35)
jwt_jwksJWKS/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

HeaderPurpose
Authorization: Bearer <token>Token or, in dev_scopes, a literal scope list (auth.rs:373)
X-Corecrux-ScopesScope list, comma- or whitespace-separated (auth.rs:363)
X-Corecrux-Passport-IdActing 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-IdTenant 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).

PrefixClassAny-of scopes
/v1/studio/library/ POSTwritefacts:write, admin:write (route_auth.rs:144)
/v1/studio/ (everything else)readquery:read, admin:read (route_auth.rs:157)
/v1/extensions GETreadadmin:read, facts:read, query:read, sessions:read (route_auth.rs:386)
/v1/extensions non-GETwriteadmin:write, facts:write, integrations:install
/v1/integrations/writeintegrations: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

MechanismRuns whereTrust anchorChapter
Integration pack (declarative)Nowhere — a client reads the recipeManifest signature + operator grant2
External toolYour own HTTPS serviceManifest signature + registry sha + per-passport grant5
WASM extensionIn-process wasmtime sandboxModule SHA-256 pin + grant + fuel/memory/epoch limits6
File-watcher packIn-process daemon runtimeOperator grant and an environment variable7

Three design rules run through all four, and they are worth internalising before you write anything:

  1. Install is not enable. Every path requires a second, explicit operator action naming exactly what the code may do.
  2. 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.
  3. 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