HTTP API · 0. Conventions and how to read this reference
The Crux Daemon serves 303 unique HTTP paths over 347 method-and-path registrations, all but five of them under /v1. This chapter is the contract they share: where they live, how they authenticate, what an error looks like, what limits apply before your request reaches a handler, and which chapter documents which routes.
This is reference material in the Diátaxis sense. It enumerates; it does not teach. If you are building your first integration, start with the daemon developer guide and come back here to look things up.
Every route table in chapters 1 to 10 carries a file:line link into the public CueCrux/Crux repository. The audit these chapters were derived from resolved all 347 registrations against commit 93b41a7 with zero unresolved handlers.
0.0 In plain English
An HTTP API is a set of addresses you can send a request to and get a structured answer back. This set documents every one the daemon has. Most of what you will want to know is in the per-topic chapters that follow, one per area: facts, sessions, query, receipts, identity, work, extensions, admin. This chapter is the part that is true of all of them at once.
That is worth having as its own chapter because a good API is boring in a specific way. The same base path prefixes every route. Errors always come back in the same shape, so your client can have one error handler rather than 303 of them. Authentication works the same way on every route that needs it. The same middleware runs before every request in the same order. Once you have read this once, every chapter that follows is just a list of addresses, because the rules never change under you.
You will read this chapter properly once, when you write your first client, and then return to two parts of it. §0.6 when something failed and you need to know how to read the error body, which is RFC 7807 and carries a machine-readable type as well as a human message. And §0.8 when your client starts getting rejected under load, because ingress limits apply before your request ever reaches a handler and the rejection is not coming from the route you called.
The thing people get wrong is assuming an error is an error. The status code alone will mislead you here: a 429 with a Retry-After header is a rate limiter telling you to slow down and try again, and a 503 from the load-shed gate is the daemon protecting itself and is also worth retrying, while a 403 naming a missing scope will fail identically for ever until you change the credential. §0.6 and §0.8 together are what let your client tell those apart, and getting it wrong produces either a retry storm or a client that gives up on a transient condition.
0.1 Base URL and ports
corecruxd is one binary with three listeners, all resolved in load_config (config.rs:793).
| Plane | Default bind | Host env | Port env | Covered by |
|---|---|---|---|---|
| HTTP (axum) | 127.0.0.1:14800 | CORECRUXD_HTTP_HOST | CORECRUXD_HTTP_PORT | This reference (config.rs:801) |
| MCP (HTTP transport) | 127.0.0.1:14801 | CORECRUXD_MCP_HOST | CORECRUXD_MCP_PORT | The MCP tool reference, not this set (config.rs:817) |
| gRPC (tonic) | 127.0.0.1:4007 | CORECRUXD_GRPC_HOST | CORECRUXD_GRPC_PORT | Chapter 10 (config.rs:812) |
The HTTP port default is a fixed contract for every client in the ecosystem. Do not change it.
0.2 Versioning: /v1, and the five paths outside it
There is exactly one API version prefix: /v1. There is no /v2, no /v0, and no unversioned /api. 301 of the 303 paths are under /v1.
Five paths sit outside it:
| Path | Why it is outside /v1 | Chapter |
|---|---|---|
GET /healthz | Ops probe. Liveness contract predates the API version. | 8 |
GET /readyz | Ops probe. Returns 503 when the node is not ready. | 8 |
GET /metrics | Prometheus scrape target. | 8 |
POST /session | Legacy invocation rail. openapi.rs:130 calls it a candidate for a future /v1 migration. | 2 |
POST /invocation/verify | Same legacy rail, same migration note. | 2 |
Both legacy rails are deliberately excluded from the daemon's own route manifest (openapi.rs:130) but are still classified by the route-auth layer, so they do not become unreachable under enforce.
Six further paths, the console SPA and its assets, are not API surface at all. They are listed in chapter 9 with their exclusion stated plainly.
0.3 Authentication
CORECRUXD_AUTH_MODE has no default. The daemon refuses to start without it (main.rs:307).
| Mode | Behaviour |
|---|---|
off | require_http_scopes returns Ok(()) immediately. Every scope check passes, on every route (auth.rs:1320). |
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 and audience via CORECRUXD_JWT_ISS and CORECRUXD_JWT_AUD (auth.rs:35). |
jwt_jwks | JWKS or OIDC. CORECRUXD_JWT_JWKS_URL, CORECRUXD_JWT_OIDC_DISCOVERY_URL and friends (auth.rs:44). |
Mode strings parse leniently: dev, dev-scopes, jwt, jwks, oidc and several casings all resolve (auth.rs:58).
off disables every scope check in this reference. Under off the auth columns in chapters 1 to 9 describe nothing that is enforced. That is the intended dev-loop posture and it is the wrong posture for anything reachable from a network you do not own.
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 and dev_scopes; under JWT modes it must match the token's passport_id claim or the response is 403 PASSPORT_HEADER_MISMATCH (auth.rs:1186). Validated at ingress: 1 to 128 ASCII characters from [A-Za-z0-9._:-] or 400 (ingress.rs:47). |
X-Corecrux-Tenant-Id | Tenant selector (auth.rs:1151) |
The complete scope list
There is no canonical scope enum in the codebase. Scopes are string literals at each call site. The full set the daemon uses:
| Scope | Used for |
|---|---|
admin:read | Every admin read, and a universal fallback on most read classes |
admin:write | Every admin write, and a universal fallback on most write classes |
compute:embed | POST /v1/compute/embed only (compute.rs:29) |
events:read | gRPC ReadStream only. Not an HTTP scope (grpc.rs:777) |
events:write | gRPC AppendBatch only (grpc.rs:764) |
exports:read | Replay exports under /v1/replay/exports/*, incident export |
facts:read | Substrate reads, features lens, orchestrators, punchcards, activity, tenant sync |
facts:write | Every fact-store write |
integrations:disable | Disconnect and disable routes on the integrations planes |
integrations:grant | Console integration grant |
integrations:install | Integration connect, install, and most non-GET routes on the wide write class |
integrations:read | Integration reads |
passport:impersonate | Acting as another passport |
passport:read | Passport reads |
provenance:write | The BYOK provenance gateway (provenance.rs:622) |
query:read | Retrieval, projections, studio reads, ops and bootstrap |
receipts:read | Receipt bodies, signatures, verification, listing |
replay:answer | Answer replay under /v1/replay/answers/* (replay.rs:205) |
replication:write | POST /v1/internal/replication/segments only |
sessions:read | Session plan, principal resolve, agent usage |
sessions:write | Session state, archive, observations, mediation receipts |
tenant:chunks:read | Console chunk listing for a tenant |
tenant:content:preview | Console chunk content preview |
tenant:metadata:read | Tenant metadata |
tool:invoke:read | Tool invocation reads |
Ten further strings are per-surface Pro capabilities, not general scopes. They gate the Agent Workbench and are listed in §0.12.
The 14-item capability allowlist in an integration manifest is a different, smaller namespace that overlaps these strings. Do not conflate them.
0.4 How to read the auth column
Each route table has an Auth column of the form any-of a, b · Class, where the first part is the check the handler itself performs and Class is the route-auth contract the middleware applies (§0.5).
The daemon has two scope combinators:
require_http_scopes(auth, headers, required)requires all ofrequired(auth.rs:1320).require_http_any_scope(auth, headers, any_of)requires any one ofany_of(auth.rs:1340).
Across the whole HTTP surface only two handler checks require more than one scope at once:
| All-of pair | Where |
|---|---|
admin:read and facts:write | Every mutating extension route (extensions.rs:126), dossier publish (dossier.rs:73), RCX emit (rcx_publish.rs:56), sharing backfill (admin.rs:2559), storybook generate (storybook.rs:49), Studio library install (studio_library.rs:293) |
exports:read and receipts:read | The three subject exports (receipts.rs:835) |
Everywhere else a multi-scope cell is any-of. The tables say all-of explicitly where it applies.
Tenant binding
Many checks are tenant-bound: require_http_any_scope_for_tenant and require_http_scopes_for_tenant additionally require that the token's tenant claim covers the tenant_id in the request (auth.rs:1359). One carve-out: if the scope that matched starts with admin:, the tenant check is skipped entirely (auth.rs:1380). An admin:read token is therefore cross-tenant by construction. Treat admin:* as the daemon's root credential, not as a convenience.
Request-field notation
| Marker | Means |
|---|---|
req | Required. No serde default; omitting it fails deserialisation. |
opt | Option<T>. Absent is allowed and meaningful. |
dflt | Carries #[serde(default)]. Absent falls back to the type's default. |
dflt <fn> | Carries a named default function; the name is given. |
0.5 Route authorization
Independent of every handler check, one middleware classifies routes by method and path template and applies a deny-by-default contract (route_auth.rs:75). Its mode is read from CORECRUXD_ROUTE_AUTH once at router build, never per request (route_auth.rs:576).
| Value | Behaviour | Status |
|---|---|---|
off | Pass-through. | SHIPPED |
shadow | The default. Unset, empty or unrecognised all resolve to shadow. It evaluates the contract, emits marker = "route_auth_shadow_mismatch" on a would-deny, and continues. | SHIPPED |
enforce | Public routes pass with no auth. Every other route requires any-of its contract scopes. An unclassified route, or a request axum could not match to a template, fails closed with 403. | SHIPPED |
classify_route() covers all 347 registrations with zero unclassified routes. enforce can be switched on today without any route becoming unreachable. That is a verified property of the current commit, not a design intention.
Route classes
| Class | Accepted scopes (any-of) |
|---|---|
Public | none: /healthz, /readyz, /metrics, /session, /invocation/verify, /v1/openapi.json, /v1/version, /v1/witness/smoke, /v1/sync/handshake/nonce, and all of /v1/auth/* (route_auth.rs:77) |
InternalReplication | replication:write (route_auth.rs:98) |
AdminRead | admin:read (console reads also accept tenant:chunks:read, tenant:content:preview) |
AdminWrite | admin:write (console writes also accept facts:write, integrations:install, integrations:grant, integrations:disable) |
Read | varies by prefix, see the map below |
Write | varies by prefix, see the map below |
FeatureGated | varies by prefix; carries a documented feature_gate label that the middleware itself never reads. Flag gating stays in the handler (route_auth.rs:44). |
Prefix to scope map
| Prefix | GET accepts | Non-GET accepts |
|---|---|---|
/v1/query/*, /v1/projections/entity/* | query:read, admin:read | same; these reads are POSTs |
/v1/studio/* | query:read, admin:read | POST /v1/studio/library/* needs facts:write, admin:write |
/v1/receipts/*, /v1/replay/*, /v1/events/*, /v1/observations/*, /v1/ops/*, /v1/bootstrap/*, /v1/audit/* | query:read, receipts:read, exports:read, admin:read | - |
/v1/cases/retrieve | query:read, admin:read | - |
/v1/cases (record) | - | facts:write, admin:write |
/v1/facts*, /v1/sessions/*, /v1/entities*, /v1/edges*, /v1/kinds* | query:read, admin:read | facts:write, sessions:write, admin:write |
/v1/features/capabilities* | facts:read, admin:read | facts:write, admin:write |
/v1/sync/tenants/* | facts:read | facts:write |
/v1/identity/candidates* | admin:read, admin:write | admin:write |
/v1/memory/import, /v1/result-envelope/import, /v1/identity/links, /v1/append | facts:write, admin:write, admin:read | facts:write, admin:write |
/v1/console/* | admin:read, tenant:chunks:read, tenant:content:preview | admin:write, facts:write, integrations:install, integrations:grant, integrations:disable |
/v1/integrations/* | admin:read | integrations:install, integrations:disable |
/v1/work*, /v1/status-feed, /v1/projects*, /v1/rcx/publish/*, /v1/workspace/*, /v1/mcp/tools*, /v1/engrams*, /v1/extensions*, /v1/passports*, /v1/principal/*, /v1/policy/*, /v1/relations*, /v1/agents/*, /v1/cost/*, /v1/cloud/*, /v1/actions/*, /v1/workbench/*, /v1/mediation/*, /v1/memory/* | admin:read, facts:read, query:read, sessions:read | admin:write, facts:write, integrations:install |
/v1/gpu1/* | query:read, admin:read | same |
/v1/compute/embed | - | compute:embed |
/v1/context* | query:read, admin:read | same |
/v1/provenance/* | - | provenance:write, admin:write |
/v1/openai/* | query:read, admin:read, admin:write | same |
/v1/quota* | query:read, admin:read | - |
/v1/credits/* | - | admin:write |
/v1/incidents* | query:read, exports:read, admin:read | facts:write, admin:write |
/v1/legal-holds* | - | admin:write |
/v1/coord/* | admin:read, sessions:read | admin:write, sessions:write |
/v1/observe/sessions/* | query:read, admin:read | facts:write, admin:write |
/v1/orchestrators*, /v1/punchcards* | facts:read, admin:read | facts:write, admin:write |
/v1/activity* | facts:read, admin:read | facts:write, admin:write |
Two routes with no handler scope check
The audit found two handlers that perform no scope check of their own and rely entirely on the route-auth middleware. Because that middleware defaults to shadow, on a default install both are reachable without any credential.
| Route | What the handler takes | What it exposes |
|---|---|---|
POST /v1/audit/bundle/verify | post_audit_bundle_verify(body: Bytes): no State, no HeaderMap, no scope check (audit_verify.rs:43) | An unauthenticated 8 MiB upload that is then decompressed. A second decompressed-size cap inside the verifier returns 413 bundle_too_large, but the upload and the decompression both happen first. |
GET /v1/console/onboarding | State<AppState> only, no scope check (console.rs:58) | The daemon's running auth mode, chosen auth mode, whether the bind is loopback, and allow_insecure_dev_auth_bind. That is the auth posture of the node, unauthenticated. |
Both routes are correctly contracted, Read and AdminRead respectively. The gap exists only because CORECRUXD_ROUTE_AUTH defaults to shadow rather than enforce. Setting CORECRUXD_ROUTE_AUTH=enforce closes both. If your daemon is reachable from anything you do not control, set it.
One documentation defect inside the code
route_auth.rs:528 labels the orchestrators and punchcards feature gate as CORECRUXD_AGENTGRAPH (route_auth.rs:528). That environment variable does not exist anywhere else in the codebase. The handlers read CORECRUXD_ORCHESTRATORS (agentgraph_kinds.rs:144) and CORECRUXD_PUNCHCARD (agentgraph_kinds.rs:161). The label is inert, the middleware never reads it, but setting CORECRUXD_AGENTGRAPH does nothing. Chapter 6 documents the real flags.
Sync mutual-auth deferral
When CORECRUXD_SYNC_MUTUAL_AUTH=1 (default off, mod.rs:212), route-auth skips its scope check for exactly five templates, because they are authorized cryptographically by an Ed25519 peer handshake inside the handlers:
/v1/sync/tenants/{tenantId}/manifest
/v1/sync/tenants/{tenantId}/collections/{collection}
/v1/sync/tenants/{tenantId}/promotions/preview
/v1/sync/tenants/{tenantId}/promotions/confirm
/v1/sync/tenants/{tenantId}/offboard
The handshake nonce TTL is 120 seconds (mod.rs:154). CORECRUXD_SYNC_DELEGATION_ENFORCE defaults off, and while off, recipient-bound v1.1 delegation tokens are rejected fail-closed (mod.rs:218).
There is no loopback-only route and no ops token
No HTTP route in this repository is restricted to loopback callers. Loopback matters in exactly two places: onboarding uses http_bind_loopback to decide whether auth_mode = off is permitted (mod.rs:421), and main.rs emits a startup warning when auth_mode = off and both HTTP and gRPC bind to loopback (main.rs:2031). The nearest thing to an ops credential is the admin:read and admin:write pair. There is no separate ops token.
0.6 The error shape
Every failure path goes through problem_response (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",
"missingAnyScope": ["facts:write", "admin:write"]
}
type, title and status are always present. detail and instance are omitted when absent. Extension members such as code, missingScopes and missingAnyScope 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.
Scope failures
| Combinator | code | Extension member |
|---|---|---|
require_http_scopes (all-of) | MISSING_SCOPE | missingScopes, the subset you are missing (auth.rs:1331) |
require_http_any_scope (any-of) | MISSING_SCOPE | missingAnyScope, the full accepted set (auth.rs:1352) |
A request with no scopes at all in dev mode gets 401 with "hint": "set X-Corecrux-Scopes or Authorization: Bearer <scopes>" (auth.rs:857).
Ingress-layer problem types
These are produced before any handler runs, so no route-specific detail is available.
| Status | type | Title | Extra headers |
|---|---|---|---|
400 | https://errors.cuecrux.com/invalid-passport-header | Invalid X-Corecrux-Passport-Id | , (ingress.rs:210) |
413 | https://errors.cuecrux.com/payload-too-large | Payload Too Large | , (ingress.rs:549) |
429 | https://errors.cuecrux.com/rate-limited | Too Many Requests | Retry-After in seconds (ingress.rs:188) |
503 | https://errors.cuecrux.com/overloaded | Service Overloaded | Retry-After: 1 (ingress.rs:508) |
500 | https://errors.cuecrux.com/internal | Internal Server Error | , (ingress.rs:521) |
A 408 REQUEST_TIMEOUT comes from the router-wide timeout layer (§0.7). A 500 from a handler panic is produced by CatchPanicLayer rather than dropping the connection (mod.rs:1561).
The daemon also publishes a storage-and-transport error-code catalogue, IO_READ_FAILED, SEGMENT_CORRUPT, SHARD_NOT_OWNER, EPOCH_MISMATCH, BACKPRESSURE, TIMEOUT, INTERNAL and others, with their HTTP status, gRPC status and retryability, at docs/error-catalogue.md.
0.7 The middleware stack, in request order
Axum's Router::layer wraps outside-in as calls accumulate: layers added later run earlier. The effective request-path order is the reverse of the source order.
| Order | Layer | Source | Behaviour |
|---|---|---|---|
| 1 | request_id_middleware | mod.rs:1564 | Reads X-Request-Id and traceparent; mints a request id when absent; sets x-request-id and traceparent on the response, plus x-trace-id under the otel feature. Emits the http_control structured op-log line with took_ms and status. |
| 2 | traceparent_middleware | mod.rs:1563 | No-op unless built with --features otel. With otel, extracts W3C trace context and sets it as the current span parent. |
| 3 | TimeoutLayer | mod.rs:1562 | 30-second router-wide request timeout, then 408 REQUEST_TIMEOUT. |
| 4 | CatchPanicLayer | mod.rs:1561 | Converts a handler panic into an RFC-7807 500 instead of dropping the connection. |
| 5 | console static assets | mod.rs:1560 | Merged after .with_state(state), so console asset routes sit outside every layer below this line. |
| 6 | route_auth_middleware | mod.rs:1554 | The deny-by-default contract of §0.5, evaluated over the axum MatchedPath template. |
| 7 | quota_middleware | mod.rs:1546 | Per-passport, per-surface token bucket. Pass-through unless CORECRUXD_QUOTA=1 and the path prefix matches CORECRUXD_QUOTA_HOSTED_SURFACES. On deny, 429 plus quota headers, before any metered execution or credit spend. |
| 8 | presence_middleware | mod.rs:1543 | If X-Corecrux-Passport-Id is present, spawns a background presence touch. Never blocks; skips the lock entirely when the header is absent. Feeds GET /v1/passports/presence. |
| 9 | Extension(case_store) | mod.rs:1542 | Injects the shared case store for /v1/cases*. |
Route-auth sits outside quota and presence deliberately, so a would-deny short-circuits before any accounting or presence write.
0.8 Ingress limits
apply_ingress_limits wraps both the API router and the MCP router, outside everything in §0.7. Documented order (ingress.rs:19):
passport-header validator → rate limiter → load-shed/concurrency gate
→ inflight gauge → 413 decorator → body limit → routes
A flood is 429'd before it can occupy an in-flight slot, and shedding happens before any body byte is read.
| Control | Env var | Default | Effect when exceeded |
|---|---|---|---|
| Request body size | CORECRUXD_MAX_REQUEST_BODY_BYTES | 16 MiB (config.rs:141) | 413 problem+json. 0 disables. |
| In-flight concurrency | CORECRUXD_MAX_INFLIGHT | 1024 (config.rs:147) | 503 load-shed with Retry-After: 1. 0 disables. Gauge corecrux_http_inflight. |
| Per-client-IP rate | CORECRUXD_RATE_LIMIT_RPS | 300 req/s (config.rs:150) | 429 with Retry-After. 0 disables. Counter corecrux_http_rate_limited_total. |
| Burst capacity | CORECRUXD_RATE_LIMIT_BURST | 600 (config.rs:152) | Clamped to at least rate_limit_rps. |
| Rate-limit exemptions | CORECRUXD_RATE_LIMIT_EXEMPT_CIDRS | 127.0.0.0/8, ::1/128 (config.rs:155) | Loopback is exempt by default, so the console SPA and local agents are never limited. |
| Trusted proxies | CORECRUXD_TRUSTED_PROXY_CIDRS | empty (config.rs:158) | Forwarded and X-Forwarded-For are ignored for rate-limit keying until an operator opts in. Behind a reverse proxy with this unset, every request keys to the proxy's IP. |
| Shutdown drain | CORECRUXD_SHUTDOWN_DRAIN_SECS | 30 s (config.rs:144) | Matches the router timeout, so nothing completable is cut short. |
Per-route body limits
Four routes get a raised limit at the ingress layer (ingress.rs:54):
| Route | Limit |
|---|---|
POST /v1/append | 64 MiB |
POST /v1/admin/append | 64 MiB |
POST /v1/memory/import | 64 MiB |
POST /v1/result-envelope/import | 64 MiB |
Three route groups get a lowered or explicit limit inside the router:
| Route | Limit | Source |
|---|---|---|
POST /v1/audit/bundle/verify | 8 MiB compressed, plus an independent decompressed-size cap that returns 413 bundle_too_large | audit_verify.rs:41 |
POST /v1/compute/embed | 512 KiB | compute.rs:25 |
POST /v1/provenance/sign, /verify, /verify-record | 16 MiB each | provenance.rs:68 |
Everything else inherits the global 16 MiB body cap and the 30-second router timeout. No other route sets a per-route timeout.
Rate limiting that is not middleware
- Community-extension dispatch has a process-wide sliding 60-second window keyed by extension id and passport fingerprint, capped per grant or by a daemon default. It applies to
POST /v1/extensions/{id}/tools/{tool_name}/invoke(mod.rs:386). - The provenance gateway applies a per-handler rate limit inside its common pre-handler gate, in the order flag, then refuse-spoofable-auth, then scope, then rate limit (provenance.rs:624).
0.9 Correlation headers
| Header | Direction | Behaviour |
|---|---|---|
X-Request-Id | Request | Honoured if present; otherwise the daemon mints one. |
traceparent | Request | W3C trace context. Parsed for correlation always; used to parent a span only under the otel build feature. |
x-request-id | Response | Always set. |
traceparent | Response | Always set. |
x-trace-id | Response | Set only when built with --features otel, which is off by default. |
Every request also produces one http_control structured op-log line carrying the correlation ids, took_ms and the response status (mod.rs:1619). At 3am, that line and x-request-id are how you tie a client-side failure to a daemon-side record.
0.10 Feature flags and their defaults
A route's existence can depend on a build feature or an environment variable. This table is the full index; each chapter repeats the flag in the affected rows.
Compile-time (Cargo features)
| Feature | Default | Effect |
|---|---|---|
hosted-surfaces | off in Community Edition | Compiles in /v1/cloud/access-contract and the seven /v1/gpu1/* routes (mod.rs:1528). Routes and handlers are absent from the default CE binary. |
wasm-extensions | off | Without it, kind: wasm extension dispatch returns 501 (mod.rs:393). |
otel | off | Enables trace-context propagation and the x-trace-id response header. |
Runtime flags that mount or unmount routes
| Env var | Default | Routes | Behaviour when off |
|---|---|---|---|
CORECRUXD_FEATURE_PROVENANCE_API | off (provenance.rs:47) | /v1/provenance/{sign,verify,verify-record} | The routes are not mounted at all (mod.rs:1496), so a 404 is returned before any body, including key material, is read. This is the only group in the daemon that unmounts rather than refusing from inside a handler. |
CORECRUXD_CONSOLE_ENABLED | on (config.rs:830) | 6 static console asset routes | Empty router, so 404. |
Runtime flags checked inside handlers
| Env var | Default | Plane | Behaviour when off |
|---|---|---|---|
CORECRUXD_COORD | on (config.rs:1336) | /v1/coord/* | 404 |
CORECRUXD_LOCAL_INGEST | on (config.rs:1344) | /v1/local/ingest | 404 |
CORECRUXD_MCP_ENABLED | on (config.rs:827) | MCP listener, and the OpenAI shim's tool source | MCP server not started |
CORECRUXD_INTEGRATIONS_ENABLED | on (config.rs:1377) | /v1/integrations/* | Gated |
CORECRUXD_CONTEXT_SURFACE | off (config.rs:1342) | /v1/context | 404 |
CORECRUXD_AUTO_CAPTURE | off (config.rs:1343) | /v1/memory/extract, /v1/memory/candidates* | 404 |
CORECRUXD_STREAM_RECEIPTS | off (config.rs:1345) | /v1/mediation/receipts stream and context drafts | Draft rejected by the legacy parse |
CORECRUXD_FEATURE_USAGE_RECEIPTS | off (config.rs:1346) | /v1/mediation/receipts usage_ping draft | Draft rejected |
CORECRUXD_HANDOFF_OBSERVATIONS | off (config.rs:1347) | /v1/workbench/handoff-v2 | No observation written |
CORECRUXD_QUOTA | off (config.rs:1362) | GET /v1/quota and the quota middleware | Route 404; middleware pass-through |
CORECRUXD_QUOTA_HOSTED_SURFACES | empty (config.rs:1363) | Quota middleware scope | Empty means every surface counts as local compute, so unlimited |
CORECRUXD_CREDIT_METER | off (config.rs:1373) | POST /v1/credits/spend, and the gpu1 rerank burn path | 404; metered paths keep the legacy no-burn shape |
CRUX_MEMORY_IMPORT | off (config.rs:1374) | POST /v1/memory/import | 404 |
CORECRUXD_IDENTITY_LINKS | off (config.rs:1375) | /v1/identity/links*, /v1/identity/candidates*, and the candidate extension of /v1/principal/resolve | 404 |
CORECRUXD_OPENAI_SHIM | off (config.rs:1376) | /v1/openai/tools.json, /v1/openai/invoke | 404 |
CORECRUXD_COMPUTE_PROVIDER | off (config.rs:1265) | POST /v1/compute/embed | Route stays mounted and returns an explicit capability-disabled envelope |
CORECRUXD_ASSEMBLY_CACHE | off (config.rs:1361) | /v1/context bundle memoisation | Cold assembly on every call |
CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS | off (mod.rs:230) | /v1/passport/mint-requests/* | 404 without touching state |
CORECRUXD_CONSOLIDATION_SCHEDULER | off (mod.rs:239) | Review scheduler, reported by /v1/version | Scheduler not run |
CORECRUXD_FEATURE_INCIDENTS | off (incidents.rs:34) | /v1/incidents* | Gated |
CORECRUXD_FEATURE_LEGAL_HOLD | off (legal_holds.rs:22) | /v1/legal-holds* | Gated |
CORECRUXD_FEATURE_ACTIVITY_LOG | off (activity.rs:58) | /v1/activity* | Gated |
CORECRUXD_FEATURE_ACTIVITY_LOG_TTL_SECS | unset | Activity retention window | No TTL applied |
CORECRUXD_OBSERVE | off (agentgraph_kinds.rs:139) | /v1/observe/sessions/* | An explicit observe-disabled response |
CORECRUXD_OBSERVE_REDACT | default Audit mode | Redaction on observe capture | - |
CORECRUXD_ORCHESTRATORS | off (agentgraph_kinds.rs:144) | /v1/orchestrators* | Surface not served |
CORECRUXD_PUNCHCARD | off; values off, advisory, enforce (agentgraph_kinds.rs:161) | /v1/punchcards* | 501 (punchcards.rs:598). advisory tracks but never denies; enforce denies on conflict. |
CORECRUXD_ENGINE_BASE_URL | unset (engine_console.rs:80) | /v1/console/engine/* | Mediation disabled |
CORECRUXD_CORECRUX_BASE_URL | unset (console.rs:1229) | /v1/console/corecrux/* | Proxy disabled |
CORECRUXD_GPU1_BASE_URL | unset (gpu1.rs:961) | /v1/gpu1/* in a hosted build | endpoint_configured: false; compute returns a fallback envelope |
CORECRUXD_SYNC_MUTUAL_AUTH | off (mod.rs:212) | /v1/sync/tenants/* | Scope auth instead of the Ed25519 handshake |
CORECRUXD_SYNC_DELEGATION_ENFORCE | off (mod.rs:218) | Sync boundary | Contextual v1.1 tokens rejected fail-closed |
CORECRUXD_RETENTION_DAYS | unset, so retention off (mod.rs:374) | The compact-facts admin action | Only already soft-deleted facts are scrubbed |
CORECRUXD_ADMIN_FORCE_SEAL | off | The force-seal admin action kind | Refused |
CORECRUXD_OPERATOR_ACTION_MAX_PENDING, _TIMEOUT_SECS | see config.rs | /v1/admin/actions queue | - |
CORECRUXD_ENABLED_PRO_SERVICES | empty | /v1/workbench/* | 402 pro_service_not_enabled per surface (workbench.rs:800) |
CORECRUXD_USAGE_RECEIPTS_SUBMIT, _ENDPOINT, _CONSENT_AT | all absent (config.rs:1348) | Outbound usage ping | The submitter never runs. A fresh install dials nothing. |
0.11 The Community Edition exclusion
/v1/cloud/access-contract and the seven /v1/gpu1/* routes are compiled in only when the binary is built with the hosted-surfaces Cargo feature (mod.rs:1528). That feature is off in Community Edition.
On a CE binary those eight routes do not exist. Not gated, not 501, absent. They return 404 because nothing is mounted, and the handler code is not compiled. Chapter 3 documents them and marks them FLAG, because they are real routes that some builds serve, but do not plan against them on a CE install.
Even in a hosted build, /v1/gpu1/* needs CORECRUXD_GPU1_BASE_URL set. With it unset, the contract route reports endpoint_configured: false and the compute routes return a fallback envelope rather than an error.
0.12 The Agent Workbench Pro gate
The twelve /v1/workbench/* routes carry a dual gate (workbench.rs:778):
- A caller passes with
admin:readoradmin:writeor with the tenant-scoped per-surface capability for that route. - Even then, the capability must appear in
CORECRUXD_ENABLED_PRO_SERVICES, or the route returns402 pro_service_not_enabled(workbench.rs:800).
CORECRUXD_ENABLED_PRO_SERVICES is empty by default, so on a stock daemon every workbench route except GET /v1/workbench/contract returns 402.
| Route | Per-surface capability |
|---|---|
GET /v1/workbench/brief | agent_brief:pro |
POST /v1/workbench/context-pack | context_pack:budgeted |
POST /v1/workbench/impact-preflight | impact:preflight |
GET and POST /v1/workbench/command-ledger | ledger:history |
GET /v1/workbench/audit-triage | audit:triage |
GET /v1/workbench/reasoning-timeline | reasoning:timeline |
POST /v1/workbench/handoff-v2 | handoff:v2 |
POST /v1/workbench/route-probe | route_probe:lab |
GET /v1/workbench/api-drift | api_drift:check |
POST /v1/workbench/policy-simulation | policy:simulate |
Capability strings are from workbench.rs:46. GET /v1/workbench/contract is not gated: it exists so a client can discover which surfaces are enabled before it gets a 402.
0.13 What /v1/openapi.json does and does not give you
The daemon maintains its own route manifest, const ROUTES at openapi.rs:140, and a drift test asserts it is set-equal to the routes actually mounted (tests/route_spec_drift.rs:757). GET /v1/openapi.json overlays that manifest onto a utoipa-derived base (openapi.rs:499).
The path coverage is exact. The schema coverage is not.
| Property | Reality |
|---|---|
| Paths in the spec but not routed | none |
| Paths routed but not in the spec | 2: POST /session and POST /invocation/verify, both deliberately excluded (openapi.rs:130) |
| Method-level mismatches across the 303 shared paths | zero |
| Operations with a full request or response schema | 26 (openapi.rs:33), facts ×12, query ×4, health ×4, receipts ×3, witness ×1, observations ×1, events ×1 |
| Components (schemas) declared | 11 (openapi.rs:67) |
So /v1/openapi.json describes request and response bodies for 26 of 303 paths, under 9%. The other paths appear as bare entries with a one-line summary, no requestBody and no responses schema. If you were planning to generate a client from it, generate the route list and hand-write the bodies. The drift test cannot catch this gap, because it only compares path and method sets.
That is precisely why these chapters exist and why every table carries request fields and response keys.
0.14 Where each route lives
The router organises the surface into 57 planes. This reference folds them into nine chapters plus gRPC. Counts are method-and-path registrations; the nine chapters sum to 347.
| Chapter | Planes covered | Routes |
|---|---|---|
| 1. Facts and memory | Facts · Substrate (entities, edges, kinds) · Relations graph · Memory (import, auto-capture, engrams) · Cases · Append and local prose ingest · Result-envelope import · Features lens · Tenant sync | 49 |
| 2. Sessions and handoffs | Session handshake and invocation verify · Sessions (state, archive, observations) · Observations and mediation receipts · Activity journal · Agent Workbench · Agent and MCP tool usage · Context surface | 33 |
| 3. Query and retrieval | Query and retrieval · Projections · Events (SSE) · Ops self-observation and bootstrap · Compute provider · Hosted surfaces | 30 |
| 4. Receipts and verification | Receipts and replay exports · Audit bundle verification · Provenance marking gateway · Observe-audit sessions · Incidents · Legal holds | 25 |
| 5. Identity and passports | Auth rails · Identity links and candidates · Passports, mint requests and presence · Principal resolution and capability policy | 25 |
| 6. Work and coordination | Work board, gates and status feed · Coordination plane · Orchestrators · Punchcards · Projects, layers, repos and context graph · Planes · Storybook · Dossiers | 65 |
| 7. Extensions and Studio | Community extensions · GitHub integration · OpenAI integration · OpenAI function-calling shim · Studio packs and template library · RCX Registry publish · Actions enrichment | 38 |
| 8. Admin and operations | Health, liveness and version · Routing, shards and GPUs · Admin and operations · Repository registry and code map · Workspace scan and storyline · Cost lens · Quota and credit meter | 42 |
| 9. Console and surfaces | Console API · CoreCrux mediation proxy · Engine mediation · plus the 6 static console asset routes, which are outside the API contract | 40 plus 6 |
| 10. gRPC | The :4007 surface: what is served, and what is compiled but never registered | 2 services |
0.15 Three structural facts worth knowing before you debug
Route ordering is load-bearing. GET /v1/receipts/list must stay registered before GET /v1/receipts/{receiptId} (mod.rs:505). matchit's static-beats-parameter precedence is relied on and covered by a router test. The same pattern protects /v1/extensions/registry against /v1/extensions/{id} (mod.rs:1073) and /v1/studio/library against /v1/studio/pack/* (mod.rs:674).
The same handler can sit in two auth classes. /v1/append and /v1/admin/append share one handler (mod.rs:643) but land in different classes: /v1/append is Write (facts:write, admin:write), /v1/admin/append is AdminWrite (admin:write only).
Adding a route means touching three places. mod.rs mounts it, openapi.rs::ROUTES declares it, and route_auth.rs::classify_route authorizes it. Miss the second and tests/route_spec_drift.rs fails; miss the third and the route fails closed with 403 under enforce.
Sources
- crates/corecruxd/src/config.rs:793,
load_config, ports, ingress defaults - crates/corecruxd/src/main.rs:307, auth-mode-required abort
- crates/corecruxd/src/auth.rs:1320,
require_http_scopes, all-of - crates/corecruxd/src/auth.rs:1340,
require_http_any_scope, any-of - crates/corecruxd/src/auth.rs:1359, tenant-bound any-of, with the
admin:carve-out - crates/corecruxd/src/http/mod.rs:455,
router() - crates/corecruxd/src/http/mod.rs:1541, the layer stack
- crates/corecruxd/src/http/mod.rs:1788,
problem_response - crates/corecruxd/src/http/ingress.rs:19, ingress ordering note
- crates/corecruxd/src/http/route_auth.rs:75,
classify_route - crates/corecruxd/src/http/route_auth.rs:576,
CORECRUXD_ROUTE_AUTHparse - crates/corecruxd/src/http/route_auth.rs:606,
route_auth_middleware - crates/corecruxd/src/http/openapi.rs:140, the
ROUTESmanifest - crates/corecruxd/tests/route_spec_drift.rs:757, manifest-to-router parity test
- crates/corecrux-types/src/lib.rs:783,
ProblemDetails

