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:

  1. HOZIRON_HOME environment variable → $HOZIRON_HOME/config.toml
  2. ~/.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)
  3. --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.

FieldTypeDefaultDescription
enabledboolapi: true, registry: false, mcp/dashboard: falseWhether this surface is active
listenstringapi: 127.0.0.1:4200, registry: 127.0.0.1:4210, mcp/dashboard: unsethost:port or unix://path. Set only on a main route
mountstringmcp/dashboard: "api" when enabledName of another enabled main-route surface (api, registry, mcp, dashboard) whose listener this surface shares. Set only on a mounted surface; no mount chains
tlstablenoneNative TLS termination for this listener. Main route only — see [surfaces.<name>.tls] below
allowed_ipsstring[]noneIP/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)
limitstablesee [surfaces.<name>.limits] belowRequest body size / timeout enforcement for this listener. Main route only
base_pathstring/, /, /mcp, /dashboardMount 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.

FieldTypeDescription
enabledboolEnable native TLS termination (real — axum-server + rustls, with 60s cert/key hot-reload polling)
cert_pathstringRequired when enabled = true
key_pathstringRequired when enabled = true

[surfaces.<name>.limits]

Main route only.

FieldTypeDefaultDescription
max_request_body_bytesint10485760 (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_secsint600Must 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

FieldTypeDefaultDescription
storage_pathstring/data/registry/packagesLocal filesystem path for package archive storage
s3tablenoneS3-compatible object storage config (optional)
authtableregistry defaultsRegistry-specific auth (separate from [auth])
corstableregistry defaultsRegistry-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)

FieldTypeRequiredDescription
idstringyesArbitrary, operator-chosen, unique instance identifier
driverstringyesCompiled-in driver name — must match a kernel-shipped driver (see table below); never inferred from id
urlstringconditionalEndpoint URL. Required for local-runtime and Ambiguous drivers, and for any AlwaysCloud driver other than anthropic
api_key_envstringnoEnv var name holding the API key. Must match [A-Z_][A-Z0-9_]*, 1–128 chars
sovereignty_attestationstringnoCarrier attestation that an Ambiguous-driver endpoint is in-boundary. Rejected on any other driver disposition
[[provider.model]]arraynoModels 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]]

FieldTypeDefaultDescription
idstringModel identifier as the driver's API expects it
max_tokensintdriver defaultPer-request max output tokens
temperaturefloatdriver defaultSampling temperature
context_windowintdriver defaultOverride 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:

DispositionMeaningDrivers
AlwaysLocalStructurally local, no attestation neededecho, ollama, vllm, lmstudio, lemonade
AmbiguousPublic protocol, endpoint may be in-boundary — resolves Local only with sovereignty_attestation, otherwise fails closed to Cloudopenai-compatible
AlwaysCloudNever local, no attestation possibleanthropic, 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]

FieldTypeDefaultDescription
default_workflow_typestringnoneFallback workflow type when no workflow_type_map prefix matches and none is given explicitly
always_standard_workflowsstring[][]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
localtablesee belowHard-walled, Local-sovereignty-only tier
economytablesee belowOptional cheaper/faster mid-band tier, Pool::Mixed
standardtablesee belowCatch-all tier, Pool::Mixed

[routing_gateway.local]

FieldTypeDefaultDescription
max_charsint500Payload length ceiling for size-based local classification
max_toolsint0Tool-count ceiling (0 = any tools disqualify from size-based local)
preferencearray[]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]

FieldTypeDefaultDescription
max_charsint0 (disabled)
max_toolsint0 (disabled)
preferencearray[]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]

FieldTypeDefaultDescription
preferencearray[]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.

FieldTypeDefaultDescription
corstablenoneSee below

[server.cors]

FieldTypeDefaultDescription
allowed_originsstring[][]Non-empty when the section is present; each entry http(s)://… or *
allow_credentialsboolfalseCannot combine with a * origin
max_age_secsint3600Preflight cache duration

[health]

FieldTypeRangeDefaultDescription
enabledbooltrueEnable circuit-breaker health monitoring
check_interval_secsint5–30030
failure_thresholdint1–505Failures to trip the breaker
recovery_cooldown_secsint10–60060Cooldown before HalfOpen probe

[auth]

FieldTypeDefaultDescription
modedisabled|local|oidcdisabledAuthentication mode
allow_local_service_keysboolfalseWhen mode is oidc, also allow local API keys (CI/CD, break-glass)
allow_insecure_no_authboolfalseExplicit escape hatch (Issue #512) permitting mode = "disabled" on a network-reachable bind. Every boot logs a loud warning when set
oidctablenoneRequired when mode = "oidc"
rate_limittablesee 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]

FieldTypeDefaultDescription
base_backoff_secsint1Must be > 0
max_backoff_secsint300Must be ≥ base_backoff_secs
max_failed_attemptsu3210Must be > 0

[auth.oidc]

