alpi's settings live in ~/.alpi/config.yaml (or ~/.alpi/profiles/<name>/config.yaml for non-default profiles). This page lists every knob, its default, and what it controls.
What ships in the YAML
On first install alpi only writes the sections you're likely to tweak — and where defaults are platform-dependent enough to deserve visibility:
model: "" # empty on a fresh scaffold; pick via `alpi setup → Model` (see docs/MODELS.md)
providers:
ollama: []
mcp:
servers: {}
gateway:
telegram: {}
matrix: {}
imap:
poll_interval: 60
mark_as_read: true
Everything else (tool limits, TUI flags, fallback models, workspace) falls back to the defaults below at load time. Add a key to the YAML only when you want to override it.
How to change settings
Three options:
- CLI wizards:
alpi setupcovers model selection, gateway credentials, MCP servers, sandbox posture, voice, peers, workgroups, disk cleanup, and the alpi daemon's lifecycle.alpi setup → Cleanupinspects the profile's heavy dirs (audio cache, old sessions, schedule output) and the RAG store's SQLite freelist (VACUUM-not-unlink), with one-shot confirmation per category.alpi setup → Servicesexposes two rows: Daemon (default profile only — install / uninstall / start / stop / restart of the per-machine launchd plist or systemd-user unit) and Subsystems (per-profile toggles for gateway / scheduler / ALP / workgroups / host, plus the inter-machine TCP port for ALP). The firstalpi setupauto-installs the daemon, so the lifecycle row is mostly read-only after that. - Edit the YAML: open
~/.alpi/config.yaml(or~/.alpi/profiles/<name>/config.yamlfor non-default profiles) and change values manually. Restart whatever surface was affected. Cosmetic knobs (tui.*,tools.max_steps_per_turn,gateway.imap.poll_interval,fallback_models) live here. - Populate
.envdirectly (non-interactive, CI / devcontainers): alpi does not ship a.env.example— the Reference tables below (Core, Gateway — Telegram / Matrix / IMAP / Gmail) list every key with its default. Create~/.alpi/.envyourself with just the keys you use and alpi picks them up on next launch.
Reference
Core
| Key | Default | Type | Takes effect |
|---|---|---|---|
model | "" (empty; pick via alpi setup → Model — see docs/MODELS.md) | string | next session |
workspace | "" (cwd at launch) | string | next session |
fallback_models | [] | list of strings | next turn |
providers.ollama | [] | list of {name, url} — one per Ollama server | next session |
providers.openrouter.models | [] | list of OpenRouter model ids the user has picked | next session |
public_bio | "" | string — one-line public tag-line broadcast to every workgroup this profile joins (source of truth for Member.bio on the hub). Empty = don't publish; peers see name only. AGENT.md stays private. | next workgroup.join |
paused | false | bool — profile-level pause flag. Surfaced in the desktop / mobile profile summary so paired apps can show + respect the state; the daemon itself does not gate turns on this flag. Persisted only when true. | next host-plane read |
Tools
| Key | Default | Type | Takes effect | ||||
|---|---|---|---|---|---|---|---|
tools.max_steps_per_turn | 40 | int | next turn | ||||
tools.deny | [] | list of tool names | next turn | ||||
tools.web_extract.model | "" (use main) | string | next turn | ||||
tools.read_image.model | "" (use main) | string | next turn | ||||
tools.terminal.sandbox | false | bool | next turn | ||||
tools.terminal.allow_network | false | bool | next turn | ||||
tools.terminal.approval.allowlist | [] | list of pattern descriptions and/or command globs (see below) | next turn | ||||
tools.browser.vision | false | bool | next turn | ||||
tools.browser.allow_local | false | bool — let the browser tool navigate loopback only (127.0.0.1, ::1, and hostnames that resolve to loopback such as localhost). RFC1918 / CGNAT / Tailscale stay blocked even when this is on; the exemption is loopback-only, matching _guards._is_loopback. Off blocks every local target; on is for hitting a local dev server you trust. | next turn | ||||
tools.budget.per_result_chars | 100_000 | int (-1 = unlimited) | next turn | ||||
tools.tts.voice | "en-US-AriaNeural" | Edge TTS voice id | next turn | ||||
tools.tts.rate | "" | string ("+10%", "-20%") — speed | next turn | ||||
tools.tts.pitch | "" | string ("+5Hz", "-10Hz") — pitch | next turn | ||||
tools.tts.auto_read | false | bool — apps auto-play each agent reply aloud | next turn | ||||
tools.stt.model | "base" | tiny \ | base \ | small \ | medium \ | large-v3 | next turn |
tools.stt.language | "" (auto) | ISO code (en, es, ...) | next turn | ||||
tools.<name>.max_result_chars | — (unset) | int (-1 = unlimited) | next turn |
max_steps_per_turn is a cost guard. When left at the default, a free model (zero per-token pricing) or a local/ollama one raises the effective ceiling to 1000 (no runaway cost to bound). An explicit value you set is always respected — it also bounds loops, refusals, and the TODO guard, not just cost.
tools.budget.per_result_chars caps the size of any tool output the LLM sees in-context, with a … [N chars elided by tool budget] suffix when hit. Prevents a single read_file on a 5 MB log from blowing up a turn. Per-tool overrides via tools.<name>.max_result_chars — set -1 on read_file if you want the LLM to get the whole source deliberately, or lower a chatty tool's cap.
Precedence: tools.<name>.max_result_chars (if set) → tools.budget.per_result_chars → hardcoded 100_000.
Not implemented (tracked, not planned): per-turn aggregate cap and inline preview. Comparable agents carry both, but alpi only ships them if real turns start burning through several large tool results.
tools.deny is a per-profile denylist of tool names. Denied tools are absent from the schema the LLM sees (it can't reach for what it doesn't know about) AND refused by the executor as defence in depth — if a stale context or a peer's link.ask names a denied tool, the call returns tool denied for this profile: <name> instead of running. Unknown names are no-ops, so typos are harmless. Denying alpi_knowledge also drops the self-knowledge rule from the system prompt, so the model is never told to call a tool it cannot reach.
Canonical names are the strings used at registration time — write_file, edit_file, terminal, email, send_message, schedule, delegate, peer, index_workspace, search_workspace, alpi_knowledge, research, browser, workgroup, etc. See alpi/tools/__init__.py for the full registry. Note: the alpi-docs tool registers as alpi_knowledge, not knowledge — writing knowledge in deny is a no-op.
Useful for tightening a profile that is exposed to less-trusted input — e.g. a "librarian" profile that other peers reach via link.ask and that has no business writing files, running shell, or sending mail:
# ~/.alpi/profiles/archi/config.yaml
tools:
deny:
- write_file
- edit_file
- terminal
- email
- send_message
- schedule
- delegate
Today this is YAML-only. There is no alpi setup wizard for deny and no surface for it in the desktop/mobile apps — the surface is power-user enough that raw names beat any UI we'd build right now.
tools.terminal.sandbox enables OS-level isolation on shell commands (macOS sandbox-exec, Linux bubblewrap). Toggle via alpi setup → Sandbox, or directly in YAML. The TUI top bar shows the current state (sandbox on / off). Most useful on profiles that run unattended (gateway, schedule, sub-agents) — see SECURITY.md for the recommended pattern + platform requirements.
allow_network has no effect unless sandbox is on. When sandbox is on and allow_network=false, the flag blocks ALL agent-initiated network:
- The
terminalsubprocess is denied sockets (sandbox-exec / bwrap). - Python-native tools (
web_fetch,web_search,web_extract,browser,tts,send_message,email,read_imageon URLs) refuse with a clear error. - The LLM call itself (litellm) is exempt — it's the agent's brain, not an exfiltration vector. The gateway inbound listener is also exempt (receiving is not exfiltrating).
The TUI top bar shows offline instead of sandbox when network is locked, so unattended profiles can be audited at a glance.
tools.terminal.approval controls the command approval system — a layer on top of the sandbox that gives the user a chance to approve borderline destructive commands instead of blocking them outright. Each terminal call is classified by a small pattern list into three severities:
- safe (default, no match) — runs without prompting.
- caution — matches a pattern that's often legitimate but
sometimes destructive. Examples:
rm -rf <dir>,chmod 777,sudo <cmd>,git push --force,git reset --hard,DROP TABLE,kill -9. These pause for user approval in the TUI with four options:Once(this call only),Session(allowlist the pattern until restart),Always(persist the pattern description totools.terminal.approval.allowlistin config), orDeny(abort the tool call). On non-interactive surfaces (gateway, schedule) these auto-deny with a clear error telling the user to rerun from the TUI or edit the config allowlist. - dangerous — matches a pattern that's almost never legitimate.
Examples:
mkfs,dd of=/dev/…, fork bomb, pipe-to-interpreter from an unknown URL (curl … | bash), recursive chmod / chown on/, reading SSH private keys, writes into/etcor/var. These are always blocked. No override — if you genuinely need to run one of these, do it directly from your shell, not through the agent.
Allowlist entries come in two shapes, sharing the same list:
Pattern descriptions — the human label attached to one of the built-in caution regexes (recursive rm, sudo, git force-push, git hard reset, chmod 777 / a+w, sql drop / truncate, process kill -9). A pattern-desc entry allows every command of that severity-category. This is what the Always button writes.
Command globs — any other string is treated as an fnmatch pattern matched against the literal command (whitespace-trimmed). Use this for per-command exceptions when the category-level bypass is too broad. Globs only override caution classification; dangerous commands stay blocked. Globs also do not apply to compound commands (containing &&, ||, ;, |, newline, backticks, or $(…)) — otherwise "sudo apt *" would also approve sudo apt update && rm -rf build. Compound commands fall back to the prompt unless a category-desc bypass covers them.
tools:
terminal:
approval:
allowlist:
- recursive rm # category: every rm -rf passes
- sudo apt * # glob: any sudo apt subcommand
- git reset --hard origin/main # glob: this exact command only
- git push --force origin my-branch # glob: only this branch's force-push
Session approvals live in memory (a module-level set) and die with the TUI process. Permanent approvals persist to config.yaml via the Always button (which writes a pattern-desc) or by hand-editing the list with globs. Dangerous commands never get an allowlist entry.
This layer composes with the sandbox (tools.terminal.sandbox): the sandbox is an OS-level boundary (network, filesystem writes outside workspace) that catches what the approval layer misses; approval is user-in-loop for the subset of commands that are legitimately destructive inside the allowed scope. Both can be on at once; the approval check runs first so the user sees the prompt before the sandbox has a chance to refuse.
tools.browser.vision lets the browser(screenshot, question=…) action auto-chain the screenshot into the vision model (tools.read_image.model or the active main model) and return the answer instead of the file path. When false (default), screenshot always returns the path and a hint pointing at read_image so the LLM can decide whether to pay for vision per call. Useful to turn on in an exploratory profile; keep off in watchdog/gateway profiles so the agent doesn't burn vision tokens silently.
Image resizing is automatic: any image whose longer edge exceeds 1568 px (Anthropic's recommended bound) is downscaled before base64-encoding to the model. Vision-model cost scales with resolution — a 4K screenshot costs ~9× more tokens than its 1568-px version for the same content. Aspect ratio is preserved, PNG-with-alpha stays PNG, everything else rounds-trips through JPEG q=85. SVG (vector) is skipped. Not a knob — it is a fixed constant (alpi.tools.read_image.MAX_EDGE).
The research sub-agent's depth tiers (quick = 8 steps, normal = 15, deep = 30) are product definition, not user config. The agent picks the depth name from intent (quick = single-answer lookups, normal = comparative research, deep = exhaustive surveys); the step ceilings live in alpi.tools.research.DEPTH_STEPS_DEFAULTS.
tools.tts.voice selects the Edge TTS voice used by the tts tool. Any Microsoft Neural voice id is valid (es-ES-AlvaroNeural, en-US-AriaNeural, fr-FR-DeniseNeural, ...). Output is an MP3 cached under ~/.alpi/cache/tts/<hash>.mp3 — same text + voice reuses the cached file. Edge TTS runs against a free Microsoft endpoint (no API key), so there's no per-call cost. To use a different voice per call the agent can pass voice=... directly without touching config. alpi setup → Voice gives you a curated shortlist (10 common-language voices) plus a "custom" entry to type any voice id.
The daemon never plays audio itself — the tts tool returns the cached file path and stops. The alpi mobile / desktop apps stream playback on demand from a per-message button, and — when tools.tts.auto_read is on — auto-play each agent reply aloud as it arrives (your own messages are never read); they synthesize through the same Edge TTS path via host.voice.preview. For an external chat (e.g. Telegram) the agent chains send_message(attachment=<path>) to deliver the MP3 as an audio attachment. Workgroups carry an analogous hub-local auto_read flag in the workgroup meta (set from the desktop/mobile workgroup settings) that auto-reads agents' messages — never your directives; it is not replicated to members.
rate and pitch are config-only (not per-call args) — persistent prosody defaults. Leave empty for neutral. Text is capped at 1000 chars (~1 minute); longer input is rejected. Output is always MP3.
tools.stt.{model,language} control the stt tool backed by faster-whisper running on CPU. First call downloads the model weights (~40 MB for tiny, ~150 MB for base, ~500 MB for small, ~1.5 GB for medium, ~3 GB for large-v3) into ~/.cache/huggingface/ and keeps them forever. Pick the smallest model that meets your accuracy bar — base is the sweet spot for spoken messages/voice notes; small or above for podcasts/meetings. language defaults to "" (auto-detect); set to an ISO code (en, es, fr, ...) only when auto-detect fails on short clips.
The Telegram gateway auto-transcribes inbound voice notes and audio files through the same stt pipeline: when a user sends a voice message, the gateway downloads it via getFile, caches under ~/.alpi/cache/inbound/, runs stt, and feeds the transcript to the agent as text ([voice note] <transcription>). The agent sees a normal text turn — nothing surface-specific to handle.
Runtime
Provider stale-call hardening for LLM streaming turns: watchdogs that fail a slow/stuck provider instead of hanging the turn, plus jittered retries before any output reaches the consumer. A timeout of 0 disables that watchdog.
| Key | Default | Type | Takes effect |
|---|---|---|---|
runtime.first_byte_timeout_s | 300 | seconds (0 = off) | next turn |
runtime.stream_idle_timeout_s | 120 | seconds (0 = off) | next turn |
runtime.max_retries | 2 | int | next turn |
runtime.retry_backoff_s | 1.5 | seconds (base; exponential + jitter) | next turn |
first_byte_timeout_s is generous so slow reasoning models aren't killed before their first token; bump it for very slow local Ollama or long-thinking models. Retries fire only for transient failures (timeouts, connection drops, 429/5xx) and only before any token has streamed — a partially-streamed turn is surfaced, not silently replayed.
Model reasoning
Optional reasoning-effort hint passed alongside cfg.model to providers that support it (Anthropic extended thinking, OpenAI o-series, DeepSeek R1, etc.). Applied only to the profile's default model — mid-chat model overrides and tool sub-models (research, delegate, web_extract, read_image) ignore it. Models that don't recognise the hint are unaffected.
| Key | Default | Type | Takes effect | |||
|---|---|---|---|---|---|---|
model_reasoning.effort | "" (no reasoning param sent) | `"" \ | "low" \ | "medium" \ | "high" — "off" written to disk normalises to ""` on load | next turn |
model_reasoning:
effort: medium
Memory
| Field | Default | Notes |
|---|---|---|
memory.review_interval | 0 (off) | Post-turn reviewer cadence. N > 0 fires a daemon-thread reviewer every N user turns that snapshots the conversation and writes durable facts via memory(action="add"). Append-only — the reviewer cannot replace/remove. Opt-in by design. |
Internal-only constants (in alpi/memory.py and alpi/compaction.py, not user knobs):
USER_CHAR_LIMIT = 3000,MEMORY_CHAR_LIMIT = 5000— file-level caps.- Jaccard
0.7max-containment — near-duplicate threshold. LOW_CONFIDENCE_MAX_AGE_DAYS = 30— low-conf pruning age.trigger_ratio = 0.75,target_ratio = 0.40,keep_head = 2,keep_tail = 8— auto-compaction policy.
Calibration stays evidence-gated: these constants should not become user knobs unless real logs/compaction.jsonl / memory-review traces show repeated failures that a fixed default cannot solve.
TUI
alpi's TUI is built on Textual — a full widget-based framework with streaming, focus management, scroll anchoring, and responsive layout. It's the primary surface (not a fallback); gateway and schedule processes inherit the same engine behind the scenes but render through their own channel (chat message, log file).
Design choices worth knowing before tweaking config:
- Single cohesive UI. No separate "legacy CLI" to maintain. alpi has one Textual app that covers every interactive use case.
- Streaming is the default. Assistant text streams into a Markdown
widget char-by-char. No full-message reload; the widget knows how to
append deltas. On
assistant_donethe final text replaces the streamed buffer so any post-processing (e.g._strip_cache_noise) takes effect without a flash. - Tool cards, not log lines. Each tool call gets a compact card
with an args preview on the left, a live state in the middle
(
synthesizing…,playing…,transcribing…— tools push these viatool_state_mod.emit_state), a result hint on the right, and a duration badge. Cards are scroll-anchored so the chat follows new activity without stealing focus from what you're reading above. - Reasoning is inline, not modal. For reasoning models
(DeepSeek-R1, OpenAI o-series, Claude extended thinking) the tail
of
reasoning_contentscrolls live inside thethinking…indicator. Full history is persisted tosessions/*.jsoneven whentui.show_reasoning=false, so you can re-enable later and replay gets the reasoning back. - Slash commands auto-suggest.
/help,/memory,/tools,/mcps,/cost,/clear,/new,/compact,/skills,/model,/exit,/quit. Typing/opens a fuzzy prefix suggester over that list. - Responsive. The top bar collapses labels when the terminal is
narrower than 60 columns; long paths are home-dir-abbreviated to
~/…. Nothing clips, nothing wraps weirdly. - Theming. Pick a
tui.accentcolour (CSS hex/name/rgb) andtui.theme: dark|light. The accent recolours interactive highlights and the profile name in the top bar. - Scroll resilience under heavy streaming.
VerticalScroll.anchor()is used during long tool outputs or streamed responses so the view tracks the bottom without the user losing scroll position when they were reading history.
What the top bar shows (left to right):
alpi <version> │ profile <name> <size> │ [sandbox|offline] │ workspace <path>
<size>is the total disk footprint of the active profile home dir (~/.alpi/for default,~/.alpi/profiles/<name>/otherwise). Cached for 30 s; refreshed when you change profile, workspace, or model. For the default profile theprofiles/subtree is excluded so it doesn't conflate with sibling profiles. Hidden in narrow mode (< 60 columns).- The sandbox segment shows
sandboxwhentools.terminal.sandbox=trueandtools.terminal.allow_network=true; it switches toofflinewhen the network is locked (see sandbox knobs above). Hidden when sandbox is off. - Workspace shows the resolved workspace path, or
not setin error colour when no workspace is configured and alpi falls back to cwd.
Config knobs (tui.*):
| Key | Default | Type | Takes effect | |
|---|---|---|---|---|
tui.show_cost | true | bool | next session | |
tui.show_tokens | true | bool | next session | |
tui.show_reasoning | true | bool | next session | |
tui.accent | #c8a24e | CSS color (hex / named / rgb) | next session | |
tui.theme | dark | dark \ | light | next session |
tui.auto_resume | false | bool | next launch |
tui.auto_resume makes bare alpi behave as if -c / --continue was passed — the last session is loaded automatically. Use /new inside the TUI to start a fresh thread without changing the config. The flag does not affect alpi chat --once (scripts and the gateway always start clean) or explicit -c usage (still an override).
tui.show_reasoning controls two channels of model-thinking output:
- Inter-tool prose — the dim
» …line that appears above a tool card with whatever text the model emitted between tool calls. - Streamed chain-of-thought — for reasoning models
(DeepSeek-R1, OpenAI o-series, Claude extended thinking), the
tail of
reasoning_contentscrolls live inside thethinking…indicator.
When false, both are hidden from the screen. The reasoning is still persisted to the session file (sessions/*.json) so that re-enabling the flag later brings it back on replay, and so that debug inspection (cat sessions/<id>.json) always has the full context. Gateway surfaces (Telegram, Email) never rendered reasoning, so this flag has no effect there.
Gateway — Telegram / Matrix
| Key | Default | Notes |
|---|---|---|
gateway.telegram | {} | Placeholder section; no per-platform knobs today. Telegram credentials live in ~/.alpi/.env (TELEGRAM_BOT_TOKEN, TELEGRAM_ALLOWED_SENDERS). |
gateway.matrix | {} | Placeholder section; same shape as Telegram. Matrix credentials in .env. |
Both keep typing indicators hardcoded on and send only the final agent reply. Use TUI, desktop, or mobile when you want live tool progress.
Gateway — IMAP
| Key | Default | Why |
|---|---|---|
gateway.imap.poll_interval | 60 (seconds) | IMAP polling cadence. Keeps CPU/network quiet for personal use. |
gateway.imap.mark_as_read | true | Processed messages marked \Seen so your mail client treats them as read. |
Gateway — Gmail
Same knobs as IMAP, different backend. Polling uses Gmail's users.history.list with the last-seen historyId so we only fetch deltas (cheaper than rescanning INBOX). Credentials live in ~/.alpi/<profile>/gmail_token.json after the one-off OAuth consent via alpi setup → Gateways → Gmail.
| Key | Default | Why |
|---|---|---|
gateway.gmail.poll_interval | 60 (seconds) | Same rationale as IMAP. |
gateway.gmail.mark_as_read | true | Removes the UNREAD label on processed messages. |
Configure both if you want: imap polls your primary mailbox via password, gmail polls another account via OAuth, each with its own allowlist (IMAP_ALLOWED_SENDERS vs GMAIL_ALLOWED_SENDERS).
Budget
One daily spending ceiling per profile, in dollars — or unlimited.
daily_usdcaps real spend (LiteLLM reports a cost per turn for paid APIs; local/free paths report zero and so never hit the cap).- Leave it unset for no ceiling.
A token-denominated cap is intentionally not offered: tokens are not a meaningful business constraint (their cost varies by model), so the only caps that mean anything are dollars or nothing.
The cap covers every turn this profile runs: interactive TUI replies, gateway responses (Telegram / IMAP / Gmail / Matrix), scheduled jobs, sub-agent spawns (research, delegate, read_image), and inbound ALP calls from pinned peers. It is re-checked before each step within a turn, so a long multi-step turn aborts as soon as it crosses the ceiling rather than running to completion. Counters reset at UTC midnight; no carry-over. The ledger lives at ~/.alpi/<profile>/logs/ledger.json and also records a per-peer breakdown for the /cost panel, though only the profile total gates new turns.
| Key | Default | Notes |
|---|---|---|
budget.daily_usd | unset | Hard daily USD cap. Exceeding it surfaces budget-exceeded (interactive) or JSON-RPC -32005 budget-exceeded (ALP). Unset = unlimited. |
budget:
daily_usd: 5.00
Edit interactively via alpi setup → Budget or the desktop app's profile detail.
Network (shared accessible address)
One address, shared by every network listener this profile runs — the device-pairing host plane and the ALP peer listener both bind/advertise on it, each on its own port. Configure it once.
| Key | Default | Effect |
|---|---|---|
network.host | "" | The address other machines and your devices reach this profile at. Empty = auto-detect (Tailscale first, then a private LAN address). Any reachable host works: a Tailscale / WireGuard / VPN address, a private hostname or MagicDNS name, a LAN IP, 0.0.0.0, or a public IP. A public IP additionally needs host.allow_public_bind: true; that gate applies to the shared bind used by both planes (host control plane + ALP listener), so without it neither binds TCP. |
network:
host: "" # empty = auto-detect Tailscale → LAN
Set it via alpi setup → Network → Accessible address or the desktop app (Devices → pairing). Ports stay per-plane (host.tcp_port, alp.tcp_port). A public IP is config.yaml-only: the desktop rejects it, and it also requires host.allow_public_bind: true — set both keys by hand.
ALP
ALP always serves the per-profile Unix socket for same-machine peers. The Noise_XK TCP listener is auto-exposed only for the default profile, whenever the machine has a reachable address — bound to the shared network.host (above), or an auto-detected overlay/LAN address, or 0.0.0.0 in Docker. Named profiles stay Unix-only unless they set their own explicit, unique alp.tcp_port (otherwise every profile would fight over the shared port). With no reachable address (no network.host, no Tailscale/LAN, not Docker) even default stays Unix-only. Turn ALP off entirely with service.alp: false.
| Key | Default | Notes |
|---|---|---|
alp.tcp_port | 7423 (default profile only) | The ALP peer TCP port. Auto-exposed for the default profile; a named profile binds TCP only if it sets its own unique port here. The address is network.host. |
alp:
tcp_port: 7423
Ollama
Ollama is a first-class provider. One entry per server — local, remote, different ports — each with its own user-chosen name that becomes the model prefix (home/gemma4:e4b, gpu-box/qwen3:14b). On every request against an Ollama server, num_ctx is auto-resolved from /api/show and injected so the model sees the full prompt instead of being truncated to Ollama's 2K default.
providers:
ollama:
- name: home
url: http://localhost:11434
- name: gpu-box
url: http://192.168.1.50:11434
Add via alpi setup → Model → Add Ollama. Remove via alpi setup → Model → Remove keys.
MCP
| Key | Default | Notes |
|---|---|---|
mcp.servers | {} | Map of <name> → {command, args, env}. Secrets in env use the env:VAR_NAME reference. Add via alpi setup → MCPs — hand-editing is supported but the wizard is easier. |
Services (per profile)
Which services the alpi daemon spawns for THIS profile. The daemon itself is one-per-machine (see OPERATIONS.md → Daemon); these flags only decide what it activates here.
service:
gateway: true # Telegram / IMAP / Gmail / Matrix / webhook listeners
schedule: true # cron tick loop
alp: true # peer-to-peer ALP listener
workgroups: true # ALP.3 outbound poller for joined workgroups
host: true # control-plane socket for desktop / mobile clients (default profile only)
The block name remains service: for back-compat; conceptually each entry is a service the daemon runs for this profile. Missing keys default to True. Toggling takes effect at the next alpi daemon restart.
host is meaningful only on the default profile; on any other profile the toggle is honoured but the runner refuses to bind a socket (the desktop / mobile client always targets default's socket and reaches sibling profiles via the profile parameter on each verb).
Host (control plane)
The host plane serves host.* verbs over a Unix socket (always) and a WebSocket on the shared network.host (see Network above); mobile / remote desktop use this path.
| Key | Default | Effect |
|---|---|---|
host.tcp_port | 49200 | WebSocket port for device pairing (the host plane's own port). |
host.device_name | "" | Optional pairing name shown in Devices. Empty = auto, otherwise embedded in the pairing QR and device list. |
host.allow_public_bind | false | Opt-in to let the shared network bind use a public IP. Affects both the host control plane and the ALP listener — both derive their bind from network.host, so without it neither binds TCP on a public address. A Tailscale / private-LAN / hostname address needs no opt-in; only a public IP does. |
host:
tcp_port: 49200
device_name: ""
The address itself lives in network.host (shared with the ALP listener), not here — this section is just the host plane's port, pairing name, and the public-bind opt-in. host.device_name controls the visible pairing label for new devices. It is optional; when empty, alpi falls back to the platform hostname.
On regular macOS/Linux installs, leaving network.host empty keeps auto mode: Tailscale first, then LAN, used as both the advertised address and the bind. Setting it advertises that address to clients/peers; the bind is derived separately — a private/Tailscale IP binds itself, a hostname or an opted-in public IP binds 0.0.0.0, and a public IP without host.allow_public_bind refuses to bind at all. In Docker the daemon binds 0.0.0.0 inside the container while clients dial the address you advertise via ALPI_NETWORK_HOST — a LAN IP, a 100.x Tailscale IP, or a MagicDNS hostname (see docker/README.md).
Pairing tokens for the WS transport live at ~/.alpi/host/devices.yaml (mode 0600). Manage them through alpi setup → Devices — generate, label, revoke. The full token appears once inside the pairing QR; the listing redacts to a token_id (last 8 chars).
Takes-effect cheat sheet
- next turn — change is live on the agent's next response.
- next session — restart
alpito pick it up. - next daemon restart —
alpi daemon restart(or reload through launchd / systemd if installed as an autorun).