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:read and tenant:content:preview all 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 503 rather than pretending.

9.1 Console API: summary, settings and infrastructure

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/summaryconsole.rs:2106 get_console_summaryadmin: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 enablementR
GET/v1/console/settingsconsole.rs:146 get_console_settingsadmin: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_URLR
PUT/v1/console/settingsconsole.rs:181 put_console_settingsadmin:write · AdminWritebody: 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_model400, 500-W
GET/v1/console/infra/summaryinfra.rs:56 get_infra_summaryadmin: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 enablementR
GET/v1/console/storage-breakdownconsole.rs:2021 get_console_storage_breakdownadmin:read · AdminRead-keys kinds, label, bytes, chunks, available, tooltip-,R
POST/v1/console/embedding/probeconsole.rs:267 post_console_embedding_probeadmin:write · AdminWritebody: url (String, req)Probe result400, 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

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/onboardingconsole.rs:58 get_console_onboardingno 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/completeconsole.rs:74 post_console_onboarding_completeadmin:write · AdminWritebody: auth_mode (String, req), hide_onboarding (bool, dflt)keys chosen_auth_mode, running_auth_mode, completed_at_unix_ms, restart_required, restart_command400, 403, 500-W
POST/v1/console/onboarding/restartconsole.rs:2084 post_console_onboarding_restartadmin:write · AdminWrite-keys chosen_auth_mode, completed_at_unix_ms500-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, including off
  • chosen_auth_mode, what the operator selected
  • bind_is_loopback, whether the HTTP listener is loopback-only
  • allow_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

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/factsconsole.rs:2854 get_console_factsadmin:read · AdminReadquery: 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/addconsole.rs:2905 post_console_fact_addfacts:write · AdminWritebody: entity (String, req), key (String, req), value (String, req), confidence (f32, dflt default_console_confidence)Stored fact400, 403-W · receipt, facts
GET/v1/console/passportsconsole.rs:2307 get_console_passportsadmin:read · AdminRead-keys agents, passport, claim_state, public_key_hex, private_key_exported, raw_tokens_exposed, mcp_path, session_endpoint, session_defaults-reports MCP enablementR
GET/v1/console/sessionsconsole.rs:2343 get_console_sessionsadmin:read · AdminReadquery: 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/detailconsole.rs:2652 get_console_session_detailadmin:read · AdminReadquery: 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_key404-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

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/review/queueconsole.rs:937 get_console_review_queueadmin:read · AdminReadquery: limit (usize, opt)keys runs, count, limit, entity, fact_id, surfaced_at, live_contradictions, live_count, schema-CORECRUXD_CONSOLIDATION_SCHEDULER, default offR · receipt
GET/v1/console/review/contradictionsconsole.rs:905 get_console_review_contradictionsadmin:read · AdminReadquery: limit (usize, opt)keys candidates, count, limit, schema-,R
POST/v1/console/review/consolidationsconsole.rs:1089 post_console_review_consolidationadmin:write · AdminWritebody: 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/undoconsole.rs:1154 post_console_review_consolidation_undoadmin:write · AdminWritebody: 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/expiriesconsole.rs:1017 post_console_review_expiriesadmin:write · AdminWritebody: fact_ids (Vec String, req)keys expired, expired_count, skipped, skipped_count, fact_id, actor, reason, schema400-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

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/tenantsconsole.rs:2959 get_console_tenantsadmin:read · AdminReadquery: category (String, opt)keys tenants, tenant_id, category, category_filter, chunk_visibility, content_preview, dataplane_enabled, override, routing, shard_count, source400-R
GET/v1/console/tenants/{tenantId}/categoryconsole.rs:3152 get_console_tenant_categoryadmin:read · AdminReadpath tenantIdCategory record400-R
PATCH/v1/console/tenants/{tenantId}/categoryconsole.rs:3169 patch_console_tenant_categoryadmin:write · AdminWritepath tenantId; body: category (String, req)Updated category400-W · facts
GET/v1/console/tenants/{tenantId}/chunksconsole.rs:3031 get_console_tenant_chunkstenant:chunks:read, tenant-bound · AdminReadpath tenantId; query: limit (usize, opt), cursor (String, opt)keys chunks, page, cursor, next_cursor, tenant_id, visibility, detail, dataplane_enabled400-R
GET/v1/console/chunks/{chunkDigest}console.rs:3064 get_console_chunkadmin:read · AdminReadpath chunkDigestkeys chunk_digest, present, metadata, visibility, detail, dataplane_enabled400, 404-R
GET/v1/console/chunks/{chunkDigest}/previewconsole.rs:3094 get_console_chunk_previewtenant:content:preview, tenant-bound · AdminReadpath chunkDigestkeys chunk_digest, preview, preview_available, redacted, detail, tenant_id400, 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

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/integrationsconsole.rs:2170 get_console_integrationsadmin:read · AdminRead-keys packs, grants, enabled, allowed_capabilities, allow_executable_helpers, safe_mode, audit_tail-CORECRUXD_INTEGRATIONS_ENABLED, default onR
POST/v1/console/integrations/{packId}/installconsole.rs:2210 post_console_integration_installintegrations:install · AdminWritepath packId; body: manifest (IntegrationManifest, opt), pack_id (String, opt), version (String, opt)Install result403CORECRUXD_INTEGRATIONS_ENABLED, default onW
POST/v1/console/integrations/{packId}/grantconsole.rs:2245 post_console_integration_grantintegrations:grant · AdminWritepath packId; body: version (String, req), capabilities (Vec String, dflt), reason (String, dflt)Grant result400, 403CORECRUXD_INTEGRATIONS_ENABLED, default onW
POST/v1/console/integrations/{packId}/disableconsole.rs:2283 post_console_integration_disableintegrations:disable · AdminWritepath packId; body: reason (String, dflt)Disable result-CORECRUXD_INTEGRATIONS_ENABLED, default onW

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

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/corecrux/graph/statsconsole.rs:488 get_console_corecrux_graph_statsadmin:read · AdminRead-Graph statistics503CORECRUXD_CORECRUX_BASE_URL, unset by defaultR
GET/v1/console/corecrux/graph/resolveconsole.rs:508 get_console_corecrux_graph_resolveadmin:read · AdminReadquery parameters as a string mapResolution result400, 503CORECRUXD_CORECRUX_BASE_URL, unset by defaultR
GET/v1/console/corecrux/graph/egoconsole.rs:552 get_console_corecrux_graph_egoadmin:read · AdminReadquery parameters as a string mapEgo-graph result400, 503CORECRUXD_CORECRUX_BASE_URL, unset by defaultR
GET/v1/console/corecrux/graph/pathconsole.rs:579 get_console_corecrux_graph_pathadmin:read · AdminReadquery parameters as a string mapPath result400, 503CORECRUXD_CORECRUX_BASE_URL, unset by defaultR
GET/v1/console/corecrux/lane-weightsconsole.rs:345 get_console_corecrux_lane_weightsadmin:read · AdminReadquery: tenant_id (String, dflt)Current configuration500, 503CORECRUXD_CORECRUX_BASE_URL, unset by defaultR
PUT/v1/console/corecrux/lane-weightsconsole.rs:373 put_console_corecrux_lane_weightsadmin:write · AdminWritebody: tenant_id (String, dflt), weights (map String to f64, dflt), fusion_rrf_enabled (bool, dflt true), reason (String, dflt), actor (String, dflt)Updated configuration400, 500, 503CORECRUXD_CORECRUX_BASE_URL, unset by defaultW
DELETE/v1/console/corecrux/lane-weightsconsole.rs:420 delete_console_corecrux_lane_weightsadmin:write · AdminWritequery: tenant_id (String, dflt)Reset result500, 503CORECRUXD_CORECRUX_BASE_URL, unset by defaultW

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.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/console/engine/summaryengine_console.rs:140 get_engine_summaryadmin:read · AdminRead-Engine summary-CORECRUXD_ENGINE_BASE_URL, unset by defaultR
GET/v1/console/engine/benchengine_console.rs:144 get_engine_benchadmin:read · AdminRead-Benchmark view-CORECRUXD_ENGINE_BASE_URL, unset by defaultR
GET/v1/console/engine/spendengine_console.rs:148 get_engine_spendadmin:read · AdminRead-Spend view-CORECRUXD_ENGINE_BASE_URL, unset by defaultR
POST/v1/console/engine/searchengine_console.rs:172 post_engine_searchadmin:read in the handler · AdminWritebody: query (String, req), top_k (u64, dflt), tenant (String, dflt)keys query, limit, tenantId, error400CORECRUXD_ENGINE_BASE_URL, unset by defaultR, 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).

