Lemon Configuration (TOML)
Lemon uses a single canonical configuration file in TOML format. Configuration is layered:
- Global:
~/.lemon/config.toml - Project:
<project>/.lemon/config.toml(overrides global) - Environment variables (override file values;
.envmay auto-populate missing env vars at startup) - Credential references resolved through Lemon's encrypted store, explicitly enabled external sources, and the ordinary same-name environment fallback
Runtime state and policy are separate from config. Per-session or per-route "current" model/thinking values override config defaults at runtime, but they are not persisted in config.toml.
Operational backup policy is also intentionally separate from TOML. Use lemon backup contract|create|list|verify|restore; there are no implicit backup directories or credential-inclusion settings in config.toml. See Back up and restore Lemon user state for the versioned ~/.lemon data contract.
Example
[providers.anthropic]
api_key_secret = "llm_anthropic_api_key_raw"
# Claude Max / Claude Code subscription auth
# auth_source = "oauth"
# oauth_secret = "llm_anthropic_api_key"
[providers.openai]
api_key_secret = "OPENAI_API_KEY"
# OpenAI-compatible local or hosted endpoint.
# Store a placeholder secret such as "local" if the endpoint ignores API keys.
# base_url = "http://127.0.0.1:11434/v1"
[providers.opencode]
api_key_secret = "OPENCODE_API_KEY"
base_url = "https://opencode.ai/zen/v1"
# OpenCode Go uses the same OPENCODE_API_KEY credential.
[providers.opencode_go]
api_key_secret = "OPENCODE_API_KEY"
[defaults]
provider = "anthropic"
model = "anthropic:claude-sonnet-4-20250514"
thinking_level = "medium"
[runtime.compaction]
enabled = true
reserve_tokens = 16384
keep_recent_tokens = 20000
[runtime.retry]
enabled = true
max_retries = 3
base_delay_ms = 1000
[runtime]
# Explicitly trusted extension directories. Files here can be compiled and executed.
extension_paths = []
[runtime.extensions]
# Disable all extension code execution while keeping manifest diagnostics available.
enabled = true
# Keep default global/project extension directories diagnostics-only unless trusted.
auto_load_default_paths = false
[runtime.tools.web.search]
provider = "brave" # "brave" | "perplexity"
cache_ttl_minutes = 15
[runtime.tools.web.search.perplexity]
model = "perplexity/sonar-pro"
[runtime.tools.web.fetch]
cache_ttl_minutes = 15
allow_private_network = false
allowed_hostnames = []
[runtime.tools.web.fetch.firecrawl]
enabled = true
[runtime.tools.wasm]
enabled = false
auto_build = true
runtime_path = ""
tool_paths = []
default_memory_limit = 10485760
default_timeout_ms = 60000
default_fuel_limit = 10000000
cache_compiled = true
cache_dir = ""
max_tool_invoke_depth = 4
[runtime.tools.execute_code]
enabled = false
python_path = ""
timeout_ms = 120000
max_rpc_calls = 100
max_rpc_result_bytes = 5242880
max_output_bytes = 50000
tools = []
kernel_mode = "per_call"
kernel_idle_timeout_ms = 1800000
max_live_kernels = 16
max_queued_cells_per_kernel = 8
[tui]
theme = "lemon"
debug = false
[logging]
# Optional: write logs to a file for later analysis.
# If unset/empty, file logging is disabled and logs go to stdout/stderr only.
file = "~/.lemon/log/lemon.log"
# Optional: handler level for the file (defaults to "debug").
level = "debug"
[gateway]
max_concurrent_runs = 2
auto_resume = false
enable_telegram = false
enable_discord = false
enable_xmtp = false
[gateway.telegram]
bot_token = "123456:token"
allowed_chat_ids = [12345678]
default_account_id = "default" # optional account for ./bin/lemon send account-scoped lookups
default_chat_id = 12345678 # optional default for ./bin/lemon send --to telegram
default_thread_id = 35 # optional forum topic/thread
[gateway.discord]
# `lemon gateway setup discord` stores the token in encrypted secrets and
# writes this secret reference plus the restricted channel scope.
bot_token_secret = "discord_bot_token"
default_channel_id = 123456789012345678
allowed_channel_ids = [123456789012345678]
deny_unbound_channels = true
# allowed_guild_ids = [123456789012345678] # optional additional restriction
[gateway.xmtp]
env = "production" # production | dev | local
wallet_address = "${XMTP_WALLET_ADDRESS}"
wallet_key_secret = "xmtp_wallet_key"
db_path = "~/.lemon/xmtp-db"
poll_interval_ms = 1500
connect_timeout_ms = 15000
require_live = true
mock_mode = false
[gateway.voice]
enabled = false
websocket_port = 4047
public_url = "https://example.com"
twilio_account_sid_secret = "twilio_account_sid"
twilio_auth_token_secret = "twilio_auth_token"
twilio_phone_number = "+1234567890"
deepgram_api_key_secret = "deepgram_api_key"
elevenlabs_api_key_secret = "elevenlabs_api_key"
elevenlabs_voice_id = "21m00Tcm4TlvDq8ikWAM"
elevenlabs_output_format = "ulaw_8000"
llm_model = "gpt-4o-mini"
max_call_duration_seconds = 600
silence_timeout_ms = 5000
[profiles.default]
name = "Daily Assistant"
system_prompt = "You are my daily assistant."
[profiles.default.tool_policy]
# Optional preset profile:
# profile = "minimal_core" # full_access | minimal_core | read_only | safe_mode | subagent_restricted | no_external | custom
allow = "all"
deny = []
require_approval = ["bash", "write", "edit"]
no_reply = false
[[gateway.bindings]]
transport = "telegram"
chat_id = 12345678
agent_id = "default"LemonCore.ProfileStore is the lifecycle owner for user-managed profiles. Commands such as lemon profile create research patch only the selected [profiles.<id>] table, preserving unrelated global-config keys and comments. Managed records may include name, description, avatar, model, system_prompt, node, status, profile_version, created_at, and updated_at. Home/workspace paths and the stable agent:<id>:main session key are derived from the validated ID rather than stored; do not add credentials or filesystem paths to the profile table. See User-managed profiles for clone, credential-safe export, recoverable deletion, and named-node routing.
Native Execution and Subagents
Lemon uses its native executor for every top-level TUI and gateway run. Configure its provider, model, and thinking defaults in [defaults]; no engine selection key is supported.
Delegated tasks run as native in-process subagents (child CodingAgent.Session executions) when the agent invokes its task tool. There are no vendor CLI task runners and no [runtime.cli.*] configuration.
Named Execution Nodes
The agent tool can route a delegated run to an authenticated execution node with its optional node parameter. Omit node or use "local" for the controller host. A named value must be online in the controller's live node registry. This placement is a per-run tool parameter, not a TOML engine or provider setting:
{
"action": "run",
"agent_id": "default",
"prompt": "Run the focused checks.",
"node": "worker-1"
}Join a destination from a source checkout with:
LEMON_NODE_OPERATOR_TOKEN=... ./bin/lemon node join \
--name worker-1 \
--controller wss://controller.example/ws \
--pair \
--cwd /srv/projectOn first connection, --pair creates the durable controller identity and stores its issued seven-day session plus recovery credential in a mode-0600 file under ~/.lemon/nodes/execution/; the containing directory is mode 0700. Each record is keyed by a hash of the durable node ID and includes the exact controller URL, so reuse fails closed for a different controller. The token store requires that exact URL even for direct durable-ID lookup and returns no recovery material for a missing or mismatched controller. Later starts omit --pair. Re-run with --pair after session expiry to keep the same identity and controller-side name while rotating the session and revoking older tokens and live sockets. Concurrent challenge exchange can mint only one credential, and result settlement is accepted only from the connection and session generation that received the invocation. Controller renames do not change the local key. Compatible legacy records without recovery credentials use the explicit operator-authorized --pair --repair --node-id ID migration path.
LEMON_NODE_OPERATOR_TOKEN supplies pairing authority and is used only during pairing. LEMON_NODE_TOKEN supplies an existing session token. Prefer these environment variables to --operator-token / --token so credentials do not enter shell history. These values are runtime CLI inputs, not config.toml keys.
Non-loopback controllers require wss:// by default. Plaintext ws:// needs --allow-insecure-controller or LEMON_NODE_ALLOW_INSECURE_CONTROLLER=true, and that override is acceptable only for development or a verified authenticated and encrypted overlay such as Tailscale.
Provider credentials and default cwd are destination-local. If the agent tool omits cwd, the destination uses the directory passed to node join; an explicit cwd is resolved and validated on that machine. Only JSON-safe run data crosses the controller WebSocket. Source executor options, resolved provider credentials, callbacks, and BEAM state do not. Explicit cancellation is delivered to the targeted worker; disconnects fail pending invocations. Remote execution still uses the native CodingAgent.Executor, never a vendor CLI runner.
Removed Top-Level Engine Configuration
Remove these legacy keys and tables from global and project configuration:
| Removed configuration | Migration |
|---|---|
engine, [defaults].engine, default_engine, and engine_preference | Remove the key. Keep [defaults] provider, model, and thinking settings for native execution. |
[gateway].default_engine, [gateway.projects.<id>].default_engine, and [[gateway.bindings]].default_engine | Remove the key. Retain project root, binding project/agent_id, and queue settings as needed. |
[gateway.engines.<id>] | Remove the entire table. Custom and external gateway executors are not supported. |
[runtime.cli.<vendor>] | Remove the entire table. Vendor CLI task runners were removed; delegated tasks run as native in-process subagents. Config validation rejects [runtime.cli]. |
There is no TOML replacement for selecting a top-level external or custom engine, and no TOML knob for delegated subagents beyond the native task tool's own parameters.
Environment Overrides
Environment variables override file values. Common overrides:
LEMON_DEFAULT_PROVIDER,LEMON_DEFAULT_MODELLEMON_CONTROL_PLANE_OPERATOR_TOKEN— shared WebSocket operator credential; required by default for every WebSocket operator connection. Named node pairing supplies the same value fromLEMON_NODE_OPERATOR_TOKENon the joining host. The source./bin/lemon-tuilauncher generates an ephemeral value when it owns a fresh local runtime; persistent or existing runtimes require the operator to provide the same high-entropy value to both runtime and client. Token origin does not control process lifetime: every runtime started by./bin/lemon-tuiis stopped with the TUI. Start persistent runtimes separately with./bin/lemon --daemon, then attach with the same token.LEMON_CONTROL_PLANE_ALLOW_UNAUTHENTICATED_LOOPBACK— explicit legacy compatibility opt-in (true/falseor1/0) for tokenless operator connections from direct loopback peers. Defaults tofalse; never enable it for a reverse-proxied control plane.
The browser monitoring client intentionally has no build-time or URL-based operator-token setting. Lemon does not yet implement the short-lived delegated browser-session exchange needed to authenticate that client safely. Never place the shared server token in VITE_* configuration.
LEMON_PROVIDER_ROUTING_ENABLED,LEMON_PROVIDER_FALLBACK_PROVIDERS,LEMON_PROVIDER_ROUTING_REQUIRE_CREDENTIALSLEMON_THEME,LEMON_DEBUG<PROVIDER>_API_KEY,<PROVIDER>_BASE_URL(e.g.,ANTHROPIC_API_KEY,OPENAI_BASE_URL,OPENCODE_API_KEY,ZAI_API_KEY,MINIMAX_API_KEY)LEMON_CODEX_EXTRA_ARGS,LEMON_CODEX_AUTO_APPROVELEMON_CLAUDE_YOLOLEMON_WASM_ENABLED,LEMON_WASM_RUNTIME_PATH,LEMON_WASM_TOOL_PATHS,LEMON_WASM_AUTO_BUILDLEMON_EXECUTE_CODE_ENABLED,LEMON_EXECUTE_CODE_PYTHON_PATH,LEMON_EXECUTE_CODE_TIMEOUT_MS,LEMON_EXECUTE_CODE_MAX_RPC_CALLS,LEMON_EXECUTE_CODE_MAX_RPC_RESULT_BYTES,LEMON_EXECUTE_CODE_MAX_OUTPUT_BYTES,LEMON_EXECUTE_CODE_TOOLSLEMON_EXECUTE_CODE_KERNEL_MODE,LEMON_EXECUTE_CODE_KERNEL_IDLE_TIMEOUT_MS,LEMON_EXECUTE_CODE_MAX_LIVE_KERNELS,LEMON_EXECUTE_CODE_MAX_QUEUED_CELLS_PER_KERNELLEMON_TERMINAL_BACKENDS_ALLOW,LEMON_TERMINAL_BACKENDS_DENY,LEMON_TERMINAL_BACKENDS_REQUIRE_APPROVALLEMON_DOCKER_TERMINAL_IMAGE,LEMON_DOCKER_TERMINAL_MEMORY,LEMON_DOCKER_TERMINAL_CPUS,LEMON_DOCKER_TERMINAL_PIDS_LIMIT,LEMON_DOCKER_TERMINAL_NETWORKLEMON_DOCKER_TERMINAL_READ_ONLY_ROOTFS,LEMON_DOCKER_TERMINAL_TMPFS_SIZE,LEMON_DOCKER_TERMINAL_ALLOWED_IMAGESLEMON_SSH_TERMINAL_TARGET,LEMON_SSH_TERMINAL_WORKDIR,LEMON_SSH_TERMINAL_PORT,LEMON_SSH_TERMINAL_CONNECT_TIMEOUT,LEMON_SSH_TERMINAL_STRICT_HOST_KEY_CHECKING,LEMON_SSH_TERMINAL_ALLOWED_TARGETSLEMON_GATEWAY_HEALTH_PORT,LEMON_ROUTER_HEALTH_PORTLEMON_NODE_OPERATOR_TOKEN,LEMON_NODE_TOKEN,LEMON_NODE_ALLOW_INSECURE_CONTROLLERLEMON_LOG_FILE,LEMON_LOG_LEVELBRAVE_API_KEY,PERPLEXITY_API_KEY,OPENROUTER_API_KEY,FIRECRAWL_API_KEY
Terminal backend policy validates Docker image/network/resource settings and SSH port/timeout/host-key settings before exec launches a backend. Invalid Docker limits, invalid Docker image/network names, invalid SSH ports, invalid SSH connect timeouts, and unsupported strict-host-key values fail closed at the policy boundary instead of reaching Docker or OpenSSH.
The Bun TUI uses the runtime's model and thinking defaults from [defaults]. The [tui] section controls terminal presentation settings loaded at startup.
Async followup queue defaults for background task completions and delegated agent completions are currently umbrella app config, not TOML. The default lives in config/config.exs as config :coding_agent, :async_followups, default_queue_mode: :steer_backlog. Per-call tool inputs still override it: task.queue_mode and agent.followup_queue_mode.
Feature Flags
Feature flags gate in-progress behaviour changes so they can be shipped incrementally without ad-hoc environment variables.
[features]
session_search = "default-on" # SessionSearch API + search_memory tool (default)
routing_feedback = "default-on" # task fingerprinting + routing feedback (default)
skill_synthesis_drafts = "default-on" # skill synthesis draft pipeline (default)session_search defaults to "default-on": durable memory ingest and search are active on a stock install (retention 30 days, max 500 documents per scope; runs whose summaries look like they contain a secret are dropped entirely, not redacted). Set it to "off" to disable. The learning-loop flags (routing_feedback, skill_synthesis_drafts) also default to "default-on".
Valid rollout states:
| State | Meaning |
|---|---|
"off" | Feature fully disabled (kill-switch). |
"opt-in" | Legacy pre-promotion state; behaves like "off" unless the caller explicitly opts in. |
"default-on" | Enabled unless explicitly disabled. |
Each flag can be overridden via an environment variable using the pattern LEMON_FEATURE_<FLAG_NAME> (SCREAMING_SNAKE_CASE):
LEMON_FEATURE_SESSION_SEARCH=off # kill switch for durable memory
LEMON_FEATURE_ROUTING_FEEDBACK=default-onConfig validation fails cleanly if a flag is set to an unrecognised state.
Canonical Sections
Use only these top-level sections:
defaultsruntimefeaturessecrets.sources.<source_id>profiles.<agent_id>providers.<name>gatewaytuilogging
Deprecated sections now fail validation and runtime loading:
[agent]-> move defaults to[defaults]and runtime settings to[runtime][agents.<id>]-> move to[profiles.<id>][agent.tools.*]-> move to[runtime.tools.*][tools.*]-> move to[runtime.tools.*]
External secret sources
External sources are read-only adapters integrated into LemonCore.Secrets.resolve/2; they are not another secret store. Resolution is ordered as follows:
- Lemon's encrypted store;
- enabled external sources ordered by
priority, then source id; - the same-name environment variable when
env_fallbackis enabled.
A successful source that does not contain the requested name continues to the next source. Any enabled source configuration, spawn, timeout, output, parse, or bootstrap failure stops resolution before the ordinary environment fallback. This fail-closed behavior prevents an unhealthy configured manager from being silently bypassed. env_fallback: false disables both the external and environment fallbacks; internal bootstrap reads use only the encrypted store and ordinary environment path so sources cannot recurse.
Every source requires the exact TOML boolean enabled = true. A quoted "true", unknown setting, unknown source type, relative executable path with a slash, shell command string, or out-of-range limit fails validation. Programs are started directly from an argv array, never through a shell. Each child gets only a small operating environment (HOME, platform path/temp/locale fields, and NO_COLOR) plus explicitly passed variables. Stdout and stderr are captured together under one byte limit and are never returned in errors, status, logs, or proof output.
Common source settings:
| Setting | Default | Contract |
|---|---|---|
type | required | onepassword, bitwarden, or command |
enabled | false | Only the exact boolean true enables execution |
priority | 100 | Integer 0..1000; lower runs first |
executable | provider default | Absolute path or bare executable name |
timeout_ms | 3000 | Integer 100..30000 |
max_output_bytes | 65536 | Integer 1..1048576, stdout and stderr combined |
cache_ttl_ms | 0 | Integer 0..300000; 0 disables caching |
The optional cache is bounded to 32 process-local entries and disappears on restart. It never persists source values to config or the encrypted store.
1Password
Map each Lemon secret name to an op:// reference. Lemon invokes op read --no-newline [--account ACCOUNT] -- REFERENCE separately for each mapping. If auth_secret is set, Lemon resolves that bootstrap credential only from its existing encrypted store or same-name environment fallback and gives it to the child as auth_env. Without auth_secret, an already authenticated local op session or the named ambient auth variable may be used.
[secrets.sources.onepassword]
type = "onepassword"
enabled = true
priority = 10
executable = "op"
timeout_ms = 3000
max_output_bytes = 65536
cache_ttl_ms = 0
account = "team"
auth_secret = "op_service_account_token"
auth_env = "OP_SERVICE_ACCOUNT_TOKEN"
refs = { anthropic_api_key = "op://Lemon/Anthropic/api-key", openai_api_key = "op://Lemon/OpenAI/api-key" }Bitwarden Secrets Manager
Bitwarden uses bws secret list PROJECT_ID --output json and accepts only a JSON list of unique non-empty {"key": ..., "value": ...} objects. Store the bootstrap access token in Lemon under access_token_secret, or provide the same-name environment variable. access_token_env controls the variable name passed to bws; an optional server_url must be credential-free HTTPS with no query or fragment.
[secrets.sources.bitwarden]
type = "bitwarden"
enabled = true
priority = 20
executable = "bws"
project_id = "team-project-id"
access_token_secret = "bws_access_token"
access_token_env = "BWS_ACCESS_TOKEN"
# server_url = "https://vault.example.com"Arbitrary command
Command sources accept only an argv array. Output is UTF-8 NAME=VALUE, one entry per line; blank lines and lines beginning with # are ignored. Names must be unique and values non-empty. pass_env copies only the listed ambient variables. secret_env maps a child environment name to an existing Lemon encrypted-store/same-name-environment secret; it never resolves another external source.
[secrets.sources.local_helper]
type = "command"
enabled = true
priority = 30
argv = ["/usr/local/bin/lemon-secret-helper", "export", "--profile", "prod"]
timeout_ms = 2000
max_output_bytes = 32768
cache_ttl_ms = 0
pass_env = ["HELPER_PROFILE"]
secret_env = { HELPER_TOKEN = "helper_bootstrap_token" }Inspect readiness without invoking any source, then perform a redaction-safe live test:
lemon secrets sources status --json
lemon secrets sources test --json
lemon secrets sources test local_helperThese commands reveal source id/type, readiness, provenance, counts, output byte count, duration, and stable error kinds only. They never reveal secret values. lemon secrets check reports a resolved credential only as present.
Dotenv Autoload
Lemon can auto-load a .env file at startup:
./bin/lemon-tui: loads<cwd>/.envwhere<cwd>is the agent working directory (--cwd, or current directory).clients/lemon-web/serverbridge: loads<cwd>/.envfrom--cwd(or current directory)../bin/lemon-gateway: loads.envfrom the directory where you launch the script.
By default, existing environment variables are preserved. .env values only fill missing variables.
OpenAI Codex (ChatGPT OAuth)
Lemon supports the Codex subscription provider as openai-codex (it uses the ChatGPT OAuth JWT, not OPENAI_API_KEY). The canonical config key is providers.openai-codex; providers.openai_codex is also accepted for backward compatibility by native Lemon/CodingAgent runs.
Primary setup paths use the installed lemon command, or the matching source wrapper from a checkout:
# Installed release
lemon setup
lemon model --provider openai-codex
# Source checkout
./bin/lemon setup
./bin/lemon model --provider openai-codex
# Contributor-level Mix alternatives
mix lemon.setup
mix lemon.onboard.codexWhat it does:
- Resolves Codex OAuth credentials via
LemonAi.Auth.OpenAICodexOAuth - Stores credentials in encrypted secrets
- Writes
providers.openai-codex.auth_source = "oauth"plusproviders.openai-codex.oauth_secret - Optionally updates
[defaults]provider/model - Uses an interactive arrow-key TUI for selection steps when running in a real terminal
- Listens on the localhost OAuth callback automatically and falls back to manual paste only if the callback cannot be captured
The onboarding flow opens the OpenAI auth URL directly and stores the returned OAuth credentials in Lemon secrets.
To force a token explicitly, set:
OPENAI_CODEX_API_KEY(preferred)CHATGPT_TOKEN(fallback)
OpenAI-Compatible Endpoints
For local or hosted services that expose an OpenAI-compatible API, configure the normal openai provider with a custom base_url:
[providers.openai]
api_key_secret = "llm_local_openai_api_key"
base_url = "http://127.0.0.1:11434/v1"
[defaults]
provider = "openai"
model = "openai:local-model-name"Then store the endpoint key:
# Installed release / source checkout
lemon secrets set llm_local_openai_api_key "local"
./bin/lemon secrets set llm_local_openai_api_key "local"
# Contributor-level Mix alternative
mix lemon.secrets.set llm_local_openai_api_key "local"The same provider config shape also works for hosted OpenAI-compatible provider ids handled by Lemon, including opencode, openrouter, zai, minimax, kimi, xai, mistral, groq, deepseek, qwen, and related compatible providers. Each provider gets its own [providers.<id>] table with api_key_secret and optional base_url.
Provider Onboarding (CLI)
For an installed release, start provider onboarding with lemon model. From a source checkout, use the matching ./bin/lemon model wrapper. The direct Mix tasks below remain the contributor interface.
# Installed release
lemon model
lemon model --provider anthropic
lemon model --provider openai-codex
lemon model --provider gemini
lemon model --provider zai
lemon model --provider minimax
# Source checkout
./bin/lemon model
./bin/lemon model --provider anthropic
./bin/lemon model --provider openai-codex
./bin/lemon model --provider gemini
./bin/lemon model --provider zai
./bin/lemon model --provider minimax
# Contributor-level Mix alternatives
mix lemon.onboard
mix lemon.onboard anthropic
mix lemon.onboard codex
mix lemon.onboard gemini
mix lemon.onboard zai
mix lemon.onboard minimax
mix lemon.onboard.antigravity
mix lemon.onboard.copilotlemon setup is the idempotent first-run journey: it derives config, secrets, and provider readiness; creates missing config and a secrets master key without replacing existing state; skips a provider that is already usable; and checks a new provider configuration before it reports setup complete. Setup always runs offline checks and normally runs a live provider check. Use lemon setup --skip-verify only to defer that live check when offline.
The focused lemon model command onboards one provider. It requires a usable encrypted secrets store; use lemon setup when a fresh machine still needs the config/secrets bootstrap and readiness verification.
Provider onboarding flows:
- Let you choose a provider when none is passed
- Use an interactive arrow-key TUI for provider/auth/model selection when a TTY is available
- Run provider OAuth flow by default when supported, or prompt for an API key/token otherwise
- Capture localhost OAuth callbacks automatically when the provider redirect URI is local
- Store credentials in encrypted secrets with provider metadata
- Write the relevant
providers.<provider>config keys - Support
--set-default,--model, and--config-path
Provider readiness is visible through the source and packaged lemon providers status command and the read-only control-plane providers.status method. They use the same LemonAgent.ModelRuntime.Credentials credential resolver as model execution, so env keys, encrypted secret references, OAuth/default-secret paths, and provider-specific credential shapes are checked the same way runtime calls check them. The response reports booleans such as credentialReady, apiKeyConfigured, apiKeySecretConfigured, oauthSecretConfigured, baseUrlConfigured, and envConfigured; it does not return raw API keys, secret names, base URLs, or env var names.
Fallbacks and credential-pool references can be edited through the same packaged/source command boundary:
# Installed release (use ./bin/lemon from a source checkout)
lemon providers fallback add zai
lemon providers pool set burst --provider openai --provider zai \
--strategy round_robin --activate
lemon providers pool credential add burst openai secret:openai_backup
lemon providers status --jsonOnly explicit secret:NAME and env:NAME references are accepted for pool credentials; values remain owned by the encrypted secret store or process environment. The command preserves unrelated comments, validates the complete resulting TOML, and atomically replaces the selected global or project config. Mutations apply by default; use --dry-run to preview. Removing a fallback, deleting or updating an existing pool, and removing or clearing credential references require the exact confirmation value shown by the preview:
lemon providers fallback remove zai --dry-run --json
lemon providers fallback remove zai --confirm zaiThe admin-scoped providers.configure control-plane method exposes the same actions (fallback.add, fallback.remove, fallback.clear, pool.upsert, pool.delete, pool.credential.add, pool.credential.remove, and pool.credential.clear). RPC mutations are preview-only unless apply: true is explicit. Remote callers may select global or project scope but cannot provide an arbitrary config path. Results expose provider and pool names plus counts; they never include raw keys, secret names, credential references, base URLs, or environment-variable names.
Preview results include an opaque configRevision. A caller that passes it back as expectedRevision on apply gets an atomic stale-write guard: the shared service compares the revision while holding its target-config lock and rejects the mutation if any config content changed after preview. /manage/providers uses this contract for all Web mutations, then adds exact confirmation for destructive actions and requires credential-reference values to be re-entered instead of retaining them in LiveView state.
Memory-provider readiness is visible through read-only memory.status and support-bundle memory_diagnostics.json. These surfaces expose provider ids, enabled state, source labels, scopes, timeout shape, and module load state without memory document contents, raw provider config, secret values, prompts, tool output, or provider error payloads.
Doctor support bundles include a core-owned provider_diagnostics.json snapshot for offline support. That snapshot reports provider setup shape, credential reference counts, ambient-provider booleans, and routing/pool/profile shape without depending on runtime provider modules. It intentionally omits raw API keys, secret names, raw base URLs, env var names, model prompts, and provider responses.
Provider route previews are controlled by runtime.provider_routing:
[runtime.provider_routing]
enabled = true
fallback_providers = ["zai", "anthropic"]
default_pool = "burst"
default_profile = "ops"
require_credentials = true
[runtime.provider_routing.credential_pools.burst]
providers = ["openai", "zai", "anthropic"]
strategy = "round_robin" # priority | round_robin
[runtime.provider_routing.profiles.ops]
fallback_providers = ["zai"]
credential_pool = "burst"
distribution = { openai = 70, zai = 20, anthropic = 10 }providers.status and lemon providers status include a redacted routingConfig block for effective fallback/pool configuration and a routing block with the requested provider/model, selected provider/model, fallback candidates, candidate readiness booleans, selected routing profile, selected credential pool, profile distribution weights, pool provider names, pool strategy, and credential-reference counts. Pool/profile names and provider names are visible; raw API keys, secret names, base URLs, and env var names are not.
Coding-agent default model resolution consumes the same routing policy conservatively: if the default provider is not credential-ready and a configured fallback/profile/pool provider is credential-ready with the same model id in LemonAi.Models, Lemon selects that fallback before starting the supervised agent loop. Pools default to priority order; strategy = "round_robin" rotates the pool's starting provider through LemonAgent.ModelRuntime.ProviderPoolRotator, a supervised BEAM process. Explicit user model specs are never rewritten at resolution time.
The supervised coding-agent loop also wraps streams — default-model and explicit-model sessions alike — with the same fallback ordering (for an explicit model, candidates are other credential-ready providers hosting the same model id; the wrapper is a no-op when there are none). If a provider returns a terminal stream error before useful assistant content or tool calls are emitted, Lemon retries the same turn against the next credential-ready fallback provider with the same model id. Once visible content or a tool call has started, the error is surfaced instead of replayed so the transcript cannot duplicate partial output.
Google Gemini CLI onboarding (lemon model --provider gemini, or ./bin/lemon model --provider gemini from a checkout) resolves OAuth credentials via LemonAi.Auth.GoogleGeminiCliOAuth, stores the encrypted payload in providers.google_gemini_cli.api_key_secret, writes providers.google_gemini_cli.auth_source = "oauth", and can take --project-id <gcp-project-id> to force a specific Code Assist project. The contributor task is mix lemon.onboard gemini. At runtime, Lemon re-resolves the active Gemini project from providers.google_gemini_cli.project_id, providers.google_gemini_cli.project_secret, LEMON_GEMINI_PROJECT_ID, GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_PROJECT_ID, or GCLOUD_PROJECT, with those values overriding the projectId stored inside the OAuth payload.
The onboarding alias gemini maps to the runtime provider google_gemini_cli. This is distinct from the AI Studio provider google, which expects a separate API key such as GOOGLE_GENERATIVE_AI_API_KEY.
Antigravity OAuth client credentials resolve from Lemon secrets first:
google_antigravity_oauth_client_idgoogle_antigravity_oauth_client_secret
Environment variables are supported as fallback:
GOOGLE_ANTIGRAVITY_OAUTH_CLIENT_IDGOOGLE_ANTIGRAVITY_OAUTH_CLIENT_SECRET
Common non-interactive usage:
# Installed release
lemon model --provider antigravity --token <token> --set-default --model gemini-3-pro-high
lemon model --provider gemini --project-id your-gcp-project
lemon model --provider gemini --token <token> --set-default --model gemini-2.5-pro
lemon model --provider openai-codex --token <token> --set-default --model gpt-5.2
lemon model --provider github-copilot --enterprise-domain company.ghe.com
lemon model --provider github-copilot --skip-enable-models
lemon model --provider github-copilot --token <token> --set-default --model gpt-5
lemon model --provider zai --token <token> --set-default --model glm-5
lemon model --provider minimax --token <token> --set-default --model MiniMax-M2.7
# Source checkout
./bin/lemon model --provider antigravity --token <token> --set-default --model gemini-3-pro-high
./bin/lemon model --provider gemini --project-id your-gcp-project
./bin/lemon model --provider gemini --token <token> --set-default --model gemini-2.5-pro
./bin/lemon model --provider openai-codex --token <token> --set-default --model gpt-5.2
./bin/lemon model --provider github-copilot --enterprise-domain company.ghe.com
./bin/lemon model --provider github-copilot --skip-enable-models
./bin/lemon model --provider github-copilot --token <token> --set-default --model gpt-5
./bin/lemon model --provider zai --token <token> --set-default --model glm-5
./bin/lemon model --provider minimax --token <token> --set-default --model MiniMax-M2.7
# Contributor-level Mix alternatives
mix lemon.onboard.antigravity --token <token> --set-default --model gemini-3-pro-high
mix lemon.onboard.gemini --project-id your-gcp-project
mix lemon.onboard.codex --token <token> --set-default --model gpt-5.2
mix lemon.onboard.copilot --enterprise-domain company.ghe.com
mix lemon.onboard zai --token <token> --set-default --model glm-5
mix lemon.onboard minimax --token <token> --set-default --model MiniMax-M2.7Anthropic provider auth supports either API keys or Claude subscription OAuth.
API key flow:
mix lemon.secrets.set llm_anthropic_api_key_raw <token>OAuth flow:
[providers.anthropic]
auth_source = "oauth"
oauth_secret = "llm_anthropic_api_key" # optional if this secret stores an Anthropic OAuth payloadWhen auth_source = "oauth", Lemon will also use ambient Claude Code credentials from ~/.claude/.credentials.json or CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_TOKEN. If both a static env token and refreshable Claude Code credentials exist, Lemon prefers the refreshable Claude Code credentials. You can also create/store that payload through onboarding with mix lemon.onboard anthropic --auth oauth.
Web Tools (websearch / webfetch)
Lemon includes web tools under runtime.tools.web. For full setup and troubleshooting, see:
[runtime.tools.web.search]
enabled = true
provider = "brave" # brave | exa | perplexity | duckduckgo | searxng | extension id
max_results = 5
timeout_seconds = 30
cache_ttl_minutes = 15
[runtime.tools.web.search.failover]
enabled = true
provider = "perplexity"
[runtime.tools.web.search.perplexity]
# Optional if PERPLEXITY_API_KEY / OPENROUTER_API_KEY is set.
api_key = "<perplexity-api-key>"
base_url = "https://api.perplexity.ai"
model = "perplexity/sonar-pro"
[runtime.tools.web.search.providers.searxng]
# Required only when selecting the SearXNG provider.
base_url = "https://search.example.com"
# Optional bearer token or Lemon secret name.
api_key_secret = "SEARXNG_API_KEY"
[runtime.tools.web.search.providers.exa]
# Optional if EXA_API_KEY is set.
api_key = "<exa-api-key>"
[runtime.tools.web.fetch]
enabled = true
max_chars = 50000
timeout_seconds = 30
cache_ttl_minutes = 15
max_redirects = 3
readability = true
allow_private_network = false
allowed_hostnames = []
[runtime.tools.web.fetch.firecrawl]
# Optional if FIRECRAWL_API_KEY is set.
enabled = true
api_key = "fc-..."
base_url = "https://api.firecrawl.dev"
only_main_content = true
max_age_ms = 172800000
timeout_seconds = 60
[runtime.tools.web.cache]
persistent = true
path = "~/.lemon/cache/web_tools"
max_entries = 100Browser backends and existing Chrome
Browser tools default to Lemon's managed local Chromium backend. These environment variables select an existing CDP browser or the opt-in MV3 relay:
| Variable | Default | Purpose |
|---|---|---|
LEMON_BROWSER_DRIVER_PATH | auto | Browser Node driver path |
LEMON_BROWSER_CDP_ENDPOINT | none | HTTP discovery or direct ws/wss CDP endpoint |
LEMON_BROWSER_ATTACH_ONLY | false | Never launch a replacement browser |
LEMON_BROWSER_CDP_PORT | 18800 | Managed local Chrome CDP port |
LEMON_BROWSER_RELAY_PORT | 9224 | Loopback MV3 relay port |
LEMON_BROWSER_RELAY_TOKEN | none | Required relay shared secret |
LEMON_BROWSER_BACKEND | local | local, controller, hybrid, browserbase, browser_use, firecrawl, or camofox |
LEMON_BROWSER_HYBRID_LOCAL_BACKEND | local | Local/private route used by hybrid |
LEMON_BROWSER_HYBRID_PUBLIC_BACKEND | none | Required public route used by hybrid |
BROWSERBASE_API_KEY / BROWSERBASE_PROJECT_ID | none | Browserbase hosted sessions |
BROWSER_USE_API_KEY | none | Browser Use Cloud hosted sessions |
CAMOFOX_URL / CAMOFOX_API_KEY | none | Camofox REST/Firefox server |
LEMON_CUA_DRIVER_CMD | PATH lookup | cua-driver executable for computer_use |
For the existing-Chrome relay, set LEMON_BROWSER_CDP_ENDPOINT to ws://127.0.0.1:<port>/cdp?token=<token> and set attach-only mode. Treat that endpoint as a secret: Lemon redacts common credential query parameters and reports only endpoint hashes in status surfaces. Backend/controller identity is explicit and fail-closed; an unavailable controller never falls back to a different browser profile. See docs/tools/web.md.
WASM Tools
WASM tools are disabled by default and run in a per-session Rust sidecar. See docs/tools/wasm.md for runtime behavior and troubleshooting.
[runtime.tools.wasm]
enabled = false
auto_build = true
runtime_path = ""
tool_paths = []
default_memory_limit = 10485760
default_timeout_ms = 60000
default_fuel_limit = 10000000
cache_compiled = true
cache_dir = ""
max_tool_invoke_depth = 4Execute Code (execute_code)
execute_code is programmatic tool calling: the model submits a python3 script that can call a fixed allowlist of agent tools (read, grep, find, ls, webfetch). The result comes back through text() blocks; stdout/stderr is returned as a labeled diagnostics tail (a script that never calls text() keeps the stdout-only result), and notify()/batch() add streaming updates and parallel helper calls. It is disabled by default and bash-equivalent — the script runs as host code with the user's permissions, not in a sandbox. For the full behavior, security, and lifecycle contract see docs/tools/execute-code.md.
[runtime.tools.execute_code]
enabled = false
python_path = "" # empty = find python3 on PATH
timeout_ms = 120000 # end-to-end wall-time cap per run, including session queue wait
max_rpc_calls = 100 # helper calls per run
max_rpc_result_bytes = 5242880
max_output_bytes = 50000 # stdout/stderr cap, returned as the diagnostics tail
max_text_bytes = 65536 # total text() result-block bytes per run
max_parallel_rpc = 4 # helper calls the pump dispatches concurrently
tools = [] # helper subset; empty = full fixed allowlist
# Persistent-kernel opt-in:
kernel_mode = "per_call" # "per_call" (default) | "session"
kernel_idle_timeout_ms = 1800000
max_live_kernels = 16
max_queued_cells_per_kernel = 8kernel_mode = "session" keeps one supervised python3 interpreter per session/cwd/ interpreter/helper identity so imports, globals, and objects survive across calls (reset = true discards them). Kernel state is live process memory only — never durable, never in the transcript — and is lost on session close/reset, idle reap (kernel_idle_timeout_ms), eviction (max_live_kernels, idle-only LRU), or node restart. Only the literal value "session" enables persistence; any other value resolves to "per_call". Cells are serialized one at a time per kernel with a bounded queue (max_queued_cells_per_kernel); a full queue is an error, not a fallback. Session mode falls back to an isolated per-call run only before code starts (missing session identity, unavailable registry, exhausted capacity, or startup failure) and reports it in result details.
Sections
providers.<name>: API keys and base URLs per provider.defaults: global default provider, model, and thinking level for native execution.runtime: runtime behavior and tool settings.runtime.tools.web:websearch/webfetchproviders, guardrails, cache, and Firecrawl fallback.runtime.tools.wasm: WASM sidecar runtime controls and discovery paths.runtime.tools.execute_code: programmatic tool calling (python3 scripts, helper allowlist, optional persistent kernels).profiles.<agent_id>: assistant profiles (identity + defaults) used by gateway/control-plane.runtime.compaction: context compaction settings.runtime.retry: retry settings.tui: terminal UI settings.gateway: Lemon gateway settings, includingqueue,telegram,discord,sms,voice,xmtp,projects, andbindings.logging: optional file logging configuration.
Gateway Projects and Bindings
When LemonGateway handles a Telegram message, it can optionally map that chat (or topic/thread) to a named project. A project is a working directory root plus optional agent and queue defaults.
Why it matters:
- The native executor runs with
cwdset to the project root (so file edits/commands happen in the right repo). - The gateway loads per-project config from
<project_root>/.lemon/config.toml(which can override agent profiles, models, tool policy, etc. compared to your global~/.lemon/config.toml). - If a chat has no bound project, gateway falls back to
gateway.default_cwd(or~/by default).
Projects
Define projects under [gateway.projects.<project_id>]:
[gateway.projects.myrepo]
root = "/path/to/myrepo"Bindings
Bindings connect an incoming chat scope to a project, agent profile, and queue defaults:
[[gateway.bindings]]
transport = "telegram"
chat_id = 123456789
# Optional: bind this chat to a project (must match the `[gateway.projects.<id>]` key)
project = "myrepo"
# Optional: choose which agent profile to use (defaults to "default")
agent_id = "default"
queue_mode = "steer"Notes:
- If you omit
project, LemonGateway will run withcwdset togateway.default_cwdwhen configured, otherwise~/. - You can also bind at the topic/thread level by setting
topic_idin the binding (takes precedence over the chat-level binding when a matching topic exists). topic_idcorresponds to Telegram'smessage_thread_id(only present in forum topics).- LemonGateway loads
gateway.*config on startup; after changinggateway.projectsorgateway.bindings, restart the gateway process.
Optional fallback cwd:
[gateway]
default_cwd = "~/"Tip:
- In Telegram, you can set or inspect the current chat/topic working directory at runtime with
/cwd [project_id|path|clear]. /new <project_id|path>still works, and setting/cwdmakes future/newsessions in that chat/topic use the same directory./newconfirmation replies include model, provider, cwd, and session context details.- If you pass a path, Lemon will register it as a project named after the last path segment (e.g.
~/dev/lemon=> projectlemon).
XMTP (Base App / Wallet Chats)
Lemon can run as an XMTP bot through the lemon_channels XMTP adapter.
1. Install XMTP bridge dependencies
./bin/lemon-xmtp-bootstrapThis installs bridge dependencies in apps/lemon_gateway/priv/node_modules (where xmtp_bridge.mjs resolves imports).
2. Configure gateway XMTP
[gateway]
enable_xmtp = true
[gateway.xmtp]
env = "production" # production | dev | local
wallet_address = "${XMTP_WALLET_ADDRESS}"
wallet_key_secret = "XMTP_WALLET_KEY" # secret ref; env fallback works if XMTP_WALLET_KEY is set
db_path = "~/.lemon/xmtp-db"
poll_interval_ms = 1500
connect_timeout_ms = 15000
require_live = true # production default: do not allow mock fallback
mock_mode = false # set true only for local bridge testing
# Optional:
# api_url = "https://api.xmtp.network"
# inbox_id = "..."
# sdk_module = "@xmtp/node-sdk"Notes:
- When
enable_xmtp = true, Lemon auto-registers and starts the XMTP channel adapter. require_live = truekeeps health/readiness red unless the bridge is truly live (not mock mode).wallet_key_secretis the canonical credential field. It can point to a Lemon secret name or to an env var name when using secret resolution with env fallback.- Non-text XMTP messages currently receive a text-only fallback response.
Voice
Voice transport is configured under [gateway.voice].
[gateway.voice]
enabled = true
websocket_port = 4047
public_url = "https://example.com"
twilio_account_sid_secret = "twilio_account_sid"
twilio_auth_token_secret = "twilio_auth_token"
twilio_phone_number = "+1234567890"
deepgram_api_key_secret = "deepgram_api_key"
elevenlabs_api_key_secret = "elevenlabs_api_key"
elevenlabs_voice_id = "21m00Tcm4TlvDq8ikWAM"
elevenlabs_output_format = "ulaw_8000"
llm_model = "gpt-4o-mini"
system_prompt = "You are a helpful phone assistant."
max_call_duration_seconds = 600
silence_timeout_ms = 5000Canonical voice settings are loaded from gateway.voice. Legacy :lemon_gateway app env fallbacks remain only as temporary compatibility shims and should not be used for new setup.
Telegram Voice Transcription
If enabled, Telegram voice notes are transcribed and the transcript is routed as a normal text message.
[gateway.telegram]
voice_transcription = true
voice_transcription_provider = "openai_transcribe" # "openai_transcribe" | "local_transcript"
voice_transcription_model = "gpt-4o-mini-transcribe" # optional
voice_max_bytes = 10485760 # optional (default: 10MB)
# Optional OpenAI-compatible endpoint override. Credentials default to [providers.openai].
voice_transcription_base_url = "https://api.openai.com/v1"Use voice_transcription_provider = "local_transcript" for deterministic no-credential voice-note proof. It routes a local transcript preview through the normal Telegram inbound path and does not require an API key. Use openai_transcribe for real speech-to-text.
The deterministic local proof runner writes a redacted artifact under .lemon/proofs/:
MIX_ENV=test mix run scripts/live_telegram_voice_local_smoke.exsWhen that artifact is present, mix lemon.doctor --verbose reports channels.telegram.voice_transcription as a passing readiness check. If voice transcription is enabled with local_transcript and the proof is missing, doctor tells the operator to run the local smoke.
Telegram File Transfer
Enable /file put and /file get (and optional auto-save for plain document uploads).
[gateway.telegram.files]
enabled = true
auto_put = true
auto_put_mode = "upload" # "upload" | "prompt"
auto_send_generated_files = true # optional: send generated files automatically after a run
auto_send_generated_max_files = 3 # optional: max generated files auto-sent per run (default: 3)
uploads_dir = "incoming"
media_group_debounce_ms = 1000 # optional (default: 1000ms)
# Optional safety rails
allowed_user_ids = [123456789] # if empty, group uploads require admin
deny_globs = [".git/**", ".env", ".envrc", "**/*.pem", "**/.ssh/**"]
max_upload_bytes = 20971520 # optional (default: 20MB)
max_download_bytes = 52428800 # optional (default: 50MB)
outbound_send_delay_ms = 1000 # optional: delay between auto-sent files/batches to reduce 429sCommands:
/file put [--force] <path>: upload a Telegram document into the active working root./file get <path>: fetch a file (or zip a directory) from the active working root back into Telegram.
If no project is bound for the chat, the active root falls back to gateway.default_cwd (or ~/).
When auto_send_generated_files = true, Lemon tracks generated files requested by browser/media tools and sends up to auto_send_generated_max_files files back to Telegram automatically at completion (using the same max_download_bytes limit as /file get). auto_send_generated_images remains accepted as a backward-compatible alias. SVG outputs are uploaded as Telegram documents instead of photos because Telegram rejects SVG photo processing.
Discord File Transfer
Enable Discord file transfer and optional generated-file auto-send.
[gateway.discord.files]
enabled = true
auto_put = true
auto_send_generated_files = true # optional: send generated files automatically after a run
auto_send_generated_max_files = 3 # optional: max generated files auto-sent per run (default: 3)
# Optional safety rails
max_upload_bytes = 26214400 # optional (default: 25MB)
max_download_bytes = 26214400 # optional (default: 25MB)When auto_send_generated_files = true, Lemon tracks generated files requested by browser/media tools and sends up to auto_send_generated_max_files files back to Discord automatically at completion. Generated files must fit within max_download_bytes; explicit file-send requests are still delivered through the normal attachment path. auto_send_generated_images remains accepted as a backward-compatible alias.
Discord Trigger Mode and Message Content Intent
Discord defaults to mention-gated routing in group channels and public threads. Use /trigger all to opt a channel or thread into free-response routing for unmentioned messages, and /trigger mentions to restore the default behavior.
[gateway.discord]
message_content_intent_enabled = true # diagnostics declaration only
default_account_id = "default" # optional account for ./bin/lemon send account-scoped lookups
default_channel_id = "1475727416549969980" # optional default for ./bin/lemon send --to discord
default_thread_id = "1475727416549969991" # optional threadmessage_content_intent_enabled does not change Discord application settings. It lets channel_diagnostics.json record that the operator has enabled the privileged Message Content Intent in the Discord Developer Portal. Free-response Discord support requires that portal setting plus a passing live external-sender proof; mention-triggered Discord prompts do not depend on this declaration.
Telegram Context Compaction
When a Telegram run approaches the model context limit, Lemon can proactively mark the session for compaction so the next user message is automatically rewritten with a compact transcript and sent as a fresh session.
[gateway.telegram.compaction]
enabled = true
context_window_tokens = 400000 # optional override; if unset Lemon infers from the model and native executor
reserve_tokens = 16384 # optional safety margin before limit
trigger_ratio = 0.9 # optional; 0.9 means trigger at 90% of context windowTrigger Mode (Mentions-Only)
In Telegram group chats, you can gate runs so Lemon only triggers when explicitly invoked:
/trigger: show current trigger mode./trigger mentions: only run on@botname, reply-to-bot, or slash commands./trigger all: run on all messages./trigger clear: clear a topic override (forum topics only)./cwd [project_id|path|clear]: show, set, or clear the chat/topic working directory override used by future/newsessions.
Forum topic management:
/topic <name>: create a new topic in the current Telegram forum supergroup.