Configuration
Everything rupu reads from config.toml — every section and key, its type, its default, and what it does. All keys are optional: an empty file (or no file) is a working configuration.
Files and layering
rupu reads up to three TOML files and merges them, most specific last:
| Layer | File | Use it for |
|---|---|---|
| Global | ~/.rupu/config.toml | Your accounts, defaults, pricing, control-plane settings. RUPU_HOME moves the whole directory. |
| Customer | ~/.rupu/customers/<slug>/config.toml | Per-customer overrides for every project assigned to that customer — see Customer layer. |
| Project | <repo>/.rupu/config.toml | Repo-local overrides, checked in with the code. |
Scalars and tables from a more specific layer override the less specific one; arrays replace rather than merge (a project that declares [[scm.rules]] replaces the global list). Resolution order, highest first:
# highest wins
global lock › customer lock › project › customer › global › built-in default
Sections are strict: an unknown key inside a section such as [bash], [cp] or [netflow] is a parse error rather than a silently ignored typo. A few retired keys are still accepted as no-ops — see Deprecated keys.
Environment variables
There is no environment layer — environment variables are never merged into the config. A handful are read directly by the code they affect and win over the matching key there:
| Variable | Effect |
|---|---|
RUPU_HOME | Moves the global directory (default ~/.rupu): config, credentials, runs, caches. |
RUPU_LOG | Wins over log_level. |
RUPU_MAX_OPEN_FILES, RUPU_MAX_CONCURRENT_JOBS, RUPU_MIN_FREE_MEMORY_MB | Win over the [runtime] keys of the same name. |
RUPU_NETFLOW_SUBPROCESS=0 | Forces [netflow].subprocess_capture off. |
RUPU_NO_UPDATE_CHECK | Suppresses the passive update notice, like [update].check = false, when set to anything other than empty, 0 or false (so RUPU_NO_UPDATE_CHECK=0 leaves it on). |
RUPU_LIVE_VIEW=0 | Turns the workflow live view off (as --plain does). |
NO_COLOR | Forces [ui].color = "never". |
VISUAL / EDITOR | Used when [ui].editor is unset. |
RUPU_<ACCOUNT>_API_KEY | API-key fallback for a provider account with no stored credential — see Providers. |
Reading and editing
rupu config get ui.theme rupu config set cp.gate_sweep_interval_secs 30 rupu config set update.channel beta
rupu config get / set work on the global file only. Dotted keys descend into tables; the value is parsed as a TOML scalar (string, integer or bool), and set refuses to replace a table with a scalar or the reverse. Edit arrays and tables ([[scm.rules]], [recovery].fallbacks) by hand, a customer layer with rupu customer edit <slug>, or everything from the control plane's Settings page, which shows each effective value with the layer it came from, a lock toggle, and a raw TOML editor.
Locks and policy — [policy]
| Key | Type | Default | Notes |
|---|---|---|---|
lock | array<string> | [] | Dotted key paths (permission_mode, autoflow.max_active) whose value from this layer can't be overridden. A global lock beats every layer; a customer lock beats the project layer only and cannot unlock a key the global list names. A project's own lock list is ignored. |
# ~/.rupu/config.toml — no project or customer can loosen these permission_mode = "readonly" [policy] lock = ["permission_mode", "bash.env_allowlist"]
provider: or auth: still runs on that provider even when default_provider is locked.Customer layer
Assign a project to a customer with rupu customer assign <slug> (a slug is 1–63 of a-z, 0-9, -; none is reserved). The customer's config.toml then sits between the global and project files for every run in that project — and its subdirectories, since a run uses the nearest assigned ancestor of its working directory.
# ~/.rupu/customers/northwind/config.toml default_provider = "anthropic-northwind" [[scm.rules]] owner = "northwind-labs" account = "github-northwind" [policy] lock = ["default_provider"] # a repo's .rupu/config.toml can't move these runs
- Accounts are declared once, globally (
rupu auth login --account anthropic-northwind --kind anthropic); the customer layer only references them. - Arrays replace, as between global and project: a customer that declares
[[scm.rules]]replaces the global rules for its projects. The project still wins on any key the customer has not locked; a customer lock cannot unlock a key the global[policy].locknames. - A run records the customer it ran under (
"customer": nullwhen it ran with none);workflow resume/approverun on what was recorded even if the project was reassigned since, and a recorded customer that no longer exists fails the resume. Only a run from before customers existed falls back to the directory it was launched from. An autoflow run uses its repo's checkout, not its issue worktree. - A dangling assignment or an unparseable customer layer fails the run — it never falls back to global.
rupu customer show <slug>prints the effective config with sources and locks;rupu customer edit <slug>edits and validates the layer. The control plane manages customers, filters its lists by?customer=and previews a launch's accounts.
More in Customers.
Top-level keys
| Key | Type | Default | Notes |
|---|---|---|---|
default_provider | string | none | Provider account used when an agent omits provider:. |
default_model | string | claude-sonnet-4-6 | Used when the agent sets no model: and the provider's own default_model is unset. |
permission_mode | string | ask | ask | bypass | readonly; fallback when neither the agent nor --mode sets one. |
log_level | string | warn | Any tracing directive (debug, rupu_scm=debug,info). RUPU_LOG wins; blank counts as unset. |
Model providers — [providers.<account>]
One table per provider account: a bare vendor name (anthropic, openai, gemini, copilot), a named account of a vendor (anthropic-work with kind = "anthropic"), or an openai-compatible endpoint you name yourself. An agent's provider: names the account. Narrative: Providers & Auth.
| Key | Type | Default | Notes |
|---|---|---|---|
kind | string | none (the table name is the vendor) | Vendor of a named account, or openai-compatible. Written for you by rupu auth login --account <name> --kind <vendor>. An unknown kind is a load error. |
base_url | string | vendor URL | Read by openai-compatible accounts. |
default_model | string | none | Beats the global default_model for agents on this account. |
stream | bool | true | false forces non-streaming for servers without SSE. |
org_id | string | none | OpenAI only; sent as OpenAI-Organization. |
region | string | none | Accepted; not used by any shipped client today. |
timeout_ms | integer | 120000 | 0 counts as unset. |
max_retries | integer | 1 | Retries after the first attempt on a retryable error. |
max_concurrency | integer | anthropic 4, openai 8, gemini 4, copilot 4 | In-flight requests per account; 0 counts as unset. |
prompt_cache | bool | true | Anthropic only. false for gateways that reject cache_control; an agent's anthropicPromptCache overrides it. |
models | array of tables | [] | Each entry: id (required), context_window and max_output (optional; when unset or 0 the limit is discovered from the provider's model list). |
[providers.anthropic] default_model = "claude-sonnet-4-6" max_concurrency = 6 [providers.lab-llm] # an OpenAI-compatible endpoint kind = "openai-compatible" base_url = "http://10.0.0.20:8000/v1" default_model = "qwen3-coder" models = [{ id = "qwen3-coder", context_window = 131072, max_output = 8192 }]
An openai-compatible account must set base_url and default_model, and may not reuse a reserved name (anthropic, openai, gemini, copilot, local, github, gitlab, linear, jira).
SCM and trackers — [scm.*], [[scm.rules]], [issues]
| Key | Notes |
|---|---|
[scm.default].platform | Fallback platform when a tool call omits platform. (owner / repo here are deprecated and never read.) |
[issues.default].tracker | Fallback tracker when a tool call omits tracker. |
[issues.default].project | <owner>/<repo> default for rupu issues list, between an explicit --repo and git-remote autodetection. Needs tracker set too. |
[scm.<account>] | One table per SCM account ([scm.github], [scm.gh-work]). Keys: kind (github | gitlab; omit when the name is the platform), base_url, timeout_ms (default 30000), max_concurrency (github 8, gitlab 6), clone_protocol (https | ssh, default https), and GitLab-only oauth_client_id — the OAuth application SSO logs in as (glab's on gitlab.com; required for self-managed; read from the global file only). |
[[scm.rules]] | Account selection when two accounts share a platform. Each entry sets account plus exactly one of owner (repo-owner glob) or path (cwd glob). Precedence: --account → owner rule → path rule → the sole account of that platform → error. rupu scm bind appends one. |
[scm.github] clone_protocol = "ssh" [scm.gh-work] kind = "github" base_url = "https://ghe.example.com/api/v3" [[scm.rules]] owner = "example-corp/*" account = "gh-work" [[scm.rules]] path = "~/Code/work/*" account = "gh-work"
See Integrations for the full routing story.
Terminal UI — [ui]
| Key | Type | Default | Notes |
|---|---|---|---|
color | string | auto | auto | always | never. NO_COLOR and --no-color force never. |
theme | string | none | One name for both syntax and palette: a palette theme name selects that palette (and its paired syntax theme); a syntax theme name selects that syntax theme. |
live_view | string | focused | focused | compact | full — density of the workflow live view. |
pager | string | auto | auto | always | never. |
editor | string | $VISUAL / $EDITOR | Command used by agent edit/create and workflow edit/create. |
[ui.syntax].theme | string | base16-ocean.dark | Code-highlighting theme; beats theme. Built-ins include base16-ocean.dark, base16-eighties.dark, base16-mocha.dark, base16-ocean.light, InspiredGitHub, Solarized (dark), Solarized (light). |
[ui.palette].theme | string | rupu-dark | CLI status colors; beats theme. Built-ins: rupu-dark, rupu-light, rupu-midnight, tokyo-night, dracula, gruvbox-dark, github-dark, github-light. |
[ui.cp].shell | string | v1 | v1 | v2 — control-plane web shell. Read from the global file on every page load, so a browser refresh switches it. |
Custom themes live in ~/.rupu/themes/ or a project's .rupu/themes/. rupu ui themes lists what's available (marking the current one); rupu ui theme show|validate|import manages them, and import accepts native rupu theme files and Base16 schemes.
[ui] live_view = "compact" pager = "never" [ui.palette] theme = "tokyo-night" [ui.syntax] theme = "base16-mocha.dark"
The bash tool — [bash]
| Key | Type | Default | Notes |
|---|---|---|---|
timeout_secs | integer | 120 | Timeout for one bash tool invocation. |
env_allowlist | array<string> | [] | Extra env vars forwarded into the subprocess, beyond the always-allowed PATH, HOME, USER, TERM, LANG (and RUPU_RUN_ID, the run marker a launched child carries). |
Machine limits — [runtime]
| Key | Type | Default | Notes |
|---|---|---|---|
max_open_files | integer | unset | Soft open-file limit rupu raises itself to at startup, and the cap for automatic growth under fan-out. Unset: raise to 10240 (capped at the hard limit) and grow on demand. Past the cap, new agent runs are paced instead of failing with "Too many open files". Env: RUPU_MAX_OPEN_FILES / --max-open-files. |
max_concurrent_jobs | integer | RAM-derived | Process-wide ceiling on local agent jobs, whatever a workflow's max_parallel: / max_concurrency: says. Unset: clamp(RAM_GB / 4, 4, 128). 0 means no ceiling. Remote (distribute:) units are bounded by their own host. Env: RUPU_MAX_CONCURRENT_JOBS. |
min_free_memory_mb | integer | RAM-derived | Memory watchdog: a new local job waits until this much memory is free (proceeding anyway after 60 s, with a warning). Unset: max(RAM × 10%, 4096). 0 disables the watchdog (the ceiling still applies). Available memory is read from /proc/meminfo (Linux) or vm_stat (macOS); where it can't be read, the watchdog does nothing. Env: RUPU_MIN_FREE_MEMORY_MB. |
Child runs — [dispatch]
Child runs an agent starts with dispatch (see Agents).
| Key | Type | Default | Notes |
|---|---|---|---|
max_concurrent_children | integer | 8 | How many children of one parent run run at once, all kinds together. More wait as pending until a slot frees. 0 is read as 1. Applies per run, so an agentiflow's units count against its lead's limit. In-process sub-agents also wait for [runtime] admission. |
Message spaces — [messaging]
A run's message space (see Messaging).
| Key | Type | Default | Notes |
|---|---|---|---|
log_max_mb | integer | 64 | Size past which a space's log.jsonl takes no more messages. A further send fails to its sender with an error and a messaging_full notice. The log is never trimmed. Receipts are still written. |
Workflow run: steps — [workflow]
| Key | Type | Default | Notes |
|---|---|---|---|
run_step_enabled | bool | false | Whether run: steps (which execute declared commands) may run. A workflow that reaches one while this is off fails; it never silently skips it. |
run_step_allowlist | array<string> | [] | Executables a run: step may invoke, matched on basename (/usr/bin/python3 and python3 gate alike). Empty means any. |
[workflow] run_step_enabled = true run_step_allowlist = ["python3", "make"]
Recovery — [recovery]
What happens when a model's reply can't be used or the provider fails the request. Background: Recovery & reliability.
| Key | Type | Default | Notes |
|---|---|---|---|
fallbacks | array of { model, provider? } | [] | Ordered fallback chain for every agent that declares no fallbacks: of its own (an agent's list replaces this one, not merged). An entry without provider means the provider the run started on; entries on that provider are tried first, then other providers. A cross-provider entry sends the conversation to that provider's API. Unknown keys are an error. |
server_side_fallback | bool | true | Ask Anthropic to fall back server-side when the requested model refuses. Applies only to API-key auth on claude-fable-5, claude-fable-5-1, claude-opus-5, claude-opus-5-5 and claude-sonnet-5-5; never OAuth. Set false to stop sending it. |
[recovery] fallbacks = [ { model = "claude-sonnet-5-5" }, { provider = "openai-codex", model = "gpt-5.6-cyber" }, ] server_side_fallback = true
Findings — [findings]
| Key | Type | Default | Notes |
|---|---|---|---|
artifact_max_bytes | integer | 500 MiB | Per-file cap for copying an artifact into the store; larger files are recorded by hash only. |
artifact_max_files | integer | 500 | Most files one report's artifacts may expand to (a directory counts every file inside). |
artifact_total_max_bytes | integer | 2 GiB | Most bytes one report's copied artifacts may add up to. |
report_max_bytes | integer | 256 KiB | Size budget for one full finding report. |
ticket_patterns | array<string> | [] | Regexes or URL prefixes identifying your existing tickets; added to the finding-writing guidance agents get. |
export_id_prefix | string | SEC | Prefix of exported report numbers (SEC-001). Global file only; must match ^[A-Za-z][A-Za-z0-9_-]{0,15}$. |
rupu findings import reads the size limits from the global file only. See Findings.
Concern catalogs — [coverage]
How a run's concern catalog is rendered into its system prompt; see Coverage for render modes.
| Key | Type | Default | Notes |
|---|---|---|---|
full_mode_max_concerns | integer | 80 | Most concerns a run's flattened catalog may hold for its mode: auto includes to render in full; a larger catalog renders them as the one-line index the agent searches with coverage.concerns.search / coverage.concerns.detail. Includes that say mode: full or mode: index keep their mode. |
Network capture — [netflow]
| Key | Type | Default | Notes |
|---|---|---|---|
subprocess_capture | bool | true | Master switch for recording the connections an agent's bash commands open, by passively observing the OS socket table (no proxy, no root). Where the backend can't start, the run records why. RUPU_NETFLOW_SUBPROCESS=0 forces it off. |
subprocess_poll_ms | integer | 50 | Linux only: interval between socket-table polls while a bash call runs. |
subprocess_linger_ms | integer | 3000 | How long after the shell exits its sockets are still attributed to that call. A socket still open at the end is recorded as still open. Clamped to one day. |
asn_auto_refresh | bool | true | Keep the IP → ASN enrichment table fresh: the control plane downloads it in the background when it is missing or stale. |
asn_refresh_interval_days | integer | 7 | Age after which the ASN table is re-downloaded. 0 treats it as always stale. |
asn_source_url | string | https://iptoasn.com/data/ip2asn-combined.tsv.gz | Where the ASN table comes from (gzipped ip2asn TSV) — point it at a mirror on an air-gapped network. |
cp_index_budget_mb | integer | 256 | Memory budget for the flow rows rupu cp serve keeps indexed. Over budget, the oldest files' rows are dropped and re-read from disk on demand — slower, never different; per-file summaries always stay in memory. Status: GET /api/netflow/index. Takes effect on the next request after a change made through the control plane; a hand edit needs a restart. |
[netflow] asn_source_url = "https://mirror.internal.example/ip2asn-combined.tsv.gz" asn_refresh_interval_days = 30 subprocess_linger_ms = 5000
See Netflow.
Control plane — [cp]
Background loops rupu cp serve runs in-process, so autoflows, triggers and gate timeouts work with no external cron.
The loop toggles and intervals below, like agentiflow.reaper_enabled and netflow.asn_auto_refresh, are read once when cp serve starts: change them and restart it. Saving the global config from the control plane lists the ones you changed as restart_required; every other key applies at once.
| Key | Type | Default | Notes |
|---|---|---|---|
max_workspace_bytes | integer | 256 MiB | Largest workspace-sync payload or delta for placed runs. |
autoflow_reconcile_enabled | bool | true | Run the autoflow reconcile loop (issue/PR entity autoflows). |
autoflow_reconcile_interval_secs | integer | 60 | |
cron_tick_enabled | bool | true | Run the cron / polled-event trigger tick (what rupu cron tick does). |
cron_tick_interval_secs | integer | 60 | |
gate_sweep_enabled | bool | true | Enforce gate on_timeout routing and reap orphaned runs whose runner process died. The same tick also drives the ASN refresh and the agentiflow reaper, each gated by its own key. |
gate_sweep_interval_secs | integer | 60 |
agent_authoring_ui and workflow_editor_ui are no longer read: the agent builder and workflow editor are the only UIs. They still parse (with a warning) so an old config keeps loading — delete them.Agentiflow supervision — [agentiflow]
| Key | Type | Default | Notes |
|---|---|---|---|
serve_enabled | bool | true | false makes rupu agentiflow serve say it's disabled and exit. |
serve_interval_secs | integer | 60 | Seconds between agentiflow serve sweeps (cp serve reaps on its gate-sweep tick). |
reaper_enabled | bool | true | Finalize a run whose coordinator died as failed and stop its units — in both agentiflow serve and cp serve. |
See Agentiflows.
Event triggers — [triggers]
| Key | Type | Default | Notes |
|---|---|---|---|
poll_sources | array of string | table | [] | Sources polled for event-triggered workflows: github:owner/repo, gitlab:group/project, linear:<team-id>, jira:<site>/<project>. A table entry adds poll_interval ("5m") and account — the only way to choose between two tracker accounts of one kind. |
max_events_per_tick | integer | 50 | Cap on events processed per source per tick. |
[triggers] poll_sources = [ "github:example-org/api", { source = "linear:team-1234", poll_interval = "10m", account = "linear-ops" }, ]
See Triggers.
Autoflow defaults — [autoflow]
| Key | Type | Default | Notes |
|---|---|---|---|
enabled | bool | false | |
repo | string | none | e.g. github:example-org/api. |
checkout | string | worktree | worktree | in_place. |
worktree_root | string | none | Only with checkout = "worktree". |
permission_mode | string | none | Only bypass or readonly; ask is rejected for unattended runs. |
strict_templates | bool | false | |
max_active | integer | unbounded | Cap on concurrently active claims. |
cleanup_after | string | never | e.g. 7d — completed/released claims and their worktrees are pruned after this. |
See Autoflows.
Pricing — [pricing]
Turns token counts into USD for rupu usage, run listings and the control plane. Lookup order: [pricing.<provider>."<model>"] override → built-in table (each vendor's standard-tier list rate) → [pricing.agents.<agent>]. A named account prices through its kind, and an account-level override beats the vendor one. Model ids match exactly, then with a trailing [tag] stripped, then with a date snapshot stripped.
| Field | Type | Required | Notes |
|---|---|---|---|
input_per_mtok | float | yes | USD per million input tokens. |
output_per_mtok | float | yes | USD per million output tokens (reasoning tokens included). |
cached_input_per_mtok | float | no | Cache reads; defaults to the input rate. |
cache_write_per_mtok | float | no | Cache writes; defaults to the input rate. |
[pricing.anthropic."claude-sonnet-4-6"] input_per_mtok = 3.0 output_per_mtok = 15.0 cached_input_per_mtok = 0.30 cache_write_per_mtok = 3.75 [pricing.lab-llm."qwen3-coder"] # an openai-compatible account input_per_mtok = 0.4 output_per_mtok = 1.2 [pricing.agents.triage-bot] # fallback when no model price is known input_per_mtok = 0.5 output_per_mtok = 1.5
Retention — [storage]
| Key | Type | Default | Notes |
|---|---|---|---|
archived_session_retention | string | 30d | Default cutoff for rupu session prune. |
archived_transcript_retention | string | 30d | Default cutoff for rupu transcript prune / rupu cleanup. |
Updates — [update]
| Key | Type | Default | Notes |
|---|---|---|---|
channel | string | stable | stable | beta — which channel rupu update follows. |
check | bool | true | Print a passive "update available" notice on normal commands. |
Deprecated keys
These still parse — so an old file doesn't lose every other setting — but drive nothing. Loading one logs a warning; delete them, as a future release will reject them.
| Key | Replacement |
|---|---|
[retry] (max_attempts, initial_delay_ms) | [providers.<name>].max_retries |
[cp].agent_authoring_ui | none — the agent builder is the only UI |
[cp].workflow_editor_ui | none — the workflow editor is the only UI |
[scm.default].owner, [scm.default].repo | pass --repo, or set [issues.default].project |