HTTP API · 9. Console and surfaces
Forty registrations exist to serve the daemon's built-in web console, and six further routes serve the console itself. The forty are ordinary API routes under /v1/console/*. The six are not API surface at all and sit outside every middleware layer that protects the rest of the daemon.
Planes covered: Console API · Console API (CoreCrux mediation proxy) · Console API (Engine mediation) · plus the static console asset routes.
Read chapter 0 first for the auth model, request-field notation and error shape.
Three things shape this chapter:
- One route on this plane performs no scope check at all, and on a default install it discloses the daemon's auth posture to an unauthenticated caller. See §9.2.
- The console read class accepts three scopes, not one.
admin:read,tenant:chunks:readandtenant:content:previewall satisfy the/v1/console/*GET contract; individual handlers then narrow it. - The two mediation planes are disabled unless a base URL is configured, and they return
503rather than pretending.
9.1 Console API: summary, settings and infrastructure
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/summary | console.rs:2106 get_console_summary | admin:read · AdminRead | - | keys daemon, console, node_id, auth_mode, mcp_enabled, mcp_agent_count, integrations, builtin_pack_count, allow_executable_helpers, safe_mode, auto_paused, capacity, free_bytes, free_ratio, emergency_free_ratio, dataplane_enabled, readiness, route, routing, compat, control_evidence_ok, control_evidence_error, external_network_dependencies, error | - | reports console, integrations and MCP enablement | R |
GET | /v1/console/settings | console.rs:146 get_console_settings | admin:read · AdminRead | - | keys auth, chosen_mode, supported_modes, allow_insecure_dev_auth_bind, bind_is_loopback, embedding, active, active_model, active_url, chosen_model, chosen_url, onboarding | - | reports CORECRUXD_EMBEDDING_MODEL and CORECRUXD_EMBEDDING_URL | R |
PUT | /v1/console/settings | console.rs:181 put_console_settings | admin:write · AdminWrite | body: auth_mode (String, dflt), embedding_enabled (bool, dflt), embedding_url (String, dflt), embedding_model (String, dflt) | keys saved, restart_required, restart_command, embedding_enabled, chosen_embedding_url, chosen_embedding_model | 400, 500 | - | W |
GET | /v1/console/infra/summary | infra.rs:56 get_infra_summary | admin:read · AdminRead | - | keys id, name, node_id, device, machines, machines_registered, machines_with_hooks, tailscale, rails, auth_mode, auth_configured, http_accept_agent_tokens, mcp_enabled, sessions, presence_count, configs, files, bytes, checklist, record, source_host, updated_at | - | reports MCP enablement | R |
GET | /v1/console/storage-breakdown | console.rs:2021 get_console_storage_breakdown | admin:read · AdminRead | - | keys kinds, label, bytes, chunks, available, tooltip | - | , | R |
POST | /v1/console/embedding/probe | console.rs:267 post_console_embedding_probe | admin:write · AdminWrite | body: url (String, req) | Probe result | 400, 500, 502 | - | W |
PUT /v1/console/settings can change auth_mode, and it returns restart_required and restart_command rather than applying it live. A settings write that comes back with restart_required: true has changed nothing yet.
POST /v1/console/embedding/probe makes an outbound HTTP request to a URL you supply, from the daemon. 502 means the probe target failed. Treat admin:write on this route as the ability to make the daemon issue arbitrary outbound GETs to a host of the caller's choosing.
free_ratio and emergency_free_ratio on the summary are the same capacity floor that fails /readyz, see chapter 8 §8.1.
9.2 Console API: onboarding
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/onboarding | console.rs:58 get_console_onboarding | no handler check · AdminRead | - | keys running_auth_mode, chosen_auth_mode, supported_auth_modes, bind_is_loopback, allow_insecure_dev_auth_bind, completed_at_unix_ms | - | , | R |
POST | /v1/console/onboarding/complete | console.rs:74 post_console_onboarding_complete | admin:write · AdminWrite | body: auth_mode (String, req), hide_onboarding (bool, dflt) | keys chosen_auth_mode, running_auth_mode, completed_at_unix_ms, restart_required, restart_command | 400, 403, 500 | - | W |
POST | /v1/console/onboarding/restart | console.rs:2084 post_console_onboarding_restart | admin:write · AdminWrite | - | keys chosen_auth_mode, completed_at_unix_ms | 500 | - | W |
GET /v1/console/onboarding performs no scope check. Its handler takes only State<AppState> (console.rs:58); there is no HeaderMap, so there is nothing to check a scope against. Its only protection is the route-auth middleware, and that defaults to shadow, which is non-blocking.
What it returns on a default install, to anybody who can reach the port:
running_auth_mode, the auth mode the daemon is actually enforcing, includingoffchosen_auth_mode, what the operator selectedbind_is_loopback, whether the HTTP listener is loopback-onlyallow_insecure_dev_auth_bind, whether the insecure-bind override is set
That is a complete description of the node's authentication posture, unauthenticated. It is correctly contracted as AdminRead; the gap is only that the contract is not enforced by default.
CORECRUXD_ROUTE_AUTH=enforce closes it, and is safe to set: classify_route() covers all 347 registrations, so nothing becomes unreachable. The other route with this shape is POST /v1/audit/bundle/verify, see chapter 4 §4.3.
9.3 Console API: facts, passports and sessions
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/facts | console.rs:2854 get_console_facts | admin:read · AdminRead | query: q (String, opt), top_k (usize, opt), as_of_unix_ms (i64, opt) | keys facts, count, visible_count, private_facts_hidden, query, top_k, as_of_unix_ms, total_tokens | - | , | R |
POST | /v1/console/facts/add | console.rs:2905 post_console_fact_add | facts:write · AdminWrite | body: entity (String, req), key (String, req), value (String, req), confidence (f32, dflt default_console_confidence) | Stored fact | 400, 403 | - | W · receipt, facts |
GET | /v1/console/passports | console.rs:2307 get_console_passports | admin:read · AdminRead | - | keys agents, passport, claim_state, public_key_hex, private_key_exported, raw_tokens_exposed, mcp_path, session_endpoint, session_defaults | - | reports MCP enablement | R |
GET | /v1/console/sessions | console.rs:2343 get_console_sessions | admin:read · AdminRead | query: include_archived (bool, dflt) | keys sessions, session_rows, session_id, count, counted, archived, archived_at, archived_count, archive_reason, include_archived, actor, agent, allocation, execplan_slug, milestone, expires_at, last_active_unix_ms, passport_id, passport_category, raw_key, raw_state_exposed, no_agent_title, no_identity, no_plan_link | - | , | R |
GET | /v1/console/sessions/detail | console.rs:2652 get_console_session_detail | admin:read · AdminRead | query: key (String, req) | keys session, session_id, state, state_first_line, state_summary, binding, coord_intent, paths, note, gates, linked_plans_heuristic, execplan_slug, milestone, project_id, actor, agent, archived, archived_at, archive_reason, expires_at, last_active_unix_ms, passport_id, passport_category, raw_key | 404 | - | R |
private_facts_hidden on the console fact read is the count the console is not showing you. raw_tokens_exposed on the passport view and raw_state_exposed on the session view are the console's own disclosure flags: they tell an operator that the response contains material that should not be screenshotted.
POST /v1/console/facts/add checks facts:write at the handler, not admin:write, so a fact-write token can add through the console path.
9.4 Console API: review queue
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/review/queue | console.rs:937 get_console_review_queue | admin:read · AdminRead | query: limit (usize, opt) | keys runs, count, limit, entity, fact_id, surfaced_at, live_contradictions, live_count, schema | - | CORECRUXD_CONSOLIDATION_SCHEDULER, default off | R · receipt |
GET | /v1/console/review/contradictions | console.rs:905 get_console_review_contradictions | admin:read · AdminRead | query: limit (usize, opt) | keys candidates, count, limit, schema | - | , | R |
POST | /v1/console/review/consolidations | console.rs:1089 post_console_review_consolidation | admin:write · AdminWrite | body: consolidation_id (String, req), entity (String, req), key (String, req), canonical_value (String, req), target_fact_ids (Vec String, req), protected_fact_ids (Vec String, dflt), confidence (f32, dflt default_confidence), source_receipt (String, dflt), actor (String, dflt), horizon_class (HorizonClass, dflt), protected_confidence_floor (f32, dflt default_consolidation_protected_confidence_floor) | keys status, receipt, signed_receipt, schema | - | , | W · receipt, facts |
POST | /v1/console/review/consolidations/undo | console.rs:1154 post_console_review_consolidation_undo | admin:write · AdminWrite | body: canonical_fact_id (String, req), source_fact_ids (Vec String, dflt), entity (String, dflt), key (String, dflt) | keys status, canonical_fact_id, restored_fact_ids, signed_receipt, schema | - | , | W · receipt, facts |
POST | /v1/console/review/expiries | console.rs:1017 post_console_review_expiries | admin:write · AdminWrite | body: fact_ids (Vec String, req) | keys expired, expired_count, skipped, skipped_count, fact_id, actor, reason, schema | 400 | - | W · facts |
Consolidation is reversible and both directions are receipted. protected_fact_ids and protected_confidence_floor exist so a consolidation cannot silently swallow a fact you marked as protected, and undo returns restored_fact_ids so you can confirm exactly what came back.
GET /v1/console/review/queue depends on CORECRUXD_CONSOLIDATION_SCHEDULER, which is off by default (mod.rs:239). With the scheduler off there are no runs to list. GET /v1/console/review/contradictions computes live and works regardless.
post_console_review_expiries returns skipped alongside expired. A 200 does not mean every id you sent was expired.
9.5 Console API: tenants and chunks
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/tenants | console.rs:2959 get_console_tenants | admin:read · AdminRead | query: category (String, opt) | keys tenants, tenant_id, category, category_filter, chunk_visibility, content_preview, dataplane_enabled, override, routing, shard_count, source | 400 | - | R |
GET | /v1/console/tenants/{tenantId}/category | console.rs:3152 get_console_tenant_category | admin:read · AdminRead | path tenantId | Category record | 400 | - | R |
PATCH | /v1/console/tenants/{tenantId}/category | console.rs:3169 patch_console_tenant_category | admin:write · AdminWrite | path tenantId; body: category (String, req) | Updated category | 400 | - | W · facts |
GET | /v1/console/tenants/{tenantId}/chunks | console.rs:3031 get_console_tenant_chunks | tenant:chunks:read, tenant-bound · AdminRead | path tenantId; query: limit (usize, opt), cursor (String, opt) | keys chunks, page, cursor, next_cursor, tenant_id, visibility, detail, dataplane_enabled | 400 | - | R |
GET | /v1/console/chunks/{chunkDigest} | console.rs:3064 get_console_chunk | admin:read · AdminRead | path chunkDigest | keys chunk_digest, present, metadata, visibility, detail, dataplane_enabled | 400, 404 | - | R |
GET | /v1/console/chunks/{chunkDigest}/preview | console.rs:3094 get_console_chunk_preview | tenant:content:preview, tenant-bound · AdminRead | path chunkDigest | keys chunk_digest, preview, preview_available, redacted, detail, tenant_id | 400, 404 | - | R |
Two narrow scopes exist precisely so content access can be separated from admin access. tenant:chunks:read lets a caller list a tenant's chunks; tenant:content:preview lets a caller read chunk content. Both are tenant-bound, and both are checked in the handler (console.rs:3037, console.rs:3107). GET /v1/console/chunks/{digest} returns metadata under plain admin:read; the /preview sibling is the one that returns content, and it needs the content scope.
redacted and preview_available tell you whether you are seeing the whole chunk. Do not treat an absent preview as an empty chunk.
9.6 Console API: integrations
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/integrations | console.rs:2170 get_console_integrations | admin:read · AdminRead | - | keys packs, grants, enabled, allowed_capabilities, allow_executable_helpers, safe_mode, audit_tail | - | CORECRUXD_INTEGRATIONS_ENABLED, default on | R |
POST | /v1/console/integrations/{packId}/install | console.rs:2210 post_console_integration_install | integrations:install · AdminWrite | path packId; body: manifest (IntegrationManifest, opt), pack_id (String, opt), version (String, opt) | Install result | 403 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
POST | /v1/console/integrations/{packId}/grant | console.rs:2245 post_console_integration_grant | integrations:grant · AdminWrite | path packId; body: version (String, req), capabilities (Vec String, dflt), reason (String, dflt) | Grant result | 400, 403 | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
POST | /v1/console/integrations/{packId}/disable | console.rs:2283 post_console_integration_disable | integrations:disable · AdminWrite | path packId; body: reason (String, dflt) | Disable result | - | CORECRUXD_INTEGRATIONS_ENABLED, default on | W |
integrations:grant appears only here. Install, grant and disable are three separate scopes on three separate routes, a token that can install a pack cannot grant it capabilities, and vice versa. That is the "install is not enable" rule expressed in the scope model.
audit_tail on the read route is the recent pack and extension audit trail, which is where a grant or disable shows up after the fact.
9.7 Console API: CoreCrux mediation proxy
Seven registrations. These mediate to a separate engine over a configured base URL. With CORECRUXD_CORECRUX_BASE_URL unset, the default, the proxy is disabled and these routes return 503 (console.rs:1229).
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/corecrux/graph/stats | console.rs:488 get_console_corecrux_graph_stats | admin:read · AdminRead | - | Graph statistics | 503 | CORECRUXD_CORECRUX_BASE_URL, unset by default | R |
GET | /v1/console/corecrux/graph/resolve | console.rs:508 get_console_corecrux_graph_resolve | admin:read · AdminRead | query parameters as a string map | Resolution result | 400, 503 | CORECRUXD_CORECRUX_BASE_URL, unset by default | R |
GET | /v1/console/corecrux/graph/ego | console.rs:552 get_console_corecrux_graph_ego | admin:read · AdminRead | query parameters as a string map | Ego-graph result | 400, 503 | CORECRUXD_CORECRUX_BASE_URL, unset by default | R |
GET | /v1/console/corecrux/graph/path | console.rs:579 get_console_corecrux_graph_path | admin:read · AdminRead | query parameters as a string map | Path result | 400, 503 | CORECRUXD_CORECRUX_BASE_URL, unset by default | R |
GET | /v1/console/corecrux/lane-weights | console.rs:345 get_console_corecrux_lane_weights | admin:read · AdminRead | query: tenant_id (String, dflt) | Current configuration | 500, 503 | CORECRUXD_CORECRUX_BASE_URL, unset by default | R |
PUT | /v1/console/corecrux/lane-weights | console.rs:373 put_console_corecrux_lane_weights | admin:write · AdminWrite | body: tenant_id (String, dflt), weights (map String to f64, dflt), fusion_rrf_enabled (bool, dflt true), reason (String, dflt), actor (String, dflt) | Updated configuration | 400, 500, 503 | CORECRUXD_CORECRUX_BASE_URL, unset by default | W |
DELETE | /v1/console/corecrux/lane-weights | console.rs:420 delete_console_corecrux_lane_weights | admin:write · AdminWrite | query: tenant_id (String, dflt) | Reset result | 500, 503 | CORECRUXD_CORECRUX_BASE_URL, unset by default | W |
The engine behind this proxy is a separate, closed system. This reference documents the mediation contract, the routes, their scopes, their request fields and their status codes, and nothing about what happens on the other side. DELETE on lane-weights resets to the engine's own defaults.
503 means the proxy is not configured. 500 means the configured upstream failed. The distinction matters when you are deciding whether to page someone.
9.8 Console API: Engine mediation
Four registrations. Mounted GET-only except for search (mod.rs:1463), so any other method returns 405 at the routing layer rather than reaching a handler check. That is a structural guarantee, not a handler decision: no mutating Engine route is mounted.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/console/engine/summary | engine_console.rs:140 get_engine_summary | admin:read · AdminRead | - | Engine summary | - | CORECRUXD_ENGINE_BASE_URL, unset by default | R |
GET | /v1/console/engine/bench | engine_console.rs:144 get_engine_bench | admin:read · AdminRead | - | Benchmark view | - | CORECRUXD_ENGINE_BASE_URL, unset by default | R |
GET | /v1/console/engine/spend | engine_console.rs:148 get_engine_spend | admin:read · AdminRead | - | Spend view | - | CORECRUXD_ENGINE_BASE_URL, unset by default | R |
POST | /v1/console/engine/search | engine_console.rs:172 post_engine_search | admin:read in the handler · AdminWrite | body: query (String, req), top_k (u64, dflt), tenant (String, dflt) | keys query, limit, tenantId, error | 400 | CORECRUXD_ENGINE_BASE_URL, unset by default | R, the single mediated read-POST |
With CORECRUXD_ENGINE_BASE_URL unset, the default, mediation is disabled (engine_console.rs:80).
Any benchmark number surfaced through /v1/console/engine/bench belongs to whatever corpus and build the upstream ran. Do not quote a number from this route without its corpus name and commit. The route does not carry that provenance for you.
9.9 The static console routes: outside the API contract
Six routes serve the console SPA itself. They are merged into the router after .with_state(state) (mod.rs:1560), which places them outside route_auth_middleware, outside quota_middleware, and outside presence_middleware. The daemon's own manifest states this exclusion and omits them from /v1/openapi.json (openapi.rs:135).
| Method | Path | Handler | Auth | Response | Gate |
|---|---|---|---|---|---|
GET | / | console.rs:92 redirect_to_console | none, outside route-auth entirely | Redirect to /console | CORECRUXD_CONSOLE_ENABLED, default on |
GET | /console | console.rs:88 serve_console | none | The console SPA shell as text/html | same |
GET | /console-assets/{name} | console.rs:157 serve_console_asset | none | Embedded static asset | same |
GET | /console-v2/{name} | console.rs:101 serve_console_v2_asset | none | Embedded v2 SPA asset, including the generated fetch layer emitted from the route manifest | same |
GET | /console-3d/{path} | console.rs:207 serve_console3d | none | Embedded 3D console assets, wildcard path | same |
GET | /activate | console.rs:314 serve_activate | none | The device-grant approval page, whose form POSTs to /v1/auth/device/approve | same |
These six serve embedded static bytes and no tenant data. They are not authenticated and are not intended to be. With CORECRUXD_CONSOLE_ENABLED=false the router is empty and all six return 404 (config.rs:830).
CORS applies to this group only
A CORS layer is applied to the static console routes and to nothing else (console.rs:287):
| Control | Setting |
|---|---|
| Origins | An explicit allowlist, not a wildcard. It replaced an earlier permissive layer. |
| Methods | GET, HEAD, OPTIONS only |
| Headers | Named explicitly: Authorization, Content-Type, Accept. The CORS * header wildcard does not cover Authorization, which is why it is named. |
| Credentials | Deliberately not allowed. The JWT rides an Authorization header, never a cookie. |
No CORS layer is applied to any /v1/* API route. A browser calling the API cross-origin will be blocked by the browser, not by the daemon. If you are building a browser client against a remote daemon, put it behind the same origin or a proxy you control.
9.10 Failure modes on this plane
| Symptom | Most likely cause |
|---|---|
An unauthenticated caller reading GET /v1/console/onboarding | Expected under the default shadow route-auth mode. The handler has no scope check. Set CORECRUXD_ROUTE_AUTH=enforce. |
PUT /v1/console/settings returns 200 but nothing changed | Read restart_required. Auth-mode and embedding changes need a restart; the response carries the command. |
502 from POST /v1/console/embedding/probe | The probe target failed. The daemon made the outbound call on your behalf. |
503 from any /v1/console/corecrux/* route | CORECRUXD_CORECRUX_BASE_URL is unset. That is the default. |
500 from the same route | The configured upstream failed. Different problem, different response. |
Nothing at all from /v1/console/engine/* | CORECRUXD_ENGINE_BASE_URL is unset. That is the default. |
405 on a PUT or DELETE under /v1/console/engine/* | Only GETs and one POST are mounted. No mutating Engine route exists. |
GET /v1/console/review/queue returning no runs | CORECRUXD_CONSOLIDATION_SCHEDULER is off by default, so there are no scheduled runs. Contradictions are computed live and still work. |
403 on /v1/console/chunks/{digest}/preview with admin:read | Content preview needs tenant:content:preview, tenant-bound. Metadata needs only admin:read. |
A browser client blocked calling /v1/facts cross-origin | There is no CORS layer on /v1/*. Only the static console routes carry one. |
A 403 you cannot explain on POST /v1/console/integrations/{id}/grant | Install, grant and disable are three different scopes. integrations:install does not grant. |
Sources
- crates/corecruxd/src/http/console.rs:58, the onboarding handler with no scope check
- crates/corecruxd/src/http/console.rs:3128, console read and write scope helpers
- crates/corecruxd/src/http/console.rs:3037,
tenant:chunks:read, tenant-bound - crates/corecruxd/src/http/console.rs:3107,
tenant:content:preview, tenant-bound - crates/corecruxd/src/http/console.rs:2216,
integrations:install - crates/corecruxd/src/http/console.rs:2251,
integrations:grant - crates/corecruxd/src/http/console.rs:2289,
integrations:disable - crates/corecruxd/src/http/console.rs:1229,
CORECRUXD_CORECRUX_BASE_URL - crates/corecruxd/src/http/engine_console.rs:80,
CORECRUXD_ENGINE_BASE_URL - crates/corecruxd/src/http/mod.rs:1463, Engine routes mounted GET-only except
search - crates/corecruxd/src/http/mod.rs:1560, console assets merged after
with_state - crates/corecruxd/src/http/mod.rs:239,
CORECRUXD_CONSOLIDATION_SCHEDULER - crates/corecruxd/src/console.rs:287, the console-only CORS layer
- crates/corecruxd/src/console.rs:294,
console::routes - crates/corecruxd/src/config.rs:830,
CORECRUXD_CONSOLE_ENABLED - crates/corecruxd/src/http/openapi.rs:135, console assets excluded from the manifest

