Configuration Reference
RivetOS uses a single YAML config file for all settings. API keys and secrets go in .env, never in the config file.
Config file locations (checked in order):
--configCLI flag./config.yaml(current directory)~/.rivetos/config.yaml
Validate without starting: rivetos config validate
Quick example
Section titled “Quick example”runtime: workspace: ~/.rivetos/workspace default_agent: opus
agents: opus: provider: anthropic default_thinking: medium
providers: anthropic: model: claude-sonnet-4-6 max_tokens: 8192
channels: # social channels removed Phase 5 — use RivetHub
memory: postgres: {}Environment variable resolution
Section titled “Environment variable resolution”Any string value can reference environment variables with ${VAR_NAME}:
providers: anthropic: api_key: ${ANTHROPIC_API_KEY}
memory: postgres: connection_string: ${RIVETOS_PG_URL}Unset variables resolve to empty strings. Recommended: put all secrets in .env and reference them.
runtime
Section titled “runtime”Top-level runtime configuration.
| Key | Type | Default | Description |
|---|---|---|---|
workspace |
string | required | Path to workspace directory containing CORE.md, USER.md, etc. |
default_agent |
string | required | Agent to use when no channel binding matches. Must match a key in agents. |
turn_timeout |
number | 900 |
Wall-clock timeout for a single agent turn, in seconds. |
context |
object | — | Context-management tuning. context.soft_nudge_pct (number[]) and context.hard_nudge_pct (number) control when the agent is nudged to compact as the window fills. |
skill_dirs |
string[] | [~/.rivetos/workspace/skills] |
Directories to scan for skills. |
plugin_dirs |
string[] | [] |
Additional directories to scan for plugins beyond the default plugins/. |
experimental |
boolean | false (omit) |
Nightly / experimental switch. When true, boot sets RIVETOS_EXPERIMENTAL=1 on the env map passed wholesale to den-server loadConfig (same map the in-process gateway builds). Not process-env prefix passthrough (RIVETOS_DEN_* / RIVETOS_USER*). den-server currently has no dedicated field for the key; it is present on the env object den is constructed from. Stable installs omit it. |
runtime.heartbeats
Section titled “runtime.heartbeats”Array of scheduled agent tasks. Each heartbeat triggers the agent periodically.
runtime: heartbeats: - agent: opus schedule: '*/30 * * * *' # Every 30 minutes prompt: 'Check for unread emails and calendar events.' output_channel: '' # no social channel output timezone: America/New_York quiet_hours: start: 23 end: 8| Key | Type | Default | Description |
|---|---|---|---|
agent |
string | required | Which agent runs this heartbeat. Must match a key in agents. |
schedule |
string | required | Cron expression (e.g., */30 * * * * = every 30 min). |
prompt |
string | required | The message sent to the agent on each heartbeat tick. |
output_channel |
string | — | Channel to deliver output (format: platform:channel_id). |
timezone |
string | UTC |
Timezone for schedule evaluation. |
quiet_hours.start |
number | — | Hour (0-23) to start quiet period (no heartbeats). |
quiet_hours.end |
number | — | Hour (0-23) to end quiet period. |
runtime.safety
Section titled “runtime.safety”Safety hooks configuration.
runtime: safety: shellDanger: true audit: true workspaceFence: allowedDirs: - /home/user/projects - /tmp alwaysAllow: - /usr/bin tools: - shell - file_write - file_edit| Key | Type | Default | Description |
|---|---|---|---|
shellDanger |
boolean | true |
Block dangerous shell commands (rm -rf /, etc.). |
audit |
boolean | true |
Log all tool executions to audit log. |
workspaceFence |
object | — | Restrict file/shell operations to specific directories. |
workspaceFence.allowedDirs |
string[] | required if fence enabled | Directories the agent can access. |
workspaceFence.alwaysAllow |
string[] | [] |
Paths always allowed regardless of fence. |
workspaceFence.tools |
string[] | all tools | Which tools the fence applies to. |
runtime.auto_actions
Section titled “runtime.auto_actions”Automatic post-tool actions. Run after tool executions complete.
runtime: auto_actions: format: true lint: false test: false gitCheck: true| Key | Type | Default | Description |
|---|---|---|---|
format |
boolean | false |
Auto-format files after edits. |
lint |
boolean | false |
Auto-lint files after edits. |
test |
boolean | false |
Auto-run tests after code changes. |
gitCheck |
boolean | false |
Check git status after file operations. |
agents
Section titled “agents”Named agent definitions. Each agent maps to a provider and has optional configuration.
agents: opus: provider: anthropic default_thinking: medium tools: exclude: - shell grok: provider: xai local: provider: ollama local: true| Key | Type | Default | Description |
|---|---|---|---|
provider |
string | required | Provider ID. Must match a key in providers. |
model |
string | provider default | Model override — use a specific model from this provider instead of its default. Lets several agents share one provider at different models. |
default_thinking |
string | off |
Default thinking level: off, low, medium, high. |
local |
boolean | false |
If true, uses extended workspace context (includes CAPABILITIES.md, daily notes). Use for local models where tokens are free. |
tools.exclude |
string[] | [] |
Tool names to block for this agent. |
tools.include |
string[] | all | If set, only these tools are available to this agent. |
providers
Section titled “providers”LLM provider configuration. Each key is a provider ID referenced by agents.
OpenAI Codex CLI (ChatGPT subscription)
Section titled “OpenAI Codex CLI (ChatGPT subscription)”providers: codex-cli: model: default sandbox: read-only session: resumeRuns codex exec --json using the same login as the Codex TUI. Install Codex, run codex login, and add @rivetos/provider-codex-cli to plugins; no OPENAI_API_KEY is required. RivetOS unsets every OPENAI_* variable in the child environment so a stray API key cannot silently bill the API instead of the ChatGPT subscription. model: default follows the CLI’s configured model. Codex uses its own tools in the sandbox — RivetOS tools are not forwarded.
Options are binary, model, reasoning_effort (low through xhigh), cwd, sandbox (read-only, workspace-write, or danger-full-access), approve_for_me, skip_git_repo_check, profile, session (resume or replay), context_window, and max_output_tokens. The default sandbox is read-only; broaden it deliberately. skip_git_repo_check defaults to true only for read-only; for workspace-write and danger-full-access it defaults to false (set the key explicitly to override). Prefer an explicit cwd when widening the sandbox.
Anthropic
Section titled “Anthropic”providers: anthropic: model: claude-sonnet-4-6 max_tokens: 8192| Key | Type | Default | Description |
|---|---|---|---|
model |
string | claude-opus-4-7 |
Model identifier. |
max_tokens |
number | 8192 |
Maximum output tokens. |
api_key |
string | ${ANTHROPIC_API_KEY} |
API key. Prefer env var. |
token_command |
string[] | — | Argv that prints a bearer token on stdout (no shell). Wins over api_key. |
token_ttl_ms |
number | 300000 |
Cache lifetime for a minted token. |
token_command_timeout_ms |
number | 5000 |
Mint timeout; the helper is SIGKILL’d on expiry. |
context_window |
number | — | Override the model’s context-window size (advanced; for budgeting). |
max_output_tokens |
number | — | Hard cap on output tokens, independent of max_tokens. |
Auth: Set ANTHROPIC_API_KEY in .env, or set token_command to an argv helper that mints a short-lived token (cached, reminted on expiry or HTTP 401). For subscription/OAuth auth instead of an API key, use the claude-cli provider (below), which delegates auth to the claude binary.
xAI (Grok)
Section titled “xAI (Grok)”providers: xai: model: grok-4.20-reasoning| Key | Type | Default | Description |
|---|---|---|---|
model |
string | grok-4.20-reasoning |
Model identifier. (grok-4-1-fast-reasoning is a cheaper tier good for compaction.) |
api_key |
string | ${XAI_API_KEY} |
API key. |
token_command |
string[] | — | Argv that prints a bearer token on stdout (no shell). Wins over api_key. |
token_ttl_ms |
number | 300000 |
Cache lifetime for a minted token. |
token_command_timeout_ms |
number | 5000 |
Mint timeout; the helper is SIGKILL’d on expiry. |
max_tokens |
number | 4096 |
Maximum output tokens. |
temperature |
number | — | Sampling temperature. |
context_window |
number | — | Override the model’s context-window size (advanced). |
max_output_tokens |
number | — | Hard cap on output tokens. |
Google (Gemini)
Section titled “Google (Gemini)”providers: google: model: gemini-2.5-pro| Key | Type | Default | Description |
|---|---|---|---|
model |
string | gemini-2.5-pro |
Model identifier. |
api_key |
string | ${GOOGLE_API_KEY} |
API key. |
max_tokens |
number | 8192 |
Maximum output tokens. |
context_window |
number | — | Override the model’s context-window size (advanced). |
max_output_tokens |
number | — | Hard cap on output tokens. |
Ollama
Section titled “Ollama”providers: ollama: model: qwen2.5:32b base_url: http://localhost:11434| Key | Type | Default | Description |
|---|---|---|---|
model |
string | required | Model name (must be pulled locally). |
base_url |
string | http://localhost:11434 |
Ollama API endpoint. |
temperature |
number | — | Sampling temperature. |
num_ctx |
number | — | Context window size passed to Ollama. |
keep_alive |
string | — | How long Ollama keeps the model loaded between requests (e.g. 5m, -1 for always). |
context_window |
number | — | Override the context-window size reported to the runtime (advanced). |
max_output_tokens |
number | — | Hard cap on output tokens. |
Dedicated provider for a vLLM server. Exposes the full vLLM surface.
- Folds any post-first
systemmessage into ausermessage with a[SYSTEM NOTICE]prefix (vLLM/Qwen/Llama templates reject mid-conversation system messages) - Consumes vLLM’s native
reasoning_contentfield when a--reasoning-parseris configured server-side model: defaultauto-discovers the served model (and its context window) from the models listing (<base><api_prefix>/models)
providers: vllm: base_url: http://vllm.local:8000 # trailing /v1 optional model: default top_k: 40 min_p: 0.05 # api_key: ${VLLM_API_KEY} # only if vLLM started with --api-keyz.ai / GLM (OpenAI-compatible coding endpoint has no /v1 segment — api_prefix: "" is enough; models listing is at <base>/models):
providers: vllm: name: GLM (Z.ai) base_url: https://api.z.ai/api/coding/paas/v4 api_prefix: '' api_key: ${ZAI_API_KEY} model: glm-5.3-flashUse models_url only if the models listing lives somewhere other than <base><api_prefix>/models.
| Key | Type | Default | Description |
|---|---|---|---|
base_url |
string | required | vLLM server URL (/v1 optional; stripped and re-appended via api_prefix). |
api_prefix |
string | "/v1" |
OpenAI-compat path prefix. "" means none (chat at <base>/chat/completions). |
models_url |
string | — | Optional absolute URL when the models listing is hosted elsewhere (overrides <base><api_prefix>/models). |
probe_models |
boolean | true |
When false, skip the models probe/discovery and treat the provider as available. |
model |
string | default |
Served model id; default auto-discovers. |
api_key |
string | ${VLLM_API_KEY} |
Bearer token (only if --api-key set). |
token_command |
string[] | — | Argv that prints a bearer token on stdout (no shell). Wins over api_key. |
token_ttl_ms |
number | 300000 |
Cache lifetime for a minted token. |
token_command_timeout_ms |
number | 5000 |
Mint timeout; the helper is SIGKILL’d on expiry. |
models |
string[] | — | Static model catalog floor for listModels() (building block — no UI/harness reader yet; floor first). |
models_ttl_ms |
number | 60000 |
Background refresh interval for the endpoint model catalog (listModels() building block). |
max_tokens |
number | 4096 |
Maximum output tokens. |
temperature |
number | 0.7 |
Sampling temperature. |
top_p |
number | 0.95 |
Nucleus sampling. |
top_k |
number | — | vLLM sampling extension. |
min_p |
number | — | vLLM sampling extension. |
presence_penalty |
number | — | Standard OpenAI penalty. |
frequency_penalty |
number | — | Standard OpenAI penalty. |
repetition_penalty |
number | — | vLLM extension. |
min_tokens |
number | — | vLLM extension; minimum output tokens. |
stop |
string[] | — | Stop sequences. |
seed |
number | — | Reproducible sampling seed. |
context_window |
number | — | Context-window size reported to the runtime. |
max_output_tokens |
number | — | Hard cap on output tokens. |
default_tool_choice |
string | auto |
auto, none, or required. |
verify_model_on_init |
boolean | false |
Reject availability when the pinned model is missing from the models listing. |
name |
string | — | Display name for the provider. |
mm_processor_kwargs |
object | — | vLLM multimodal processor kwargs (passthrough). |
chat_template_kwargs |
object | — | vLLM chat-template kwargs (passthrough). |
extra_body |
object | — | Arbitrary JSON merged into the request body (vLLM passthrough). |
llama-server
Section titled “llama-server”Dedicated provider for llama.cpp’s llama-server. Lean by design: standard OpenAI sampling plus llama.cpp’s top_k / min_p and a generic extra_body escape hatch. None of the vLLM-only machinery (no mm_processor_kwargs, chat_template_kwargs, repetition_penalty, min_tokens, or video).
For native <think> reasoning, start llama-server with --reasoning-format deepseek.
providers: llama-server: base_url: http://localhost:8080 model: default top_k: 40 min_p: 0.05| Key | Type | Default | Description |
|---|---|---|---|
base_url |
string | required | llama-server URL (/v1 optional). |
model |
string | default |
Served model id; default auto-discovers. |
api_key |
string | ${LLAMA_SERVER_API_KEY} |
Bearer token (only if --api-key set). |
token_command |
string[] | — | Argv that prints a bearer token on stdout (no shell). Wins over api_key. |
token_ttl_ms |
number | 300000 |
Cache lifetime for a minted token. |
token_command_timeout_ms |
number | 5000 |
Mint timeout; the helper is SIGKILL’d on expiry. |
models |
string[] | — | Static model catalog floor for listModels() (building block — no UI/harness reader yet). |
models_ttl_ms |
number | 60000 |
Background refresh interval for the endpoint model catalog (listModels() building block). |
max_tokens |
number | 4096 |
Maximum output tokens. |
temperature |
number | 0.7 |
Sampling temperature. |
top_p |
number | 0.95 |
Nucleus sampling. |
top_k |
number | — | llama.cpp sampling extension. |
min_p |
number | — | llama.cpp sampling extension. |
presence_penalty |
number | — | Standard OpenAI penalty. |
frequency_penalty |
number | — | Standard OpenAI penalty. |
stop |
string[] | — | Stop sequences. |
seed |
number | — | Reproducible sampling seed. |
context_window |
number | — | Context-window size reported to the runtime. |
max_output_tokens |
number | — | Hard cap on output tokens. |
default_tool_choice |
string | auto |
auto, none, or required. |
verify_model_on_init |
boolean | false |
Probe /v1/models at boot to confirm the model is served. |
name |
string | — | Display name for the provider. |
extra_body |
object | — | Arbitrary JSON merged into the request body (e.g. grammar, n_probs). |
claude-cli
Section titled “claude-cli”Drives the local claude binary (Claude Code CLI) using the user’s subscription OAuth token, the sanctioned third-party-harness pattern per Anthropic’s April 2026 policy. The CLI owns auth, session caching, and the wire protocol; this provider drives it via stream-json and brings up a per-spawn embedded MCP server that exposes every executable RivetOS tool to claude-cli through --mcp-config.
providers: claude-cli: binary: claude # path or name on PATH model: claude-opus-4-7 # optional — defaults to whatever the CLI picks| Key | Type | Default | Description |
|---|---|---|---|
binary |
string | claude |
Path to the claude binary. |
model |
string | — | Model alias to pass to the CLI. |
extra_args |
string[] | [] |
Additional CLI flags (advanced). |
allowed_api_key_sources |
string[] | — | Extra apiKeySource values besides none. Unset keeps the OAuth-only gate. |
permission_mode |
string | default |
--permission-mode. The provider code default is default (Claude Code’s manual mode). dontAsk elsewhere in this document is the grok-cli default and matches that provider’s code; it is not this provider’s default. |
permission_prompts |
ui | none |
unset | Headless claude-code task executor only (not the interactive chat path). Unset passes no --permission-prompts flag, so the CLI invocation is unchanged. none passes --permission-prompts none and the CLI denies a prompt immediately instead of waiting out its decision timeout. ui passes --permission-prompts host and --permission-prompt-tool mcp__rivetos__request_permission. The embedded bridge parks the call, emits an approval-request, and returns the decision. Answer from the task page or POST /api/tasks/:id/approvals/:requestId with {"decision":"allow"|"deny"}. Unanswered prompts deny after permission_timeout_ms (default 60s); the outcome is appended to the row at spec.permissionDecisions. GET /api/tasks/:id/wait?onApproval=return yields the parked prompt instead of blocking until the task ends. The default wait does not. |
permission_timeout_ms |
number | 60000 |
How long a headless ui prompt stays parked before it denies. Positive integer, at most 600000. Unset uses 60000. The interactive chat path does not read this key. |
Auth: claude login (via the CLI itself). RivetOS does not handle the OAuth flow; the CLI does. On system init, a reported apiKeySource other than none kills the spawn unless it is listed in allowed_api_key_sources. Set that only for an Anthropic-compatible proxy the CLI reaches through its own apiKeyHelper (the value is the source string the CLI prints, matched exactly). The default stays unset: subscription OAuth is the sanctioned pattern, and API-key auth bills the console. Listing a source does not stop RivetOS from deleting ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN on the child.
opencode-cli
Section titled “opencode-cli”Drives the local OpenCode CLI (opencode) for harness id opencode by shelling opencode run --format json. Default model is zai/glm-5.3-flash. The installed CLI owns backend, endpoint, and credentials. RivetOS sets no HTTP protocol. Add @rivetos/provider-opencode-cli to plugins.
providers: opencode-cli: binary: opencode # path or name on PATH # model: zai/glm-5.3-flash # RivetOS default --model; CLI owns backend| Key | Type | Default | Description |
|---|---|---|---|
binary |
string | opencode |
Path or name on PATH. |
model |
string | zai/glm-5.3-flash |
Model id passed as --model. |
Auth: The installed OpenCode CLI owns backend, endpoint, and credentials. RivetOS sets no HTTP protocol and ships no OpenCode key or OAuth.
pi-cli
Section titled “pi-cli”Drives the local pi binary (@earendil-works/pi-coding-agent) headlessly — print/JSON or RPC. Harness id is pi; roster command is pi. Recommended default backend is z.ai GLM (reuse the coding-plan / Anthropic-compat key). Add @rivetos/provider-pi-cli to plugins.
providers: pi-cli: binary: pi # path or name on PATH # model: glm-4.6 # optional — omit for the CLI's configured model| Key | Type | Default | Description |
|---|---|---|---|
binary |
string | pi |
Path to the pi binary. |
model |
string | — | Model alias to pass to the CLI. |
Auth: whatever backend pi is configured to use (z.ai GLM recommended). RivetOS does not ship a dedicated pi API key; reuse the coding-plan credentials.
qwen-code
Section titled “qwen-code”Drives the local qwen binary (@qwen-code/qwen-code) headlessly — -p plus Claude-shaped stream-json. Harness id is qwen-code; roster command is qwen; provider id matches harness id. Add @rivetos/provider-qwen-code to plugins.
providers: qwen-code: binary: qwen # path or name on PATH; $QWEN_BINARY when unset # model: qwen3-coder-plus # optional — omit for the CLI's configured model # home: ~/.qwen # accepted for parity with the other CLI providers; currently unused (RivetOS reads the node's ~/.qwen; qwen itself always writes there)| Key | Type | Default | Description |
|---|---|---|---|
binary |
string | qwen |
Path to the qwen binary. $QWEN_BINARY overrides when binary is unset (provider + setup script). |
model |
string | — | Model id passed via -m. Optional. |
home |
string | ~/.qwen |
Accepted for parity with the other CLI providers; currently unused (RivetOS reads the node’s ~/.qwen; qwen itself always writes there). |
cwd |
string | — | Working directory for the spawn. |
name |
string | — | Display name for the provider. |
context_window |
number | — | Context-window size reported to the runtime. |
max_output_tokens |
number | — | Hard cap on output tokens. |
qwen-code 0.23.4 has no env or flag to relocate ~/.qwen; qwen always writes there. $QWEN_HOME is the same RivetOS-side lookup override for rivetos plugins install, rivetos doctor, and the setup script.
Auth: OpenAI-compatible / API-key only (Qwen OAuth free tier is discontinued). Configure modelProviders in ~/.qwen/settings.json. Effort is per-model (capabilities.reasoning.efforts); there is no CLI --effort flag.
channels
Section titled “channels”Messaging channel configuration. Each key is a channel type / plugin name.
Phase 5: Telegram, Discord, and voice-discord channel plugins were removed. Human UX is RivetHub. Optional remaining first-party channel:
channels.agent(mesh). Stalechannels.telegram:/channels.discord:/channels.voice*in fleet config yields an unknown channel type warning at boot; registration is skipped; nodes do not crash-loop.
grok-cli
Section titled “grok-cli”Drives the local Grok Build grok binary headlessly — one grok -p <prompt> --output-format streaming-messages-json --include-partial-messages call per turn — on the user’s Grok Build subscription (OIDC login in ~/.grok), not the metered xAI API. The CLI owns auth and its own tools/MCP servers. Default session: resume keeps one grok session per RivetOS conversation (--session-id on the first turn with the full transcript, --resume after with only the newest user turn). session: replay re-sends the whole conversation every turn. NDJSON stream_event deltas (reasoning, text) are emitted as they arrive; usage, sessionId, and cost come from the final result line. This is what lets provider: grok-cli agents answer mesh delegations, heartbeat tasks and chat.
providers: grok-cli: binary: ~/.grok/bin/grok # default ~/.grok/bin/grok, then `grok` on PATH # model: grok-4.5 # optional; omit for the CLI's configured model permission_mode: dontAsk # tools denied unless `allow` rules cover them reasoning_effort: medium # low|medium|high; a turn's `thinking` overrides # max_turns: 20 # optional cap; omit to let grok decide no_plan: true system_prompt: prepend # prepend | override | off session: resume # resume | replay cwd: ~/.rivetos/workspace # allow: [Read, Grep] # --allow rules for tool-using turns| Key | Default | Notes |
|---|---|---|
binary |
~/.grok/bin/grok, else grok |
Grok Build CLI. isAvailable() = grok --version exits 0. |
model |
CLI default (optional) | Passed as -m when set. Omit to use the CLI’s configured model. |
permission_mode |
dontAsk |
--permission-mode. dontAsk auto-denies tools not covered by allow. |
reasoning_effort |
CLI default | --reasoning-effort. Per-turn thinking (low/medium/high+) overrides. |
max_turns |
unset | --max-turns, only passed when set. Unset lets grok run its tool loop to completion; 1 = answer only, and any tool call then ends the turn as error_max_turns. |
no_plan |
true |
--no-plan — plan mode would swallow a headless run. |
system_prompt |
prepend |
prepend = RivetOS system prompt at the top of the prompt, grok keeps its own; override = --system-prompt-override; off = dropped. Applies on first turn and on later --resume turns. |
session |
resume |
resume = one grok session per RivetOS conversation (~/.rivetos/grok-cli-sessions.json). replay = full transcript every turn, no session flags. |
allow |
— | List of --allow rules (Claude Code rule syntax). |
tools |
— | --tools pass-through. |
cwd |
— | Working directory for the spawned grok (--cwd). |
Limits: incremental streaming is live. streaming-messages-json prints NDJSON stream_event deltas as they arrive. The older --output-format json blob is only a fallback when a turn emits no NDJSON and exits 0. There is no RivetOS tool bridge (grok cannot call delegate_task/memory_* as RivetOS tools; it has its own MCP servers from ~/.grok/config.toml). Session capture is the rivet-memory Grok hooks’ job.
channels
Section titled “channels”Messaging channel configuration. Each key is a channel type / plugin name.
Phase 5: Telegram, Discord, and voice-discord channel plugins were removed. Human UX is RivetHub. Optional remaining first-party channel:
channels.agent(mesh). Stalechannels.telegram:/channels.discord:/channels.voice*in fleet config yields an unknown channel type warning at boot; registration is skipped; nodes do not crash-loop.
Agent (HTTP)
Section titled “Agent (HTTP)”Inter-agent communication channel. Enables delegation between agents and mesh networking.
Note: for cross-node (mesh) auth,
secretis superseded by mutual TLS (mesh.tls) as of Phase 0.5; configuremesh:for node-to-node traffic. The standalonechannels.agentplugin still enforces its bearersecretwhen configured; it is deprecated, not dead. The plugin’s fate is decided when the gateway subsumes agent HTTP ingress (phase 1/5).
channels: agent: port: 3100 secret: ${AGENT_CHANNEL_SECRET} # still enforced by this plugin when set| Key | Type | Default | Description |
|---|---|---|---|
port |
number | 3100 |
HTTPS port for agent-to-agent messaging. |
secret |
string | — | Deprecated but enforced. Bearer token checked by the standalone agent channel plugin when set. Mesh node-to-node auth uses mTLS via mesh.tls instead. |
Multi-node mesh networking. Allows agents on different nodes to delegate tasks
to each other via mTLS. See docs/mesh.md for full documentation.
mesh: enabled: true node_name: <node_name> # must match the cert CN tls: true # default cert paths derived from node_name and RIVETOS_SHARED_DIR agent_channel_port: 3000 # storage_dir omitted → $RIVETOS_SHARED_DIR (unset → product default shared root) heartbeat_interval_ms: 30000 stale_threshold_ms: 90000 discovery: mode: seed seed_host: <node_name>.mesh # use .mesh DNS — matches cert SAN seed_port: 3000| Key | Type | Default | Description |
|---|---|---|---|
mesh.enabled |
bool | false |
Enable mesh networking. |
mesh.node_name |
string | hostname | Node name — must match cert CN. |
mesh.tls |
bool | object | — | mTLS config. Required when mesh.enabled: true. |
mesh.tls.ca_path |
string | $RIVETOS_SHARED_DIR/rivet-ca/intermediate/ca-chain.pem |
CA chain PEM. Unset RIVETOS_SHARED_DIR → product default. |
mesh.tls.cert_path |
string | $RIVETOS_SHARED_DIR/rivet-ca/issued/<node_name>.crt |
Node cert PEM. |
mesh.tls.key_path |
string | $RIVETOS_SHARED_DIR/rivet-ca/issued/<node_name>.key |
Node private key PEM. |
mesh.agent_channel_port |
number | 3000 |
HTTPS port for the agent channel. |
mesh.storage_dir |
string | $RIVETOS_SHARED_DIR (unset → product default) |
Directory containing mesh.json. |
mesh.heartbeat_interval_ms |
number | 30000 |
Heartbeat write interval. |
mesh.stale_threshold_ms |
number | 90000 |
Age before a node is marked stale. |
mesh.discovery.mode |
string | — | seed | static | mdns. |
mesh.discovery.seed_host |
string | — | Seed node hostname (use <nodeName>.mesh). |
mesh.discovery.seed_port |
number | 3100 |
Seed node port. |
mesh.secret |
string | — | Ignored — mesh agent-channel auth is mTLS only. Accepted with a warning for back-compat; remove it from your config. |
Embedded node gateway (den-server in-process). Off by default. See docs/DEN.md and docs/GATEWAY-MTLS.md. Independent of mesh.discovery.mode.
den: enabled: true host: 127.0.0.1 port: 5174 advertise_mdns: false # Off-loopback (host: 0.0.0.0) will not boot without TLS: # tls_cert: $RIVETOS_SHARED_DIR/rivet-ca/issued/<node_name>.crt # tls_key: $RIVETOS_SHARED_DIR/rivet-ca/issued/<node_name>.key| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Embed the den gateway in this process. |
host |
string | 127.0.0.1 |
Bind address. Off-loopback requires TLS. |
port |
number | 5174 |
HTTP/WS (or HTTPS) port. |
tls_cert |
string | — | Node TLS cert PEM path. Required off-loopback. Env: RIVETOS_DEN_TLS_CERT. |
tls_key |
string | — | Node TLS key PEM path. Env: RIVETOS_DEN_TLS_KEY. |
token |
string | — | Legacy; ignored. Gateway auth is device mTLS. |
terminal |
object | — | Local PTY terminals. Off by default. See den.terminal.*. |
static_dir |
string | hub dist | Override for the built hub app served at /. |
root_redirect |
string | — | 302 target for GET /. |
files_root |
string | $RIVETOS_SHARED_DIR (unset → product default) |
Shared filestore root for /api/files/*. Empty string disables the routes. |
files_open |
boolean | — | Opt-out of the files security gate. Defaults to terminal.open when unset. |
devices |
object | — | Mesh device enrollment (Settings → Devices). Off unless devices.enabled. |
advertise_mdns |
boolean | false |
Publish _rivethub._tcp via mDNS so LAN apps can find this node. No-op unless the gateway actually started. |
allowed_origins |
string[] | — | Extra browser origins (scheme://host[:port]) allowed to call the gateway. See Browser origin policy below. Env: RIVETOS_DEN_ALLOWED_ORIGINS. |
allowed_hosts |
string[] | — | Extra Host names a plain-HTTP (no TLS) gateway accepts from loopback callers, e.g. a local reverse proxy’s name. Env: RIVETOS_DEN_ALLOWED_HOSTS. |
allowed_harnesses |
string[] | RIVETOS_DEN_ALLOWED_HARNESSES (standalone den) |
Harness ids this node offers for new launches (Agents picker, POST /term, control-plane session create, harness-session tasks). Unset = every registered harness. Empty = none. Unknown ids warn at validate time. Boot copies the YAML key onto the embedded den; a standalone den reads the env (comma/space list). GET /api/harnesses stamps allowed when set; off-list create/spawn/preset/task is refused (harness_not_allowed). Existing sessions / real resumes still work; a never-seen resume key on /term or POST /api/harnesses/:id/sessions does not mint an off-list spawn. The task runner re-checks on the node that claims the row (legacy claude-cli targets canonicalize to claude-code first). |
Harness allow-list. When den.allowed_harnesses is set, the den stamps each GET /api/harnesses row with allowed: true|false (alongside installed). RivetHub web and Android new-conversation / Agents pickers keep only rows where installed !== false and allowed !== false (absent fields count as true, so older dens and unset allow-lists behave as today). Existing-session drawers still list off-list harnesses so previously started sessions remain visible and resumable. A fresh spawn, preset save, or harness-session task that names an off-list harness is refused with harness_not_allowed (HTTP 403). Resumes of existing sessions still work when the resume target exists. The task runner enforces the list on the node that claims the row.
Phone pairing. Den redeems pairing codes at POST /api/devices/pair from
the records in RIVETOS_DEN_PAIRING_DIR (default ~/.rivetos/devices/pairing).
The CLI (rivetos pair / rivetos local --device) reads the same variable when
writing records, so both sides stay on one directory; device PKCS#12 files still
land under ~/.rivetos/devices/ and den deletes them from there after redeem.
Settings → Pair a phone runs the CLI named by RIVETOS_DEN_PAIR_CLI (default
$RIVETOS_ROOT/packages/cli/dist/index.js; the in-process gateway passes the
install’s own CLI) as rivetos pair <name> --json, and rivetos pair --check --json to decide whether to offer pairing at all. The QR points at den.host
when that is a concrete address, else the node’s first LAN address.
Browser origin policy. The gateway answers a browser only when the request’s Origin is one of:
- the gateway itself (same host and port — the RivetHub web app it serves);
- the RivetHub desktop app (
app://bundle); - another den in the mesh roster (
mesh.json), so cross-node RivetHub works without configuration; - an entry in
den.allowed_origins.
Requests without an Origin (the Android app, hooks, CLI tools, mesh peers) are unaffected. Any other origin gets 403, on HTTP requests and on WebSocket upgrades alike, and no response ever carries Access-Control-Allow-Origin: *. On a plain-HTTP gateway, loopback callers must also address it by a loopback name (127.0.0.1, localhost, [::1], *.localhost) or a name in den.allowed_hosts. If you open RivetHub through a name the mesh roster doesn’t list (a Tailscale MagicDNS name, a reverse proxy), add that origin to den.allowed_origins on the nodes it calls.
memory
Section titled “memory”Memory backend configuration. Supported backends: postgres (default path today) and
sqlite (opt-in file store for the in-process Memory contract). Set exactly one — both
together is a validation error. Optional memory.capture controls write-path behaviour
for harness capture hooks that post through @rivetos/capture-core.
Capture redaction
Section titled “Capture redaction”Off by default. When enabled, @rivetos/capture-core redacts common secret
shapes (and optional operator regexes) in message content, tool_result, and
tool_args before the batch is posted or spooled. Logs report a span count
only — never the matched text. Placeholders are deterministic
([REDACTED:bearer], [REDACTED:pattern:0], …). Regex scanning of content
and tool_result is limited to the first 16,000 UTF-16 units (the same budget
the writer keeps after the field cap). tool_args string leaves are not
field-capped, so they are scanned in full. Built-in assignment / secret-key
detectors match exact stems and underscore/hyphen compounds (SECRET_KEY,
db_password) — bare key/auth only behind a separator, so ordinary fields
like author / token_count are kept. Split secrets (half in content, half
in tool_result) are not reassembled — each field is redacted independently.
memory: capture: redaction: enabled: false builtins: true # patterns: # - '\\b[Mm][Yy][Pp]refix-[a-z0-9]{20,}\\b'| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Run the write-path redactor. |
builtins |
boolean | true |
Built-in detectors (Bearer/Basic auth, credential assignments, PEM private keys, common token shapes, JWTs). Ignored when enabled is false. |
patterns |
string[] | — | Extra JS regex source strings. Only the g flag is applied — do not wrap in /…/flags, and do not use Python-style (?i) at the start of the pattern (it does not compile in JS). For case-insensitivity spell out character classes (e.g. [Mm][Yy][Pp]refix); RegExp modifier groups like (?i:…) need a newer V8 than the repo’s Node 22 floor. Invalid sources are a config error. Nested-quantifier shapes such as (a+)+ are rejected (ReDoS). |
Hooks that do not load YAML yet can enable the same built-ins with
RIVETOS_CAPTURE_REDACTION=1 (or true / yes / on). An explicit
redaction: { enabled: false } on createCaptureWriter wins over the env.
Wiring YAML → every hook process is separate from validation; until boot
injects this block, set the env (or pass CaptureWriterOptions.redaction) to
turn it on. Config validation warns when enabled: true because nothing
consumes the YAML block yet.
PostgreSQL
Section titled “PostgreSQL”memory: postgres: connection_string: ${RIVETOS_PG_URL} # Optional — point the background memory loop at your own endpoints: # embed_endpoint: http://your-embed-host:9402/v1 # delegation_tracking: true| Key | Type | Default | Description |
|---|---|---|---|
connection_string |
string | ${RIVETOS_PG_URL} |
PostgreSQL connection URL. |
embed_endpoint |
string | — | Embeddings endpoint base used by the embedding worker and query-time search. |
embed_model |
string | — | Embedding model id (required when an embed URL is set). |
embed_api_key |
string | — | Static bearer for the embed endpoint (also honors RIVETOS_EMBED_API_KEY). Does not fall back to OPENAI_API_KEY — that would send a global OpenAI credential to whatever embed_endpoint is configured. |
embed_token_command |
string[] | — | Argv that prints a bearer token on stdout (no shell). Wins over embed_api_key. |
embed_token_ttl_ms |
number | 300000 |
Cache lifetime for a minted embed token. |
embed_token_command_timeout_ms |
number | 5000 |
Mint timeout for embed_token_command. |
embed_wire_shape |
string | openai |
openai → POST <base>/v1/embeddings; native → POST <base> with {texts,input}. |
embed_expected_dims |
number | — | When set, must equal the embedding column width (halfvec(1024)). Rejects any vector whose length differs (longer vectors null out — truncation is EMBED_TRUNCATE_DIMS only). Any other configured value bricks inserts and vector search. Worker env: RIVETOS_EMBED_EXPECTED_DIMS. |
delegation_tracking |
boolean | false |
Persist delegation events into memory (ros_messages, channel delegation) for auditing. |
embedded |
object | — | In-process PGlite transport for the same postgres backend. Mutually exclusive with connection_string. |
Required extensions: pgvector (for embedding storage and similarity search).
The memory plugin handles schema creation and migration automatically on first boot.
Embedded PGlite
Section titled “Embedded PGlite”Presence of memory.postgres.embedded starts Postgres-in-WASM inside rivetos start and exposes it on a loopback wire socket. Existing pg clients keep using RIVETOS_PG_URL (injected at boot). Do not set connection_string in the same block — that is a validation error.
memory: postgres: embedded: data_dir: ~/.rivetos/pglite port: 5433 auto_migrate: true max_connections: 96Effective URL: postgres://postgres:postgres@127.0.0.1:<port>/postgres.
| Key | Type | Default | Description |
|---|---|---|---|
data_dir |
string | ~/.rivetos/pglite |
File-backed PGlite directory (~ expanded). |
port |
integer | 5433 |
Loopback TCP port (5432 may already be a host Postgres). |
auto_migrate |
boolean | true |
Run memory migrations in-process after the owner starts. |
max_connections |
integer | 96 |
Socket multiplexer cap. The library default is 1. |
Contract:
- The socket exists only while the node process runs. A second process on the same
data_dirattaches (does not open the directory twice) viarivetos-owner.lock. - Single owner. Stale lock (dead pid) is unlinked and replaced.
LISTEN/NOTIFYis not delivered across socket connections — task completion waiter and graphile-worker use polling.- Export with
pg_dump≥ 18 (this engine is PostgreSQL 18.3). RSS is about 650 MB per 170 MB on-disk database. - Without
RIVETOS_EMBED_URL/embed_endpoint(lite mode), boot setsrivet.defer_embed_enqueue=onso capture INSERTs do not require the graphile schema. In lite mode nothing ever enqueues embed jobs: rows are stored un-embedded (full-text + trigram recall only). When you later configure an embedding endpoint, restart the node — the embedding worker’senqueue-idlecron backfills every un-embedded row.
Day-2 commands (no extra daemon):
rivetos start/rivetos start --role migrate— foreground start now loads~/.rivetos/.env(same non-overriding merge as systemdEnvironmentFile=). Migrate acquires or attaches; in-process when this process owns the engine, async spawn when attaching.rivetos db migrate/rivetos db status— same acquire-or-attach wrap.--config <path>selects the YAML (not forwarded to the migrator).db migrate --urlbypasses the embedded engine and talks to that Postgres URL.db statuson embedded prints data dir, size on disk, owner, socket port, and_rivetos_migrationscount. If no node is running,db statusboots the engine for the duration of the command and labels the ownerthis command (no node running).rivetos doctor— does not warn thatRIVETOS_PG_URLis missing whenmemory.postgres.embeddedis set; if the socket refuses, it says to start the node.
SQLite (opt-in)
Section titled “SQLite (opt-in)”Presence of memory.sqlite registers the @rivetos/memory-sqlite plugin. No Postgres or
PGlite process is required for the in-process Memory path (chat append, session/task
history, settings, search). With an embedding endpoint set, messages are embedded in the
background by a job loop inside the runtime (no worker service), vectors are stored in the
file, and search fuses full-text, a literal-match arm and a vector arm with the same ranking
policy as Postgres; without one, search is full-text only. One file per user: the configured file is the node owner’s, and every other user in the tenancy
registry gets a file of their own under users/ beside it (users_dir), with their own job
loop, wiki directory (<wiki_dir>/users/<userId>) and tags. A turn, a capture or a memory request
that den resolved to such a user is served from that user’s file and never from the owner’s; a
user whose file cannot be opened, or whose id differs from another’s only by case, is refused. The
registry is checked on every lookup and re-read when it changes; a user seen once stays routed to
their own store until restart. With an in-memory owner store (path: ':memory:') the other
users’ stores are in-memory too and do not survive a restart. New conversations and messages record whose store they
were written to (owner_user_id). With per_user_files: false those users get no memory on
this node at all (nothing stored, nothing read); den still accepts their devices for everything
that is not memory. rivetos memory export / import work on one file
per command (each other user’s file is exported on its own with --sqlite <file>);
rivetos local backup copies the owner’s file and every user’s. If memory.sqlite
is set, remove or ignore a stale RIVETOS_PG_URL in ~/.rivetos/.env so the MCP sidecar
does not keep reading an old Postgres store while chat appends write sqlite. The parent
directory is created mode 0700 and the DB file (plus -wal/-shm) is 0600. With this
block unset, behaviour is unchanged.
memory: sqlite: path: ~/.rivetos/memory.sqlite| Key | Type | Default | Description |
|---|---|---|---|
path |
string | — | Required. File path (~ expanded) or :memory:. Relative paths are cwd-relative. |
embed_endpoint |
string | RIVETOS_EMBED_URL |
Embedding endpoint. Set → background embedding + vector search. Unset → full-text only; nothing is queued, and rows written meanwhile are embedded by a sweep once an endpoint is set. |
embed_model |
string | RIVETOS_EMBED_MODEL |
Required with an endpoint. Changing it clears the stored vectors and re-embeds (in the process that runs the job loop): vectors from different models are not comparable. |
embed_api_key |
string | RIVETOS_EMBED_API_KEY |
Bearer key. No fallback to any other provider key. |
embed_token_command |
string[] | RIVETOS_EMBED_TOKEN_COMMAND (JSON argv) |
Command that prints a bearer token; wins over the key. |
embed_token_ttl_ms |
number | — | How long a minted token is reused before the command runs again. |
embed_wire_shape |
openai | native |
RIVETOS_EMBED_WIRE_SHAPE, else openai |
Request shape, as for Postgres. |
embed_expected_dims |
number | RIVETOS_EMBED_EXPECTED_DIMS |
Require exactly this vector width. Unset: vectors longer than 1024 are truncated to 1024. A store keeps one width: a vector of another width fails its job unless this key names the new width, in which case the stored vectors are cleared and re-embedded. |
embed_timeout_ms |
number | RIVETOS_EMBED_TIMEOUT_MS, else 8000 |
Per-request timeout, clamped to 500–60000. |
embed_query_instruction |
string | RIVETOS_EMBED_QUERY_INSTRUCTION, else the same default as Postgres |
Prefix for search queries. Empty string disables. |
compactor_endpoint |
string | RIVETOS_COMPACTOR_URL |
OpenAI-compatible chat base URL (/chat/completions is appended). Unset → no summaries are written and no conversation text is sent anywhere. |
compactor_model |
string | RIVETOS_COMPACTOR_MODEL |
Model that writes summaries. Required with an endpoint. |
compactor_api_key |
string | RIVETOS_COMPACTOR_API_KEY |
Bearer key for the summarization endpoint. |
compactor_token_command |
string[] | — | Command that prints a bearer token; wins over the key. |
compactor_timeout_ms |
number | 600000 |
Per-request timeout, clamped to 5 seconds – 60 minutes. |
tagging |
boolean | on unless SESSION_TAGGING=0 |
Suggest key:value tags for each leaf summary. Needs a summarization endpoint (or the tagger’s own). |
tagger_endpoint, tagger_model, tagger_api_key |
string | RIVETOS_TAGGER_URL, RIVETOS_TAGGER_MODEL, RIVETOS_TAGGER_API_KEY |
A separate endpoint for the tagger. Unset: the compactor’s. |
tagger_token_command |
string[] | RIVETOS_TAGGER_TOKEN_COMMAND (JSON argv) |
Mints a bearer for that separate tagger, re-minted per call. Wins over tagger_api_key. A command that is not an argv array turns tagging off (it does not fall back to the static key). Ignored when no separate tagger endpoint and model are set. |
tagger_wire_shape |
openai | native |
RIVETOS_TAGGER_WIRE_SHAPE |
openai (default): a chat endpoint, given the built-in prompt. native: a classifier service; tagger_endpoint is the URL posted to, and it and tagger_model are required. Any other value, or native without them, turns tag suggestions off with an error in the log. |
tagger_allow_protected_removals |
boolean | RIVETOS_TAGGER_ALLOW_PROTECTED_REMOVALS |
When true, the tagger may suggest removing a tag a person added or accepted. Default false: those tags are never proposed for removal. A removal is only a review-queue suggestion either way. This does not gate capture. |
project_rule |
boolean | true |
Tag a captured session with project:<name> from its working directory’s git root. |
per_user_files |
boolean | true |
Give every other user in the users registry a SQLite file of their own. false: those users get no memory on this node. |
users_dir |
string | users/ beside the owner’s file |
Where the other users’ files go (<users_dir>/<userId>/memory.sqlite). |
wiki_dir |
string | WIKI_DIR, else the shared directory’s wiki |
Where the wiki’s page files live (a git repository the writer creates). |
wiki_extraction |
boolean | WIKI_EXTRACTION=1 |
Mine leaf summaries into wiki pages. Needs a summarization endpoint. Off by default. |
workers |
boolean | true when an embedding or summarization endpoint is set |
Run the in-process job loop. false queues work without draining it, and a model change is not applied on open: stored vectors are cleared only by a process that runs the job loop (or calls runJobs()). |
The file is opened with WAL, a 5s busy timeout, and foreign keys on. Without an embedding endpoint search is FTS5 only and no embedding work is queued; rows written meanwhile are embedded once an endpoint is configured. Embedding jobs retry with backoff; a job that runs out of attempts during an outage is revived by a ten-minute sweep.
With a summarization endpoint the same job loop compacts conversations into leaf, branch and
root summaries, using the prompts and batch policy of the Postgres compaction worker. A sweep
every five minutes queues conversations that have a full window of unsummarized messages, have
been idle, or have gone stale. Summaries are searchable (scope: summaries or both) and are
embedded when an embedding endpoint is set. The worker’s COMPACT_LEAF_BATCH,
COMPACT_BRANCH_BATCH, COMPACT_ROOT_BATCH, COMPACT_MIN_LEAFS, COMPACT_MIN_BRANCHES,
COMPACT_IDLE_MINUTES, COMPACT_STALE_MINUTES and COMPACT_STALE_MIN_BATCH variables tune it.
With wiki_extraction on, each leaf summary is mined into wiki pages by the same prompts, patch
rules and page writer as the Postgres pipeline: pages are markdown files under wiki_dir/topics,
committed to a git repository there, and indexed in the SQLite file for search. A sweep every ten
minutes picks up leaves written before extraction was turned on, failed attempts older than a day,
and leaves mined by an older pipeline version. The turn context gains a “Wiki (curated state)”
section, and the den serves /api/wiki and /wiki from this index. Topics are embedded when an
embedding endpoint is set.
Tagging works as on Postgres. A captured session whose settings.cwd sits in a git checkout gets
an accepted project: tag once (a batch from another machine is never resolved against this
host’s filesystem). Each leaf summary is sent to the tagger, and its proposals are stored as
suggestions on the summary and on the session, with new values proposed to the vocabulary; a
person accepts or rejects them in the hub. A rejected tag is not proposed again for the session
or summary it was rejected on (a later summary can be offered the same tag).
A node with memory.sqlite serves POST /api/capture and /api/memory/* (search, browse, stats,
health, tags, and the memory tools the MCP sidecar’s den transport calls) from the file, with the
same request and response shapes as a Postgres node, vocabulary edits included. A
request the den stamped for another user is served from that user’s own file (their batches are
never resolved against this host’s filesystem, and they decide tags as themselves), or refused
with 503 when they have none. The agent’s memory tools follow the turn’s user the same way.
Durable task engine (phase 1). The embedded run-task runner starts when
Postgres is configured and the 0002_ros_tasks migration has been applied
(rivetos-memory-migrate); on unmigrated nodes it logs a warning and stays
inert instead of failing boot.
With no pgUrl, sqlite_path is the engine instead: same ros_tasks rows
in that file, polled by this process (no graphile-worker, no LISTEN). If
both are set, Postgres wins and the sqlite file is not opened. Relative
paths are cwd-relative. Keep this file separate from any other app database.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Start the embedded task runner. Inert while nothing creates tasks. |
sqlite_path |
string | — | Task file when there is no pgUrl. Ignored (with a warning) when pgUrl is set. |
Env knobs: RIVETOS_TASKS_CONCURRENCY (default 4), RIVETOS_TASKS_POLL_MS (default 2000).
Headless harness executors can also be keyed under tasks.harnesses (pi, qwen-code, …) with binary / model / cwd / home — see the site architecture sample. For qwen-code, providers.qwen-code.home is accepted for parity with the other CLI providers and currently unused; tasks.harnesses.qwen-code.home is where the task executor looks for qwen’s projects/ sessions (default ~/.qwen). Neither key relocates qwen’s own writes.
Task isolation (claude-code): isolation / allowed_tools
Section titled “Task isolation (claude-code): isolation / allowed_tools”A delegated claude-code task runs as the service user, so by default it loads that user’s personal Claude Code setup: ~/.claude/settings.json (permission rules and default mode, hooks, enabled plugins), the plugins’ MCP servers, and the user-level CLAUDE.md. tasks.harnesses.claude-code.isolation chooses whether a task inherits that:
tasks: harnesses: claude-code: isolation: isolated # inherit (default) | isolated allowed_tools: - mcp__rivetos # the embedded RivetOS bridge's tools - 'Bash(git status:*)'inherit(default) passes no extra flags; nothing changes for a node that does not set the key.isolatedspawns with--setting-sources projectand--strict-mcp-config: no personal settings, permission rules, hooks, plugins or userCLAUDE.md, and the embedded RivetOS bridge is the only MCP server. The per-checkout.claude/settings.local.jsonis personal too and is not loaded either. The RivetOS capture hooks are supplied by the runtime through an inline--settingsobject, so task transcripts keep working. Withproviders.claude-cli.permission_promptsunset it also passes--permission-prompts none, so every prompt — built-in or MCP — is denied rather than left to the CLI’s default.- Project settings (
.claude/settings.jsonin the task’s working directory) load at both levels: they belong to the repository, not the operator. allowed_toolsis passed as--allowedToolsat both levels. Underisolatedthe operator’s own allow rules are gone, so list what a headless run may call without a prompt (or setproviders.claude-cli.permission_mode).- The node setting is a floor. A task can tighten it with
spec.isolation: isolatedonPOST /api/tasks; a spec cannot loosen anisolatednode (the spec is caller-controlled, and a task can create child tasks), and an unknown value is ignored. isolatedremoves the operator’s personal setup. It does not sandbox the working tree: a repository’s own.claude/settings.jsonhooks and allow rules still apply, so point isolated tasks at trees you trust.- Under
isolated, unlesspermission_modeisbypassPermissionsorpermission_promptsisui(the broker answers prompts), listmcp__rivetosinallowed_toolsor the bridge’s own tools are denied; boot warns when it is missing.
Model lists: tasks.harnesses.<id>.models / efforts / models_mode
Section titled “Model lists: tasks.harnesses.<id>.models / efforts / models_mode”The den discovers each harness’s model list from the harness itself where it can (Claude’s global config cache, Grok’s and Codex’s catalog caches, Codex’s codex debug models, Kimi’s and OpenCode’s config, Hermes’s configured endpoint) and falls back to a built-in static list. GET /api/harnesses reports where the list came from as capabilities.modelsSource: discovered, config, merged, or static.
tasks: harnesses: codex: models_mode: merge # discover (default) | replace | merge models: - { id: my-gateway/gpt-x, label: 'GPT-X via gateway', default: true } - { id: gpt-5.5, efforts: [{ id: low }, { id: high, default: true }] } efforts: # same treatment as models - { id: ultra, label: Ultra }discover— the discovered list, falling back to the static one. Amodels/effortslist is ignored (the validator warns).replace— the key you set (models,efforts, or both) replaces that list; a key you leave out keeps the discovered or static one. Amodelsoreffortslist with nomodels_modemeansreplace(the historical meaning, so older configs do not change). Under a pinned, non-emptymodelslist (modelsSource: config), an agent preset that names a model outside it is refused with a 400 on save; under the other modes it is saved with one warning in the den log.merge— the discovered list plus the config entries, deduped by id; a config entry wins onlabel,default, andefforts, and a configdefault: truebecomes the only default. Use this for a custom gateway that serves ids the harness’s own catalog does not know.
Entries are { id, label?, default?, efforts? }; malformed rows are dropped and an empty list keeps the discovered one. Model ids pass the same token rule as the spawn path: 1–64 characters from A-Z a-z 0-9 . _ [ ] : / -, ~ allowed after the first character, no ... For Codex, the literal id default (what pre-discovery clients stored) means “the CLI’s own default”: no model flag is passed and it is never vetted.
transports
Section titled “transports”Inbound surfaces that expose RivetOS tools to external clients. Currently: the MCP server transport (@rivetos/mcp-server), a StreamableHTTP MCP server that exposes memory_*, web_*, skill_*, and runtime tools to any MCP-speaking client (Claude Code, Cursor, etc.).
transports: mcp: port: 4321 bind: 127.0.0.1 # default localhost tls: # optional mTLS ca_path: $RIVETOS_SHARED_DIR/rivet-ca/intermediate/ca-chain.pem cert_path: $RIVETOS_SHARED_DIR/rivet-ca/issued/<node_name>.crt key_path: $RIVETOS_SHARED_DIR/rivet-ca/issued/<node_name>.keyThe transport is only activated when the matching transports.<name> slice is present. The MCP server can also run standalone via the rivetos-mcp-server bin shipped by @rivetos/mcp-server.
Outbound Model Context Protocol. RivetOS connects to external MCP servers and exposes their tools to agents (the inverse of the transports.mcp plugin above).
mcp: servers: memory: transport: stdio command: npx args: ['-y', '@modelcontextprotocol/server-memory'] toolPrefix: mcp_memory
github: transport: streamable-http url: http://localhost:8080/mcp connectTimeout: 5000 autoReconnect: trueMCP server config
Section titled “MCP server config”| Key | Type | Default | Description |
|---|---|---|---|
transport |
string | required | stdio, streamable-http, or sse. |
command |
string | — | Command to launch (stdio transport). |
args |
string[] | [] |
Command arguments (stdio transport). |
env |
object | {} |
Environment variables for the spawned process. |
cwd |
string | — | Working directory for the spawned process. |
url |
string | — | Server URL (HTTP/SSE transport). |
toolPrefix |
string | — | Prefix for tool names (prevents collisions between servers). |
connectTimeout |
number | 10000 |
Connection timeout in milliseconds. |
autoReconnect |
boolean | true |
Auto-reconnect on disconnect. |
deployment
Section titled “deployment”Optional. Declares the intended deployment target so tooling (rivetos update,
rivetos config) can choose the right path. Provisioning itself is driven by
the Compose files under infra/docker/ and the scripts under infra/scripts/.
Only target is read; any other key under deployment is warned as unknown.
deployment: target: docker| Key | Type | Default | Description |
|---|---|---|---|
target |
string | required | docker, proxmox, kubernetes, or manual. |
Environment variables
Section titled “Environment variables”These are typically set in .env.
The MCP sidecar (services/mcp-sidecar, one process per harness) selects its
memory backend with RIVETOS_MCP_TRANSPORT. den (the default when
RIVET_DEN_URL is set and RIVETOS_USER_ID is empty) calls this node’s den
over loopback HTTPS and opens no Postgres pool. RIVET_DEN_URL and
RIVET_DEN_CA are set by the den for sessions it spawns; a hand-started
harness gets them from rivetos_resolve_den (den.port, default 5174, and
den.tls_ca or RIVETOS_DEN_TLS_CA). A non-empty RIVETOS_USER_ID stays on
pg, because loopback den calls are the owner pool, unless the session also
has RIVETOS_USER_TOKEN.
RIVETOS_USER_TOKEN is how a session spawned for another user reaches that
user’s memory on a node whose stores are files (memory.sqlite), where there
is no per-user database URL to hand it. The node mints one random token per
user, in memory, and puts it in the environment of the sessions it spawns for
that user (a claude-cli turn, a hub terminal). Capture hooks and the sidecar
send it to the den as x-rivetos-user-token; the den accepts it from this
machine only and serves the request as that user. A token the den does not
know (the node restarted) is refused, never served as the owner: capture
keeps the batch in that user’s own spool (~/.rivetos/capture-spool-users/)
until a session spawned after the restart replays it; a batch still refused
after a week is moved to that spool’s dead/. Every capture integration
gets this from the shared writer, which reads the two variables itself and
refuses to post for a session that has a user id and no token. Such a
session gets its user’s memory and wiki tools and no delegation. Terminals
never inherit either variable from the node or from a running tmux server.
Nothing sets the token on a node whose users each have a database.
| Variable | Used By | Description |
|---|---|---|
ANTHROPIC_API_KEY |
provider-anthropic | Anthropic API key |
XAI_API_KEY |
provider-xai | xAI API key |
GOOGLE_API_KEY |
provider-google | Google AI API key |
RIVETOS_PG_URL |
memory-postgres, mcp-sidecar | PostgreSQL connection string (node owner database). The sidecar uses it only for transport pg. |
RIVETOS_MCP_TRANSPORT |
mcp-sidecar | den or pg. Default den when RIVET_DEN_URL is set and RIVETOS_USER_ID is empty; otherwise pg when RIVETOS_PG_URL is set. den is HTTPS to the local den with no sidecar Postgres pool. A routed user id keeps pg. |
RIVET_DEN_URL |
mcp-sidecar | Den origin for the sidecar (https://127.0.0.1:<den.port>, default port 5174). Den-spawned sessions already have it. The memory launcher fills it from ~/.rivetos/config.yaml when unset. |
RIVET_DEN_CA |
mcp-sidecar | PEM path for the den’s CA. The launcher exports it as NODE_EXTRA_CA_CERTS when that is unset, from den.tls_ca, else RIVETOS_DEN_TLS_CA, else /rivet-shared/rivet-ca/intermediate/chain.pem. For an https den URL a missing file unsets RIVET_DEN_URL; a plain-http URL needs no CA and is kept. |
RIVETOS_PG_POOL_MAX |
boot | Max connections for the one host-owned Postgres pool per runtime process (shared by the task engine, heartbeats, memory and the API). Default 8, min 4. |
RIVETOS_USERS_FILE |
den, memory-postgres, claude-cli | Optional explicit path to the tenancy registry (users.json). When unset, RivetOS loads $RIVETOS_SHARED_DIR/rivetos/users.json, then ~/.rivetos/users.json. Per-user memory routing comes only from this file — a user is routable iff their record has a usable pgUrl. A present-but-invalid shared-dir file fails closed (does not fall through to the home file). |
RIVETOS_OWNER_USER_ID |
den, users-registry, rivetos user add |
Node-owner user id used by the fail-closed seed and the CLI missing-file seed. Default owner (fleet compatibility); deployments override this env var. Forwarded to the embedded den. |
RIVETOS_AGENT_SECRET |
channel-agent | Deprecated — was the bearer secret for agent mesh. No longer used for agent-channel auth (replaced by mTLS). |
RIVETOS_LOG_LEVEL |
core | Log level: error, warn, info, debug |
RIVETOS_LOG_FORMAT |
core | Log format: pretty (default) or json |
GOOGLE_CSE_ID |
tool-web-search | Google Custom Search Engine ID |
GOOGLE_CSE_KEY |
tool-web-search | Google CSE API key |
RIVETOS_EMBED_API_KEY |
memory-postgres / embedding-worker | Optional static bearer for the configured embed endpoint. Opt-in — OPENAI_API_KEY is not used as a fallback. |
RIVETOS_TAGGER_URL |
compaction-worker | Session tagger endpoint (env only, like the compactor: there is no memory.postgres key, the worker is the sole consumer). Unset → the worker tags with RIVETOS_COMPACTOR_URL / _MODEL. SESSION_TAGGING=0 (or false/no/off) turns tagging off; the worker then reads none of the RIVETOS_TAGGER_* settings, so leftovers cannot stop it from starting. |
RIVETOS_TAGGER_MODEL |
compaction-worker | Tagger model id; defaults to RIVETOS_COMPACTOR_MODEL. |
RIVETOS_TAGGER_API_KEY |
compaction-worker | Static bearer for the tagger endpoint; defaults to RIVETOS_COMPACTOR_API_KEY only when the URL also defaulted. |
RIVETOS_TAGGER_TOKEN_COMMAND |
compaction-worker | JSON argv that prints a bearer token (no shell). Wins over the static key. RIVETOS_TAGGER_TOKEN_TTL_MS / RIVETOS_TAGGER_TOKEN_COMMAND_TIMEOUT_MS tune it. A rejected token (401) is re-minted once. Like the static key, it requires RIVETOS_TAGGER_URL: the worker refuses to start rather than send a tagger credential to the compactor endpoint. |
RIVETOS_TAGGER_WIRE_SHAPE |
compaction-worker | openai (default) or native. |
RIVETOS_TAGGER_TIMEOUT_SECONDS |
compaction-worker | Per-attempt tagger timeout, default 60. Tagging is best-effort: after the job’s attempts it is logged and dropped, never left dead. |
RIVETOS_TAGGER_TRANSIENT_STATUSES |
compaction-worker | 4xx codes the tagger endpoint returns while overloaded, retried like a 5xx (openai shape only). |
QWEN_BINARY |
provider-qwen-code, setup script | Override path/name of the qwen binary (default qwen on PATH). Honoured by the provider and the rivet-memory setup script. |
QWEN_HOME |
plugins install, doctor, setup script | Override where RivetOS looks for qwen’s settings.json / projects/ (default ~/.qwen). Does not relocate where qwen itself writes — qwen-code 0.23.4 has no env/flag to move ~/.qwen. |
Compaction worker
Section titled “Compaction worker”services/compaction-worker reads its own environment (systemd unit, or the
compaction-worker service in infra/docker/rivetos/docker-compose.yml).
The full list is the header of services/compaction-worker/src/index.ts;
these control which LLM it calls. An invalid value exits at startup.
| Variable | Description |
|---|---|
RIVETOS_COMPACTOR_URL |
Required. OpenAI-compatible base URL (http:// or https://); the worker appends /chat/completions. |
RIVETOS_COMPACTOR_MODEL |
Required. Chat model id. |
RIVETOS_COMPACTOR_API_KEY |
Bearer key for the primary, if it needs one. |
RIVETOS_COMPACTOR_TRANSIENT_STATUSES |
Comma list of 4xx codes the primary returns while overloaded (e.g. 403,404 for NVIDIA’s free tier). They retry with the 5xx backoff and, once retries run out, fail as retryable instead of marking the level terminal. Only 4xx codes are accepted. |
RIVETOS_COMPACTOR_FALLBACKS |
Ordered, comma-separated url|model|KEY_ENV|STATUSES entries tried when the primary fails. KEY_ENV names the variable holding that endpoint’s key (empty for none), so keys stay out of the list. STATUSES is that endpoint’s own ;-separated transient 4xx list; the primary’s list does not apply to fallbacks, since OpenRouter’s 403/404 are permanent. Example: https://integrate.api.nvidia.com/v1|google/gemma-4-31b-it|NVIDIA_API_KEY|403;404,https://openrouter.ai/api/v1|openai/gpt-oss-120b|OPENROUTER_API_KEY. |
RIVETOS_COMPACTOR_FALLBACK_COOLDOWN_MINUTES |
Default 15. After an outage (network, timeout, 5xx, empty answers, 401/402/403/404/429, and other non-request-scoped 4xx such as 405/409/415/426), later calls stay on the endpoint that answered this long before trying the primary again. A request-scoped 400/413/422, or an answer the caller rejects (unparseable wiki JSON), moves only that call to the next endpoint and does not clear an active failover. Listing 400/401/413/422 in a transient-status list is accepted but warned: those codes then retry like a 5xx and can sticky-failover after exhaustion. |
RIVETOS_COMPACTOR_FALLBACK_ATTEMPT_TIMEOUT_SECONDS |
Default 300. Per-attempt timeout on a middle fallback only (not the primary, not the last endpoint), so a hung fallback hands over in minutes. The primary and the last endpoint keep the full LLM_TIMEOUT_MS (60 min) — configuring fallbacks does not cut a slow local primary. |
Full annotated example
Section titled “Full annotated example”See config.example.yaml in the repository root for a complete annotated config file with all options commented.
