Primary config files in this repo:
- production template:
deploy/config/geniepod.toml - development template:
deploy/config/geniepod.dev.toml - profile example:
deploy/config/profile.toml.example
Runtime load path:
- default:
/etc/geniepod/geniepod.toml - override:
GENIEPOD_CONFIG=/path/to/file.toml
| Section | Purpose |
|---|---|
data_dir |
Root directory for runtime databases and profile data |
[optional_ai_provider] |
Disabled-by-default API provider planning, including API-key or OAuth bearer auth mode |
[core] |
genie-core runtime behavior |
[governor] |
governor polling and day/night behavior |
[governor.pressure] |
memory-pressure thresholds |
[health] |
service polling and alert forwarding |
[services.*] |
local service endpoints and systemd unit names |
[telegram] |
Telegram long-poll adapter |
[web_search] |
public web search tool behavior |
[connectivity] |
coprocessor boundary enablement |
[connectivity.esp32c6_uart] |
UART transport settings for ESP32-C6 |
This path is disabled by default. It exists for better testing, development
portability, and transitional validation while preserving the local Jetson
genie-ai-runtime default. Remote or alternate OpenAI-compatible providers are
not the product runtime and must not become a shortcut around the small-context
home harness.
| Key | Purpose |
|---|---|
enabled |
Turn optional provider planning on |
provider |
open_ai_compatible, open_ai, anthropic, gemini, or custom |
auth_mode |
api_key for provider keys or oauth_bearer for OAuth access tokens |
base_url |
Provider endpoint, for example https://api.openai.com/v1 |
api_key_env |
Env var that stores an API key when auth_mode = "api_key" |
oauth_token_env |
Env var that stores an OAuth access token when auth_mode = "oauth_bearer" |
context_window_tokens |
Provider context budget, which must fit the agent budget |
allow_remote_base_url |
Required opt-in for non-loopback provider URLs |
OAuth bearer mode never stores the token in TOML. For an OpenAI OAuth-token development/test deployment, use a secret env var and point config at it:
[optional_ai_provider]
enabled = true
provider = "open_ai"
auth_mode = "oauth_bearer"
base_url = "https://api.openai.com/v1"
oauth_token_env = "OPENAI_OAUTH_ACCESS_TOKEN"
context_window_tokens = 4096
allow_remote_base_url = trueDo not enable this path for household production by default. If a provider is
used for validation, keep context_window_tokens at or below
[agent].context_window_tokens and avoid sending household memory unless the
operator has explicitly accepted that privacy tradeoff.
| Key | Purpose |
|---|---|
port |
HTTP port for genie-core |
bind_host |
HTTP bind host; defaults to 127.0.0.1 because the API can trigger physical actions |
ha_token |
Home Assistant token when not supplied by env |
llm_model_name |
Logical model family name for prompt optimization |
whisper_model |
Whisper model path |
whisper_port |
Whisper server port, 0 means CLI mode |
whisper_cli_path |
Path to whisper-cli |
stt_language |
STT language hint, "auto" enables detection |
piper_model |
Default Piper voice model path |
piper_path |
Path to Piper binary |
piper_pipe_mode |
Keep Piper hot for lower latency |
voice_tts_models |
Optional per-language Piper voices |
max_history_turns |
Max conversation history included per turn |
llm_connect_timeout_secs |
Max wait to open the LLM backend TCP connection (default 10) |
llm_read_timeout_secs |
Max idle seconds between LLM tokens/bytes before a read is abandoned; bounds every backend read so a hung read can't wedge chat (default 60) |
llm_request_timeout_secs |
Max seconds for a non-streaming completion's response body (default 120) |
expected_runtime_contract_hash |
Optional pinned contract hash for runtime drift detection |
audio_device |
ALSA device or "auto" |
audio_sample_rate |
Capture sample rate |
voice_enabled |
Enable voice mode by config |
voice_record_secs |
Recording duration per turn |
voice_continuous |
Auto-listen for follow-up after speaking |
voice_continuous_secs |
Shorter recording length in continuous mode |
llm_model_path |
Model path used by voice mode time-sharing logic |
wakeword_script |
Wake-word listener helper path |
speaker_identity.* |
Optional voice speaker-identity provider settings |
skill_policy.* |
Runtime load policy for native skills |
tool_policy.* |
Runtime allow/deny policy for model-callable tools by origin |
actuation_safety.* |
Final home-actuation safety gate settings |
origin_auth.* |
How a request may assume a privileged origin over HTTP |
| Key | Purpose |
|---|---|
enabled |
Turn speaker-identity enrichment on for voice flows |
provider |
none, fixed, or local_biometric |
fixed_name |
Speaker label used by the fixed provider |
fixed_confidence |
low, medium, or high confidence reported by the fixed provider |
local_profile_dir |
Local enrolled speaker profile directory for the biometric provider |
local_min_score |
Minimum score required to accept a biometric match |
Behavior notes:
noneis the default and behaves like anonymous shared-room voice.fixedis mainly for single-user boxes, testing, and plumbing validation.local_biometricuses local WAV-derived speaker profiles fromlocal_profile_dir.- Enroll a profile with
genie-ctl speaker enroll <NAME> <WAV>. - On device, use
genie-ctl speaker enroll-live <NAME>to record and enroll through ALSA in one step. - Test matching with
genie-ctl speaker identify <WAV>. - This affects memory read context in voice mode today. A recognized speaker can unlock person-scoped memory recall when the match clears
local_min_score. - The current recognizer is useful for household routing, not adversarial authentication. Do not treat it as a door-lock or payment authorization factor.
| Key | Purpose |
|---|---|
require_manifest |
Reject skills without an ok sidecar manifest |
require_signature |
Reject skills whose manifest has no signature material |
denied_permissions |
Permission labels that must block skill loading |
Behavior notes:
- Defaults are audit-only: skills load even when the manifest is missing.
- A sidecar manifest is preferred as
<skill>.skill.json, for examplehello.skill.json. require_manifest = trueblocks missing, invalid, and mismatched manifests.require_signature = trueblocks manifests without a non-emptysignaturefield.denied_permissionscompares against the manifestpermissionslist.- Current signing is presence-only; cryptographic signature verification is future signed-skill work.
| Key | Purpose |
|---|---|
enabled |
Turn origin-aware tool policy checks on or off |
allowed_tools_by_origin |
Optional origin allowlists; if present, only listed tools can run |
denied_tools_by_origin |
Optional origin denylists; deny rules override allow rules |
Behavior notes:
- Defaults allow all tools unless a rule exists.
- Origin keys are
voice,dashboard,api,telegram,repl,confirmation,unknown, or*. - Tool lists can include explicit tool names or
*. - Deny rules override allow rules.
- This policy applies before tool execution and is separate from actuation safety.
- Tool policy blocks are recorded in the privacy-preserving tool audit log.
| Key | Purpose |
|---|---|
enabled |
Turn the final actuation safety gate on or off |
min_target_confidence |
Minimum target-match confidence for ordinary actions |
min_sensitive_confidence |
Minimum target-match confidence for medium/high-risk actions |
deny_multi_target_sensitive |
Block medium/high-risk actions that fan out to multiple entities |
require_available_state |
Require a successful current-state check before executing non-scene/script actions |
allowed_origins |
Request origins allowed to execute physical actuation |
max_actions_per_minute |
Default per-origin actuation rate limit |
max_actions_per_minute_by_origin |
Optional per-origin rate-limit overrides |
Behavior notes:
- This gate runs after target resolution and before the actual Home Assistant service call.
- It is separate from prompt rules and separate from the first local policy check.
- Default behavior is fail-closed for ambiguous or degraded physical actions.
unknownis not in the defaultallowed_origins; direct tool execution without a channel context cannot actuate the home.- Valid origin keys are
voice,dashboard,api,telegram,repl, andconfirmation. - Rate limits apply before physical execution and are tracked per origin over a 60-second window.
- The origin these policies key off is resolved per
[core.origin_auth]below, not taken at face value from the header.
The X-Genie-Origin header is client-supplied, so it cannot by itself be a
trusted security principal for the policies above (issue #232). A request may
assume an origin more privileged than the api baseline only when it proves
entitlement — by originating from a loopback peer or by presenting a matching
token. Otherwise it is downgraded to api.
| Key | Purpose |
|---|---|
require_token |
Require a valid token even from loopback peers (default false) |
tokens |
Map of origin name to the shared secret expected in X-Genie-Origin-Token |
Behavior notes:
- Default (
require_token = false, no tokens): loopback peers are trusted to set any origin; non-loopback peers cannot assume a privileged origin at all. - A configured token makes that origin require the token everywhere, including loopback — so one local process cannot impersonate another's channel.
- Each token may instead be supplied via the
GENIE_ORIGIN_TOKEN_<ORIGIN>environment variable (preferred; keep config files0600). - The in-process Telegram adapter automatically presents its configured token.
After a known-good deployment, read the active hash:
curl -s http://127.0.0.1:3000/api/health | jq -r '.runtime_contract.contract_hash'Then set:
[core]
expected_runtime_contract_hash = "<known-good-contract-hash>"If prompt, tool schema, policy, or hydrated boot state changes, /api/health
and /api/runtime/contract report validation.status = "drift", and
genie-core logs a warning on startup.
| Key | Purpose |
|---|---|
poll_interval_ms |
Sampling interval |
night_start_hour |
When night mode begins |
day_start_hour |
When day mode resumes |
night_model_swap |
Optional larger-model-at-night behavior |
| Key | Purpose |
|---|---|
stop_optins_mb |
Stop optional services below this free-memory threshold |
reduce_context_mb |
Trigger smaller LLM context behavior |
swap_stt_mb |
Trigger lower-cost STT behavior |
zram_mb |
Enable last-resort zram behavior |
| Key | Purpose |
|---|---|
interval_secs |
Health polling interval |
alert_enabled |
Enable alert forwarding |
alert_webhook_url |
Local alert receiver base URL |
Required service blocks:
[services.core][services.llm]
Optional service blocks:
[services.homeassistant][services.nextcloud][services.jellyfin]
Each service block has:
| Key | Purpose |
|---|---|
url |
Health or base URL (http:// or https://) |
systemd_unit |
Associated systemd unit name |
genie-ctl status, genie-health, and the dashboard latency rows probe
configured https:// service URLs using the shared helper in
genie-common::probe.
| Behavior | Detail |
|---|---|
| Trust roots | Mozilla CA bundle from the webpki-roots crate — not the host OS trust store |
| Self-signed LAN certs | Rejected (common for local Home Assistant HTTPS). Use http:// on the LAN, terminate TLS at a reverse proxy with a public CA, or wait for a future opt-in tls_ca_file / pin policy |
| Keep-alive responses | Probes parse the status line (and Content-Length / chunked body when present) and return without waiting for EOF, so healthy keep-alive servers do not time out |
| Listen addresses | [services.api].url / api_http_addr still require http:// — HTTPS is probe-only, not a listen scheme |
| Key | Purpose |
|---|---|
enabled |
Enable Telegram adapter |
bot_token |
Bot token when not supplied by env |
api_base |
Telegram Bot API base URL |
poll_timeout_secs |
Long-poll timeout |
allowed_chat_ids |
Allowlist of chat IDs |
allow_all_chats |
Disable allowlist enforcement |
Telegram is also gated at build/runtime by the telegram feature in
crates/genie-core/Cargo.toml.
| Key | Purpose |
|---|---|
enabled |
Enable the web_search tool and quick router |
provider |
duckduckgo or searxng |
base_url |
SearXNG base URL when using searxng |
allow_remote_base_url |
Permit non-loopback SearXNG URLs |
timeout_secs |
Request timeout |
max_results |
Upper bound on returned items |
cache_enabled |
Enable in-process cache |
cache_ttl_secs |
Cache freshness window |
cache_max_entries |
Cache size cap |
Behavior notes:
- DuckDuckGo is the default and requires no key.
- SearXNG is treated as local-first by default.
- Queries that look like secrets or local credentials are blocked before network use.
| Key | Purpose |
|---|---|
enabled |
Turn the coprocessor path on |
transport |
Current supported value: esp32c6_uart |
device |
Logical device name, currently descriptive only |
| Key | Purpose |
|---|---|
device_path |
Linux serial device path |
baud_rate |
UART baud rate |
reset_gpio |
Optional ESP32-C6 reset GPIO |
hardware_flow_control |
RTS/CTS support |
mtu_bytes |
Max frame size |
response_timeout_ms |
UART response timeout |
Legacy alias support exists for esp32c6_spi, but the current boundary is
UART-oriented and the detailed SPI hosted work belongs in the platform/OS layer.
| Variable | Purpose |
|---|---|
GENIEPOD_CONFIG |
Override config path |
HA_TOKEN |
Home Assistant token fallback |
TELEGRAM_BOT_TOKEN |
Telegram token fallback |
GENIEPOD_WEB_SEARCH_BASE_URL |
Override SearXNG base URL |
GENIEPOD_VOICE |
Force voice mode when set to 1 |
RUST_LOG |
Logging level/filter |
Operational variables used by systemd/deploy surfaces outside the Rust config:
| Variable | Purpose |
|---|---|
GENIEPOD_LLM_MODEL |
Model path used by the active local LLM unit (genie-ai-runtime.service by default, or genie-llm.service for llama.cpp fallback) |
Config::load()readsGENIEPOD_CONFIGfirst, else/etc/geniepod/geniepod.toml.- Home Assistant token resolution prefers
[core].ha_token, thenHA_TOKEN. - Telegram bot token resolution prefers
[telegram].bot_token, thenTELEGRAM_BOT_TOKEN. - SearXNG base URL resolution prefers
GENIEPOD_WEB_SEARCH_BASE_URLwhen set, else[web_search].base_url.