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:
- resolves its config through the customer’s layer, between the global config and the project’s own;
- records the customer it ran under, so history stays attributed even if the project is reassigned later;
- is priced with that customer’s pricing, and shows up under the customer in the control plane’s filters and rollups.
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
- The project still wins on any key the customer hasn’t locked.
- A customer locks keys with its own
[policy].lock— e.g.["default_provider"], so a repo’s config can’t move the customer’s runs onto another account. It can’t unlock anything the global[policy].locknames. A lock covers config keys only: an agent whose frontmatter names its ownprovider:still runs on it. - Arrays replace rather than merge: a customer that declares
[[scm.rules]]replaces the global rules for its projects. - A run uses the customer of the nearest assigned ancestor of its working directory, so subdirectories inherit their project’s customer and a repo needs no
.rupu/of its own. - A project assigned to a customer that no longer exists, or a customer layer that doesn’t parse, fails the run — it never silently falls back to the global config.
# ~/.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 disk | Means | Attributed as |
|---|---|---|
"customer": "acme" | recorded slug | acme |
"customer": null | recorded no customer | none — even if the project has been assigned since |
no customer key | legacy, written before customers existed | the 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.
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 scope | Shows 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
- Customers page (
/customers): one row per customer with its rollup — projects, runs, spend, open findings, last activity — over a 7d / 30d / all range, with search, an Active / Archived / All view, and create. - Customer detail: Overview, Projects, Runs, Findings, Usage and Config tabs; edit, archive, delete, and assign or unassign projects. The Config tab edits the customer layer, showing each field as inherited from global, owned by the customer (with a “Lock for projects” switch), or pinned by the global lock.
- Projects: a customer column, and an “Assign to customer” menu in the project header that previews the effect (which keys the customer locks, which default applies) before you commit.
- Runs: a customer column and chip; derived attributions are marked as such.
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.