FieldTypeDefaultDescription
issuerstring""Required when mode = "oidc"
audiencestring""Required when mode = "oidc"
client_idstring""Defaults to audience if empty
client_secretstring""For confidential clients
jwks_uristring""Auto-discovered from {issuer}/.well-known/openid-configuration if blank
role_claimstring"roles"Dot-path notation accepted (e.g. realm_access.roles)
allowed_algorithmsstring[]["RS256"]One of RS256/RS384/RS512/ES256/ES384/PS256/PS384/PS512/EdDSA
role_mappingtable{}IdP role → Hoziron role. Required non-empty when mode = "oidc"; every value must be a valid Hoziron role
jwks_cache_ttl_secsint3600

[catalog]

FieldTypeDefaultDescription
verify_signaturesbooltrue
packages_dirstring"packages"Relative to HOZIRON_HOME
cache_ttl_secsint3600
default_publish_registrystringnoneUsed by hoziron-cli catalog publish without --registry
scopestable{}@scope/name → registry URL mapping
registriesarrayone entry (see below)

[[catalog.registries]]

FieldTypeDefaultDescription
namestringIdentifier used in CLI/API
urlstringRegistry API base URL
priorityint100Lower = higher priority, queried first
tokenstringnoneInline token — takes precedence over auth_token_env
auth_token_envstringnoneEnv var holding the auth token, checked if token unset
enabledbooltrue
require_signaturesbooltrueFail-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.

FieldTypeDefaultDescription
segment_max_size_mbint64Must be > 0. Segment rotation size
retention_daysint00 = unlimited (default — deletion requires explicit opt-in). A non-zero value below 7 is rejected at boot as almost certainly a mistake (Issue #1240)
exporttablesee belowSIEM 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]

FieldTypeDefaultDescription
enabledboolfalse
targetwebhook|syslog|filewebhook
endpointstring""Required when enabled: http(s)://… for webhook, host:port for syslog, a file path for file
batch_sizeint100Must be > 0
flush_interval_secsint5Must be > 0
max_retry_attemptsint5Must be > 0
syslog_transportudp|tcpudpOnly used when target = "syslog"
headerstable{}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.

FieldTypeDefaultDescription
formattext|jsontextNot read at runtime — see note above
levelstringnoneNot 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.

FieldTypeDefaultDescription
urlsmap<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]

FieldTypeDefaultDescription
manifests_dirstring$HOZIRON_HOME/agents/Directory of agent manifest .toml files auto-loaded at boot

[trust]

FieldTypeDefaultDescription
allowed_toolsstring[][]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]

FieldTypeDefaultDescription
preferencearray[]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)

FieldTypeDefaultDescription
preferencearray[]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

[tool_mediation]

SoR (system-of-record) write escalation — retry/backoff before alerting on transient vault/pipeline failures (ADR-042 OQ2).

FieldTypeDefaultDescription
max_attemptsu323Must be ≥ 1
initial_backoff_msu64100Must be ≤ max_backoff_ms
max_backoff_msu645000
backoff_multiplierf642.0Must be > 1.0

[cron_auto_tick]

FieldTypeDefaultDescription
enabledbooltrueWhether the automatic cron-tick loop runs at boot
interval_secsu6415Seconds 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.

FieldTypeDefaultDescription
enabledbooltrueWhether the automatic condition-tick loop runs at boot
interval_secsu6430Seconds between due-condition polls

[background_agents]

Dormant background-agent subsystems (issue #497). Every flag defaults to false — opt in individually.

FieldTypeDefaultDescription
agent_self_promptboolfalseContinuous/Periodic agent self-prompt loops
heartbeat_monitorboolfalseReactive-agent unresponsiveness detection
provider_probeboolfalseLocal-provider reachability probing + model-catalog discovery
metering_cleanupboolfalsePeriodic usage/metering record cleanup
memory_consolidationboolfalseAlso requires memory.consolidation_interval_hours > 0 (kernel-level)
mcp_autoconnectboolfalseMCP server auto-connect on boot
extension_healthboolfalseExtension health monitor loop
workflow_autoloadboolfalseWorkflow auto-load from workflows_dir on boot
whatsapp_gatewayboolfalseAlso 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).

FieldTypeDefaultDescription
enabledboolfalse
endpointstringnoneOverrides OTEL_EXPORTER_OTLP_ENDPOINT. Falls back to http://localhost:4317 if neither is set
service_namestring"hoziron"

Top-level scalars

FieldTypeDefaultDescription
licence_pathstring$HOZIRON_HOME/licence.jsonCarrier licence file path
pii_policy_pathstring$HOZIRON_HOME/carrier-pii-policy.tomlCarrier PII policy file path
home_regionstringnoneISO 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_dirstring$HOZIRON_HOME/compliance/packsDirectory 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_secsbase_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_msmax_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):

  1. 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
  2. Config file: [surfaces.<name>].listen (main route) or [surfaces.<name>].mount (shares another surface's listener)
  3. 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: