# API Reference Crux Daemon exposes three network surfaces by default: - HTTP API on `14800` - gRPC API on `4007` - Built-in MCP server on `14801` when `CORECRUXD_MCP_ENABLED=true` (default) ## HTTP Endpoints ### Infrastructure | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/healthz` | Health check with build metadata | None | | GET | `/readyz` | Readiness probe (lock held, routing loaded, capacity OK) | None | | GET | `/metrics` | Prometheus metrics | None | | GET | `/v1/version` | Build version, feature flags, sync posture, and cached git update status (`current`, `behind`, `ahead`, `diverged`, `disabled`, `unavailable`, or `error`) | None | ### Query & Retrieval | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/query/text-search` | BM25 text search with token budget and coverage scoring | `query:read` | | POST | `/v1/query/text-search/expand` | Progressive retrieval — expand scan results with full content | `query:read` | | POST | `/v1/query/graph-expand` | Graph traversal from seed artifacts with budget | `query:read` * | | POST | `/v1/query/time-range` | Temporal range query over artifact state changes | `query:read` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. #### Segment coordinates on a result Each `/v1/query/text-search` result (and each `/expand` chunk) carries two segment coordinates, which are **not the same number**: | Field | Meaning | |---|---| | `segment_index` | Position in the loaded-reader list, ascending by sequence. **0-based, global, and mutable** — see the warning below. Pairs with `doc_id` to form `result_id`, and is the value `/expand` takes back. | | `segment_seq` | The sealed segment's own sequence — the `segment_seq` the `/v1/local/ingest` receipt returned. **1-based and stable for the life of the segment.** | To join a query result back to the ingest that produced it, **join on `segment_seq`**. **Never derive one from the other.** `segment_index` is a position in the daemon's loaded-reader list, so it moves when *any* segment is loaded, sealed or erased — including segments belonging to other tenants, written by someone else, while your query runs. Measured on one host on 2026-08-07: `segment_seq - segment_index` was **1** in the morning, **18** that afternoon and **17** minutes later, uniformly across every tenant. There is no offset to store, not even a freshly measured one. A consumer that translates between them scores a plausible, uniform **0%** instead of erroring, because every hit unmaps at once and nothing raises. That is how the BEAM-100K mapping was wrong for a full run before anyone noticed. If you are on a daemon old enough not to return `segment_seq`, do not guess the offset: discover the tenant's live segment ids through the same query path you score with, and pair them in ascending order with your ingest batches — segment sequences are allocated monotonically, so ascending live order matches ingest order whatever the absolute numbers have become. ### Local prose ingest | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/local/ingest` | Seal pre-chunked prose into a local segment (BM25 + optional dense) | `admin:write` (tenant-scoped) | The 202 response reports the dense lane explicitly, because a corpus that failed to embed still indexes over BM25 and otherwise looks healthy: | Field | Meaning | |---|---| | `dense_vectors` | Vectors actually persisted to the segment's `.ccxv` companion | | `dense_expected` | Vectors this ingest expected — every chunk when the node embeds server-side, the caller-vectored subset otherwise | | `dense_status` | `ok`, `partial`, `skipped`, `not_configured` (no vectors expected — BM25-only by configuration), or `not_applicable` (idempotent re-ingest, nothing sealed) | **Assert `dense_status == "ok"`** (or `dense_vectors == dense_expected`) after every ingest that expects semantic recall. `skipped` means the embed step failed and the segment sealed lexical-only; the daemon logs `local-ingest-dense-gap-sealed` at WARN with the segment sequence, and the cause in the preceding `local-ingest-embedding-failed` line. Both fields are additive — a client that ignores them sees the response shape it saw before ([local_ingest.rs](../crates/corecruxd/src/http/local_ingest.rs)). ### Fact Store | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | PUT | `/v1/facts` | Store or update a shared fact (entity + key + value + confidence) | `query:read` | | PUT | `/v1/facts/bulk` | Bulk-store multiple facts | `query:read` | | GET | `/v1/facts` | Query facts by text with token budget | `query:read` | | GET | `/v1/facts/{factId}` | Retrieve a specific fact by ID | `query:read` | | DELETE | `/v1/facts/{factId}` | Delete a fact | `query:read` | | GET | `/v1/facts/entity/{entity}` | List all facts for an entity | `query:read` | HTTP fact writes do not support `private=true`. Private facts and per-agent visibility are MCP-only features. ### Work and Orchestrators Work and orchestrator records are authority-sensitive, tenant-scoped surfaces. In JWT modes, creator/updater/commenter identity and tenant come from verified claims; matching body fields are compatibility constraints, not an impersonation mechanism. A caller cannot list, read, mutate, comment on, attach members to, or resolve gates for another tenant. In local `off`/`dev_scopes` mode, an explicit passport header or matching body assertion is recorded as `operator:unverified:`. It is not a verified human identity: work state changes always queue for review. Human gate decisions in authenticated modes require `facts:write`, a canonical JWT `passport_id`, and the work tenant; MCP agent tokens and `sub`-only JWTs cannot approve or reject. An unmapped MCP agent token is attributed as `agent:` and is gated as automation; only an explicit `CRUX_AGENT_PASSPORTS` mapping may resolve it to a real passport id. The generic `/v1/entities/{kind}/{id}` and MCP `entity_*` APIs reject governed `orchestrator` records and omit them from unfiltered listings. Use the typed `/v1/orchestrators` routes so tenant and actor checks cannot be bypassed. ### Session Store | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | PUT | `/v1/sessions/{sessionId}/state` | Store session state (JSON blob) | `query:read` | | GET | `/v1/sessions/{sessionId}/state` | Retrieve session state | `query:read` | ### Event Append | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/admin/append` | Append events to a stream (`/v1/append` compatibility alias) | `admin:write` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. ### CROWN Receipts | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/receipts/{receiptId}` | Retrieve a CROWN receipt body | `events:read` | | GET | `/v1/receipts/{receiptId}/signature` | Retrieve receipt Ed25519 signature | `events:read` | | GET | `/v1/receipts/{receiptId}/verification` | Verify receipt signature and chain | `events:read` | | GET | `/v1/receipts/signing-keys` | Receipt-signing public keyring (`Ed25519KeyRingV1`) for offline verifiers | public | ### Credits | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/credits/spend` | Default-off comped-wallet spend rail. Requires `CORECRUXD_CREDIT_METER=1`; consumes a pinned quote, reserves/spends seeded credits idempotently, and returns a signed `crux.credit_spend_receipt.v1`. Does not mint fiat credits or call Paddle. | `admin:write` | When `CORECRUXD_CREDIT_METER=1`, successful RCX-verified `POST /v1/gpu1/rerank` calls also reserve and spend 3 comped-wallet credits. The pinned `crux.credit_quote.v1` rides at `options.credit_quote`; the `crux.gpu1.compute_response.v1` envelope adds `credit_spend_receipt`, `credits_spent`, and `wallet_balance`. Failed/degraded compute releases the reservation and emits no spend stamp. ### BYOK Provenance (default off) | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/provenance/sign` | Sign an asset with a request-scoped caller P-256 key and leaf-first certificate chain | `provenance:write` or `admin:write` | | POST | `/v1/provenance/verify` | Verify envelope integrity and optional asset binding without retaining a record | `provenance:write` or `admin:write` | | POST | `/v1/provenance/verify-record` | Verify and retain a passport-signed record; supports `Idempotency-Key` | `provenance:write` or `admin:write` | Set `CORECRUXD_FEATURE_PROVENANCE_API=1` to mount the routes. They require an explicit authorized tenant and a safe transport posture; see the [BYOK provenance quickstart](provenance-byok-quickstart.md). Exact leaf pins can establish a narrow operator-selected identity policy, but the beta does not perform CA-chain/root validation. Metering remains a no-op until the fractional-credit contract is ratified and implemented. `CORECRUXD_PROVENANCE_RETENTION_DAYS=1..3650` enables activity-driven retained-record expiry; unset means no automatic deletion. Sweeps preserve active tenant-wide or `provenance::verification_record::`-scoped legal holds and mint count-only governance receipts. A non-empty sweep reports its receipt status/id and deletion count in `X-Cuecrux-Retention-*` response headers. See the quickstart for the exact lifecycle and fail-closed behavior. All three routes share a tenant-scoped 120-request/minute budget keyed by the verified stable JWT `sub` or `passport_id`, so token rotation and switching operations do not create fresh allowance. A rejection is `429` with `Retry-After: 60`. This sits behind the daemon-wide effective-client-IP token bucket, body limits, and concurrency/load-shed layer. The principal table is process-local: a multi-replica hosted deployment must additionally enforce a shared limit at its edge or gateway. Configure `CORECRUXD_TRUSTED_PROXY_CIDRS` before relying on forwarded client addresses; loopback is exempt by default. ### Replay Exports | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/replay/exports/receipts/{receiptId}` | Export receipt bundle (ZIP/TAR+ZST) | `events:read` | | GET | `/v1/replay/exports/answers/{answerId}` | Export answer bundle | `events:read` | | GET | `/v1/replay/exports/actions/{actionId}` | Export action bundle | `events:read` | | GET | `/v1/replay/exports/streams/{streamType}/{streamId}` | Export stream bundle | `events:read` | ### Self-Observation (crux-observe) | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/ops/facts` | Query operational facts | `admin:read` | | GET | `/v1/ops/errors` | Query recent errors since timestamp | `admin:read` | | GET | `/v1/ops/health` | Operational health summary | `admin:read` | | POST | `/v1/bootstrap/pull` | Pull bootstrap facts with token budget | `admin:read` | | GET | `/v1/bootstrap/status` | Check bootstrap seeded state | `admin:read` | ### Projections | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/projections/entity/count` | Entity count by tenant | `query:read` * | | GET | `/v1/projections/entity/timeline` | Entity state timeline | `query:read` * | | GET | `/v1/projections/entity/current-state` | Current entity state | `query:read` * | | GET | `/v1/admin/projections/meta` | Projection cursor metadata per shard | `admin:read` * | | POST | `/v1/admin/projections/rebuild` | Trigger projection rebuild | `admin:write` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/state` | Artifact living state | `admin:read` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/relations` | Artifact relations | `admin:read` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/dependents` | Artifact dependents | `admin:read` * | | GET | `/v1/admin/projections/artifacts/{artifactId}/pressure-events` | Artifact pressure events | `admin:read` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. ### Repos & Code Map AST-derived code structure for registered repositories. Registration with a `root_path` runs a one-shot scan (Rust natively; TS/TSX/Vue/Python via tree-sitter) and persists it; the repo watch loop re-indexes on change. The codemap endpoint is the read side — the daemon serving its own code understanding back to agents. | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/repos?tenant_id=…` | List registered repos for a tenant | `admin:read` | | POST | `/v1/repos` | Register a repo (`root_path` scans now; `clone_url` defers) | `admin:write` | | GET | `/v1/repos/{repoId}?tenant_id=…` | One registration | `admin:read` | | DELETE | `/v1/repos/{repoId}?tenant_id=…` | Unregister (stops watch) | `admin:write` | | GET | `/v1/repos/{repoId}/codemap?tenant_id=…&format=summary\|full` | AST code map: `summary` = stats + per-crate rollup; `full` = files, symbols, deps, routes | `admin:read` | | POST | `/v1/workspace/scan` | Scan the daemon's own workspace (`CORECRUXD_WORKSPACE_PATH`) | `admin:write` | | GET | `/v1/workspace/scan` | Latest self-scan in full | `admin:read` | | GET | `/v1/workspace/storyline?format=tree\|json` | Per-route call trees from the self-scan | `admin:read` | ### Routing & Shards | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | GET | `/v1/shards` | List shards with store status | `admin:read` | | GET | `/v1/shard-map` | Current shard map (shard → node assignment) | `admin:read` | | GET | `/v1/route` | Route a stream to its owning shard | `admin:read` | | GET | `/v1/routing/route` | Debug route resolution | `admin:read` | | GET | `/v1/routing/status` | Routing table version and reload status | `admin:read` | | GET | `/v1/gpus` | GPU inventory | `admin:read` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. ### Admin & Operations | Method | Path | Description | Auth Scope | |--------|------|-------------|------------| | POST | `/v1/admin/shard-map` | Update shard map | `admin:write` | | GET | `/v1/admin/control` | Current control state | `admin:read` | | POST | `/v1/admin/restart` | Request daemon process restart | `admin:write` | | GET | `/v1/admin/ops-log` | Structured operations log | `admin:read` | | POST | `/v1/admin/valves` | Set valve states (throttle, pause, emergency brake) | `admin:write` | | GET | `/v1/admin/replication/status` | Replication topology status | `admin:read` * | | POST | `/v1/admin/actions` | Submit admin action (seal, scrub, verify, rebalance) | `admin:write` | | GET | `/v1/admin/actions/{actionId}` | Get admin action status | `admin:read` | | POST | `/v1/admin/stream-meta` | Update stream metadata | `admin:write` * | | GET | `/v1/admin/tenants/{tenantId}/footprint` | Segments, docs and bytes a tenant occupies in the retrieval corpus | `admin:read` † | | POST | `/v1/admin/forget-tenants` | Erase the named tenants' retrieval corpora (`/v1/admin/forget-tenant` is the singular alias) | `admin:write` † | | DELETE | `/v1/admin/forget-tenants/{tenantId}` | Lift a mask-only erasure | `admin:write` † | | POST | `/v1/internal/replication/segments` | Receive replicated segments | `replication:write` * | \* Requires a dataplane-enabled deployment. Returns 501 in Crux Daemon. † Requires `CORECRUXD_TENANT_ERASURE=1`; the routes 404 while it is unset. #### Tenant corpus erasure `POST /v1/admin/forget-tenants` takes `{"tenant_ids": ["…"], "reclaim": false}`. Empty ids are dropped and duplicates collapsed in first-seen order; the batch is capped at 4096, `__`-prefixed (reserved) tenant ids are refused, and `admin:write` is checked for **every** named tenant before anything is mutated — a partially authorised batch is `403` and erases nothing. Two layers: - **Layer 1 (default).** Segments sealed up to the current `watermark_segment_seq` become invisible to that tenant, and the mask is persisted to `/forgotten-tenants.json` before the response returns. Reversible via the `DELETE` route. Anything ingested afterwards is served normally, so a corpus can be erased and re-paved under the same tenant id. - **Layer 2 (`"reclaim": true`).** Additionally deletes the file group of every segment whose documents all belong to that tenant. **Irreversible** — recovery is restore-from-backup only. Segments shared with another tenant are never deleted; they stay masked and are reported as `mixed_segments_retained`. Both routes re-read the shard directories from disk before answering, so a segment sealed since the last scan is inside the blast radius rather than silently outside it. Segments are found by their `.ccxseg` file, not by a companion: a segment holding fact records has no `.ccxi` to key off, and it is still erasable. Tenant membership comes from the `.ccxi` doc table where one exists and from the segment's own frame headers where it does not. A segment that is on disk but cannot be read at all is reported as `unattributable_segments` on both routes, and is neither masked nor reclaimed — deleting a segment whose owner cannot be established risks a co-tenant's data. A non-zero count means an erasure is incomplete and needs an operator. Scope is the **retrieval corpus only**. A tenant's facts, sessions and activity rows are untouched, so the response says `corpus_erased`, not `tenant_forgotten`. Each tenant's erasure mints a signed governance receipt carrying counts, the watermark and the scope — never document content. --- ## gRPC Services Default port: `4007`. Proto files in `proto/`. ### CoreCruxDataPlaneV1 All RPCs return `UNIMPLEMENTED` in Crux Daemon and require a dataplane-enabled deployment. | RPC | Request | Response | Description | |-----|---------|----------|-------------| | `AppendBatch` | `AppendBatchRequest` | `AppendBatchResponse` | Append events with deduplication | | `ReadStream` | `ReadStreamRequest` | stream `ReadStreamResponse` | Read events from a stream | | `ReadStreamBatched` | `ReadStreamBatchedRequest` | stream `ReadStreamBatchResponse` | Batched read with configurable limits | | `ReadStreamBatchedUnary` | `ReadStreamBatchedRequest` | `ReadStreamBatchResponse` | Unary batched read | | `ReadManyBatchedUnary` | `ReadManyBatchedRequest` | `ReadManyBatchedResponse` | Read multiple streams in one call | | `ReadManyFramesBatchedUnary` | `ReadManyFramesBatchedRequest` | `ReadManyFramesBatchedResponse` | Read raw frames from multiple streams | | `ReadFramesBatchedUnary` | `ReadStreamBatchedRequest` | `ReadFramesBatchRawResponse` | Raw frame read | | `ReplaySession` | stream `ReplaySessionRequest` | stream `ReplaySessionResponse` | Bidirectional streaming replay | | `ReadFrames` | `ReadFramesRequest` | stream `ReadFramesResponse` | Stream raw frame bytes | ### CoreCruxExportV1 | RPC | Request | Response | Description | |-----|---------|----------|-------------| | `ExportReceiptBundle` | `ExportReceiptBundleRequest` | stream `ExportChunk` | Stream large export bundles | Returns `UNIMPLEMENTED` in Crux Daemon. ### CoreCruxObserveV1 | RPC | Request | Response | Description | |-----|---------|----------|-------------| | `QueryOpsFacts` | `QueryOpsFactsRequest` | `QueryOpsFactsResponse` | Query operational facts | | `QueryOpsErrors` | `QueryOpsErrorsRequest` | `QueryOpsErrorsResponse` | Query error log | | `GetOpsHealth` | `GetOpsHealthRequest` | `GetOpsHealthResponse` | Health summary (JSON) | | `BootstrapPull` | `BootstrapPullRequest` | `BootstrapPullResponse` | Bootstrap fact pull with token budget | | `GetBootstrapStatus` | `GetBootstrapStatusRequest` | `GetBootstrapStatusResponse` | Seeded state and fact count | --- ## MCP (JSON-RPC over HTTP) Endpoint: `GET/POST http://:14801/mcp` - `GET /mcp` returns server info and protocol metadata. - `POST /mcp` serves JSON-RPC 2.0 requests such as `tools/list` and `tools/call`. - If `CRUX_AGENT_TOKEN` or `CRUX_AGENT_TOKENS` is configured, MCP requests must include `Authorization: Bearer `. - `Accept: text/event-stream` opens a Streamable HTTP SSE stream. SSE streams use the same bearer-token rule, validate `Mcp-Session-Id`, and are capped by `CRUX_MCP_SSE_MAX_SESSIONS` and `CRUX_MCP_SSE_MAX_SESSIONS_PER_OWNER`. - Private facts, agent-scoped sessions, and handoff workflows are available through MCP tools, not the HTTP `/v1/facts` surface. - `sync_status` tells agents whether the node is local-only, sync-enabled, or degraded before they attempt hosted-platform integration. - `update_status` tells agents whether the local checkout is current, behind, ahead, diverged, disabled, unavailable, or erroring before they propose an upgrade or restart. See [agent-guide.md](/home/myles/CueCrux/Crux/docs/agent-guide.md) and [examples/mcp-configs/README.md](/home/myles/CueCrux/Crux/examples/mcp-configs/README.md) for JSON-RPC examples and client configs. --- ## Authentication Configured via `CORECRUXD_AUTH_MODE`: | Mode | Description | Use Case | |------|-------------|----------| | `off` | No authentication | Local development only | | `dev_scopes` | Scopes parsed from header, no signature verification | Development/testing | | `jwt_hs256` | JWT with HMAC-SHA256 signature | Simple production setups | | `jwt_jwks` | JWT with JWKS key rotation | Production with key management | Scopes are passed via `Authorization: Bearer ` header. Required scopes are listed per endpoint above. `X-Corecrux-Passport-Id` is only an unverified local assertion in `off` and `dev_scopes`; production authority must come from verified token claims. ### HTTP fact tenant isolation (`CORECRUXD_TENANT_WRITE_STAMP`) In `jwt_hs256` and `jwt_jwks` modes, the default is `on`: affected HTTP fact-backed writes stamp the verified JWT tenant and reads filter to the same tenant. A token with one tenant needs no selector. On tenant-implicit routes, a token with multiple tenants or a wildcard tenant must send `X-Corecrux-Tenant-Id`; an explicit route/body tenant is itself a selector and must agree with that header when both are present. A missing tenant claim, ambiguous selection, mismatch, or unauthorized selection is rejected. The separately documented raw-admin fact reads remain intentionally cross-tenant. The policy is parsed once at startup, and an invalid value aborts startup. `off` is a deliberate legacy migration override: reads and writes use the shared `default` tenant even when JWT claims differ. `shadow` preserves that same storage behaviour while logging requests that `on` would move or reject. Historical `default` rows are not migrated automatically. This switch covers wired HTTP fact-backed surfaces, including generic and console facts, context recall, engram overlays, memory candidates, result envelopes, replay capsules, and their paired HTTP audit/export reads. It is not a universal daemon tenant switch: the MCP compatibility plane still uses `default`, while entity, edge, session, projection, and other control stores retain their own tenant contracts. ### Route authorization gate (`CORECRUXD_ROUTE_AUTH`) Independently of `CORECRUXD_AUTH_MODE`, the daemon runs a deny-by-default route authorization layer as HTTP middleware, in front of (and in addition to) the per-handler scope checks. It maps every routed request to a route contract — the accepted (any-of) scope set for that route template and method — and is controlled by `CORECRUXD_ROUTE_AUTH` (read once at startup): | Value | Behaviour | |-------|-----------| | `off` | Pass-through; the middleware does nothing. | | `shadow` | Evaluates the contract and logs a structured `route_auth_shadow_mismatch` warning on any would-deny, but never blocks. It is the derived default only for auth-off, loopback-only operation; otherwise it is an explicit migration override. | | `enforce` | Public routes (`/healthz`, `/readyz`, `/metrics`, `/session`, `/invocation/verify`, `/v1/openapi.json`, `/v1/version`, `/v1/receipts/signing-keys`, `/v1/witness/smoke`, and the `/v1/auth/*` bootstrap rails) pass with no auth headers. Every other route requires one of its contract scopes via the same primitive the handlers use. A route with **no** contract entry — or a request axum could not match to a route template — **fails closed with `403`**. | With the variable unset, authentication enabled or a non-loopback listener selects `enforce`; only auth-off plus loopback derives `shadow`. An empty or unknown explicit value also selects `enforce` and emits a startup warning. The gate authorizes scopes only; feature-flag gating for optional surfaces stays in the handler. When `CORECRUXD_AUTH_MODE=off`, the scope check is a no-op (there is nothing to enforce), but `enforce` still fails closed on uncontracted routes. --- ## Error Format All HTTP errors use [RFC 7807 Problem Details](https://www.rfc-editor.org/rfc/rfc7807): ```json { "type": "https://errors.cuecrux.com/bad-request", "title": "Bad Request", "status": 400, "detail": "query must not be empty" } ``` See `docs/error-catalogue.md` for the full error code reference.