MethodPathHandlerAuthResponseGate
GET/console.rs:92 redirect_to_consolenone, outside route-auth entirelyRedirect to /consoleCORECRUXD_CONSOLE_ENABLED, default on
GET/consoleconsole.rs:88 serve_consolenoneThe console SPA shell as text/htmlsame
GET/console-assets/{name}console.rs:157 serve_console_assetnoneEmbedded static assetsame
GET/console-v2/{name}console.rs:101 serve_console_v2_assetnoneEmbedded v2 SPA asset, including the generated fetch layer emitted from the route manifestsame
GET/console-3d/{path}console.rs:207 serve_console3dnoneEmbedded 3D console assets, wildcard pathsame
GET/activateconsole.rs:314 serve_activatenoneThe device-grant approval page, whose form POSTs to /v1/auth/device/approvesame

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

ControlSetting
OriginsAn explicit allowlist, not a wildcard. It replaced an earlier permissive layer.
MethodsGET, HEAD, OPTIONS only
HeadersNamed explicitly: Authorization, Content-Type, Accept. The CORS * header wildcard does not cover Authorization, which is why it is named.
CredentialsDeliberately 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

SymptomMost likely cause
An unauthenticated caller reading GET /v1/console/onboardingExpected 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 changedRead restart_required. Auth-mode and embedding changes need a restart; the response carries the command.
502 from POST /v1/console/embedding/probeThe probe target failed. The daemon made the outbound call on your behalf.
503 from any /v1/console/corecrux/* routeCORECRUXD_CORECRUX_BASE_URL is unset. That is the default.
500 from the same routeThe 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 runsCORECRUXD_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:readContent preview needs tenant:content:preview, tenant-bound. Metadata needs only admin:read.
A browser client blocked calling /v1/facts cross-originThere is no CORS layer on /v1/*. Only the static console routes carry one.
A 403 you cannot explain on POST /v1/console/integrations/{id}/grantInstall, grant and disable are three different scopes. integrations:install does not grant.

Sources