Customers

Group projects under a customer, give that customer its own config layer — its provider account, SCM rules, pricing and locks — and see every run, finding and dollar attributed to it.

What a customer is

A customer is a named record (slug, display name, optional notes, contact and color) with a config layer of its own. Assign a project to it and every run launched in that project:

Customers live under the global directory: ~/.rupu/customers/<slug>/customer.toml (metadata) and ~/.rupu/customers/<slug>/config.toml (the layer). A slug is 1–63 characters of a-z, 0-9 and -; none is reserved, because it means “no customer” in filters.

The rupu customer command

rupu customer list [--archived]                  List customers (slug, name, projects, archived, color)
rupu customer show <slug>                       Metadata, assigned projects, effective config with sources and locks
rupu customer create <slug> --name <name>       Create one (also --notes, --contact, --color #rrggbb)
rupu customer set <slug> [--name|--notes|--contact|--color]  Change metadata; "" clears notes/contact/color
rupu customer edit <slug> [--editor <cmd>]       Edit the config layer in $EDITOR, validated on save
rupu customer archive <slug>                    Hide from pickers; its projects keep running
rupu customer unarchive <slug>
rupu customer delete <slug>                     Refused while any project is assigned
rupu customer assign <slug> [--project <dir|ws_id>]  Assign a project (default: the current directory)
rupu customer unassign [--project <dir|ws_id>]       Remove a project's customer

A typical setup, run from the repo root:

# declare the accounts once, globally
rupu auth login --account anthropic-acme --kind anthropic
rupu auth login --account github-acme --kind github

# create the customer and point its layer at those accounts
rupu customer create acme --name "Acme Corp" --color "#2563eb"
rupu customer edit acme

# assign this checkout, then check what its runs will use
rupu customer assign acme
rupu customer show acme

edit validates the layer on top of the global config when you save; show prints a KEY / VALUE / SOURCE / LOCKED BY table of the effective config. list honours the global --format table|json|csv.

The customer config layer

The customer’s config.toml sits between the global file and the project’s .rupu/config.toml. Precedence, highest first:

global lock  ›  customer lock  ›  project  ›  customer  ›  global  ›  default
global ~/.rupu/config.toml customer customers/<slug>/config.toml project <repo>/.rupu/config.toml each layer overrides the one before it — except keys a [policy].lock above it pins
Layers merge left to right; a lock in the global or customer layer pins a key so the project can’t move it.
# ~/.rupu/customers/acme/config.toml
default_provider = "anthropic-acme"

[[scm.rules]]
owner   = "acme-corp"
account = "github-acme"

[policy]
lock = ["default_provider"]

Accounts are declared once, globally (rupu auth login --account …); the customer layer only references them through default_provider or [[scm.rules]]. A customer layer can also carry its own [pricing], which prices that customer’s runs.

Run attribution

A run records the customer it ran under at launch — in run.json, in every transcript’s RunStart, and in a session’s session.json. The field is tri-state:

On diskMeansAttributed as
"customer": "acme"recorded slugacme
"customer": nullrecorded no customernone — even if the project has been assigned since
no customer keylegacy, written before customers existedthe project’s current assignment, marked derived

Reassigning a project never rewrites history. rupu workflow resume / approve run on what was recorded: the recorded customer (a recorded customer that no longer exists fails the resume), or no customer for a recorded null. Findings and projects record no customer of their own — they follow the project’s current assignment.

Multi-host note. A remote host resolves customers from its own customer store and assignments; the coordinator’s customer layer and credentials are not shipped to remote hosts yet. A host whose runs can’t report a customer is never counted as “no customer” — it’s named as unfilterable instead.

In the control plane

Customer scope

A global customer picker (sidebar and top bar) sets the scope every scoped page filters to: All customers, a single customer, or Unassigned (work with no customer). The scope persists in the browser, and a deep link such as /runs?customer=acme opens already filtered. Archived customers stay valid scopes; the picker lists them on demand.

Filters by the scopeShows an “unscoped” note instead
Dashboard (runs, findings, autoflow cycles, spend), workflow-run and agent-run lists, Sessions, Findings, Projects, Usage (headline, timeline, run rows, outliers)autoflow listings, Coverage, the concern catalog, fleet counts, definition lists and pages, Live Events, Network, the ⌘K palette

A page that can’t be filtered says so while a scope is set, so a number that isn’t the customer’s is never passed off as theirs. Remote hosts that can’t be filtered are named in a banner, never counted as zero.

Customers page and detail

Pricing rollups

Each run is priced with the pricing of the customer it’s attributed to (global + that customer’s layer); work with no customer is priced at the global rates. A run costs the same everywhere it appears — its list row, its detail, the usage views and the rollups. A customer whose layer doesn’t parse is still listed, and its work is priced at the global rates visibly, with a pricing error naming the customer.

Launch preview

The workflow and agent launchers include a billing panel that shows, before anything runs, which customer the launch falls under and which provider, fallback and SCM accounts it will authenticate as — each with where the choice came from (“agent frontmatter”, “customer default”, “global default · locked”, “rule owner = acme-corp”, …) — plus any warnings. It resolves exactly like a launch, so a launch that would fail (a dangling customer assignment, a config that doesn’t load) is blocked with the same error.

API

The web UI is a thin client of rupu cp serve’s routes, which are also usable directly: /api/customers (list, create, detail with rollups, edit, archive, delete, project assignment), GET /api/config?customer=<slug> and PUT /api/config/customer/:slug for the layer, POST /api/launch/preview, and a ?customer=<slug>|none filter on the runs, sessions, findings, projects, usage and dashboard endpoints.