Configuration reference
Complete reference for config/config.toml. See config/config.example.toml in the repository for a commented example.
Top-level settings
| Setting |
Default |
Description |
bot_name |
"Istota" |
User-facing name (chat, emails, folder names) |
emissaries_enabled |
true |
Include emissaries.md in system prompts |
model |
"" |
Claude model override (empty = CLI default). Pin to a version like "claude-opus-5". |
effort |
"" |
Effort level: low, medium, high, xhigh, or max (empty = model default) |
advisor_model |
"" |
Advisor model — Anthropic-namespace brains only (claude_code / tmux_claude); resolves through the same alias table as model but carries no effort. Must resolve to a model capable of being an advisor (a weak/cheap tier fails every task it runs on). Dropped for any task carrying its own model pin (!model, !room model, a [[jobs]] model). |
custom_system_prompt |
false |
Use config/system-prompt.md instead of Claude Code default. That file — and nothing else in the config directory — is bind-mounted read-only into the sandbox, since the CLI opens it there |
namespace |
"istota" |
Instance namespace — prefixes systemd units, lock paths and similar, so two instances can share a host |
db_path |
"data/istota.db" |
SQLite database path |
module_data_dir |
derived |
Root for per-user module DBs; defaults to {db_path.parent}/modules |
rclone_remote |
"nextcloud" |
rclone remote name |
nextcloud_mount_path |
not set |
Local mount path (enables mount mode when set) |
skills_dir |
"config/skills" |
Operator skill overrides directory |
disabled_skills |
[] |
Instance-wide skills to exclude |
temp_dir |
"/tmp/istota" |
Temporary directory for task execution |
max_memory_chars |
0 |
Cap total memory in prompts (0 = unlimited) |
max_knowledge_facts |
50 |
Cap knowledge graph facts per prompt (0 = unlimited) |
[nextcloud]
| Setting |
Default |
Description |
url |
"" |
Nextcloud server URL |
username |
"" |
Bot's Nextcloud username |
app_password |
"" |
Nextcloud app password |
share_default_expire_days |
14 |
Default expiry on links the bot creates |
[talk]
| Setting |
Default |
Description |
enabled |
true |
Enable Talk polling |
bot_username |
"istota" |
Bot's username (to filter own messages) |
[email]
| Setting |
Default |
Description |
enabled |
false |
Enable email |
imap_host |
"" |
IMAP server |
imap_port |
993 |
IMAP port |
imap_timeout_seconds |
30 |
IMAP socket timeout |
imap_user |
"" |
IMAP username |
imap_password |
"" |
IMAP password |
smtp_host |
"" |
SMTP server |
smtp_port |
587 |
SMTP port |
smtp_user |
"" |
SMTP username (defaults to imap_user) |
smtp_password |
"" |
SMTP password (defaults to imap_password) |
poll_folder |
"INBOX" |
Folder to poll |
bot_email |
"" |
Bot's email address |
confirm_sender_match |
false |
Declares that nothing upstream authenticates From:, so mail claiming a user's own address is held for confirmation. Default assumes the MTA enforces DMARC — see confirm_sender_match |
dmarc_canary |
true |
Warns when mail routed on a user's own address arrives without a dmarc=pass from the receiving MTA. Monitoring for the assumption above; never blocks mail — see the DMARC canary |
dmarc_canary_warn_on_missing |
false |
Also warn when the mail carries no DMARC verdict at all. Off by default because a path that stamps nothing would warn on every message |
[conversation]
| Setting |
Default |
Description |
enabled |
true |
Enable conversation context |
lookback_count |
25 |
Messages to consider |
skip_selection_threshold |
3 |
Include all if history <= this |
selection_model |
"fast" |
Role alias for relevance matching (resolves to Haiku by default) |
selection_timeout |
30.0 |
Timeout for selection |
use_selection |
true |
Use LLM selection |
always_include_recent |
5 |
Always include this many recent |
context_truncation |
0 |
Max chars per bot response (0 = no limit) |
context_recency_hours |
0 |
Exclude old messages (0 = disabled) |
context_min_messages |
10 |
Min messages when recency filtering |
previous_tasks_count |
3 |
Unfiltered tasks to inject |
talk_context_limit |
100 |
Messages from Talk API |
[logging]
| Setting |
Default |
Description |
level |
"INFO" |
Log level (INFO or DEBUG) |
output |
"console" |
Destination: console, file, or both |
file |
"" |
Log file path |
rotate |
true |
Enable log rotation |
max_size_mb |
10 |
Max log file size |
backup_count |
5 |
Rotated files to keep |
[scheduler]
Polling intervals
| Setting |
Default |
Description |
poll_interval |
2 |
Seconds between queue checks |
dispatch_interval |
0.5 |
Sub-tick cadence for pool.dispatch() within a poll tick — bounds cold pending-task pickup latency. 0 or ≥ poll_interval = legacy one-dispatch-per-tick |
talk_poll_interval |
10 |
Seconds between Talk polls |
talk_poll_timeout |
30 |
Talk long-poll timeout |
talk_poll_wait |
2.0 |
Max wait before processing available rooms |
email_poll_interval |
60 |
Seconds between email polls |
briefing_check_interval |
60 |
Seconds between briefing/job/cleanup checks |
tasks_file_poll_interval |
30 |
Seconds between TASKS.md polls |
shared_file_check_interval |
120 |
Seconds between shared file checks |
heartbeat_check_interval |
60 |
Seconds between heartbeat checks |
db_health_check_interval |
86400 |
Seconds between SQLite quick_check + self-heal REINDEX sweeps over framework + per-user DBs (24h) |
scheduler_stats_interval |
60 |
Seconds between scheduler_stats health-line emits (threads / fds / rss / running-tasks / active-workers) — one key=value INFO line per interval on the istota.scheduler.stats logger, for catching resource leaks early. 0 disables |
loop_stall_alert_seconds |
180 |
Defense-in-depth: a watchdog thread logs an ERROR and fires one operator alert if the single-threaded main dispatch loop hasn't ticked in this long (a slow call that slipped onto the loop thread, a wedged check), then re-arms when the loop recovers. Suspended around known multi-minute in-loop work (sleep cycles, DB-health sweep) to avoid false pages. 0 disables |
Progress & event streaming
One persisted, typed event stream per task (the task_events table) feeds Talk, the web SSE endpoint, the log channel, and push notifications.
| Setting |
Default |
Description |
progress_updates |
true |
Master toggle for Talk progress updates |
progress_show_tool_use |
true |
Emit tool_start / tool_end events |
progress_show_text |
false |
Emit progress_text events (intermediate text; noisy) |
event_log_enabled |
true |
Write events to the task_events table (kill-switch for task-event-streaming) |
stream_text_gate_chars |
280 |
Narration gate for streamed answer text on stream surfaces (web/REPL). A text run emits no text_delta until it crosses this many chars without an intervening tool call, so short lead-in narration ("Let me check…") is discarded at the tool boundary instead of leaking into the answer area. Never loses text — only animation. 0 disables |
push_notification_threshold_seconds |
30 |
Min task duration before an ntfy completion push fires |
push_notification_sources |
[] |
Source types that trigger a completion push; empty = ntfy opt-in only (never a default surface) |
Worker pool
| Setting |
Default |
Description |
max_foreground_workers |
5 |
Instance-level fg worker cap |
max_background_workers |
3 |
Instance-level bg worker cap |
user_max_foreground_workers |
2 |
Global per-user fg default |
user_max_background_workers |
1 |
Global per-user bg default |
worker_idle_timeout |
10 |
Seconds before idle worker exits |
worker_idle_poll_interval |
0.5 |
Idle worker's queue re-check cadence |
main_loop_read_timeout_ms |
2000 |
SQLite read timeout on the main loop |
Robustness
| Setting |
Default |
Description |
task_timeout_minutes |
30 |
Claude Code execution timeout |
confirmation_timeout_minutes |
120 |
Auto-cancel confirmations after |
stale_pending_warn_minutes |
30 |
Warn for long-pending tasks |
stale_pending_fail_hours |
2 |
Auto-fail ancient tasks |
worker_heartbeat_seconds |
60 |
How often a running worker pings liveness (0 disables). Stuck-task reclaim uses the heartbeat to tell a slow-but-alive worker from a dead one. |
worker_stuck_minutes |
10 |
Reclaim a heartbeating worker's task after this much heartbeat silence. Independent of task_timeout_minutes. |
task_retention_days |
7 |
Delete old completed tasks |
email_retention_days |
7 |
Delete old IMAP emails (0 = disable) |
processed_email_retention_days |
90 |
Prune the processed-email dedup ledger (0 = disable). Floored at email_retention_days + 1; disabled entirely when email_retention_days is 0 |
talk_cache_max_per_conversation |
200 |
Max cached Talk messages |
scheduled_job_max_consecutive_failures |
5 |
Auto-disable threshold |
cron_max_staleness_minutes |
60 |
Skip cron-driven catch-up fires older than this (jobs + briefings). After a long daemon outage, fires missed by more than N minutes are skipped and last_run_at is bumped so the schedule resumes from the next future fire. 0 = legacy unconditional catch-up. |
log_channel_show_skills |
true |
Include selected skills in log channel messages |
max_retry_age_minutes |
60 |
A task older than this is failed rather than retried |
temp_file_retention_days |
7 |
Delete task temp files older than this |
location_ping_retention_days |
365 |
Prune per-user location.db pings older than this |
Subtasks
| Setting |
Default |
Description |
max_subtasks_per_task |
10 |
Cap on subtasks one task may queue |
max_subtask_depth |
3 |
Cap on subtask nesting depth |
max_subtask_prompt_chars |
8000 |
Cap on a subtask's prompt length |
Database backup
| Setting |
Default |
Description |
db_backup_enabled |
true |
Take timed online-backup snapshots of the local DBs |
db_backup_interval |
86400 |
Seconds between snapshots (24h) |
db_backup_dir |
"" |
Destination for dated snapshot dirs; empty derives {nextcloud_mount_path}/istota-db-backups. Use db_backup_enabled = false to disable |
db_backup_retention |
7 |
Keep this many snapshot dirs |
[security]
| Setting |
Default |
Description |
sandbox_enabled |
true |
Bubblewrap filesystem isolation (Linux only) |
skill_proxy_enabled |
true |
Credential proxy via Unix socket. Required wherever sandbox_enabled is true — the databases are masked out of the sandbox, so a skill CLI that can't reach the proxy refuses rather than reading nothing |
skill_proxy_timeout |
300 |
Proxy command timeout (seconds) |
passthrough_env_vars |
["LANG", "LC_ALL", "LC_CTYPE", "TZ"] |
Extra env vars for subprocess |
sandbox_ro_paths |
[] |
Extra RO bind-mounts in the sandbox, for co-located services. Keep entries narrow — a broad path sweeps in whatever lives under it. The DB directories are masked after this list either way |
sandbox_admin_db_write was removed: the framework DB is no longer bound into the sandbox for anyone, so there is no bind left to widen. A stale key logs a warning and is ignored.
[security.network]
| Setting |
Default |
Description |
enabled |
true |
Network isolation via CONNECT proxy |
allow_pypi |
true |
Allow PyPI access |
extra_hosts |
[] |
Additional allowed hosts |
[skills]
There is no [skills] config section. Skill disclosure is single-axis (a skill is either eager or a menu entry the model loads on demand) with no config knobs. A stale [skills] block only logs a warning at load time.
[models.aliases]
The operator-visible model alias registry — one table holding both the portable tiers and the provider shortcuts, overlaying the code-shipped defaults (brain.claude_code.DEFAULT_ALIASES). Used by !model <name> <prompt> in Talk/web and by internal subsystems (fast for triage/classification, general for sleep cycle, smart is user-facing only).
Shipped defaults (base names, no baked effort):
| Alias |
Default target |
fast |
Haiku |
general |
Sonnet |
smart |
Opus |
opus / sonnet / haiku |
current-latest of each |
default |
no override (brain/config default) |
Effort is an orthogonal :effort modifier appended to any reference (opus:high, smart:low, claude-opus-5:xhigh) — never baked into a name. An alias override is per model namespace so one definition covers every brain family: anthropic = the CLI brains (claude_code / tmux_claude), openai_compat = native. Two forms, both accepted:
# Flat (namespace-agnostic, resolved by whichever brain runs the task):
[models.aliases]
smart = "claude-opus-4-6:high" # pin smart to Opus 4.6, effort high
deep = "opus:max" # a custom alias
# Per-namespace (define once, correct on every brain):
[models.aliases.smart]
anthropic = "opus:high" # CLI brains
openai_compat = { model = "anthropic/claude-opus-4.8", effort = "high" } # native endpoint slug
[models.aliases.general]
anthropic = "claude-sonnet-5"
openai_compat = "anthropic/claude-sonnet-4.6" # bare string = no effort
[models.aliases.deep]
anthropic = "opus:max"
openai_compat = "anthropic/claude-opus-4.8"
portable = true # a cross-brain custom tier
Alias targets carry effort: a :effort modifier on the target (opus:high → high) or an explicit effort = on a per-namespace table reaches the wire (explicit wins). An alias uses one form (TOML: a key can't be both a string and a table); a per-namespace table missing the active brain's key falls to that brain's code default. A custom alias is a non-portable pin unless flagged portable = true (then it re-resolves across the cross-brain fallback boundary like the built-in tiers). Invalid anthropic targets (neither a known alias nor a canonical claude-* ID) are warned at config-load time via Brain.validate_alias_override; openai_compat slugs are sent verbatim (no alias table to validate against).
The old [models.roles] key is a hard rename to [models.aliases] — no longer read; a stale one present logs a one-time migration warning. The old effort-in-name forms (opus-high, opus-46) no longer resolve.
[brain]
Selects which model-invocation backend the executor uses. See architecture/brain for the protocol and the native brain runbook for the full [brain.native] settings.
| Setting |
Default |
Description |
kind |
"claude_code" |
Brain implementation. "claude_code" (default) wraps the headless claude -p CLI subprocess; "native" runs Istota's own in-process agent loop against any OpenAI-compatible model (configured under [brain.native]); "tmux_claude" drives the interactive claude TUI in a detached tmux session to keep traffic on subscription billing (configured under [brain.tmux], with automatic fallback to claude_code). |
source_type_overrides |
{} |
Per-source_type brain override (e.g. route scheduled to native while interactive tasks stay on claude_code). |
fallback |
"" |
Brain to rerun a request on when the primary is unavailable |
fallback_on_transient |
true |
Also reroute a persistent transient_api_error |
fallback_cooldown_seconds |
900 |
Skip an unavailable primary this long before retrying it; 0 disables |
[brain.native] (used when kind = "native", when it is the fallback, or when a source_type_overrides entry routes to it): provider (only "openai_compat"), model (explicit id), base_url, effort, model_overrides, extra_headers, context_window, max_turns, max_tokens, prompt_caching, compaction_reserve_tokens, compaction_keep_recent_tokens, bash_spill_full_output, turn_budget_nudge, turn_budget_nudge_early_percent, turn_budget_nudge_remaining, model_catalog_fetch, model_catalog_cache_ttl_hours, plus the nested [brain.native.web_fetch] SSRF-policy block. The API key comes from ISTOTA_BRAIN_NATIVE_API_KEY, never the TOML file. Full annotations in the native brain runbook.
[brain.tmux] (used when kind = "tmux_claude" or routed-to): every field defaults in code to the prototype's pinned values, so an absent block is behavioral parity. Knobs include fallback_trip_threshold, fallback_cooldown_seconds, ready_timeout_seconds, tmux_command_timeout, cli_version_pin, and the pane-text marker lists (ready_markers, trust_markers, theme_markers, bypass_warning_marker, bypass_accept_marker, error_markers, usage_limit_markers — the last is what drives stop_reason=usage_limit and therefore failover) — heuristics pinned to a claude CLI version, so a CLI reword that breaks readiness detection is a config hotfix, not a code release. See config.example.toml for the full annotated block.
[sleep_cycle]
| Setting |
Default |
Description |
enabled |
true |
Enable nightly memory extraction |
cron |
"0 2 * * *" |
Schedule (user's timezone) |
lookback_hours |
24 |
How far back to gather day data |
memory_retention_days |
0 |
Prune dated memory files and ephemeral memory_chunks rows (conversation, memory_file, channel_memory) older than N days. Durable user_memory chunks are not touched. 0 = unlimited |
auto_load_dated_days |
3 |
Days of dated memories injected into prompts; 0 disables |
extraction_model |
"general" |
Role used for the nightly extraction call |
curation_model |
"general" |
Role used for the USER.md curation call |
curate_user_memory |
false |
Run op-based USER.md curation after extraction |
curation_log_summary |
true |
Post a one-line summary to the user's log_channel after applied curation ops |
knowledge_graph_audit_retention_days |
365 |
Prune knowledge_facts_audit rows older than N days. Independent of memory_retention_days. 0 = unlimited |
[channel_sleep_cycle]
| Setting |
Default |
Description |
enabled |
true |
Enable channel memory extraction |
cron |
"0 3 * * *" |
Schedule (UTC) |
lookback_hours |
24 |
How far back to gather channel data |
memory_retention_days |
0 |
Prune dated channel files and channel_memory chunks older than N days. 0 = unlimited |
[memory_search]
| Setting |
Default |
Description |
enabled |
true |
Enable memory search |
auto_index_conversations |
true |
Index after task completion |
auto_index_memory_files |
true |
Index after sleep cycle |
auto_recall |
false |
BM25 auto-recall in prompts |
auto_recall_limit |
5 |
Max recall results |
recency_half_life_days |
180.0 |
Age half-life for the recency down-weight; 0 disables |
[briefings]
Module-level settings for the briefings module (per-user content store + archive). All defaulted, so an absent block is the shipped behaviour.
| Setting |
Default |
Description |
archive_retention_days |
90 |
Prune archived briefing results older than this, on insert (0 = keep forever) |
default_lookback_hours |
12 |
Seeds the email / rss source window when a source omits it |
newsletter_max_links_per_source |
20 |
Cap on links pulled from one newsletter source |
max_source_chars |
5000 |
Cap on a single source's gathered text. The todos source spends it item by item and never cuts one in half — a half-item would read as a todo the file does not contain — dropping from the end and saying in its provenance line how many were left out |
max_browse_chars |
20000 |
The same cap for a browse source, which gathers markdown rather than flattened text. Bigger because the URLs the markdown keeps cost characters, and a frontpage spends its first couple of thousand on masthead chrome before the headline grid starts. A browse source's own max_chars wins over either cap — it is the only kind that reads one; email, notes and todos take the module cap directly. |
shared_block_timezone |
"UTC" |
Timezone module-owned shared blocks evaluate their cron in. Shared blocks are global (generated once, no per-user timezone), so this is one operator-chosen zone — typically the operator's own, so morning/evening regeneration lines up with their day. An invalid name falls back to UTC at run time. |
[[briefing_shared_blocks]]
Module-owned shared blocks generated once globally under the reserved __system__ identity and read by any user's briefing through a shared_block source. Seeded once into the shared_block_configs table, after which the DB is authoritative and admins manage them from the web UI or istota briefings shared. Leave unset for the canonical defaults (world-headlines, markets-summary); an explicit empty list opts out. Only user-agnostic source kinds are allowed (browse, markets, email). See briefings.
[[default_briefings]]
A canonical shared briefing set, seeded once into each opted-in user (per-user
default_briefings flag, default on). Same name/cron/output/blocks
shape as a per-user briefing; content is blocks-only. (Replaces the retired
[briefing_defaults] boolean-component defaults.)
[[default_briefings]]
name = "Daily"
cron = "0 7 * * *"
output = "talk"
[[default_briefings.blocks]]
title = "World News"
render_mode = "synthesis"
[[default_briefings.blocks.sources]]
kind = "browse"
config = { preset = "ap" }
[developer]
| Setting |
Default |
Description |
enabled |
false |
Enable developer skill |
repos_dir |
"" |
Base directory for clones + worktrees |
gitlab_url |
"https://gitlab.com" |
GitLab instance URL |
gitlab_token |
"" |
API token |
gitlab_username |
"" |
GitLab username for HTTPS auth |
gitlab_default_namespace |
"" |
Default namespace for short repo names |
gitlab_reviewer_id |
"" |
User ID for MR reviewer |
github_url |
"https://github.com" |
GitHub instance URL |
github_token |
"" |
Personal access token |
github_username |
"" |
GitHub username |
github_default_owner |
"" |
Default org/user for short repo names |
github_reviewer |
"" |
PR reviewer username |
author_credit |
"" |
Name credited as author on commits the bot makes |
gitlab_api_allowlist |
(GitLab API paths) |
API paths the GitLab wrapper may call |
github_api_allowlist |
(GitHub API paths) |
API paths the GitHub wrapper may call |
api_timeout_seconds |
30 |
Timeout for GitLab/GitHub API calls |
devbox_proxy_enabled |
true |
Keep tokens host-side behind the devbox proxy |
devbox_proxy_socket_dir |
"/var/run/istota" |
Where the per-user devbox proxy sockets live |
devbox_proxy_audit_log |
"" |
Optional path for a devbox proxy audit log |
ntfy push notifications
ntfy is a per-user connected service — there is no [ntfy] config block. Each user supplies their own server URL, topic, and (optional) auth via the encrypted secrets table (see credentials for the full per-user credential inventory):
istota secret ensure --user alice --service ntfy --key topic --value alice-alerts
istota secret ensure --user alice --service ntfy --key server_url --value https://ntfy.example.com
istota secret ensure --user alice --service ntfy --key token --value tk_…
Or via the web UI at /istota/settings (Connected services → ntfy push). Priority is hardcoded to 3 (the ntfy default).
What it IS: a one-way push channel (bot → device) used by heartbeat alerts and scheduled-job output (output_target = "ntfy"). What it ISN'T: two-way (no replies), a Talk replacement, operator-shared infrastructure, or required.
Money
Money is a module (on by default; opt out per user via disabled_modules = ["money"]). Per-user money settings live in the per-user money DB, not in config.toml; the one instance-level knob is:
| Setting |
Default |
Purpose |
autoclass_lookup |
true |
Allow transaction auto-classification to look up unknown payees |
The bot auto-discovers *.beancount files at the top level of {user_workspace}/ledgers/ — no per-resource path is required. Monarch credentials are a cookie pair in the encrypted secrets table — provision both keys (session_id and csrftoken) via the CLI or the web settings UI: |
|
|
istota secret ensure --user alice --service monarch --key session_id --value …
istota secret ensure --user alice --service monarch --key csrftoken --value …
[google_workspace]
| Setting |
Default |
Description |
enabled |
false |
Enable Google Workspace skill |
client_id |
"" |
Google OAuth client ID |
client_secret |
"" |
Google OAuth client secret (or ISTOTA_GOOGLE_WORKSPACE_CLIENT_SECRET env var) |
scopes |
Drive, Gmail, Calendar, Sheets, Docs |
OAuth scopes to request |
See Google Workspace for setup instructions.
[site]
hostname is load-bearing beyond OAuth2 and the origin check: minting a location ingest token refuses with a 409 while it is unset, because the webhook URL it assembles would be a relative path and the phone's QR decoder accepts only https://. A standalone local install with no hostname therefore cannot provision a device by QR.
The deployment's public DNS name.
| Setting |
Default |
Description |
hostname |
"" |
Public DNS name; used by the web app for OAuth2 redirect derivation, origin/CSRF checks, and webhook URLs |
The agent-writable static web root (enabled / base_path) was removed. A publicly-served directory the agent could write to with an ordinary cp was an outbound egress channel the confirmation model treated as a benign local write, so anything the agent could read could be published to a public URL without a gate. Serve static assets outside istota, from a directory the agent cannot reach. A stale enabled / base_path key logs a warning at config load and is ignored.
[web]
| Setting |
Default |
Description |
enabled |
false |
Enable web interface |
auth |
"nextcloud" |
Auth mode. "nextcloud" is OAuth2 against Nextcloud; "none" disables auth entirely for the single-user local install and must never be used on a reachable host. Env override: ISTOTA_WEB_AUTH |
token_storage |
"ephemeral" |
Where per-user Nextcloud tokens live. "ephemeral" keeps them in the session only; "encrypted" retains them in web_user_tokens and requires ISTOTA_WEB_TOKEN_KEY. Any other value warns and falls back to ephemeral. Env override: ISTOTA_WEB_TOKEN_STORAGE |
port |
8766 |
Web app port |
oauth2_provider |
"" |
Public Nextcloud URL (browser-facing), no trailing slash |
oauth2_client_id |
"" |
NC OAuth 2.0 client ID |
oauth2_client_secret |
"" |
NC OAuth 2.0 client secret (or ISTOTA_WEB_OAUTH2_CLIENT_SECRET env) |
oauth2_token_endpoint |
"" |
Optional server-to-server token URL override |
oauth2_userinfo_endpoint |
"" |
Optional server-to-server userinfo URL override |
oauth2_redirect_uri |
"" |
Explicit redirect URI override; otherwise derived from request |
session_secret_key |
"" |
Session signing key (or ISTOTA_WEB_SESSION_SECRET_KEY env) |
[web.chat]
Knobs for the in-app web chat surface (the "Chat" tab). The surface is always enabled when the web UI is on; these tune limits and streaming cadence.
| Setting |
Default |
Description |
max_prompt_chars |
32000 |
Max characters accepted per chat message |
max_attachment_mb |
25 |
Max attachment size, in MB. Application default — the Ansible role sets 100 and renders nginx's client_max_body_size from the same variable (see below) |
attachment_extensions |
pdf png jpg jpeg heic webp gif txt md csv wav mp3 m4a ogg webm docx xlsx |
Allowed attachment file extensions — images (including heic, what an iPhone photo is), documents, text, and the audio formats a voice message arrives in |
rate_limit_messages |
30 |
Messages allowed per user per window |
rate_limit_window_seconds |
300 |
Rate-limit window (5 minutes) |
sse_poll_interval_ms |
200 |
Server-side task_events poll cadence for the SSE stream |
client_poll_interval_ms |
1500 |
Client fallback poll cadence when SSE is unavailable |
talk_read_sync_interval |
60 |
Talk→web read-state pull cadence, seconds (0 disables) |
room_stream_poll_interval_ms |
1000 |
Server-side messages tail cadence for the live room stream |
room_stream_keepalive_seconds |
20 |
SSE comment-frame cadence, so a proxy can't drop an idle stream |
room_stream_max_batch |
500 |
Rows before a gap frame tells the client to reload instead of replay |
room_stream_max_bytes |
2000000 |
Serialized-byte budget for the same guard |
room_stream_room_check_seconds |
10 |
Per-connection room-metadata diff cadence (0 disables) |
[location]
| Setting |
Default |
Description |
enabled |
false |
Enable GPS webhook receiver |
webhooks_port |
8765 |
Receiver port |
accuracy_threshold_m |
100.0 |
Discard pings less accurate than this, in metres |
visit_exit_minutes |
5.0 |
Minutes away from a place before a visit is closed |
reconcile_enabled |
true |
Batch-reconcile visits from pings, cleaning up state-machine drift |
reconcile_lookback_hours |
6.0 |
How far back a reconcile pass looks |
reconcile_buffer_minutes |
10.0 |
Buffer around the lookback window |
reconcile_grace_minutes |
10.0 |
Grace period before a gap counts as a departure |
reconcile_min_pings |
3 |
Minimum pings for a reconstructed visit to count |
reconcile_min_dwell_sec |
60 |
Minimum dwell for a reconstructed visit to count |
[caldav]
Explicit CalDAV override. When any field is set it wins over the value derived from [nextcloud], which is how a standalone install points calendar at an external CalDAV server (Radicale, Fastmail, Google) with no Nextcloud in the picture. All-blank — the default — falls back to the Nextcloud derivation, so server deployments are unaffected.
| Setting |
Default |
Description |
url |
"" |
CalDAV base URL |
username |
"" |
CalDAV username |
password |
"" |
CalDAV password |
[browser]
| Setting |
Default |
Description |
enabled |
false |
Enable the headless browser container |
api_url |
"http://localhost:9223" |
Browser container's Flask API |
vnc_url |
"" |
External noVNC URL, surfaced to the user for observation |
[devbox]
A persistent per-user Linux container — the escape hatch for work the bwrap sandbox can't do (installing packages, network diagnostics, compiling).
| Setting |
Default |
Description |
enabled |
false |
Enable the devbox skill |
container_prefix |
"devbox-" |
Container name is {prefix}{user_id} |
docker_cli |
"/usr/bin/docker" |
Host path to the Docker CLI binary |
docker_socket |
"/var/run/docker.sock" |
Host path to the real Docker socket (the proxy's upstream) |
exec_timeout_seconds |
300 |
Default per-exec timeout |
max_output_bytes |
102400 |
Cap per output stream |
api_proxy_enabled |
true |
Bind an allowlist proxy at /var/run/docker.sock instead of the raw socket |
api_proxy_socket_dir |
"/var/run/istota-docker" |
Where the per-user Docker-API proxy sockets live (distinct from [developer] devbox_proxy_socket_dir) |
api_proxy_audit_log |
"" |
Optional path for a proxy audit log |
The raw Docker socket is root-equivalent, so it is never bound into the sandbox. The allowlist proxy permits only exec/cp/inspect/restart against the user's own container.
[playbooks]
Procedural memory — see memory.
| Setting |
Default |
Description |
enabled |
false |
Master switch |
recall_limit |
3 |
Top-K playbooks injected per task |
min_tool_calls |
4 |
Tool calls a task needs to qualify for distillation |
retention_days |
90 |
Age-prune by last use; 0 = keep forever |
max_chars |
0 |
0 = share the global max_memory_chars budget |
[experimental]
[health]
| Setting |
Default |
Description |
max_document_bytes |
26214400 (25 MiB) |
Cap on a single stored document (scan, discharge summary, vaccination card). 0 = unlimited |