hal0-api serves an OpenAI-compatible /v1/* inference surface (see
OpenAI-compatible API reference) plus an /api/*
management surface that backs the dashboard and the hal0-admin MCP server. This page
indexes every /api/* router group mounted in src/hal0/api/__init__.py — for full
request/response schemas for a given route, read the OpenAPI spec served live at
/api/openapi.json (interactive docs at /api/docs).
Auth tiers
Every route resolves to one of five classes in src/hal0/security/exposure.py
(hal0.security.exposure.classify), evaluated first-match-wins against an explicit
rule table:
- OPEN — no auth ever. A deliberately tiny, enumerated allowlist (the
/v1/models
probe, the Prometheus scrape, the two liveness checks, the dashboard bootstrap-urls
endpoint, the login/status/logout routes themselves, the static SPA shell — same
HTML/JS bundle for every visitor, no server data — and the Hermes
dashboard-plugin static asset proxy at
/dashboard-plugins/{plugin_name}/{file_path}). - BOOTSTRAP — open only until an admin key is configured (
/api/install/*). - CLIENT — the
/v1/*inference/writer surface plus a short list of genuinely
read-only introspectionGETs. - BRIDGE — the agent bridge’s own surfaces (ADR-0043):
GET /api/bridge/meand the
/mcp/agentsmount, open to a bridge client key (hal0bk_…) or the admin key and never
to the dev-open bypass. - ADMIN — everything else, including every mutating route and every route that can
return secrets or config. Unclassified paths fall back to ADMIN (deny-by-default).
These classes are fully enforced once [security].require_auth is on. While it’s off,
the surface is trusted-LAN-open with one exception (since v1.3.0): with an admin key
configured and the API bound past loopback, ADMIN-class requests from a non-loopback
peer still need an admin credential (_lan_admin_gate in src/hal0/api/auth.py; see
Authentication). The table below states
each group’s class as enforced by exposure.py, not as a claim about the current
default posture.
Slot tune API
PUT /api/slots/{name}/config accepts {"tune":{"vision":"off"}}.
Only supplied keys change. A null key removes one override; {"tune":null}
removes all. Slot creation applies the same type/device and thinking guards.
Slot snapshots (GET /api/slots, GET /api/slots/{name}) expose
tune_effective: ctx_size, enable_thinking, reasoning_effort,
reasoning_budget, vision, speculation, sources,
reasoning_effort_unavailable and tune_unavailable. So does
GET /api/slots/{name}/resolved for a llama-server slot; the answer for a halogen
slot, like that of every other non-llama runtime, has argv: null and no
tune_effective. Sources are profile, slot or profile-flags;
reasoning_effort_unavailable is a string or null. tune_unavailable maps each
tune value the slot’s engine cannot run to the reason, and is {} on a
llama-server slot; on a halogen slot it is
{"speculation": "ngram: this engine has no n-gram drafter (auto or off)"}.
This is an additive read contract. Model feasibility accepts a slot name per model
row to resolve its profile plus tune for the candidate; explicit ctx wins.
Every GET /api/slots/{name}/resolved answer also carries footprint: null for a
slot that is not on the halogen runtime and for a halogen slot with no model bound
or no figure, otherwise {"resident_mb", "advisory_cache_mb", "host_free_mb", "verdict"}. resident_mb is the memory the engine’s layout holds once loaded (the
checkpoint, KV pool, per-slot state, working memory, prompt cache and vision tower,
never the lookup table); advisory_cache_mb is the page cache the lookup table
wants, about 10 GiB, which is advised and never part of resident_mb;
host_free_mb is the host’s free GTT pool, null when the host does not expose it.
verdict is the advisory fit of the first against the third: fits, tight
(within 10% of what is free), exceeds, exceeds_total (more than the whole pool)
or unknown (no host reading), and loaded when the slot already holds the memory.
It never blocks a load.
Router groups
| Prefix | Auth | Purpose |
|---|---|---|
/api/auth |
Mixed | login/status/logout are OPEN (you can’t require a cookie to obtain one); require (toggle enforcement) and rotate (mint a new admin/client key) are ADMIN; exposure (serialize this whole rule table) is ADMIN. |
/api/secrets |
ADMIN | Operator-managed secrets store. See Secrets for the routes and the server-side validation — dedicated section, not just a table row. |
/api/oauth |
Mixed | OAuth passthrough for Hermes skills (see Run agents): GET /api/oauth/providers, POST /api/oauth/{provider_id}/start (returns authorize_url), GET /api/oauth/{provider_id}/status, DELETE /api/oauth/{provider_id} (best-effort revoke, then delete the token), POST /api/oauth/{provider_id}/client-secret, and GET /api/oauth/{provider_id}/callback, the provider’s redirect target. The callback is OPEN (it carries no hal0 credential, only the provider’s single-use state nonce, which the route validates); every other route is ADMIN. Tokens and client secrets persist to /etc/hal0/api.env and are never returned. |
/api/settings |
ADMIN | Root config read/write/reload, JSON schema, apply-plan registry, schema-driven field metadata (fields), and a dry-run ChangeSet preview (preview) sharing the same diff the write path applies. |
/api/settings/models/store |
ADMIN | Model store path get/set/migrate (part of the /api/settings prefix). |
/api/settings/proxmox |
ADMIN | Proxmox integration config (host/token) + live cluster status. See Proxmox integration — no dedicated exposure rule; it inherits ADMIN from the generic /api/settings prefix. |
/api/slots |
CLIENT (list) / ADMIN (else) | Slot lifecycle — list, create, delete, restart, model swap, resolved-argv provenance by layer, metrics, capacity. Slot views carry ctx_effective, ctx_source and, for a slot file that still carries tuning keys, conversion_blocked. A slot write that carries a tuning key is refused with slot.tuning_key_denied; rebind a model with PUT /api/slots/{name}/config and {"model": {"default": ...}}. See Slot lifecycle. |
/api/models |
CLIENT (GET) / ADMIN (mutations) | Model registry — list, pull, delete, inspect, scan, catalogue, update checks, and speculative-decoding drafters (PUT /api/models/{id} takes {"draft": path|null}; DELETE /api/models/{id}/draft unpairs and deletes the file; POST /api/models/inspect-draft fit-checks a drafter against a target before download; the pull body takes draft_filename and draft_repo; POST /api/models/inspect also returns drafts and companion_repos). The registry holds no image models: a pull of an image catalog id, and a create, update or add-from-path whose path is in ComfyUI’s models folder, answer 422 model.image_asset. GET /api/models/health is registered by hardware.router, not models.router — it’s mounted ahead of /api/models so the literal path wins over the {model_id} catch-all. GET /api/models also takes installed, type, capability, device, q, fields, limit and offset to filter, project and page the list (the reply gains total); with none of them it returns the full list. See Model roster & benchmarks. |
/api/runner-images |
CLIENT (list/detail) / ADMIN (sync/pull) | Runner Image catalogue — sync from images.json, pull-job orchestration, tag removal and restart-affected-slots for GHCR-hosted runner images. |
/api/ports |
CLIENT (GET) / ADMIN (else) | Port allocation view. |
/api/hardware |
CLIENT (GET) / ADMIN (/probe) |
Hardware probe read + on-demand re-probe. |
/api/stats/hardware |
CLIENT | Live hardware probe/stats, and the memory block the dashboard’s pool, RAM and per-owner memory figures are rendered from. See the Memory budget API reference and the Hardware matrix. |
/api/stats (/slots, /requests, /throughput/history, /power) |
CLIENT (GET) / ADMIN (else) | Dispatcher rollups, throughput history, power stats. |
/api/status |
CLIENT | Version/build summary. Includes build_sha, the short git SHA of the tree the running process started from — null on a release (non-git) install. |
/api/health, /api/health/system |
OPEN | Shallow + deep liveness probes hit by the installer/systemd watchdog before any credential exists. |
/api/doctor |
ADMIN | GET /api/doctor — the same report card hal0 doctor verify --json prints, aggregated in-process from health/urls/system/memory/services, over HTTP. Classified ADMIN (not CLIENT) because it re-surfaces detail from ADMIN-only subsystems. |
/api/metrics |
CLIENT | JSON metrics snapshot. |
/api/metrics/prometheus |
OPEN | Prometheus scrape target. |
/api/features |
CLIENT (GET) | Feature-flag surface. GET /api/features is the only route health.py registers on this prefix — no mutating route is mounted, so exposure.py’s ADMIN-mutations rule is currently latent, not backing a real endpoint. The CLIENT rule is an exact match on /api/features only, so a hypothetical GET /api/features/<sub> would fall through to the prefix rule and resolve ADMIN. |
/api/system-info, /api/system-stats |
CLIENT | Host system info / fleet metrics over the SQLite tables (OBS-1). |
/api/capabilities |
ADMIN | Capability-slot configuration (embed/rerank/stt/tts/vision child slots). GET takes section (backends, catalogs, selections; comma list) and summary=true to shrink the payload; with neither it returns everything. |
/api/backends |
CLIENT (GET) / ADMIN (else) | Backend introspection (NPU/GPU-Vulkan/GPU-ROCm/CPU status) + NPU load/unload. |
/api/npu |
CLIENT (GET) / ADMIN (else) | NPU trio swap-status for the “swap incoming” dashboard banner. |
/api/providers |
ADMIN | Local provider entries (providers.toml). See Providers, profiles & devices. |
/api/upstreams |
ADMIN | Remote/cloud upstream entries (upstreams.toml), including credential write and connectivity test. Mounted on the same router as /api/providers. Each entry also carries managed_by (null, or agent:hermes for the per-profile Hermes upstreams hermes and hermes.<profile>), pinned and owned_models. An agent-managed upstream is read-only here: PATCH, DELETE and the credential write return 400 upstream.protected; change it through the agent. |
/api/stacks |
ADMIN | Stack bundles — list, create, apply, import/export, snapshot. |
/api/profiles |
CLIENT (GET) / ADMIN (mutations) | Profiles, the tune a slot launches a model with (flags, ctx_size, enable_thinking, vision, speculation, runner) — list (each entry carries used_by, the slot names), create, update, import/export (envelope schema 2). speculation is auto, off or ngram; a body that still sends the retired mtp is 422 profiles.field_renamed. |
/api/benchmarks |
ADMIN | Benchmark queue, run status, control. See Model roster & benchmarks. |
/api/chat-templates |
ADMIN | Chat-template catalog — bundled + operator-added templates. |
/api/logs |
ADMIN | Secret-redacted log tail. |
/api/mcp |
ADMIN | MCP server lifecycle/introspection — not tool invocation. See MCP tools for the full endpoint table. |
/api/comfyui |
ADMIN | ComfyUI generation-engine status and image-gen slot control, plus the image-model catalog. GET /api/comfyui/models lists the catalog with each model’s install state (none, partial, complete), the size still to download and in total (missing_gb and approx_gb; a GB is 2^30 bytes here, as in the memory budget) and its latest install job, plus a top-level engine: {available, reason} (available is false with a reason on a host with no ROCm lane; the catalog is listed either way); POST /api/comfyui/models/{model_id}/install starts a download and returns 202 with the job (it also clears a removal DELETE recorded), and DELETE /api/comfyui/models/{model_id} removes the model’s weight files and workflow and answers {"removed": [...], "kept": [{"relpath": ..., "models": [...]}], "affected": [...]}: a weight file another complete model loads stays and is listed in kept with the models that hold it, and ?include_shared=true (default false) deletes it too, lists in affected the models that were complete and no longer are, and deletes their workflows. A delete records the removal, so a model that stays complete on shared weights is not given its workflow back at the next start. An install is refused with 404 comfyui.model_unknown, 409 comfyui.install_running, 409 comfyui.model_installed (every weight is on disk and its workflow is provisioned; a model with the weights but no workflow is accepted, downloads nothing and writes the workflow), 409 comfyui.staging_cross_device, 409 comfyui.models_dir_not_writable (a destination directory the hal0 user cannot write, or a models tree it cannot read, which GET /models and DELETE also answer; error.details.path names it) or 507 comfyui.insufficient_disk. A delete is refused with 404 comfyui.model_unknown, with 409 comfyui.install_running while that model’s install, or the install of a model sharing a weight file with it, is active, and with 409 comfyui.remove_failed when a file cannot be deleted (error.details has removed, the paths deleted before the failure, failed_path and error). GET /api/comfyui/jobs lists the latest install job of each model (state, bytes_downloaded, bytes_total), POST /api/comfyui/jobs/{model_id}/cancel stops the model’s install (model_id is the model’s id, not the job’s id; 404 comfyui.job_not_found when the model has no job), and GET /api/comfyui/files lists the weight files under the models tree with the catalog models that own each. DELETE /api/comfyui/files/{subdir}/{relpath} deletes one file no catalog model loads and answers {"deleted": "<subdir>/<relpath>"}; it is refused with 400 comfyui.file_path_invalid (subdir is not a model directory, or relpath is absolute, climbs out with .., has a hidden part or does not name a weight file: .safetensors, .ckpt, .pt, .pth, .gguf, .bin or .sft), 400 comfyui.file_symlink, 404 comfyui.file_not_found, 409 comfyui.file_catalog_owned (error.details.models names the owners) or 403 comfyui.file_permission. The model rows’ fit verdicts, GET /api/comfyui/models/{model_id}/fit, the arbiter’s phase fields, progress, and the GPU gate’s gate and gate_refusal (POST /api/comfyui/gate-refusal is the gate’s own) are in the Image readiness API reference. |
/api/install |
BOOTSTRAP | First-run wizard endpoints, including GET /api/install/curated-models. Open until an admin key is configured, then treated as ADMIN. |
/api/memory |
ADMIN | Two routers share this prefix: the [memory.graph] gate/status, and the Hindsight engine admin surface (banks, graph, recall/reflect, mental models, directives, async operations). Every DELETE under this prefix is pinned ADMIN by an explicit rule independent of the generic one (issues #1024/#1302 — a prior unauthenticated bank-delete cascade-dropped ~632 live records); bank-scoped deletes additionally require a ?confirm= echo + dry-run preview. GET /api/memory/graph/status reports the engine target as target ({kind: "slot"|"upstream", model, alias: "hal0/memory"}), the upstream ids the engine can run on as available_models ([{id, owned_by}]) and the LLM hindsight-api really loads as engine_llm ({provider, base_url, model, overridden, override_file, override_keys, source}; read from the running hindsight-api process when hal0-api can (source: "process"), else from the unit plus /etc/hal0/hindsight-llm.env as systemd would apply them at the next start ("unit+file", or "unit" when there is no readable file); overridden is true when that file sets the provider, base URL or model, and null when hal0-api can read neither the process nor the file, which the dashboard and CLI show as “cannot verify”). The status call also fills an empty upstream model cache (after a hal0-api restart) by fetching each provider’s catalog, retrying an offline provider at most once a minute, so available_models is populated on the first poll. PUT /api/memory/graph accepts extraction_model (an upstream model id, or null for the local extraction_slot); an id no configured upstream advertises is refused with 422 config.memory_graph_model_invalid and neither a slot nor a model change restarts the engine. A bank_id under /api/memory/banks/{bank_id}/… may contain dots (coding-agent::ultrareview-1.4) as well as colons; a leading dot, a dot-only segment and any .. return 400 memory.invalid_bank. |
/api/board |
ADMIN | Operator Board — a live, audited proxy to the Hermes kanban plugin: task/board CRUD, links, dispatch, and WS /api/board/events. Frozen FE↔BE contract (ui/CONTRACTS.md §4). Needs a running Hermes: 503 board.not_installed (no Hermes agent), 503 board.unreachable (dashboard not answering), 503 board.no_token (token file missing); Hermes’s own errors come back as board.upstream_error (4xx passed through, 5xx as 502). |
/api/brain |
ADMIN | POST /api/brain/chat (SSE) — the primary hal0-brain platform-steward chat surface. Drives the full admin tool surface (slot mutations, model pulls, board writes), hence ADMIN. |
/api/updates |
ADMIN | Self-update: release-manifest check, prepare/commit/apply/rollback, channel get/set, post-update slot-drift report + restart. Job state is also kept under /var/lib/hal0/update-jobs/: a job read back from there after a hal0-api restart that is still queued or running was killed with the old process and reads failed with error_code: "api_restarted" and an error that says what was interrupted (“hal0-api restarted during the repair; the live release is unchanged” for a Hermes repair, “… during the build; …” for a Hermes converge, the one atomic builder, and “hal0-api restarted before this job finished; check hal0 update status before retrying” for any other job); a finished job reads as it was. |
/api/events |
ADMIN | Dashboard dock event feed — backfill + GET /api/events/stream (SSE). See the auth-tier note above. |
/api/activity |
ADMIN | Durable audit surface (SQLite-backed) — the source of truth for the dock’s activity rows (kind=action), with /export (csv/json) and /stream (SSE). See the auth-tier note above. |
/api/journal |
ADMIN | Unified journal panel — flattens /api/events into one envelope for the dashboard journal view. See the auth-tier note above. |
/api/images |
ADMIN | Cache for PNGs written by POST /v1/images/generations. ADMIN because a cached-image URL could otherwise leak a prompt via filename. |
/api/agents |
ADMIN | Bundled-agent lifecycle (install/uninstall/list/status), personas, service restart, Hermes gateway restart and Hermes repair (see the Agents API reference), memory stats, and Hermes command approvals (GET/PUT /api/agents/hermes/command-approvals). The command-approvals PUT also refuses a request whose X-hal0-Agent header names an agent. POST /api/agents/hermes/reconcile-upstreams re-registers the Hermes profiles as upstreams on demand (it runs the profile API-key sweep first) and returns {ok: true, ...report}; 404 agent.not_installed when Hermes is not installed. hal0-api also reconciles every 120 s and after an agent install or uninstall. The agent chat proxy is gone: /api/agents/{id}/events, /submit and /session/handshake|create|resume|history return 404. GET /api/agents/{name}/overview is the state behind the Hermes and Pi cards (see the Agents API reference). |
/api/agent/approvals |
ADMIN | Gated-tool approval inbox — every method (GET list, POST approve/deny, GET /events SSE) is ADMIN; exposure.py’s rule on this prefix carries no method filter. Approve and deny also check the caller themselves at every posture: a key of any tier gets 403 auth.forbidden, and only a session cookie decides (who can decide). |
/api/bridge |
BRIDGE (GET /me) / ADMIN (else) |
Agent bridge clients for remote coding agents (ADR-0043) and the runs they delegate to Hermes; every route, the bridge.* error codes, the output paging rule and which routes work while [bridge].enabled is false are in the Agent bridge API reference. |
/api/config |
Mixed | GET /api/config/urls (dashboard bootstrap hostnames) is OPEN; the rest of this prefix is ADMIN (may hold config-derived secrets). |
/api/meta |
CLIENT (GET) / ADMIN (else) | GET /api/meta/enums — the canonical device/backend/capability vocabulary from hal0.model_meta. |
/api/hf |
CLIENT (GET) / ADMIN (else) | HuggingFace Hub search proxy backing the dashboard’s “Search HF” button. |
/api/services |
ADMIN | services_health.router (GET /api/services/health, stable overview iterating hal0.services.registry.SERVICES — comfyui, hermes, hindsight, openwebui) and services_routes.router (full per-service detail + lifecycle actions + mDNS) share this prefix. |
/api/dashboard/plugins |
ADMIN | Hermes dashboard-plugin manifest (kanban plugin discovery for <AgentView>). |
/api/openrouter |
ADMIN | OpenRouter OAuth callback scaffold — only mounted when HAL0_OPENROUTER_OAUTH_ENABLED is truthy; 501 placeholder otherwise absent from the route table entirely. |
POST /api/memory/add returns 503 memory.engine_unavailable when the
memory engine connection fails or connecting times out. The message asks the
caller to retry shortly; error.details.retry_after_s and Retry-After are
both 5 seconds. An error response from the engine is not reclassified as
engine unavailability.
Every prefix above is backed by at least one route confirmed directly against
src/hal0/api/routes/ and the mount list in src/hal0/api/__init__.py. This brings
documented coverage to every /api/* prefix currently mounted (previously 18 of
roughly 43). WS /v1/realtime is documented separately — see
Realtime API reference.
Secrets and Proxmox
The /api/secrets routes and their server-side validation, and the
/api/settings/proxmox routes, are documented in
REST API: secrets and Proxmox credentials.
Agent overview, restart and repair
GET /api/agents/{name}/overview (the state behind the Hermes and Pi cards),
POST /api/agents/hermes/restart, POST /api/agents/hermes/gateway/restart and
POST /api/agents/hermes/repair (a tracked job, served by
GET /api/updates/status/{id}) are documented in the
Agents API reference.