Configuration Reference
⚙️ Complete reference for all PiSovereign configuration options
This document covers every configuration option available in config.toml.
Table of Contents
- Overview
- Environment Settings
- Server Settings
- Inference Engine
- Security Settings
- Memory & Knowledge Storage
- Database & Cache
- Integrations
- Model Selector
- Telemetry
- Resilience
- Health Checks
- Event Bus
- Agentic Mode
- Vault Integration
- Environment Variables
- Example Configurations
Overview
PiSovereign uses a layered configuration system:
- Default values - Built into the application
- Configuration file -
config.tomlin the working directory - Environment variables - Override config file values (prefix:
PISOVEREIGN_)
Configuration File Location
The application loads config.toml from the current working directory:
# Default location (relative to working directory)
./config.toml
Environment Variable Mapping
Config values can be overridden using environment variables:
[server]
port = 3000
# Becomes:
PISOVEREIGN_SERVER_PORT=3000
Nested values use double underscores:
[speech.local_stt]
threads = 4
# Becomes:
PISOVEREIGN_SPEECH_LOCAL_STT__THREADS=4
Environment Settings
# Application environment: "development" or "production"
# In production:
# - JSON logging is enforced
# - Security warnings block startup (unless PISOVEREIGN_ALLOW_INSECURE_CONFIG=true)
# - TLS verification is enforced
environment = "development"
| Value | Description |
|---|---|
development | Relaxed security, human-readable logs |
production | Strict security, JSON logs, TLS enforced |
Server Settings
[server]
# Network interface to bind to
# "127.0.0.1" = localhost only (recommended for security)
# "0.0.0.0" = all interfaces (use behind reverse proxy)
host = "127.0.0.1"
# HTTP port
port = 3000
# Enable CORS (Cross-Origin Resource Sharing)
cors_enabled = true
# Allowed CORS origins
# Empty array = allow all (WARNING in production)
# Example: ["https://app.example.com", "https://admin.example.com"]
allowed_origins = []
# Graceful shutdown timeout (seconds)
# Time to wait for active requests to complete
shutdown_timeout_secs = 30
# Log format: "json" or "text"
# In production mode, defaults to "json" even if set to "text"
log_format = "text"
# Secure session cookies (requires HTTPS)
# Set to false for local HTTP development
secure_cookies = false
# Maximum request body size for JSON payloads (optional, bytes)
# max_body_size_json_bytes = 1048576 # 1MB
# Maximum request body size for audio uploads (optional, bytes)
# max_body_size_audio_bytes = 10485760 # 10MB
| Option | Type | Default | Description |
|---|---|---|---|
host | String | 127.0.0.1 | Bind address |
port | Integer | 3000 | HTTP port |
cors_enabled | Boolean | true | Enable CORS |
allowed_origins | Array | [] | CORS allowed origins |
shutdown_timeout_secs | Integer | 30 | Shutdown grace period |
log_format | String | text | Log output format |
secure_cookies | Boolean | false | Secure cookie mode (HTTPS) |
max_body_size_json_bytes | Integer | 1048576 | (Optional) Max JSON payload size |
max_body_size_audio_bytes | Integer | 10485760 | (Optional) Max audio upload size |
Inference Engine
[inference]
# Ollama-compatible server URL
# Works with both hailo-ollama (Raspberry Pi) and standard Ollama (macOS)
base_url = "http://localhost:11434"
# Default model for inference
default_model = "qwen2.5:1.5b"
# Request timeout (milliseconds)
timeout_ms = 60000
# Maximum tokens to generate
max_tokens = 2048
# Sampling temperature (0.0 = deterministic, 2.0 = creative)
temperature = 0.7
# Top-p (nucleus) sampling (0.0-1.0)
top_p = 0.9
# System prompt (optional)
# system_prompt = "You are a helpful AI assistant."
| Option | Type | Default | Range | Description |
|---|---|---|---|---|
base_url | String | http://localhost:11434 | - | Inference server URL |
default_model | String | qwen2.5:1.5b | - | Model identifier |
timeout_ms | Integer | 60000 | 1000-300000 | Request timeout |
max_tokens | Integer | 2048 | 1-8192 | Max generation length |
temperature | Float | 0.7 | 0.0-2.0 | Randomness |
top_p | Float | 0.9 | 0.0-1.0 | Nucleus sampling |
system_prompt | String | None | - | (Optional) System prompt |
Security Settings
[security]
# Whitelisted phone numbers for WhatsApp
# Empty = allow all, Example: ["+491234567890", "+491234567891"]
whitelisted_phones = []
# API Keys (hashed with Argon2id)
# Generate hashed keys using: pisovereign-cli hash-api-key <your-key>
# Migrate existing plaintext keys: pisovereign-cli migrate-keys --input config.toml --dry-run
#
# [[security.api_keys]]
# hash = "$argon2id$v=19$m=19456,t=2,p=1$..."
# user_id = "550e8400-e29b-41d4-a716-446655440000"
#
# [[security.api_keys]]
# hash = "$argon2id$v=19$m=19456,t=2,p=1$..."
# user_id = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
# Trusted reverse proxies (IP addresses) - optional
# Add your proxy IPs here if behind a reverse proxy
# trusted_proxies = ["127.0.0.1", "::1"]
# Rate limiting
rate_limit_enabled = true
rate_limit_rpm = 120 # Requests per minute per IP
# TLS settings for outbound connections
tls_verify_certs = true
connection_timeout_secs = 30
min_tls_version = "1.2" # "1.2" or "1.3"
| Option | Type | Default | Description |
|---|---|---|---|
whitelisted_phones | Array | [] | (Optional) Allowed phone numbers |
api_keys | Array | [] | API key definitions with Argon2id hash |
trusted_proxies | Array | - | (Optional) Trusted reverse proxy IPs |
rate_limit_enabled | Boolean | true | Enable rate limiting |
rate_limit_rpm | Integer | 120 | Requests/minute/IP |
tls_verify_certs | Boolean | true | Verify TLS certificates for outbound connections |
connection_timeout_secs | Integer | 30 | Connection timeout for external services |
min_tls_version | String | 1.2 | Minimum TLS version (“1.2” or “1.3”) |
Prompt Security
Protects against prompt injection and other AI security threats.
[prompt_security]
# Enable prompt security analysis
enabled = true
# Sensitivity level: "low", "medium", or "high"
# - low: Only block high-confidence threats
# - medium: Block medium and high confidence threats (recommended)
# - high: Block all detected threats including low confidence
sensitivity = "medium"
# Block requests when security threats are detected
block_on_detection = true
# Maximum violations before auto-blocking an IP
max_violations_before_block = 3
# Time window for counting violations (seconds)
violation_window_secs = 3600 # 1 hour
# How long to block an IP after exceeding max violations (seconds)
block_duration_secs = 86400 # 24 hours
# Immediately block IPs that send critical-level threats
auto_block_on_critical = true
# Custom patterns to detect (in addition to built-in patterns) - optional
# custom_patterns = ["DROP TABLE", "eval("]
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable prompt security analysis |
sensitivity | String | medium | Detection level: “low”, “medium”, or “high” |
block_on_detection | Boolean | true | Block requests when threats detected |
max_violations_before_block | Integer | 3 | Violations before IP auto-block |
violation_window_secs | Integer | 3600 | Time window for counting violations |
block_duration_secs | Integer | 86400 | IP block duration after violations |
auto_block_on_critical | Boolean | true | Auto-block critical threats immediately |
custom_patterns | Array | - | (Optional) Custom threat detection patterns |
API Key Authentication
API keys are now securely hashed using Argon2id. Use the CLI tools to generate and migrate keys.
Generate a new hashed key:
pisovereign-cli hash-api-key <your-api-key>
Migrate existing plaintext keys:
pisovereign-cli migrate-keys --input config.toml --dry-run
pisovereign-cli migrate-keys --input config.toml --output config-new.toml
Configuration:
[[security.api_keys]]
hash = "$argon2id$v=19$m=19456,t=2,p=1$..."
user_id = "550e8400-e29b-41d4-a716-446655440000"
Usage:
curl -H "Authorization: Bearer <your-api-key>" http://localhost:3000/v1/chat
Memory & Knowledge Storage
Persistent AI memory for RAG-based context retrieval. Stores interactions, facts, preferences, and corrections using embeddings for semantic similarity search.
[memory]
# Enable memory storage (default: true)
# enabled = true
# Enable RAG context retrieval (default: true)
# enable_rag = true
# Enable automatic learning from interactions (default: true)
# enable_learning = true
# Number of memories to retrieve for RAG context (default: 5)
# rag_limit = 5
# Minimum similarity threshold for RAG retrieval (0.0-1.0, default: 0.5)
# rag_threshold = 0.5
# Similarity threshold for memory deduplication (0.0-1.0, default: 0.85)
# merge_threshold = 0.85
# Minimum importance score to keep memories (default: 0.1)
# min_importance = 0.1
# Decay factor for memory importance over time (default: 0.95)
# decay_factor = 0.95
# Enable content encryption (default: true)
# enable_encryption = true
# Path to encryption key file (generated if not exists)
# encryption_key_path = "memory_encryption.key"
[memory.embedding]
# Embedding model name (default: nomic-embed-text)
# model = "nomic-embed-text"
# Embedding dimension (default: 384 for nomic-embed-text)
# dimension = 384
# Request timeout in milliseconds (default: 30000)
# timeout_ms = 30000
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | (Optional) Enable memory storage |
enable_rag | Boolean | true | (Optional) Enable RAG context retrieval |
enable_learning | Boolean | true | (Optional) Auto-learn from interactions |
rag_limit | Integer | 5 | (Optional) Number of memories for RAG |
rag_threshold | Float | 0.5 | (Optional) Min similarity for RAG (0.0-1.0) |
merge_threshold | Float | 0.85 | (Optional) Similarity for deduplication (0.0-1.0) |
min_importance | Float | 0.1 | (Optional) Min importance to keep memories |
decay_factor | Float | 0.95 | (Optional) Importance decay over time |
enable_encryption | Boolean | true | (Optional) Encrypt stored content |
encryption_key_path | String | memory_encryption.key | (Optional) Encryption key file path |
Embedding Settings:
| Option | Type | Default | Description |
|---|---|---|---|
embedding.model | String | nomic-embed-text | (Optional) Embedding model name |
embedding.dimension | Integer | 384 | (Optional) Embedding vector dimension |
embedding.timeout_ms | Integer | 30000 | (Optional) Request timeout |
Database & Cache
Database
[database]
# SQLite database file path
path = "pisovereign.db"
# Connection pool size
max_connections = 5
# Auto-run migrations on startup
run_migrations = true
| Option | Type | Default | Description |
|---|---|---|---|
path | String | pisovereign.db | Database file path |
max_connections | Integer | 5 | Pool size |
run_migrations | Boolean | true | Auto-migrate |
Cache
PiSovereign uses a 3-layer caching architecture:
- L1 (Moka) - In-memory cache for fastest access
- L2 (Redb) - Persistent disk cache for exact-match lookups
- L3 (Semantic) - pgvector-based similarity cache for semantically equivalent queries
[cache]
# Enable caching (disable for debugging)
enabled = true
# TTL values (seconds)
ttl_short_secs = 300 # 5 minutes - frequently changing
ttl_medium_secs = 3600 # 1 hour - moderately stable
ttl_long_secs = 86400 # 24 hours - stable data
# LLM response caching
ttl_llm_dynamic_secs = 3600 # Dynamic content (briefings)
ttl_llm_stable_secs = 86400 # Stable content (help text)
# L1 (in-memory) cache size
l1_max_entries = 10000
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable caching |
ttl_short_secs | Integer | 300 | Short TTL |
ttl_medium_secs | Integer | 3600 | Medium TTL |
ttl_long_secs | Integer | 86400 | Long TTL |
ttl_llm_dynamic_secs | Integer | 3600 | Dynamic LLM TTL |
ttl_llm_stable_secs | Integer | 86400 | Stable LLM TTL |
l1_max_entries | Integer | 10000 | Max memory cache entries |
Semantic Cache
The semantic cache provides an additional layer that matches queries based on embedding similarity rather than exact string matching. This enables cache hits for semantically equivalent queries like:
- “What’s the weather?” ≈ “How’s the weather today?”
- “Tell me about the capital of France” ≈ “What is Paris?”
[cache.semantic]
# Enable semantic caching
enabled = true
# Minimum cosine similarity for cache hit (0.0-1.0)
# Higher = stricter matching, lower = more cache hits
similarity_threshold = 0.92
# TTL for cached entries (hours)
ttl_hours = 48
# Maximum cached entries
max_entries = 10000
# Patterns that bypass semantic cache (time-sensitive queries)
bypass_patterns = ["weather", "time", "date", "today", "tomorrow", "now", "latest", "current", "recent"]
# How often to evict expired entries (minutes)
eviction_interval_minutes = 60
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable semantic caching |
similarity_threshold | Float | 0.92 | Minimum cosine similarity (0.0-1.0) |
ttl_hours | Integer | 48 | Time-to-live in hours |
max_entries | Integer | 10000 | Maximum cache entries |
bypass_patterns | Array | See above | Queries containing these words skip cache |
eviction_interval_minutes | Integer | 60 | Expired entry cleanup interval |
Integrations
Messenger Selection
PiSovereign supports one messenger at a time:
# Choose one: "whatsapp", "signal", or "none"
messenger = "whatsapp"
| Value | Description |
|---|---|
whatsapp | Use WhatsApp Business API (webhooks) |
signal | Use Signal via signal-cli (polling) |
none | Disable messenger integration |
WhatsApp Business
[whatsapp]
# Meta Graph API access token (store in Vault)
# access_token = "your-access-token"
# Phone number ID from WhatsApp Business
# phone_number_id = "your-phone-number-id"
# App secret for webhook signature verification
# app_secret = "your-app-secret"
# Verify token for webhook setup
# verify_token = "your-verify-token"
# Require webhook signature verification
signature_required = true
# Meta Graph API version
api_version = "v18.0"
# Phone numbers allowed to send messages (empty = allow all)
# whitelist = ["+1234567890"]
# Conversation Persistence Settings
[whatsapp.persistence]
# Enable conversation persistence (default: true)
# enabled = true
# Enable encryption for stored messages (default: true)
# enable_encryption = true
# Enable RAG context retrieval from memory system (default: true)
# enable_rag = true
# Enable automatic learning from interactions (default: true)
# enable_learning = true
# Maximum days to retain conversations (optional, unlimited if not set)
# retention_days = 90
# Maximum messages per conversation before FIFO truncation (optional)
# max_messages_per_conversation = 1000
# Number of recent messages to use as context (default: 50)
# context_window = 50
| Option | Type | Default | Description |
|---|---|---|---|
access_token | String | - | (Optional) Meta Graph API token (store in Vault) |
phone_number_id | String | - | (Optional) WhatsApp Business phone number ID |
app_secret | String | - | (Optional) Webhook signature secret |
verify_token | String | - | (Optional) Webhook verification token |
signature_required | Boolean | true | Require webhook signature verification |
api_version | String | v18.0 | Meta Graph API version |
whitelist | Array | [] | (Optional) Allowed phone numbers |
Persistence Options:
| Option | Type | Default | Description |
|---|---|---|---|
persistence.enabled | Boolean | true | (Optional) Store conversations in database |
persistence.enable_encryption | Boolean | true | (Optional) Encrypt stored messages |
persistence.enable_rag | Boolean | true | (Optional) Enable RAG context retrieval |
persistence.enable_learning | Boolean | true | (Optional) Auto-learn from interactions |
persistence.retention_days | Integer | - | (Optional) Max retention days (unlimited if not set) |
persistence.max_messages_per_conversation | Integer | - | (Optional) Max messages before truncation |
persistence.context_window | Integer | 50 | (Optional) Recent messages for context |
Signal Messenger
[signal]
# Your phone number registered with Signal (E.164 format)
phone_number = "+1234567890"
# Path to signal-cli JSON-RPC socket
socket_path = "/var/run/signal-cli/socket"
# Path to signal-cli data directory (optional)
# data_path = "/var/lib/signal-cli"
# Connection timeout in milliseconds
timeout_ms = 30000
# Phone numbers allowed to send messages (empty = allow all)
# whitelist = ["+1234567890", "+0987654321"]
# Conversation Persistence Settings
[signal.persistence]
# Enable conversation persistence (default: true)
# enabled = true
# Enable encryption for stored messages (default: true)
# enable_encryption = true
# Enable RAG context retrieval from memory system (default: true)
# enable_rag = true
# Enable automatic learning from interactions (default: true)
# enable_learning = true
# Maximum days to retain conversations (optional, unlimited if not set)
# retention_days = 90
# Maximum messages per conversation before FIFO truncation (optional)
# max_messages_per_conversation = 1000
# Number of recent messages to use as context (default: 50)
# context_window = 50
| Option | Type | Default | Description |
|---|---|---|---|
phone_number | String | - | Your Signal phone number (E.164) |
socket_path | String | /var/run/signal-cli/socket | signal-cli daemon socket |
data_path | String | - | (Optional) signal-cli data directory |
timeout_ms | Integer | 30000 | Connection timeout |
whitelist | Array | [] | (Optional) Allowed phone numbers |
Persistence Options:
| Option | Type | Default | Description |
|---|---|---|---|
persistence.enabled | Boolean | true | (Optional) Store conversations in database |
persistence.enable_encryption | Boolean | true | (Optional) Encrypt stored messages |
persistence.enable_rag | Boolean | true | (Optional) Enable RAG context retrieval |
persistence.enable_learning | Boolean | true | (Optional) Auto-learn from interactions |
persistence.retention_days | Integer | - | (Optional) Max retention days (unlimited if not set) |
persistence.max_messages_per_conversation | Integer | - | (Optional) Max messages before truncation |
persistence.context_window | Integer | 50 | (Optional) Recent messages for context |
📖 See Signal Setup Guide for installation instructions.
Speech Processing
Voice message support for speech-to-text (STT) and text-to-speech (TTS).
Cloud Provider (OpenAI):
- Works on all platforms
- Requires API key
Local Provider (whisper.cpp + Piper):
- Raspberry Pi: Models in
/usr/local/share/{whisper,piper}/ - macOS: Models in
~/Library/Application Support/{whisper,piper}/ - Install whisper.cpp:
brew install whisper-cpp(Mac) or build from source (Pi) - Install Piper: Download from https://github.com/rhasspy/piper/releases
[speech]
# Speech provider: "openai" (cloud) or "local" (whisper.cpp + Piper)
# provider = "openai"
# OpenAI API key for Whisper (STT) and TTS
# openai_api_key = "sk-..."
# OpenAI API base URL (for custom endpoints)
# openai_base_url = "https://api.openai.com/v1"
# Speech-to-text model (OpenAI Whisper)
# stt_model = "whisper-1"
# Text-to-speech model
# tts_model = "tts-1"
# Default TTS voice: alloy, echo, fable, onyx, nova, shimmer
# default_voice = "nova"
# Output audio format: opus, ogg, mp3, wav
# output_format = "opus"
# Request timeout in milliseconds
# timeout_ms = 60000
# Maximum audio duration in milliseconds (25 min for Whisper)
# max_audio_duration_ms = 1500000
# Response format preference: mirror, text, voice
# response_format = "mirror"
# TTS speaking speed (0.25 to 4.0)
# speed = 1.0
| Option | Type | Default | Description |
|---|---|---|---|
provider | String | openai | (Optional) Speech provider: “openai” or “local” |
openai_api_key | String | - | (Optional) OpenAI API key (store in Vault) |
openai_base_url | String | https://api.openai.com/v1 | (Optional) OpenAI API base URL |
stt_model | String | whisper-1 | (Optional) Speech-to-text model |
tts_model | String | tts-1 | (Optional) Text-to-speech model |
default_voice | String | nova | (Optional) TTS voice (alloy, echo, fable, onyx, nova, shimmer) |
output_format | String | opus | (Optional) Audio format (opus, ogg, mp3, wav) |
timeout_ms | Integer | 60000 | (Optional) Request timeout |
max_audio_duration_ms | Integer | 1500000 | (Optional) Max audio duration (25 minutes) |
response_format | String | mirror | (Optional) Response format (mirror, text, voice) |
speed | Float | 1.0 | (Optional) TTS speaking speed (0.25 to 4.0) |
Weather
[weather]
# Open-Meteo API (free, no key required)
# base_url = "https://api.open-meteo.com/v1"
# Connection timeout in seconds
# timeout_secs = 30
# Number of forecast days (1-16)
# forecast_days = 7
# Cache TTL in minutes
# cache_ttl_minutes = 30
# Default location (when user has no profile)
# default_location = { latitude = 52.52, longitude = 13.405 } # Berlin
| Option | Type | Default | Description |
|---|---|---|---|
base_url | String | https://api.open-meteo.com/v1 | (Optional) Open-Meteo API URL |
timeout_secs | Integer | 30 | (Optional) Request timeout |
forecast_days | Integer | 7 | (Optional) Forecast days (1-16) |
cache_ttl_minutes | Integer | 30 | (Optional) Cache TTL |
default_location | Object | - | (Optional) Default location { latitude, longitude } |
CalDAV Calendar
[caldav]
# CalDAV server URL (Baïkal, Radicale, Nextcloud)
# server_url = "https://cal.example.com"
# When using Baïkal via Docker (setup --baikal):
# server_url = "http://baikal:80/dav.php"
# Authentication (store in Vault)
# username = "your-username"
# password = "your-password"
# Default calendar path (optional)
# calendar_path = "/calendars/user/default"
# TLS verification
# verify_certs = true
# Connection timeout in seconds
# timeout_secs = 30
| Option | Type | Default | Description |
|---|---|---|---|
server_url | String | - | (Optional) CalDAV server URL |
username | String | - | (Optional) Username for authentication (store in Vault) |
password | String | - | (Optional) Password for authentication (store in Vault) |
calendar_path | String | /calendars/user/default | (Optional) Default calendar path |
verify_certs | Boolean | true | (Optional) Verify TLS certificates |
timeout_secs | Integer | 30 | (Optional) Connection timeout |
Email (IMAP/SMTP)
PiSovereign supports any email provider that offers IMAP/SMTP access, including Gmail, Outlook, Proton Mail (via Bridge), and custom servers. Authentication is supported via password or OAuth2 (XOAUTH2).
Migration note: The config section was previously named
[proton]. The old name still works (via a serde alias) but[email]is the canonical name going forward.
Quick setup with provider presets:
The easiest way to configure email is using the provider field, which automatically sets sensible defaults for IMAP/SMTP hosts and ports:
[email]
provider = "gmail" # or "proton" or "custom"
email = "user@gmail.com"
password = "app-password"
Available providers:
| Provider | IMAP Host | IMAP Port | SMTP Host | SMTP Port |
|---|---|---|---|---|
proton | 127.0.0.1 | 1143 | 127.0.0.1 | 1025 |
gmail | imap.gmail.com | 993 | smtp.gmail.com | 465 |
custom | (must specify) | (must specify) | (must specify) | (must specify) |
Explicit
imap_host,imap_port,smtp_host,smtp_portvalues always override provider presets.
Full configuration:
[email]
# Provider preset: "proton" (default), "gmail", or "custom"
# provider = "proton"
# IMAP server host (overrides provider preset)
# imap_host = "imap.gmail.com" # Gmail
# imap_host = "outlook.office365.com" # Outlook
# imap_host = "127.0.0.1" # Proton Bridge
# IMAP server port (993 for TLS, 1143 for Proton Bridge STARTTLS)
# imap_port = 993
# SMTP server host
# smtp_host = "smtp.gmail.com" # Gmail
# smtp_host = "smtp.office365.com" # Outlook
# smtp_host = "127.0.0.1" # Proton Bridge
# SMTP server port (465 for TLS, 587 for STARTTLS, 1025 for Proton Bridge)
# smtp_port = 465
# Email address
# email = "user@gmail.com"
# Authentication: password or OAuth2
# For password-based auth (app passwords, Bridge passwords):
# password = "app-password"
# For OAuth2 (Gmail, Outlook):
# [email.auth]
# type = "oauth2"
# access_token = "ya29.your-token"
# TLS configuration
[email.tls]
# Verify TLS certificates (set false for self-signed certs like Proton Bridge)
# verify_certificates = true
# Minimum TLS version
# min_tls_version = "1.2"
# Custom CA certificate path (optional)
# ca_cert_path = "/path/to/ca.pem"
| Option | Type | Default | Description |
|---|---|---|---|
provider | String | proton | (Optional) Provider preset: proton, gmail, or custom. Sets default host/port values. |
imap_host | String | 127.0.0.1 | (Optional) IMAP server host (overrides provider preset) |
imap_port | Integer | 1143 | (Optional) IMAP server port (overrides provider preset) |
smtp_host | String | 127.0.0.1 | (Optional) SMTP server host (overrides provider preset) |
smtp_port | Integer | 1025 | (Optional) SMTP server port (overrides provider preset) |
email | String | - | (Optional) Email address (store in Vault) |
password | String | - | (Optional) Password (store in Vault) |
auth.type | String | password | (Optional) Auth method: password or oauth2 |
auth.access_token | String | - | (Optional) OAuth2 access token (store in Vault) |
tls.verify_certificates | Boolean | true | (Optional) Verify TLS certificates |
tls.min_tls_version | String | 1.2 | (Optional) Minimum TLS version |
tls.ca_cert_path | String | - | (Optional) Custom CA certificate path |
Provider-specific examples:
Gmail
[email]
provider = "gmail"
email = "user@gmail.com"
# Use an App Password (not your Google account password)
# Generate at: https://myaccount.google.com/apppasswords
password = "xxxx xxxx xxxx xxxx"
Outlook / Microsoft 365
[email]
provider = "custom"
imap_host = "outlook.office365.com"
imap_port = 993
smtp_host = "smtp.office365.com"
smtp_port = 587
email = "user@outlook.com"
password = "your-app-password"
Proton Mail (via Bridge)
[email]
provider = "proton" # default — uses Bridge at 127.0.0.1
email = "user@proton.me"
# Use the Bridge password (from Bridge UI), NOT your Proton account password
password = "bridge-password"
[email.tls]
verify_certificates = false # Bridge uses self-signed certs
Web Search
[websearch]
# Brave Search API key (required for primary provider)
# Get your key at: https://brave.com/search/api/
# api_key = "BSA-your-brave-api-key"
# Maximum results per search query (default: 5)
max_results = 5
# Request timeout in seconds (default: 30)
timeout_secs = 30
# Enable DuckDuckGo fallback if Brave fails (default: true)
fallback_enabled = true
# Safe search: "off", "moderate", "strict" (default: "moderate")
safe_search = "moderate"
# Country code for localized results (e.g., "US", "DE", "GB")
country = "DE"
# Language code for results (e.g., "en", "de", "fr")
language = "de"
# Rate limit: requests per minute (default: 60)
rate_limit_rpm = 60
# Cache TTL in minutes (default: 30)
cache_ttl_minutes = 30
| Option | Type | Default | Description |
|---|---|---|---|
api_key | String | - | (Optional) Brave Search API key (store in Vault) |
max_results | Integer | 5 | (Optional) Max search results (1-10) |
timeout_secs | Integer | 30 | (Optional) Request timeout |
fallback_enabled | Boolean | true | (Optional) Enable DuckDuckGo fallback |
safe_search | String | moderate | (Optional) Safe search: “off”, “moderate”, “strict” |
country | String | DE | (Optional) Country code for results |
language | String | de | (Optional) Language code for results |
rate_limit_rpm | Integer | 60 | (Optional) Rate limit (requests/minute) |
cache_ttl_minutes | Integer | 30 | (Optional) Cache time-to-live |
Security Note: Store the Brave API key in Vault rather than config.toml:
vault kv put secret/pisovereign/websearch brave_api_key="BSA-..."
Public Transit (ÖPNV)
Provides public transit routing for German transport networks via transport.rest API. Used for “How do I get to X?” queries and location-based reminders.
[transit]
# Base URL for transport.rest API (default: v6.db.transport.rest)
# base_url = "https://v6.db.transport.rest"
# Request timeout in seconds
# timeout_secs = 10
# Maximum number of journey results
# max_results = 3
# Cache TTL in minutes
# cache_ttl_minutes = 5
# Include transit info in location-based reminders
# include_in_reminders = true
# Transport modes to include:
# products_bus = true
# products_suburban = true # S-Bahn
# products_subway = true # U-Bahn
# products_tram = true
# products_regional = true # RB/RE
# products_national = false # ICE/IC
# User's home location for route calculations
# home_location = { latitude = 52.52, longitude = 13.405 } # Berlin
| Option | Type | Default | Description |
|---|---|---|---|
base_url | String | https://v6.db.transport.rest | (Optional) transport.rest API URL |
timeout_secs | Integer | 10 | (Optional) Request timeout |
max_results | Integer | 3 | (Optional) Max journey results |
cache_ttl_minutes | Integer | 5 | (Optional) Cache TTL |
include_in_reminders | Boolean | true | (Optional) Include in location reminders |
products_bus | Boolean | true | (Optional) Include bus routes |
products_suburban | Boolean | true | (Optional) Include S-Bahn |
products_subway | Boolean | true | (Optional) Include U-Bahn |
products_tram | Boolean | true | (Optional) Include tram |
products_regional | Boolean | true | (Optional) Include regional trains (RB/RE) |
products_national | Boolean | false | (Optional) Include national trains (ICE/IC) |
home_location | Object | - | (Optional) Home location { latitude, longitude } |
Reminder System
Configures the proactive reminder system including CalDAV sync, custom reminders, and scheduling settings.
[reminder]
# Maximum number of snoozes per reminder
# max_snooze = 5
# Default snooze duration in minutes
# default_snooze_minutes = 15
# How far in advance to create reminders from CalDAV events (minutes)
# caldav_reminder_lead_time_minutes = 30
# Interval for checking due reminders (seconds)
# check_interval_secs = 60
# CalDAV sync interval (minutes)
# caldav_sync_interval_minutes = 15
# Morning briefing time (HH:MM format)
# morning_briefing_time = "07:00"
# Enable morning briefing
# morning_briefing_enabled = true
| Option | Type | Default | Description |
|---|---|---|---|
max_snooze | Integer | 5 | (Optional) Max snoozes per reminder |
default_snooze_minutes | Integer | 15 | (Optional) Default snooze duration |
caldav_reminder_lead_time_minutes | Integer | 30 | (Optional) CalDAV event advance notice |
check_interval_secs | Integer | 60 | (Optional) How often to check for due reminders |
caldav_sync_interval_minutes | Integer | 15 | (Optional) CalDAV sync frequency |
morning_briefing_time | String | 07:00 | (Optional) Morning briefing time (HH:MM) |
morning_briefing_enabled | Boolean | true | (Optional) Enable daily morning briefing |
Model Selector (Deprecated)
Deprecated since v0.6.0: Use
[model_routing]instead. See Adaptive Model Routing.
The old [model_selector] section with small_model / large_model is still accepted but will be removed in a future release.
Adaptive Model Routing
Routes requests to different LLM models based on complexity. See the dedicated Adaptive Model Routing page for full documentation.
[model_routing]
enabled = true
[model_routing.models]
trivial = "template"
simple = "gemma3:1b"
moderate = "gemma3:4b"
complex = "gemma3:12b"
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable adaptive routing |
models.trivial | String | "template" | Model for trivial tier (usually "template") |
models.simple | String | "gemma3:1b" | Small model for simple queries |
models.moderate | String | "gemma3:4b" | Medium model for moderate queries |
models.complex | String | "gemma3:12b" | Large model for complex queries |
classification.confidence_threshold | Float | 0.6 | Below this, upgrade tier |
Telemetry
[telemetry]
# Enable OpenTelemetry export
enabled = false
# OTLP endpoint (Tempo, Jaeger)
# otlp_endpoint = "http://localhost:4317"
# Sampling ratio (0.0-1.0, 1.0 = all traces)
# sample_ratio = 1.0
# Service name for traces
# service_name = "pisovereign"
# Log level filter (e.g., "info", "debug", "pisovereign=debug,tower_http=info")
# log_filter = "pisovereign=info,tower_http=info"
# Batch export timeout in seconds
# export_timeout_secs = 30
# Maximum batch size for trace export
# max_batch_size = 512
# Graceful fallback to console-only logging if OTLP collector is unavailable.
# When true (default), the application starts with console logging if the collector
# cannot be reached. Set to false to require a working collector in production.
# graceful_fallback = true
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable OpenTelemetry export |
otlp_endpoint | String | http://localhost:4317 | (Optional) OTLP collector endpoint |
sample_ratio | Float | 1.0 | (Optional) Trace sampling ratio (0.0-1.0) |
service_name | String | pisovereign | (Optional) Service name for traces |
log_filter | String | pisovereign=info,tower_http=info | (Optional) Log level filter |
export_timeout_secs | Integer | 30 | (Optional) Batch export timeout |
max_batch_size | Integer | 512 | (Optional) Max batch size for export |
graceful_fallback | Boolean | true | (Optional) Fallback to console logging if collector unavailable |
Resilience
Degraded Mode
[degraded_mode]
# Enable fallback when backend unavailable
enabled = true
# Message returned during degraded mode
unavailable_message = "I'm currently experiencing technical difficulties. Please try again in a moment."
# Cooldown before retrying primary backend (seconds)
retry_cooldown_secs = 30
# Number of failures before entering degraded mode
failure_threshold = 3
# Number of successes required to exit degraded mode
success_threshold = 2
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable degraded mode fallback |
unavailable_message | String | See above | Message returned during degraded mode |
retry_cooldown_secs | Integer | 30 | Cooldown before retrying primary backend |
failure_threshold | Integer | 3 | Failures before entering degraded mode |
success_threshold | Integer | 2 | Successes to exit degraded mode |
Retry Configuration
Exponential backoff for retrying failed requests.
[retry]
# Initial delay before first retry in milliseconds
initial_delay_ms = 100
# Maximum delay between retries in milliseconds
max_delay_ms = 10000
# Multiplier for exponential backoff (delay = initial * multiplier^attempt)
multiplier = 2.0
# Maximum number of retry attempts
max_retries = 3
| Option | Type | Default | Description |
|---|---|---|---|
initial_delay_ms | Integer | 100 | Initial retry delay (milliseconds) |
max_delay_ms | Integer | 10000 | Maximum retry delay (milliseconds) |
multiplier | Float | 2.0 | Exponential backoff multiplier |
max_retries | Integer | 3 | Maximum retry attempts |
Formula: delay = min(initial_delay * multiplier^attempt, max_delay)
Health Checks
[health]
# Global timeout for all health checks in seconds
global_timeout_secs = 5
# Service-specific timeout overrides (uncomment to customize):
# inference_timeout_secs = 10
# email_timeout_secs = 5
# calendar_timeout_secs = 5
# weather_timeout_secs = 5
| Option | Type | Default | Description |
|---|---|---|---|
global_timeout_secs | Integer | 5 | Global timeout for all health checks |
inference_timeout_secs | Integer | 5 | (Optional) Inference service timeout override |
email_timeout_secs | Integer | 5 | (Optional) Email service timeout override |
calendar_timeout_secs | Integer | 5 | (Optional) Calendar service timeout override |
weather_timeout_secs | Integer | 5 | (Optional) Weather service timeout override |
Event Bus
The in-process event bus decouples post-processing from the user-facing response path. When enabled, background handlers asynchronously handle fact extraction, audit logging, conversation persistence verification, and metrics collection — reducing perceived latency by 100–500 ms per request.
[events]
# Enable or disable the event bus (default: true)
enabled = true
# Broadcast channel buffer capacity (default: 1024)
# Increase if handlers can't keep up under high load.
channel_capacity = 1024
# Error handling policy: "log" or "retry" (default: "log", reserved for future use)
# handler_error_policy = "log"
# Retry settings (reserved for future use)
# max_retry_attempts = 3
# retry_delay_ms = 500
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enable or disable the event bus |
channel_capacity | Integer | 1024 | Broadcast channel buffer size. Values 256–4096 suit most workloads |
handler_error_policy | String | "log" | (Reserved) "log" = log-and-continue, "retry" = retry with backoff |
max_retry_attempts | Integer | 3 | (Reserved) Max retries when policy is "retry" |
retry_delay_ms | Integer | 500 | (Reserved) Base delay between retries in milliseconds |
Background handlers spawned automatically:
| Handler | Requires | Purpose |
|---|---|---|
FactExtractionHandler | Memory context | Extracts structured facts from conversations via LLM |
AuditLogHandler | Database | Records audit trail entries for chat/command/security events |
ConversationPersistenceHandler | Conversation store | Verifies conversation integrity after each interaction |
MetricsHandler | (always) | Feeds event data into the metrics collector |
Tip: Set
enabled = falseto disable all background processing and fall back to synchronous inline behavior.
Agentic Mode
Multi-agent orchestration for complex tasks. When enabled, the system decomposes complex user requests into parallel sub-tasks, each handled by an independent AI agent.
Note: Requires
[agent.tool_calling] enabled = trueto be set.
[agentic]
# Enable agentic mode (default: false)
enabled = false
# Maximum concurrent sub-agents running in parallel
max_concurrent_sub_agents = 4
# Maximum sub-agents spawned per task
max_sub_agents_per_task = 10
# Total timeout for the entire agentic task (minutes)
total_timeout_minutes = 30
# Timeout for each individual sub-agent (minutes)
sub_agent_timeout_minutes = 10
# Operations that require user approval before execution
# Example: ["send_email", "delete_contact", "execute_code"]
require_approval_for = []
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable agentic multi-agent orchestration |
max_concurrent_sub_agents | Integer | 4 | Max sub-agents running in parallel |
max_sub_agents_per_task | Integer | 10 | Max sub-agents per task |
total_timeout_minutes | Integer | 30 | Total task timeout (minutes) |
sub_agent_timeout_minutes | Integer | 10 | Per sub-agent timeout (minutes) |
require_approval_for | Array | [] | Operations requiring user approval |
Vault Integration
[vault]
# Vault server address
# address = "http://127.0.0.1:8200"
# AppRole authentication (recommended)
# role_id = "your-role-id"
# secret_id = "your-secret-id"
# Or token authentication
# token = "hvs.your-token"
# KV engine mount path
# mount_path = "secret"
# Request timeout in seconds
# timeout_secs = 5
# Vault Enterprise namespace (optional)
# namespace = "admin/pisovereign"
| Option | Type | Default | Description |
|---|---|---|---|
address | String | http://127.0.0.1:8200 | (Optional) Vault server address |
role_id | String | - | (Optional) AppRole role ID (recommended) |
secret_id | String | - | (Optional) AppRole secret ID |
token | String | - | (Optional) Vault token (alternative to AppRole) |
mount_path | String | secret | (Optional) KV engine mount path |
timeout_secs | Integer | 5 | (Optional) Request timeout |
namespace | String | - | (Optional) Vault Enterprise namespace |
Environment Variables
All configuration options can be set via environment variables.
Use __ (double underscore) as the nesting separator to avoid conflicts
with field names containing underscores (e.g., phone_number):
| Config Path | Environment Variable |
|---|---|
server.port | PISOVEREIGN_SERVER__PORT |
inference.base_url | PISOVEREIGN_INFERENCE__BASE_URL |
signal.phone_number | PISOVEREIGN_SIGNAL__PHONE_NUMBER |
database.path | PISOVEREIGN_DATABASE__PATH |
vault.address | PISOVEREIGN_VAULT__ADDRESS |
Special variables:
| Variable | Description |
|---|---|
PISOVEREIGN_ALLOW_INSECURE_CONFIG | Allow insecure settings in production |
RUST_LOG | Log level override |
Example Configurations
Development
environment = "development"
[server]
host = "127.0.0.1"
port = 3000
log_format = "text"
[inference]
base_url = "http://localhost:11434"
default_model = "qwen2.5:1.5b"
[database]
path = "./dev.db"
[cache]
enabled = false # Disable for debugging
[security]
rate_limit_enabled = false
tls_verify_certs = false
Production
environment = "production"
[server]
host = "127.0.0.1" # Behind reverse proxy
port = 3000
log_format = "json"
cors_enabled = true
allowed_origins = ["https://app.example.com"]
[inference]
base_url = "http://localhost:11434"
default_model = "qwen2.5:1.5b"
timeout_ms = 120000
[database]
path = "/var/lib/pisovereign/pisovereign.db"
max_connections = 10
[security]
rate_limit_enabled = true
rate_limit_rpm = 30
min_tls_version = "1.3"
[prompt_security]
enabled = true
sensitivity = "high"
block_on_detection = true
[vault]
address = "https://vault.internal:8200"
role_id = "..."
mount_path = "secret"
[telemetry]
enabled = true
otlp_endpoint = "http://tempo:4317"
sample_ratio = 0.1
Minimal (Quick Start)
environment = "development"
[server]
port = 3000
[inference]
base_url = "http://localhost:11434"
default_model = "qwen2.5:1.5b"
[database]
path = "pisovereign.db"