Config schema reference
hal0 config lives under /etc/hal0/ (or $HAL0_HOME/etc/hal0/ in a dev install — see
Environment variables). Config files are TOML, parsed
with tomllib/validated with Pydantic models in hal0.config.schema, except
hardware.json and manifest.json, which are JSON. All writes go through an
atomic tempfile-write + fsync + os.replace (write_toml_atomic/write_env_atomic),
so a crash mid-write can’t corrupt a config file.
| File | Format | Purpose |
|---|---|---|
/etc/hal0/hal0.toml |
TOML | Main config — [meta], [slots], [dispatcher], [telemetry], [models], [memory], [activity], [brain_chat], [security], [realtime]. |
/etc/hal0/slots/<name>.toml |
TOML | One file per slot. |
/etc/hal0/providers.toml |
TOML | Local cloud-provider API entries ([[provider]]). |
/etc/hal0/upstreams.toml |
TOML | Remote/cloud LLM upstream entries ([[upstream]]). |
/etc/hal0/profiles.toml |
TOML | Named backend-tuning profiles. Optional — seeds apply if absent. |
/etc/hal0/stacks.toml |
TOML | Named stack bundles (slot + profile + model sets). Optional. |
/etc/hal0/agents/<name>.toml |
TOML | Per-agent MCP allowlist/config. |
/etc/hal0/hardware.json |
JSON | Hardware probe snapshot, written by hal0 config hardware --refresh (formerly hal0 probe). |
/etc/hal0/manifest.json |
JSON | Release manifest — toolbox image digest pins. |
/etc/hal0/api.env, /etc/hal0/openwebui.env |
env-file (KEY=value) |
systemd EnvironmentFile= — secrets/keys, not part of the Pydantic schema. |
hal0.toml
Section titled “hal0.toml”Root model Hal0Config, extra="allow".
[meta]
Section titled “[meta]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
schema_version |
int | 1 |
>= 1 |
[slots]
Section titled “[slots]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
max_slots |
int | 0 (unlimited) |
>= 0 |
port_range_start |
int | 8081 |
1024-65535 |
port_range_end |
int | 8099 |
1024-65535; must be >= port_range_start |
idle_timeout_s |
int | 300 |
>= 0 |
evict_pressure_mb |
int | 8192 |
>= 0 |
preload_evict_enabled |
bool | true |
— |
preload_evict_headroom_mb |
int | 1024 |
>= 0 |
publish_host |
str | "127.0.0.1" |
non-empty, no whitespace/:// |
network_mode |
str | "" |
must be "" or "host" |
[dispatcher]
Section titled “[dispatcher]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
prefetch_timeout_s |
float | 8.0 |
> 0.0 |
direct_read_timeout_s |
float | 300.0 |
30.0-600.0 |
prefetch_parallel_cap |
int | 4 |
>= 1 |
[telemetry]
Section titled “[telemetry]”extra="forbid". Opt-in.
| Key | Type | Default | Constraints |
|---|---|---|---|
enabled |
bool | false |
— |
channel |
"stable"|"preview"|"nightly" |
"stable" |
— |
[models]
Section titled “[models]”extra="allow".
| Key | Type | Default | Constraints |
|---|---|---|---|
roots |
list[str] | [models_dir()] = ["/var/lib/hal0/models"] |
each entry non-empty and absolute |
auto_scan_on_start |
bool | true |
— |
file_extensions |
list[str] | [".gguf", ".safetensors"] |
— |
pull_root |
str | "/var/lib/hal0/models" |
deprecated; absolute or empty |
store |
str | "" |
absolute if set, else empty |
flm_store |
str | "" |
absolute if set (NPU/FLM model store) |
effective_store() resolves to store if set, else pull_root. scan_roots() returns
roots plus the effective store, deduplicated. Env override: HAL0_MODEL_STORE
(precedence: env → store/effective_store() → models_dir() default). NPU store env
override: HAL0_FLM_MODELS_DIR.
[memory]
Section titled “[memory]”extra="allow".
| Key | Type | Default | Constraints |
|---|---|---|---|
enabled |
bool | true |
— |
unified_bank |
bool | true |
— |
engine |
str | "hindsight" |
one of hindsight, mem0, pgvector — "cognee" is explicitly rejected (retired) |
graph |
table | — | see below |
embedding |
table | — | see below |
[memory.graph]
Section titled “[memory.graph]”extra="ignore".
| Key | Type | Default | Constraints |
|---|---|---|---|
enabled |
bool | false |
informational only |
extraction_slot |
str | "utility" |
regex ^[a-z0-9][a-z0-9_-]{0,31}$ |
llm_timeout_s |
int | 300 |
30-3600 |
[memory.embedding]
Section titled “[memory.embedding]”extra="ignore".
| Key | Type | Default | Constraints |
|---|---|---|---|
rerank_gateway_url |
str | "http://127.0.0.1:8080" |
non-empty |
rerank_model |
str | "builtin.jina-reranker-v1-tiny-en-q8" |
non-empty |
rerank_connect_timeout_s |
float | 1.0 |
0.05-10.0 |
rerank_read_timeout_s |
float | 8.0 |
0.05-60.0 |
[activity]
Section titled “[activity]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
enabled |
bool | true |
— |
retention_days |
int | 30 |
>= 1 |
max_rows |
int | null | 50000 |
>= 100; null disables the cap |
[brain_chat]
Section titled “[brain_chat]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
enabled |
bool | true |
— |
read_only |
bool | true |
— |
model |
str | "" |
empty → persona default |
max_rounds |
int | 8 |
1-100 |
completion_timeout_s |
float | 300.0 |
> 0 |
[security]
Section titled “[security]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
require_auth |
bool | null | null (resolves to off) |
HAL0_REQUIRE_AUTH env wins if set |
trust_forwarded_for |
bool | null | null (resolves to off) |
HAL0_TRUST_FORWARDED_FOR env wins if set |
[realtime]
Section titled “[realtime]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
enabled |
bool | true |
— |
sample_rate |
int | 24000 |
8000-48000 |
default_model |
str | "" |
— |
stt_model |
str | "" |
— |
tts_model |
str | "kokoro" |
— |
tts_voice |
str | "" |
— |
vad_energy_threshold |
float | 0.02 |
0.0-1.0 |
vad_silence_ms |
int | 500 |
50-10000 |
vad_min_speech_ms |
int | 200 |
0-10000 |
vad_window_ms |
int | 20 |
5-100 |
frame_ms |
int | 20 |
5-200 |
approval_wait_s |
float | 20.0 |
> 0, <= 300.0 |
max_buffer_seconds |
float | 30.0 |
> 0, <= 600.0 |
slots/<name>.toml
Section titled “slots/<name>.toml”Root model SlotConfig, extra="allow". Both a legacy nested [slot] table and flat
top-level scalars are accepted on load and normalized.
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
id |
int | null | null |
— | >= 1 |
name |
str | — | required | regex ^[a-z0-9][a-z0-9_-]{0,31}$ |
port |
int | — | required | 8081-8200 |
device |
str | "gpu-rocm" |
— | one of gpu-rocm, gpu-vulkan, gpu-cuda, cpu, npu |
gpu_index |
int | null | null |
— | >= 0 |
n_gpu_layers |
int | -1 |
— | -1 = all, 0 = CPU-only |
threads |
int | 0 |
— | >= 0; 0 = unset |
binary |
str | "" |
— | key into RUNNER_IMAGES |
image_pin |
str | null | null |
— | escape-hatch image ref override |
provider |
str | "llama-server" |
— | one of llama-server, flm, moonshine, kokoro, qwen3tts, comfyui; deprecated, legacy label only — all slots run as podman containers now |
runtime |
"container" |
"container" |
— | only value; deprecated field |
profile |
str | null | null |
— | references profiles.toml |
parallel |
int | null | null |
— | >= 1; inert (sunset) |
chat_template |
str | null | null |
— | inert (sunset) |
default_voice |
str | null | null |
— | TTS slots |
default_speed |
float | null | null |
— | 0.25-4.0 |
default_response_format |
str | null | null |
— | one of mp3, wav, opus, flac, pcm |
workers |
int | 1 |
— | >= 1; deprecated/inert |
idle_timeout_s |
int | 300 |
— | >= 0; 0 disables IDLE-TTL eviction for this slot only — pressure and pre-load eviction are separate paths, exempted only by pinned. Read via the raw-dict sweep path (not the typed loader), an out-of-range value there is clamped/floored at read time, not errored — the typed POST/PUT API path still hard-rejects via pydantic’s ge |
pinned |
bool | false |
— | exempts the slot from auto-eviction; blocks manual unload without force |
autoload |
bool | null | null |
— | true → the Quadlet unit gets [Install] WantedBy=hal0.target and the slot starts at boot; false → unit exists, nothing starts it. null (key absent) is the migration shim: derived from a non-empty model.default. POST /api/slots persists an explicit value (false) |
priority |
int | 50 |
— | 0-100; eviction order, lowest evicted first, last_used breaks ties. 100 is not a pin — use pinned. Read via the raw-dict sweep path, an out-of-range TOML value is clamped to 0-100 at read time, not errored — the typed POST/PUT API path still hard-rejects via pydantic’s ge/le |
model |
table | — | — | [model], see below |
server |
table | — | — | [server], see below |
npu |
table | null | null |
— | [npu], see below |
image (alias image_gen) |
table | — | — | [image], see below |
extra |
dict | {} |
— | catch-all |
There is no enabled field — a non-empty model.default is the activation signal.
Boot start is separate and explicit (autoload); a retired lru key is still accepted
but ignored, with a one-time deprecation warning. See
Slot lifecycle → Activation.
[model] (nested under a slot)
Section titled “[model] (nested under a slot)”extra="allow".
| Key | Type | Default | Constraints |
|---|---|---|---|
default |
str | "" |
model id from the registry |
context_size |
int | null | null |
>= 128. On the v1.0 branch this is a ceiling only — the model’s own context window is authoritative; effective = min(model_window, slot_value), this value never raises it. |
n_gpu_layers |
int | -1 |
inert (sunset) |
rope_freq_base |
float | 0.0 |
>= 0.0; deprecated, ignored |
extra |
dict | {} |
— |
[server] (nested under a slot)
Section titled “[server] (nested under a slot)”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
extra_args |
str | null | null |
inert (sunset) |
env |
dict[str,str] | null | null |
keys must match ^[A-Za-z_][A-Za-z0-9_]*$; values must not contain \n/\r |
[npu] (nested under a slot)
Section titled “[npu] (nested under a slot)”extra="forbid".
| Key | Type | Default |
|---|---|---|
asr |
bool | false |
embed |
bool | false |
chat |
bool | true |
[image] (nested under a slot, alias for image_gen)
Section titled “[image] (nested under a slot, alias for image_gen)”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
idle_restore_minutes |
int | 60 |
>= 0 |
default_size |
str | "1024x1024" |
— |
default_steps |
int | 0 |
>= 0 |
providers.toml
Section titled “providers.toml”Root model ProvidersConfig, extra="allow": provider: list[ProviderEntry] (default []).
[[provider]]
Section titled “[[provider]]”extra="forbid".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
catalog_id |
str | — | required | non-empty |
name |
str | "" |
— | display override |
base_url |
str | "" |
— | — |
auth_value_env |
str | "" |
— | name of the env var holding the API key — the key itself is never stored in TOML |
enabled |
bool | true |
— | — |
models |
list[str] | [] |
— | — |
upstreams.toml
Section titled “upstreams.toml”Root model UpstreamsConfig, extra="allow": upstream: list[UpstreamEntry]
(default []); names must be unique.
[[upstream]]
Section titled “[[upstream]]”extra="forbid".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
name |
str | — | required | non-empty |
kind |
str | "remote" |
— | slot or remote |
url |
str | — | required | non-empty |
auth_style |
str | "bearer" |
— | bearer, anthropic, google_query, header, none |
auth_header |
str | "" |
— | required if auth_style = "header" |
auth_value_env |
str | "" |
— | — |
timeout_seconds |
float | 300.0 |
— | > 0.0 |
slot_name |
str | null | null |
— | required if kind = "slot" |
warmup_strategy |
str | "none" |
— | none, ondemand, always; aliases lazy → ondemand, eager → always |
advertise_models |
bool | true |
— | — |
enabled |
bool | true |
— | — |
model_filters |
table | null | null |
— | see below |
[[upstream]].model_filters
Section titled “[[upstream]].model_filters”extra="forbid".
| Key | Type | Default | Notes |
|---|---|---|---|
models |
list[str] | [] |
exact id list |
include |
list[str] | [] |
fnmatch globs |
exclude |
list[str] | [] |
fnmatch globs; always wins over include |
profiles.toml
Section titled “profiles.toml”Root model ProfilesConfig, extra="forbid": profile: dict[str, ProfileConfig] keyed
by name. Seed profiles are always reapplied from code on load and overlay any on-disk
copy — they can’t be edited or deleted, only cloned.
[profile.<name>]
Section titled “[profile.<name>]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
flags |
str | "" |
bench-tuned llama-server CLI flags |
mtp |
bool | false |
informational only — MTP is model-owned, not profile-owned |
device_class |
"gpu"|"cpu"|"npu"|"img" | null |
null |
null = device-agnostic |
backend |
"rocm"|"vulkan"|"cuda" | null |
null |
|
cloned_from |
str | null | null |
provenance |
intent |
str | "" |
dashboard card headline |
quant |
str | "" |
display chip only — runtime reads quant from the model, not the profile |
Profiles no longer carry an image field — image resolution is slot-owned
(slot.image_pin or RUNNER_IMAGES[slot.binary]).
stacks.toml
Section titled “stacks.toml”Root model StacksConfig, extra="forbid": stack: dict[str, StackConfig] keyed by
slug. STACK_SCHEMA_VERSION_CURRENT = 1.
[stack.<slug>]
Section titled “[stack.<slug>]”extra="forbid".
| Key | Type | Default |
|---|---|---|
name |
str | "" |
description |
str | "" |
author |
str | "" |
icon |
str | "" |
tags |
list[str] | [] |
schema_version |
int | 1 |
hal0_version |
str | "" |
slots |
list[table] | [] — see StackSlotEntry below |
profiles |
dict[str, table] | {} — ProfileConfig entries |
models |
dict[str, table] | {} — see StackModelMeta below |
StackSlotEntry
Section titled “StackSlotEntry”extra="forbid".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
slot |
str | — | required | regex ^[a-z0-9][a-z0-9_-]{0,31}$ |
profile |
str | null | null |
— | — |
model |
str | null | null |
— | — |
device |
str | null | null |
— | must be a valid device id if set |
provider |
str | null | null |
— | — |
vision |
bool | false |
— | — |
mtp |
bool | null | null |
— | — |
enable_thinking |
bool | null | null |
— | — |
server_extra_args |
str | null | null |
— | — |
capabilities |
list[table] | [] |
— | see StackCapabilityRow below |
StackCapabilityRow
Section titled “StackCapabilityRow”extra="forbid".
| Key | Type | Req | Constraints |
|---|---|---|---|
child |
str | required | e.g. embed, rerank, stt, tts, vision |
device |
str | required | must be a valid device id |
provider |
str | required | — |
model |
str | required | — |
enabled |
bool | — | default true |
StackModelMeta
Section titled “StackModelMeta”extra="forbid".
| Key | Type | Default | Req |
|---|---|---|---|
id |
str | — | required, non-empty |
name |
str | "" |
— |
hf_repo |
str | "" |
— |
hf_filename |
str | "" |
— |
size_bytes |
int | 0 |
— |
quant |
str | "" |
— |
capabilities |
list[str] | [] |
— |
backends |
list[str] | [] |
— |
mmproj |
str | null | null |
— |
agents/<name>.toml
Section titled “agents/<name>.toml”Root model AgentConfig, extra="allow". AGENT_CONFIG_SCHEMA_VERSION = 1.
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
schema_version |
int | 1 |
— | >= 1; future versions rejected |
agent |
table | — | required | [agent], see below |
mcp |
table | {} |
— | [mcp], see below |
[agent]
Section titled “[agent]”extra="allow".
| Key | Type | Default | Req | Constraints |
|---|---|---|---|---|
name |
str | — | required | regex ^[a-z0-9][a-z0-9-]{0,31}$ |
display |
str | "" |
— | — |
workspace |
str | "" |
— | empty → canonical path |
extra="allow": servers: dict[str, MCPServerConfig] (default {}).
[mcp.servers.<name>]
Section titled “[mcp.servers.<name>]”extra="allow".
| Key | Type | Default | Constraints |
|---|---|---|---|
url |
str | null | null |
required unless builtin = true |
enabled |
bool | true |
— |
builtin |
bool | false |
— |
auth |
table | {} |
see below |
tools |
table | {} |
see below |
[mcp.servers.<name>.auth]
Section titled “[mcp.servers.<name>.auth]”extra="forbid".
| Key | Type | Default | Constraints |
|---|---|---|---|
kind |
"none"|"bearer-from-env" |
"none" |
— |
env |
str | null | null |
required if kind = "bearer-from-env" |
[mcp.servers.<name>.tools]
Section titled “[mcp.servers.<name>.tools]”extra="forbid".
| Key | Type | Default |
|---|---|---|
allow |
list[str] | [] |
gated |
list[str] | [] |
blocked |
list[str] | [] |
allow, gated, and blocked must be disjoint — validation raises naming the
overlapping tool names.
hardware.json
Section titled “hardware.json”Root model HardwareInfo, extra="allow" (JSON, not TOML). Written by
hal0 config hardware --refresh.
| Key | Type | Default | Constraints |
|---|---|---|---|
hostname |
str | "" |
— |
uptime_s |
int | 0 |
>= 0 |
kernel |
str | "" |
— |
distro |
str | "" |
— |
cpu_model |
str | "" |
— |
cpu_cores |
int | 0 |
>= 0 |
cpu_threads |
int | 0 |
>= 0 |
ram_mb |
int | 0 |
>= 0 |
ram_available_mb |
int | 0 |
>= 0 |
swap_mb |
int | 0 |
>= 0 |
unified_memory_mb |
int | 0 |
>= 0 — the true UMA pool; use this, not ram_mb + vram_mb, on unified-memory systems |
gpus |
list[table] | [] |
see GPUInfo below |
gpu_group_gids |
dict[str,int] | {} |
resolved render/video group ids |
npu |
table | {} |
see NPUInfo below |
disk_free_mb |
int | 0 |
>= 0 |
cgroup_max_mb |
int | null | null |
>= 0 |
probed_at |
str | "" |
ISO-8601 |
platform |
str | "unknown" |
one of strix-halo, wsl2, proxmox-kvm, kvm, lxc, bare-metal-amd-gpu, bare-metal-nvidia-gpu, bare-metal-intel-igpu, bare-metal-cpu-only, unknown |
extra |
dict | {} |
— |
GPUInfo
Section titled “GPUInfo”extra="allow".
| Key | Type | Default |
|---|---|---|
vendor |
str | "" |
index |
int | 0 |
name |
str | "" |
vram_mb |
int | 0 |
pci_id |
str | "" |
driver |
str | "" |
drm_path |
str | "" |
compute_capable |
bool | false |
vulkan_capable |
bool | false |
NPUInfo
Section titled “NPUInfo”extra="allow".
| Key | Type | Default |
|---|---|---|
present |
bool | false |
vendor |
str | "" |
name |
str | "" |
driver |
str | "" |
accel_path |
str | "" |
render_path |
str | "" |
aie_columns |
int | 0 |
validated |
bool | null | null |
manifest.json / env files
Section titled “manifest.json / env files”manifest.json holds toolbox container image digest pins keyed by backend. api.env
and openwebui.env are systemd EnvironmentFile=-format KEY=value files, not part
of the Pydantic schema — see Environment variables for
what lives in api.env (admin/client bearer keys, etc).