config.toml Reference
Complete field reference for $HOZIRON_HOME/config.toml (default ~/.hoziron/config.toml).
Resolution
The config file location is resolved in this order:
HOZIRON_HOME environment variable → $HOZIRON_HOME/config.toml
~/.hoziron/config.toml (falls back to the system temp dir joined with
.hoziron if the user's home directory can't be resolved — this matters
in containers, see environment-variables.md)
--config <path> on hoziron-server overrides both, for a one-off custom path
Two structs, one file
hoziron-server reads the config file once and deserializes it into two
independent structs — every section belongs to exactly one of them, and
unrecognized sections are silently ignored by the other (both use
#[serde(default)] throughout, no deny_unknown_fields at the top level):
hoziron-server's own ServerConfig — [surfaces.*], plus the shared
[server]/[auth]/[audit]/[catalog] sections. Decides which HTTP
surfaces (API, registry, MCP, dashboard) bind where.
hoziron_core::PlatformConfig — everything else: [[provider]],
[routing_gateway], [health], [trust], [competency_manager],
[run_summary_manager], [tool_mediation], [cron_auto_tick], [background_agents],
[telemetry], licence_path, pii_policy_path, home_region, plus its
own copies of [server]/[auth]/[audit]/[catalog] (same TOML
sections, read independently by each struct).
In practice you write one config.toml and never need to think about the
split — it only matters if you're grepping the Rust source for a field and
wondering why it isn't on the struct you expected.
Full Example
# ── Surfaces (which HTTP endpoints this hoziron-server process serves) ───
# Each surface is either a *main route* (`listen`, owns a listener, may set
# `tls`/`allowed_ips`/`limits`) or *mounted* on another surface's listener
# (`mount`, shares its hardening, must not set its own tls/allowed_ips/limits).
# See "surfaces.*" section reference and tls-and-networking.md for the full
# topology rules (Issue #682).
[surfaces.api]
enabled = true
listen = "0.0.0.0:4200"
base_path = "/"
# allowed_ips = ["10.0.0.0/8", "172.16.0.0/12"]
[surfaces.api.tls]
enabled = false
# cert_path = "/path/to/cert.pem"
# key_path = "/path/to/key.pem"
[surfaces.api.limits]
max_request_body_bytes = 10485760 # 10 MB
request_timeout_secs = 600 # 10 minutes
[surfaces.registry]
enabled = false
listen = "0.0.0.0:4210"
base_path = "/"
[surfaces.mcp]
enabled = false
mount = "api" # shares the API surface's listener/tls/allowed_ips/limits
base_path = "/mcp"
[surfaces.dashboard]
enabled = false
mount = "api" # shares the API surface's listener/tls/allowed_ips/limits
base_path = "/dashboard"
# ── Provider / model inventory (ADR-053) ──────────────────────────────────
# [[provider]] is the *only* eligible provider/model set the routing gateway
# considers. Each entry declares an explicit `driver` (never inferred from
# `id`). id is an arbitrary, operator-chosen, unique instance identifier —
# use it to run two instances of the same driver (e.g. "ollama.west" /
# "ollama.east").
[[provider]]
id = "anthropic"
driver = "anthropic"
api_key_env = "ANTHROPIC_API_KEY"
[[provider.model]]
id = "claude-sonnet-4-5-20250929"
[[provider]]
id = "ollama"
driver = "ollama"
url = "http://127.0.0.1:11434/v1"
[[provider.model]]
id = "llama3.2:1b"
# Provider URL overrides for the agent-manifest-driven driver dispatch path
# (an agent manifest naming an arbitrary model.provider string). Unrelated to
# the ADR-053 inventory above.
[provider_urls]
# openai-compatible = "https://api-proxy.internal.company.com/v1"
# Optional: override the HTTP client timeouts every provider driver's
# underlying reqwest::Client uses. Omit both to keep the built-in defaults
# (request: 600s, connect: 30s) — a last-resort backstop against a hung
# socket, well above any legitimate step-level timeout. Applied once at
# boot; changing these requires a daemon restart.
# provider_request_timeout_secs = 600
# provider_connect_timeout_secs = 30
# Optional: tune the retry budget for LLM calls that hit a provider
# rate-limit (429) or overload (529/503) error. The real `retry-after`
# header (when the provider sends one) is always honored as a floor —
# these settings only govern the exponential backoff/jitter and overall
# deadline around it. Omit any/all to keep the built-in defaults, which
# are sized to ride out a multi-minute provider outage. Applied once at
# boot; changing these requires a daemon restart.
# llm_retry_max_attempts = 8 # includes the first try
# llm_retry_base_delay_ms = 1000 # first retry's backoff
# llm_retry_max_delay_ms = 60000 # per-attempt backoff ceiling
# llm_retry_max_total_secs = 180 # cumulative retry deadline for one call — keep well under the 300s dispatch-turn timeout
# ── Routing gateway (ADR-034 / ADR-054) ───────────────────────────────────
# Every tier's `preference` is a ranked list over the inventory above.
# `local` is required whenever the inventory has any Local-sovereignty
# provider; `standard` is required whenever the inventory is non-empty at
# all; `economy` is optional (disabled — max_chars/max_tools both 0 — unless
# you widen it).
[routing_gateway]
default_workflow_type = "general"
always_standard_workflows = []
[routing_gateway.local]
max_chars = 500
max_tools = 0
preference = [{ provider = "ollama", model = "llama3.2:1b" }]
[routing_gateway.economy]
max_chars = 0
max_tools = 0
preference = []
[routing_gateway.standard]
preference = [{ provider = "anthropic", model = "claude-sonnet-4-5-20250929" }]
# Optional: route agent IDs to a licence workflow_type by prefix, first match wins.
# [[routing_gateway.workflow_type_map]]
# prefix = "fnol-"
# workflow_type = "fnol"
# ── Server (shared settings — CORS only; TLS/allowed_ips/limits/listen ────
# moved to per-surface config above, Issue #682) ───────────────────────────
[server.cors]
allowed_origins = ["https://dashboard.company.com"]
allow_credentials = true
max_age_secs = 3600
# ── Health monitoring (circuit breaker) ───────────────────────────────────
[health]
enabled = true
check_interval_secs = 30 # Range: 5–300
failure_threshold = 5 # Range: 1–50
recovery_cooldown_secs = 60 # Range: 10–600
# ── Authentication ─────────────────────────────────────────────────────────
[auth]
mode = "local" # disabled | local | oidc
allow_local_service_keys = false
allow_insecure_no_auth = false # explicit escape hatch (Issue #512), off by default
[auth.rate_limit]
base_backoff_secs = 1
max_backoff_secs = 300
max_failed_attempts = 10
# [auth.oidc]
# issuer = "https://login.microsoftonline.com/{tenant}/v2.0"
# audience = "api://hoziron-platform"
# jwks_uri = "" # auto-discovered from issuer if blank
# role_claim = "roles"
# allowed_algorithms = ["RS256", "ES256"]
# jwks_cache_ttl_secs = 3600
#
# [auth.oidc.role_mapping]
# "HozironAdmins" = "admin"
# "PlatformOps" = "operator"
# ── Catalog / Registry ──────────────────────────────────────────────────────
[catalog]
verify_signatures = true
packages_dir = "packages"
cache_ttl_secs = 3600
default_publish_registry = "hoziron"
[[catalog.registries]]
name = "hoziron"
url = "https://catalog.hoziron.com"
priority = 100
enabled = true
require_signatures = true
# [catalog.scopes]
# internal = "https://packages.internal.company.com"
# ── Audit trail (always on — this section only tunes retention/export) ────
[audit]
segment_max_size_mb = 64
retention_days = 0 # 0 = unlimited; deletion is opt-in (Issue #1240)
[audit.export]
enabled = false
target = "webhook" # webhook | syslog | file
endpoint = "https://siem.corp.internal/api/events"
batch_size = 100
flush_interval_secs = 5
max_retry_attempts = 5
# ── Logging (NOT WIRED UP — see note below) ─────────────────────
[logging]
format = "json" # text | json
# level = "info,hoziron_core=debug"
# ── Reachability-check URLs for the dashboard's fixed connector set
# (Issue #698) — GET /connectors/health. A connector with no entry here
# reports NotConfigured rather than Unreachable.
[connectors]
urls = { guidewire = "https://claimcenter.internal.company.com/health", salesforce = "https://mycompany.my.salesforce.com" }
# ── Agent auto-loading ─────────────────────────────────────────────────────
[agents]
manifests_dir = "/data/agents"
# ── Tool-category access boundary (ADR-049) ────────────────────────────────
# Platform-wide default; agents without their own boundary inherit this.
[trust]
allowed_tools = [] # empty = all tools permitted
# ── Competency manager model (ADR-054, always-local, operator-only) ──────
[competency_manager]
preference = [{ provider = "ollama", model = "llama3.2:1b" }]
# ── Run Summary Manager model (ADR-054, always-local, operator-only,
# Issue #803) — infers a workflow run's subject/title/party/outcome so the
# console's run list/detail screens show something meaningful instead of
# raw input or a generic fallback ──────────────────────────────────────────
[run_summary_manager]
preference = [{ provider = "ollama", model = "llama3.2:1b" }]
# ── SoR write escalation / retry (ADR-042 OQ2) ─────────────────────────────
[tool_mediation]
max_attempts = 3
initial_backoff_ms = 100
max_backoff_ms = 5000
backoff_multiplier = 2.0
# ── Automatic cron tick ─────────────────────────────────────────────────────
[cron_auto_tick]
enabled = true
interval_secs = 15
# ── Automatic condition tick (ADR-065, condition-driven agent turns) ──────
# Polls the kernel's ConditionScheduler for due matches at this interval.
# Sibling of [cron_auto_tick] above; the broader condition-scheduling
# design is still demand-gated (#571) but this tick loop and the
# PUT /conditions/jobs/{id}/enable toggle are real and live today.
[condition_tick]
enabled = true
interval_secs = 30
# ── Dormant background-agent subsystems (opt-in, all default false) ──────
[background_agents]
agent_self_prompt = false
heartbeat_monitor = false
provider_probe = false
metering_cleanup = false
memory_consolidation = false
mcp_autoconnect = false
extension_health = false
workflow_autoload = false
whatsapp_gateway = false
# ── Telemetry (only if hoziron-server was built with the `telemetry` feature) ─
[telemetry]
enabled = false
endpoint = "http://otel-collector:4317"
service_name = "hoziron"
# ── Top-level scalars ────────────────────────────────────────────────────
licence_path = "/data/licence.json" # default: $HOZIRON_HOME/licence.json
pii_policy_path = "/data/carrier-pii-policy.toml" # default: $HOZIRON_HOME/carrier-pii-policy.toml
home_region = "ZA" # ISO 3166-1 alpha-2
compliance_pack_dir = "/data/compliance/packs" # default: $HOZIRON_HOME/compliance/packs
Section Reference
[surfaces.api] / [surfaces.registry] / [surfaces.mcp] / [surfaces.dashboard]
Every surface is either a main route (owns a listener via listen) or
mounted on another surface's listener (via mount) — never both, and
never neither. This is validated once at boot across the active surface set
(validate_surface_topology, Issue #682); see
tls-and-networking.md for
the full topology rules and enforcement details.
| Field | Type | Default | Description |
|---|
enabled | bool | api: true, registry: false, mcp/dashboard: false | Whether this surface is active |
listen | string | api: 127.0.0.1:4200, registry: 127.0.0.1:4210, mcp/dashboard: unset | host:port or unix://path. Set only on a main route |
mount | string | mcp/dashboard: "api" when enabled | Name of another enabled main-route surface (api, registry, mcp, dashboard) whose listener this surface shares. Set only on a mounted surface; no mount chains |
tls | table | none | Native TLS termination for this listener. Main route only — see [surfaces.<name>.tls] below |
allowed_ips | string[] | none | IP/CIDR allowlist for this listener. Main route only. Cannot combine with a unix:// listen address (hard config error — the allowlist middleware fails open on Unix sockets, which have no TCP peer address) |
limits | table | see [surfaces.<name>.limits] below | Request body size / timeout enforcement for this listener. Main route only |
base_path | string | /, /, /mcp, /dashboard | Mount path within the shared router |
A main route's tls/allowed_ips/limits apply to the whole merged
router on that listen address, including anything mounted onto it (e.g. the
default mcp/dashboard mount onto api inherits api's TLS, allowlist,
and limits). A mounted surface setting any of these itself is a hard
config-validation error at boot.
[surfaces.<name>.tls]
Main route only.
| Field | Type | Description |
|---|
enabled | bool | Enable native TLS termination (real — axum-server + rustls, with 60s cert/key hot-reload polling) |
cert_path | string | Required when enabled = true |
key_path | string | Required when enabled = true |
[surfaces.<name>.limits]
Main route only.
| Field | Type | Default | Description |
|---|
max_request_body_bytes | int | 10485760 (10 MB) | Must be > 0. Enforced via tower_http::limit::RequestBodyLimitLayer against Content-Length, unconditionally (not axum::extract::DefaultBodyLimit, which only fires if a handler extracts the body) |
request_timeout_secs | int | 600 | Must be > 0. Enforced via tower_http::timeout::TimeoutLayer — a request running longer is cut off with 408 |
There is no idle_timeout_secs field.
[surfaces.registry] additional fields
| Field | Type | Default | Description |
|---|
storage_path | string | /data/registry/packages | Local filesystem path for package archive storage |
s3 | table | none | S3-compatible object storage config (optional) |
auth | table | registry defaults | Registry-specific auth (separate from [auth]) |
cors | table | registry defaults | Registry-specific CORS (separate from [server.cors]) |
Enable a surface with hoziron-server --surfaces api,registry or by setting
enabled = true here — CLI flag and config are independent ways to select
the same thing; see start-stop.md. Per-surface
--api-listen/--registry-listen/--mcp-listen/--dashboard-listen CLI
flags override listen and promote that surface to a main route, clearing
any configured mount.
[[provider]] (ADR-053)
| Field | Type | Required | Description |
|---|
id | string | yes | Arbitrary, operator-chosen, unique instance identifier |
driver | string | yes | Compiled-in driver name — must match a kernel-shipped driver (see table below); never inferred from id |
url | string | conditional | Endpoint URL. Required for local-runtime and Ambiguous drivers, and for any AlwaysCloud driver other than anthropic |
api_key_env | string | no | Env var name holding the API key. Must match [A-Z_][A-Z0-9_]*, 1–128 chars |
sovereignty_attestation | string | no | Carrier attestation that an Ambiguous-driver endpoint is in-boundary. Rejected on any other driver disposition |
[[provider.model]] | array | no | Models this instance serves — see below |
[providers.*] (the pre-ADR-053 table-keyed-by-id schema), [models.*],
and [default_model] are hard boot errors if present — from_toml
rejects them explicitly rather than silently ignoring them, precisely so a
stale config doesn't boot with an empty, misconfigured provider set.
[[provider.model]]
| Field | Type | Default | Description |
|---|
id | string | — | Model identifier as the driver's API expects it |
max_tokens | int | driver default | Per-request max output tokens |
temperature | float | driver default | Sampling temperature |
context_window | int | driver default | Override the model's context window size |
Driver dispositions
A driver's sovereignty disposition is a compile-time property (never
inferred from config) that gates whether it can ever serve the local tier:
| Disposition | Meaning | Drivers |
|---|
AlwaysLocal | Structurally local, no attestation needed | echo, ollama, vllm, lmstudio, lemonade |
Ambiguous | Public protocol, endpoint may be in-boundary — resolves Local only with sovereignty_attestation, otherwise fails closed to Cloud | openai-compatible |
AlwaysCloud | Never local, no attestation possible | anthropic, openai, gemini, google, groq, azure/azure-openai, bedrock, vertex-ai/vertex/google-vertex, openrouter, deepseek, together, mistral, fireworks, perplexity, cohere, ai21, cerebras, sambanova, huggingface, xai, replicate, github-copilot/copilot, codex/openai-codex, claude-code, qwen-code, moonshot/kimi/kimi2/kimi_coding, qwen/dashscope/model_studio, minimax, zhipu/glm/zhipu_coding/codegeex/zai/z.ai/zai_coding, qianfan/baidu, volcengine/doubao/volcengine_coding, chutes, venice, nvidia/nvidia-nim, novita/novita-ai, requesty, openrouter |
An unrecognized driver string is a hard boot error (unknown driver).
Only anthropic has a fixed endpoint hardcoded into dispatch — every other
AlwaysCloud driver requires url on the [[provider]] entry.
[routing_gateway]
| Field | Type | Default | Description |
|---|
default_workflow_type | string | none | Fallback workflow type when no workflow_type_map prefix matches and none is given explicitly |
always_standard_workflows | string[] | [] | Workflow types that always classify Standard regardless of payload size |
[[workflow_type_map]] | array | [] | Ordered { prefix, workflow_type } — agent ID prefix → workflow type, first match wins |
local | table | see below | Hard-walled, Local-sovereignty-only tier |
economy | table | see below | Optional cheaper/faster mid-band tier, Pool::Mixed |
standard | table | see below | Catch-all tier, Pool::Mixed |
[routing_gateway.local]
| Field | Type | Default | Description |
|---|
max_chars | int | 500 | Payload length ceiling for size-based local classification |
max_tools | int | 0 | Tool-count ceiling (0 = any tools disqualify from size-based local) |
preference | array | [] | Ranked { provider, model } over the Local pool. Required (non-empty) whenever the boot-validated inventory has any Local-sovereignty provider — boot error otherwise. Every entry must resolve to a Local-sovereignty provider; a Cloud-sovereignty reference here is a boot error, never a runtime divert |
[routing_gateway.economy]
| Field | Type | Default | Description |
|---|
max_chars | int | 0 (disabled) | |
max_tools | int | 0 (disabled) | |
preference | array | [] | Pool::Mixed — Local and/or Cloud entries both valid. Not boot-required. If you widen the thresholds, its size band must actually be reachable (dominated-by-local is a boot error) |
[routing_gateway.standard]
| Field | Type | Default | Description |
|---|
preference | array | [] | Pool::Mixed. Required (non-empty) whenever the inventory has any provider at all — it's the mandatory fallback |
local/economy/standard supersede the pre-ADR-054
routing_gateway.local_provider/local_endpoint/local_model/
local_max_tokens/local_temperature fields — those are hard boot errors
if present. A bare top-level routing_gateway.local_preference (flat, no
local. nesting) is not a recognized field either; it silently parses
as ignored TOML and then fails the "local.preference must be declared"
boot check. Always nest under [routing_gateway.local].
[server]
Shared settings independent of any specific listener — CORS only. TLS,
IP allowlist, request limits, and listen all live per-surface under
[surfaces.<name>] instead (Issue #682), since only hoziron-server knows
each surface's real bind address; there is no top-level [server].listen
anymore.
| Field | Type | Default | Description |
|---|
cors | table | none | See below |
[server.cors]
| Field | Type | Default | Description |
|---|
allowed_origins | string[] | [] | Non-empty when the section is present; each entry http(s)://… or * |
allow_credentials | bool | false | Cannot combine with a * origin |
max_age_secs | int | 3600 | Preflight cache duration |
[health]
| Field | Type | Range | Default | Description |
|---|
enabled | bool | — | true | Enable circuit-breaker health monitoring |
check_interval_secs | int | 5–300 | 30 | |
failure_threshold | int | 1–50 | 5 | Failures to trip the breaker |
recovery_cooldown_secs | int | 10–600 | 60 | Cooldown before HalfOpen probe |
[auth]
| Field | Type | Default | Description |
|---|
mode | disabled|local|oidc | disabled | Authentication mode |
allow_local_service_keys | bool | false | When mode is oidc, also allow local API keys (CI/CD, break-glass) |
allow_insecure_no_auth | bool | false | Explicit escape hatch (Issue #512) permitting mode = "disabled" on a network-reachable bind. Every boot logs a loud warning when set |
oidc | table | none | Required when mode = "oidc" |
rate_limit | table | see below | |
Boot-time posture gate: hoziron-server refuses to boot (or reload) a
config that leaves any surface fully unauthenticated (auth.mode = "disabled") on a non-loopback listen address, unless
allow_insecure_no_auth = true. hoziron-cli config set/unset (a pure CLI
config edit, not a boot) is exempt from this specific check — it will
happily write such a config to disk, but the daemon will then refuse to
boot with it.
[auth.rate_limit]
| Field | Type | Default | Description |
|---|
base_backoff_secs | int | 1 | Must be > 0 |
max_backoff_secs | int | 300 | Must be ≥ base_backoff_secs |
max_failed_attempts | u32 | 10 | Must be > 0 |
[auth.oidc]
| Field | Type | Default | Description |
|---|
issuer | string | "" | Required when mode = "oidc" |
audience | string | "" | Required when mode = "oidc" |
client_id | string | "" | Defaults to audience if empty |
client_secret | string | "" | For confidential clients |
jwks_uri | string | "" | Auto-discovered from {issuer}/.well-known/openid-configuration if blank |
role_claim | string | "roles" | Dot-path notation accepted (e.g. realm_access.roles) |
allowed_algorithms | string[] | ["RS256"] | One of RS256/RS384/RS512/ES256/ES384/PS256/PS384/PS512/EdDSA |
role_mapping | table | {} | IdP role → Hoziron role. Required non-empty when mode = "oidc"; every value must be a valid Hoziron role |
jwks_cache_ttl_secs | int | 3600 | |
[catalog]
| Field | Type | Default | Description |
|---|
verify_signatures | bool | true | |
packages_dir | string | "packages" | Relative to HOZIRON_HOME |
cache_ttl_secs | int | 3600 | |
default_publish_registry | string | none | Used by hoziron-cli catalog publish without --registry |
scopes | table | {} | @scope/name → registry URL mapping |
registries | array | one entry (see below) | |
[[catalog.registries]]
| Field | Type | Default | Description |
|---|
name | string | — | Identifier used in CLI/API |
url | string | — | Registry API base URL |
priority | int | 100 | Lower = higher priority, queried first |
token | string | none | Inline token — takes precedence over auth_token_env |
auth_token_env | string | none | Env var holding the auth token, checked if token unset |
enabled | bool | true | |
require_signatures | bool | true | Fail-closed. Set false only for a registry explicitly known not to sign packages |
Default when [catalog] is entirely omitted: one registry, name = "hoziron", url = "https://catalog.hoziron.com", priority = 100,
enabled = true, require_signatures = true.
[audit]
Audit logging is not opt-in — there is no enabled toggle for the
local audit trail itself. Every deployment produces an audit record
(PII access, routing decisions, agent lifecycle, permission checks)
unconditionally, including kernel mechanical events (dispatch, tokens,
tool calls) — those write into this same segmented chain, not a separate
store; this section only tunes retention and (optional) export.
| Field | Type | Default | Description |
|---|
segment_max_size_mb | int | 64 | Must be > 0. Segment rotation size |
retention_days | int | 0 | 0 = unlimited (default — deletion requires explicit opt-in). A non-zero value below 7 is rejected at boot as almost certainly a mistake (Issue #1240) |
export | table | see below | SIEM export — genuinely optional |
There is no max_entries field — the single-file, count-pruning
AuditStore it belonged to was removed entirely (Issue #1240); the
segmented store has no count-based pruning, only the time-based
retention_days reaper below. A config.toml still setting max_entries
has the key silently ignored (no deny_unknown_fields).
If retention_days > 0 and audit.export.enabled = false, boot logs a
warning naming the deletion horizon — data that old will be permanently
deleted by the retention reaper with nowhere else it was ever sent.
[audit.export]
| Field | Type | Default | Description |
|---|
enabled | bool | false | |
target | webhook|syslog|file | webhook | |
endpoint | string | "" | Required when enabled: http(s)://… for webhook, host:port for syslog, a file path for file |
batch_size | int | 100 | Must be > 0 |
flush_interval_secs | int | 5 | Must be > 0 |
max_retry_attempts | int | 5 | Must be > 0 |
syslog_transport | udp|tcp | udp | Only used when target = "syslog" |
headers | table | {} | Extra HTTP headers for webhook requests |
[logging]
Not wired up. PlatformConfig.logging is parsed and validated but
never read anywhere outside its own struct definition — server-side
logging is controlled purely by the HOZIRON_LOG / HOZIRON_LOG_FORMAT
env vars (see environment-variables.md), the
same as the CLI. Setting [logging] here has no effect on either format
or filter level; [backup] below gets the same treatment.
| Field | Type | Default | Description |
|---|
format | text|json | text | Not read at runtime — see note above |
level | string | none | Not read at runtime — see note above |
[connectors]
Reachability-check URLs for the dashboard Integrations region's fixed
six-connector set (Issue #698) — backs GET /connectors/health.
| Field | Type | Default | Description |
|---|
urls | map<string, string> | {} | Keyed by connector id: guidewire, salesforce, docusign, s3, stripe, sso_okta. A connector with no entry here reports NotConfigured rather than Unreachable — "not wired up" and "down" are different facts |
[agents]
| Field | Type | Default | Description |
|---|
manifests_dir | string | $HOZIRON_HOME/agents/ | Directory of agent manifest .toml files auto-loaded at boot |
[trust]
| Field | Type | Default | Description |
|---|
allowed_tools | string[] | [] | Platform-wide default tool-category allowlist. Empty = all tools permitted. Applied to agents that don't define their own boundary. Governs tool-category access only — carrier PII policy and the routing gateway are the sole authorities for PII-egress and model-selection, never this struct (ADR-049) |
[competency_manager]
| Field | Type | Default | Description |
|---|
preference | array | [] | Ranked { provider, model } over the Local pool only (ADR-054's always-local, non-gateway-mediated degenerate case). Empty falls back to the Local pool's first declared entry. A non-Local entry here is a boot error |
[run_summary_manager] (Issue #803)
| Field | Type | Default | Description |
|---|
preference | array | [] | Ranked { provider, model } over the Local pool only — same resolution mechanism as [competency_manager]. Empty falls back to the Local pool's first declared entry. An unresolvable or non-Local entry does not fail boot: the manager fails open (skips enrichment for that run, falling back to the run's raw input head or terminal state) on any resolution/call/parse failure. Not settable via any agent/workflow manifest (ADR-049) — this is a bounded, local-pool-only call that infers a run's subject/title/party/outcome at run start and completion, replacing reliance on the hoziron/run-summary agent-authored convention |
SoR (system-of-record) write escalation — retry/backoff before alerting on
transient vault/pipeline failures (ADR-042 OQ2).
| Field | Type | Default | Description |
|---|
max_attempts | u32 | 3 | Must be ≥ 1 |
initial_backoff_ms | u64 | 100 | Must be ≤ max_backoff_ms |
max_backoff_ms | u64 | 5000 | |
backoff_multiplier | f64 | 2.0 | Must be > 1.0 |
[cron_auto_tick]
| Field | Type | Default | Description |
|---|
enabled | bool | true | Whether the automatic cron-tick loop runs at boot |
interval_secs | u64 | 15 | Seconds between due-job polls |
[condition_tick] (ADR-065)
Sibling of [cron_auto_tick] for the kernel's ConditionScheduler. The
broader condition-driven-scheduling design is still demand-gated (#571),
but this tick loop and the PUT /conditions/jobs/{id}/enable toggle
(see cron.md) are real and live today.
| Field | Type | Default | Description |
|---|
enabled | bool | true | Whether the automatic condition-tick loop runs at boot |
interval_secs | u64 | 30 | Seconds between due-condition polls |
[background_agents]
Dormant background-agent subsystems (issue #497). Every flag defaults to
false — opt in individually.
| Field | Type | Default | Description |
|---|
agent_self_prompt | bool | false | Continuous/Periodic agent self-prompt loops |
heartbeat_monitor | bool | false | Reactive-agent unresponsiveness detection |
provider_probe | bool | false | Local-provider reachability probing + model-catalog discovery |
metering_cleanup | bool | false | Periodic usage/metering record cleanup |
memory_consolidation | bool | false | Also requires memory.consolidation_interval_hours > 0 (kernel-level) |
mcp_autoconnect | bool | false | MCP server auto-connect on boot |
extension_health | bool | false | Extension health monitor loop |
workflow_autoload | bool | false | Workflow auto-load from workflows_dir on boot |
whatsapp_gateway | bool | false | Also requires a configured whatsapp channel (see reference/api/channels.md — channels are no longer configured in this file) |
[telemetry] (feature-gated)
Only present on the PlatformConfig schema — and only has any effect —
when hoziron-server was built with the telemetry Cargo feature. If the
feature isn't compiled in, this field doesn't exist on the struct at all
and the section is silently ignored (zero overhead).
| Field | Type | Default | Description |
|---|
enabled | bool | false | |
endpoint | string | none | Overrides OTEL_EXPORTER_OTLP_ENDPOINT. Falls back to http://localhost:4317 if neither is set |
service_name | string | "hoziron" | |
Top-level scalars
| Field | Type | Default | Description |
|---|
licence_path | string | $HOZIRON_HOME/licence.json | Carrier licence file path |
pii_policy_path | string | $HOZIRON_HOME/carrier-pii-policy.toml | Carrier PII policy file path |
home_region | string | none | ISO 3166-1 alpha-2 code (e.g. "ZA"). Warns (doesn't block) on catalog install when a package's declared regions don't include it. Does not narrow PII detection — pattern loading is deliberately unconditional across all regions (ADR-049) |
compliance_pack_dir | string | $HOZIRON_HOME/compliance/packs | Directory of carrier-supplied compliance policy packs (ADR-018), loaded alongside the built-in POPIA pack — see compliance.md |
Validation Rules
- Unknown
driver on any [[provider]] entry is a hard boot error
[providers.*], [models.*], [default_model], and
routing_gateway.local_provider/local_endpoint/local_model/
local_max_tokens/local_temperature are hard boot errors (ADR-053)
api_key_env values must match [A-Z_][A-Z0-9_]*, 1–128 chars
provider_urls values and telemetry.endpoint must be http:// or https://
provider_request_timeout_secs and provider_connect_timeout_secs must be
0 when set (omit the field entirely to use the default)
llm_retry_max_attempts, llm_retry_base_delay_ms, llm_retry_max_delay_ms,
and llm_retry_max_total_secs must each be > 0 when set (omit to use the
default); llm_retry_base_delay_ms must not exceed llm_retry_max_delay_ms
routing_gateway.local.preference required (non-empty) whenever the
inventory has a non-empty Local pool; every entry must resolve to a
Local-sovereignty provider (cross-pool reference is a boot error)
routing_gateway.standard.preference required (non-empty) whenever the
inventory has any provider at all
routing_gateway.economy's thresholds must not be dominated by local's
on both max_chars and max_tools if economy is enabled (unreachable tier)
- Every
preference entry (local/economy/standard/competency_manager)
must reference a real [[provider]]/[[provider.model]] pair — dangling
references are boot errors
- Every active surface (
hoziron-server's [surfaces.*]) must set exactly
one of listen/mount; a mount target must be a known, enabled,
main-route surface (no mount chains); no two main routes may share a
listen address; a mounted surface can't also set tls/allowed_ips/
limits; allowed_ips cannot combine with a unix:// listen address —
all checked by validate_surface_topology at boot (Issue #682)
surfaces.<name>.tls.cert_path/key_path required when
surfaces.<name>.tls.enabled = true
surfaces.<name>.limits.max_request_body_bytes and request_timeout_secs
must be > 0
surfaces.<name>.allowed_ips cannot be empty when configured (remove the
key to allow all)
health.check_interval_secs must be 5–300, failure_threshold 1–50,
recovery_cooldown_secs 10–600
server.cors.allowed_origins cannot be empty when [server.cors] is
present; wildcard * cannot combine with allow_credentials = true
auth.rate_limit.base_backoff_secs > 0, max_backoff_secs ≥
base_backoff_secs, max_failed_attempts > 0
auth.oidc.* required fields (issuer, audience, non-empty role_mapping
with valid Hoziron role values) enforced only when auth.mode = "oidc"
- Boot-time auth posture gate: for each active surface (
api, mcp,
dashboard), auth.mode = "disabled" on that surface's effective
listen address (its own if a main route, its mount target's if mounted)
that isn't loopback/unix:// is refused unless
auth.allow_insecure_no_auth = true. This runs in bin/hoziron-server
(check_surface_auth_posture / effective_listen) against each surface's
real resolved bind address — not inside PlatformConfig::validate(),
which has no visibility into per-surface addresses
audit.segment_max_size_mb must be > 0 (checked unconditionally — there
is no "audit disabled" bypass)
audit.retention_days must be 0 (unlimited) or at least 7 — a smaller
non-zero value is rejected as almost certainly a mistake (Issue #1240)
audit.export.endpoint required (and format-checked per target) only
when audit.export.enabled = true
tool_mediation.max_attempts ≥ 1, backoff_multiplier > 1.0,
initial_backoff_ms ≤ max_backoff_ms
home_region must be a currently-assigned ISO 3166-1 alpha-2 code
PlatformConfig::validate() is the single validation entry point — a
previous validate_for_config_write() used by hoziron-cli config set/unset
for a lighter check was collapsed into it.
Listen Address Priority
Resolved independently per surface (API, Registry, MCP, Dashboard):
- CLI flag:
hoziron-server --api-listen 0.0.0.0:4200 (and --registry-listen/--mcp-listen/--dashboard-listen; see start-stop.md) — also promotes the surface to a main route, clearing any configured mount
- Config file:
[surfaces.<name>].listen (main route) or [surfaces.<name>].mount (shares another surface's listener)
- Default:
127.0.0.1:4200 for API (main route); mount = "api" for MCP/Dashboard when enabled; 127.0.0.1:4210 for Registry
There is no unified [server].listen field and no HOZIRON_LISTEN environment variable.
Related: