Crux Daemon · 1. Architecture

1.0 In plain English

This chapter is the ground your extension stands on. Not how the daemon is built internally, which is the daemon reference set's job, but the parts you will actually touch: what it listens on, where it puts things, how it decides whether to let you do something, and what a failure looks like when it comes back to you.

The single most useful thing to absorb early is that extensions do not get storage of their own. There is no extensions table. An install is a fact write, a grant is a fact write, and a scheduler job's health is a fact write. The right picture is a shared filing cabinet in which you have a labelled drawer, not a database of your own alongside the daemon's. That is a smaller surface than you might expect and it is deliberate, because it means the daemon has one storage engine to keep correct instead of two.

The pay-off is immediate and worth knowing before you go looking for endpoints that do not exist. Anything you can read with GET /v1/facts you can use to inspect extension state, including your own, without a new endpoint being added for you. The cost is that fact-store semantics are now your semantics: an uninstall is a tombstone rather than a row removal, and a read takes the latest entry per key rather than the only entry. If you expect deleted things to vanish, they will not; they will be present and empty.

You will read this chapter once, before you write anything, and come back to §1.4 when a call is rejected and §1.5 when you need to know how to surface a failure to your own users in the shape the rest of the ecosystem expects. §1.6 is the fork in the road: four ways to extend, and the trust anchor each one requires.

The thing people get wrong is treating the three listeners as three services and trying to reach the MCP surface through the HTTP port. It is a separate listener on a separate port, and no amount of path guessing on 14800 will find it.

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