Control plane
A local web app for rupu — launch runs, watch them stream live, author agents and workflows, and review findings and coverage, all in one browser tab.
A web command center
The control plane is rupu's window onto everything it's doing on this
host. One command opens a browser UI where you can observe
every run, session, and event as it happens; control what
runs and approve the steps that ask for it; and author
your agents and workflows — without leaving the page. It's a thin web
adapter over the same on-disk stores the CLI uses (your runs, agents,
workflows, sessions, and the coverage ledger under
~/.rupu/), so the browser and the terminal are
always looking at the same reality. Anything the CLI started — a
rupu run, a cron trigger, an autoflow — shows up
in the browser, and anything the browser starts is an ordinary run the CLI
can see.
Start it with one command:
# serve the control plane and open it in your browser $ rupu cp serve # ➜ rupu Control Plane → http://127.0.0.1:7878
By default it binds to 127.0.0.1:7878 and opens
the URL in your browser when you run it interactively; the URL is always
printed.
| Flag | Default | Meaning |
|---|---|---|
--bind <addr> | 127.0.0.1:7878 | Address and port to listen on. |
--token <token> | none | Require the token on every /api/* route — a bearer header, or the browser cookie the printed link sets (see Authentication). |
--no-open | off | Don't open the URL in a browser. |
--no-web | off | Serve /api and /mcp only, no web UI (implies --no-open). |
--no-mcp | off | Don't serve /mcp. Refused together with --no-web. |
--tools <grant> | every tool | Narrow the MCP to these tools, operator tools and wait included (exact names, ns.*, *; comma-separated). Refused with --no-mcp; a bad grant fails before binding. |
--max-open-files <N> | — | Raise the process's open-file limit (overrides RUPU_MAX_OPEN_FILES and [runtime].max_open_files). |
# bind a custom port and require a token on /api/* $ rupu cp serve --bind 127.0.0.1:9000 --token "$RUPU_CP_TOKEN" --no-open # try it against an empty store $ RUPU_HOME=$(mktemp -d) rupu cp serve --bind 127.0.0.1:9100 --no-open
Serve modes
Besides the web UI and /api, the server serves an
MCP endpoint at /mcp so Claude Code, Cursor and
other MCP clients can operate rupu (see The MCP endpoint).
The two --no-* flags choose what is served:
| Command | Serves |
|---|---|
rupu cp serve | web UI + /api + /mcp |
rupu cp serve --no-web | /api + /mcp, no web UI (implies --no-open) |
rupu cp serve --no-mcp | web UI + /api, no /mcp |
--no-web --no-mcp is refused (nothing to serve),
and so is --tools with
--no-mcp. At startup the server prints the MCP
URL and the command that connects Claude Code to it.
One server per port. If the bind address is taken,
cp serve exits non-zero at once; when the
occupant is another rupu control plane, the error names its PID
(/healthz answers ok
with x-rupu-pid and
x-rupu-version headers).
Restart after upgrading. cp serve
is a long-running daemon: installing a new rupu
binary does not change a running server. bind
and token are restart-only settings, and so are
the switches and intervals of its background loops (see
Background loops).
Authentication
Without --token the API is open to anything
that can reach the bind address. That is the intended posture on
127.0.0.1. Do not bind a non-loopback
address without a token. With --token,
every /api/* route needs the token, in one of
two forms:
-
Scripts and peer control planes send
Authorization: Bearer <token>. -
Browsers sign in once. The URL
cp serveprints (and opens) carries?token=<token>. Opening it sets a cookie and redirects to the same page without the parameter, so the token leaves the address bar and the history at once. From then on the web UI's requests, live streams included, carry the cookie. To sign another browser in, openhttp://<host>:<port>/?token=<token>there. A wrong token in that link is a 401 and sets nothing.
The cookie is HttpOnly,
SameSite=Strict, lasts 30 days, and is named
rupu_cp_token_<port>, so control planes on
different ports never overwrite each other. Its value is derived from the
token, not the token itself, and it is refused as a bearer: browsers send
localhost cookies to every port, so another
local server could read it, but it never learns the token. A write made
with the cookie must also carry an Origin
naming this server, so a page served from another origin (another
localhost port included) can't drive runs with
it. Behind a reverse proxy, preserve the Host
header: the cookie name and the origin check both read it. Restarting with
a different token makes the old cookie a 401 until the browser opens the
new link.
/healthz, the node WebSocket and the static UI
shell stay open; they hold no data, since everything the UI shows comes
from /api/*. The server log prints the URL
without the token, and Settings only ever shows whether a token is set.
/mcp sits behind the same token. See the
HTTP API
for the wire details.
rupu binary — there's nothing to install or
build. rupu cp serve serves the embedded app and
reads your existing file-based stores directly; there is no database.
Background loops
Besides the HTTP server, cp serve runs background
work in-process, so autoflows, triggers and gate timeouts work without an
external cron:
| Loop | What it does | Keys |
|---|---|---|
| Resume worker | Every 4 s, claims runs the UI marked for resume (an approved gate, a resume of a paused run) and spawns a detached rupu workflow approve / rupu workflow resume --if-unfinished child for each. | always on |
| Autoflow reconcile | The same entry point as rupu autoflow tick: issue and PR entity autoflows. | [cp].autoflow_reconcile_enabled, [cp].autoflow_reconcile_interval_secs |
| Cron tick | rupu cron tick's core: cron- and polled-event-triggered workflows. | [cp].cron_tick_enabled, [cp].cron_tick_interval_secs |
| Gate sweep | Fires overdue gates' on_timeout routing (reject runs the on_reject chain; approve spawns a detached workflow approve) and reaps orphaned Running/Pending runs whose recorded runner_pid is dead, marking them Failed. | [cp].gate_sweep_enabled, [cp].gate_sweep_interval_secs |
| Agentiflow reaper | On the gate-sweep tick: finalizes an agentiflow run whose coordinator died. | [agentiflow].reaper_enabled |
| ASN refresh | On the gate-sweep tick: refreshes the netflow ASN table when missing or stale. | [netflow].asn_auto_refresh |
| Bucket poller | Every 15 s, reads results from bucket-transport hosts. | always on (no-op without bucket hosts) |
All enabled flags default to true, all intervals
to 60 s. The gate-sweep tick runs if any of its three duties is
enabled; each duty is then checked on its own, so turning off gate timeouts
doesn't stop the reaper. See
Configuration.
# ~/.rupu/config.toml [cp] cron_tick_interval_secs = 30 # tick triggers twice a minute gate_sweep_enabled = false # no gate timeouts; reaper and ASN refresh still run
Observe
The observability surface is the heart of the control plane — watch everything rupu is doing, live, in the browser.
-
Overview. The landing page, organised around triage: a
Needs you deck of what is waiting on you, counts as a divided
strip of type (label over number, the attention value in red), fleet host
cards, and token spend over time per model. The charts are
living charts: thin stacked areas that split the measure
(outcomes = completed/failed; throughput = by trigger), with a real
baseline, a direct-labelled peak and a crosshair with a tooltip; they
draw in and keep a breathing now pulse, with a
prefers-reduced-motionfallback, and their colours are validated colourblind-safe. - Runs. One run list per kind, and a live run view that paints the workflow's real graph — forks, joins, gates, actions and fan-out units, rather than everything flattening into one line — with each step's status updating in place, the focused step's transcript as it works, and Events, Findings, Messages and Network tabs plus the run's controls. The Events tab is the same event rail as Live Events, scoped to the run (an agentiflow's Events tab too). Runs, steps, units and sub-agents carry their codenames, tinted per crew.
-
Live Events. A page for everything happening across the
host: a vitals strip (active runs, runs awaiting you, findings by severity,
events per minute, errors), the Needs you deck, a newest-first
rail that merges the live event firehose with newly filed findings —
each with the run, step and codename it came from — and a per-project
roster. The query line filters the stream
(
group:error,workflow:nightly,agent:recon since:1h); the server filters the event history by it, so it reaches runs the unfiltered stream never loaded. The deck holds open gates, unseen failures and critical or high findings from the last day, most urgent first; a gate in it can be approved or rejected in place. A source that fails to load reads as idle, never as made-up activity. - Sessions. Every session as a chat view, with its transcript streaming token by token and tool calls and thinking in position.
- Usage. Spend over time, pivoted by model, provider, agent, workflow, host or project, with an outlier panel for runs that cost far more than their workflow usually does and a banner that counts unpriced spend instead of hiding it. Usage comes from a per-run ledger written as tokens are spent, so in-flight steps, retried fan-out units, sub-agents and compaction calls all count, and a running run's or session's totals move while you watch.
- Coverage & Findings. The coverage status of each project and the findings filed against it, read from the coverage ledger (see Findings below).
-
Agentiflows. A list of
agentiflow runs and a
detail page with Flow, Assets, Findings,
Messages, Transcript and Events tabs. The Flow
tab draws the lead's rounds as a spine with each round's units branching
off it; earlier rounds collapse to a one-line summary, and a workflow unit
links to its own run page and expands in place to list its steps with
their status. While the run's coordinator is alive, the Messages tab
reaches the lead, the same channel as
rupu agentiflow send; it is refused once the coordinator is gone. A Security → Assets page shows the asset inventory across every project. -
Network. The netflow
explorer at run, project and global scope: connections made by agents'
tools and their subprocesses, with ASN enrichment. The server keeps a
resident index of the netflow ledgers
(
[netflow].cp_index_budget_mb); its state is atGET /api/netflow/index.
Live updates arrive over server-sent events: a global firehose behind Live
Events, the new-run pill and status patching, and per-run streams for each
run view. The firehose covers orchestrator runs (workflows, autoflows); a
standalone rupu run never enters the run store
and so doesn't appear on it. Runs the control plane started stream straight
from the executor; runs started elsewhere (the CLI, a cron trigger, an
autoflow) are tailed from their on-disk event log — so the same live view
works no matter what kicked the run off. A run's stream resumes where it
left off after a dropped connection rather than replaying the run, and
closes once the run has settled; the run page keeps checking every 15 s,
so a retried run streams back in.
The query line
Every list — Workflow Runs, Agent Runs, Sessions, Findings, Live Events and
a project's Runs and Sessions tabs — has one query line, sent to the server
as ?q= and evaluated there before the page is
cut. The grammar is the findings grammar: a term's values OR, terms AND,
- negates, a bare word searches the list's text
fields, comparisons are case-insensitive, and time fields take
30m, 12h,
7d, 2w, a UTC date or
an RFC 3339 time.
# workflow runs: failed or cancelled, not the nightly, last week status:failed,cancelled -workflow:nightly since:7d "two words" # agent runs on one host; sessions still active host:sutajio agent:recon is:running is:active model:sonnet
| List | Fields |
|---|---|
| Workflow runs | status, workflow, id, codename, trigger (manual / cron / event), host, customer, since, until, is:archived (lists the archive; local only) |
| Agent runs | status, agent, provider, model, is (running / completed / failed), source (standalone / session), session, trigger, codename, id, host, customer, since, until |
| Sessions | status (idle / running / failed / stopped), agent, model, provider, codename, id, host, customer, project, is:active / is:archived, since, until |
| Events | group (activity / await / warning / error), type, run, project, workflow, agent, step, codename, provider, model, severity, message, since, until |
| Findings | The findings query language — severity>=high tag:needs-poc -tag:false-positive "sql injection"; see Findings. |
A query that doesn't parse is flagged on the line: the server answers it
with the token and its span rather than a page. Facets —
counts of each facet field's values over the list's scope before the query
— come back with the rows (?facets=1). A
host: term asks only the hosts it names; with
remote hosts, their rows are filtered on this server, and a host whose rows
can't report a field the query names (an older rupu) is left out and
named in a one-line host alert rather than quietly shown unfiltered. A
list's implicit defaults (the running statuses on the run lists, Agent
Runs' source:standalone, Sessions'
is:active) apply only while the query doesn't
name that key.
Token flow
A run page carries a Token flow chart: tokens per model call across the whole run, in order, with the step each call belongs to. Selecting a span of it narrows the page — the Transcript, Events, Findings and Messages tabs show only what happened in those calls — because every point knows which transcript it is in, which turn of that transcript, and when the call finished (from the usage ledger; runs that predate the ledger have no time). A session page draws the same chart over its turns in its Session details disclosure. Older remote hosts send the points without the position fields, so a remote run's chart can't narrow the page.
Transcripts
Every transcript surface — the standalone /transcript
page, a run's focused step, a session's turns, an agentiflow's units — renders
through the same reader: one continuous reading column with a spine down
the left, read top to bottom like a document rather than nested boxes. Turns
are numbered markers on the spine (the live turn's marker glows), and each
event carries a gutter glyph keyed to its kind: an operator message, thinking
(shown in full, quiet, collapsible), what the assistant said (the loudest
text), a tool call as one scannable row — verb, target, a result chip, its
duration — that expands its input and output inline, a
bash command rendered as a terminal line with its
exit code, and a finding as a severity card, the one place colour lives.
Skill loads, notices, gate pauses and decisions, context compaction, the
seed prompt, and the outcome and recovery events written when a reply needed
recovering are quiet one-line rows. A line the viewer doesn't recognise is
shown as raw data with an unparsed badge rather than dropped, so a
transcript from a newer rupu never silently loses content.
A long run is navigated from a metrics strip (turns, tool calls, commands,
findings, tokens), sticky filter chips (All · Said · Thinking · Commands ·
Findings, with per-kind counts), Expand-all / Collapse-all, and a thin
clickable outline rail of turn, finding and command tick marks with the
current position highlighted. In a run transcript, an
ast_grep tool call's matches open a line-numbered
source slice around the match and a syntax-tree viewer for that region,
read from the run's own working directory (or its host); a file that's gone
or outside the workspace shows the reason instead.
Messages
A run with a message space — a workflow run, a session, an agentiflow, or a standalone run's transcript page — has a Messages tab over the space's log: every message the participants exchanged (posts, directs, broadcasts, directives and retractions), who it was delivered to and who has seen it, and the participants; a local live run's log is followed as it grows. Messages also appear as bubbles in transcripts, cross-linked to the tab.
The tab's composer sends as the operator, the same channel
as rupu message send: pick a recipient — a
participant, a codename, a role (role:<agent>),
a step (step:<id>), an agentiflow's
lead or everyone — and mark it urgent to put it
ahead of everything else (and end an agentiflow lead's round early). The
message reaches its recipients before their next model call, framed as an
instruction from the human running the run. A run that isn't running takes
no message: the composer is disabled with the server's reason. A send is
shown as pending until its id reaches the log, never optimistically. Remote
hosts answer over HTTP and SSH when their rupu advertises messages; tunnel
and bucket hosts can't carry them, and the tab says so.
Findings
Each finding has its own page at /findings/<id>.
A finding recorded as a full report opens with a section rail (description,
impact, location, root cause, call chain, evidence, proof-of-concept
artifacts, replication, remediation, patch, CI/CD and regression commands,
references), a ledger of ownership and risk fields, and a completeness meter
(n/11) that names the gaps. Every evidence claim
carries a badge saying whether the file still matches what the agent saw
(current, changed, missing), and the artifact browser
previews text and images and pulls a remote host's artifacts on first view.
Reports export to Markdown, HTML or PDF. When a finding's project has a
known remote, its rows carry a View on repository link to the file
and line range on the forge's web UI.
The findings tables triage in place: expand a row for root cause, attack
path, owner and completeness; filter with the query line
(severity>=high tag:needs-poc -tag:false-positive "sql injection");
and tag findings one at a time from a finding's page or in bulk from a table
selection, with every change kept in a per-project tag history. See
Findings for the report
format, the query language and tagging.
Many hosts, loaded host by host
With remote hosts
registered, the Activity tables, the Usage headline and the ⌘K palette's
runs and sessions ask each host on its own (?host=<id>)
and merge the answers as they arrive. A fast host paints at once; a slow one
fills in when it answers; each request is bounded at 45 s. A host that
doesn't answer is shown as:
| Shown | Status | Meaning |
|---|---|---|
| offline | 502 | The host couldn't be reached or failed (SSH down, connection refused, an error). |
| unavailable | 501 | The host answered but can't serve this listing — typically an older rupu that lacks the command. |
| (row dropped) | 404 | The host id is no longer registered. |
A per-host freshness strip shows who has answered. The hosts list serves each host's last health probe from a cache (fresh for 15 s; a stale answer is served while one background refresh runs), so opening a page never waits on a fresh SSH probe, and SSH listings share a 5 s single-flight cache that any mutation clears. Pages that read history are served from in-process caches re-checked against the files on every request, so opening a page doesn't re-read every run ever recorded.
Control
Beyond watching, the control plane drives rupu from the browser. Because runs are detached processes, a control action's 200 means recorded, not done: the run's own status, arriving on the live stream, is what confirms it.
-
Launch runs. A Run button on a workflow or agent
(list row, detail page, Library) opens the launcher: target, inputs or
prompt, permission mode, skills to add to the run, and — with remote hosts
registered — the host. The launch pins the definition's scope (global or
one project's
.rupu/), so the run uses exactly that file. Before you launch, a billing panel shows the customer the run will be attributed to and the provider, fallback and SCM accounts it will authenticate as, resolved the same way the launch resolves them — a dangling customer assignment blocks the launch here exactly as it would fail it. A launch returns as soon as the run is spawned: the run is a detached subprocess owned by nobody, so closing the browser or restartingcp servedoesn't stop it. -
Approve & reject gated steps. When a run pauses at a
gate awaiting approval, approve or reject it in place — per gate, when a
graph workflow has several parked at once. Approve records the decision
and a resume marker; the resume worker inside
rupu cp serveclaims the marked run and spawns a detachedrupu workflow approve(or hands the decision to the run's still-live runner). Reject prunes only what is reachable solely through that gate on a DAG run and runs itson_rejectchain; a linear run finalizes. A gate already decided, or a run no longer awaiting approval, is a 409. -
Cancel & pause. Cancel sends the recorded runner a
SIGTERM(a run parked at a single gate with no live runner is cancelled by rejecting it), and pause leaves a marker the runner picks up at its next safe boundary. Only workflow runs pause: a standalone agent run has no safe boundary, so pausing one is refused (409). A paused run shows a Resume button; the resume worker spawnsrupu workflow resume --if-unfinished, which refuses a run that finished in the meantime rather than retry it, and continues interrupted steps from their transcripts rather than restarting them. Controls carry the host for a run on a remote host and answer as they would locally (404 no such run, 409 refused, 501 the transport can't do it, 502 the host can't be reached). -
Retry. A failed, rejected or cancelled workflow run has a
Retry control in its header. It records the retry on the run and
spawns a detached
rupu workflow resume— in the run's own launch mode unless you pick another, so areadonlyrun retriesreadonly— continuing interrupted steps andfor_eachunits, or starting them over with restart interrupted. A run leftRunningby a dead runner is marked failed first and retried. Local runs only; a standalone agent run is continued instead. -
Continue. An ended agent run (a standalone
rupu runor a sub-agent) has a Continue composer under its transcript: an optional prompt and a Model & provider disclosure showing the original values. It spawns a detachedrupu run --continueunder a new run id, in the directory the run was launched from, restoring every flag the run was launched with; each field you set replaces only its own. With a prompt, any ended run carries on — an interrupted run gets the interruption note then the prompt, a finished run gets it as a new turn. Without one, a failed run continues only with a new model or provider, and a finished run answers with its recorded answer. On success the page links to the new run; a run that is still running is a 409. -
Drive sessions. Send a prompt to a live session and
watch the turn stream back as a chat. A message that starts with
/opens a menu of the installed skills to invoke in that turn. Compact compacts the stored conversation now (a popover takes the token window, defaulting to the session's own); while the compaction runs, send, stop, archive and delete answer 409, and the conversation shows how it ended. Stop (confirmed first) ends the session's worker and running turn; a later send reactivates it. Sending queues the turn for the session's worker, a long-lived daemon: upgradingrupudoesn't change a running session until it is stopped. - Message a run. The Messages tab's composer reaches any participant of a live run as the operator (see Messages).
- Autoflows. Runs → Autoflows is one table, shaped like Workflow Runs: autoflow-launched runs newest first (workflow, issue with a link to the tracker and the PR it opened, trigger, host, usage, duration), plus a failed row for each attempt that never produced a recorded run, with its error. A problems strip above the table names failures with no run to show — a repo whose autoflow config fails to load, a worker whose cycle crashed — with the latest error and how long it has been happening. Claim actions (requeue, release) live on the run page. The Library's Autoflows tab lists the workflows that declare triggers, each with an enable/disable toggle; see Autoflows.
-
Archive, restore & delete. Tidy up the workspace by
archiving finished runs and sessions (moving them out of
the active lists without losing them), restoring them
when you need them back, or deleting them for good.
Archiving never deletes anything by itself: archived items are removed
only when you prune them with
rupu session prune,rupu transcript pruneorrupu cleanup. The[storage]section'sarchived_session_retentionandarchived_transcript_retention(e.g.30d) are those commands' default age cutoffs.
Author
You can edit your agents (.md) and workflows
(.yaml) right in the browser. Agents open in a
code editor; workflows open in the visual workflow editor.
The workflow editor is a single screen: a drag-and-drop DAG on top and the live YAML below, kept in lock-step. Drag node cards from a palette onto the canvas, wire steps left→right exactly like the Runs graph, and watch the YAML rewrite itself underneath — or type in the YAML and watch the graph reconcile as you go. YAML stays the source of truth: the graph is a projection of it, and every save passes through the same validation the orchestrator already applies.
Every step kind has its own shape. The editor paints
nodes as flowchart silhouettes rather than identical cards, so a workflow
still reads when it's zoomed out far enough to see whole: a
branch is a decision diamond, an
action connector a parallelogram, an approval
gate the trapezoid of a manual operation, a
for_each a hexagon, and
parallel / panel keep
growing rectangular bodies for their sub-steps. Connection handles anchor to
the silhouette itself, so nodes drag and wire by their real shape — and
shape carries the kind independently of colour.
A Blocks palette with its own tab. The rail is one tab bar — Blocks · Step · Settings · Reference — so the palette and the editor no longer compete for height. Blocks sits on a dense chip grid with a filter across both the step kinds and the whole connector catalog, each chip previewing the silhouette it will place. Selecting a chip shows what it does, its required fields, and a short example before you commit it; drag-to-place or Add to canvas drops the node and jumps you straight to its form.
Per-kind authoring forms. The Step tab renders the form the
selected node actually needs. Connector (action)
steps get a form generated from the tool catalog's own JSON schema — required
parameters marked, values that round-trip whether they're numbers, booleans,
lists, or {{ … }}
expressions — so new tools appear in the editor with no editor changes.
Approval gates get editors for their notify hooks and their on-reject cleanup
steps; panels and branches get their sub-schemas, with constrained choices
(severity thresholds, minimums) instead of free text you can typo into a
failed run. Any step that runs an agent has skills
and preload_skills fields.
One visual language, author and run. The live run graph paints from the same kind palette as the editor, with connectors animating along the active path — kind colour carries identity while status stays the overlay, so a running step pulses, a gate awaiting you glows amber, and a failure still reads as a failure. Watching a run and editing it look like the same thing.
Every expression field — a step's prompt,
when, a branch's
condition, or a panel's subject — gets syntax
highlighting and
context-aware autocomplete. Inside
{{ … }} it suggests the right vocabulary
for where you are: declared inputs, the outputs
of steps that run before this one (never a forward reference),
loop locals like item inside a
for_each, panel findings, and the available
filters. A searchable expression reference panel surfaces the whole
vocabulary and inserts at the cursor. Validity is checked live — at the
field, on the node, and in a header badge — so mistakes show up before you
ever save. New workflow starts one from YAML, or has a
model generate a draft from a description.
Agent Builder. New agent opens a full-page card
composer: drag field cards (identity, prompt, model, tools, permission,
reasoning, output and more) from a palette onto a canvas and watch the
.md file build in a live preview beside it. A
Cards · Raw · AI switch lets you edit the file
directly or describe the agent and have a model draft it. Its
Skills card edits the agent's skills:
and preloadSkills: with the installed names as
suggestions, and the control plane checks them the way
rupu agent validate does: an agent whose skills
aren't all installed shows This agent can't be launched with the
reason and the install hint. Saving such an agent is allowed, so it can be
written before its skills are installed; only a malformed entry is refused.
Library: agents, workflows, autoflows, skills
The Library holds every definition the control plane can see, global or
project-scoped, with a Run on each. Its Skills
tab lists the installed Agent Skills:
the global layer, plus the picked project's layer, like
rupu skill list. Each row shows its layer, status,
source and description; an invalid skill shows its error, and a layer that
can't be read is left out with a warning while the other layer still lists.
A skill's page shows its description, compatibility, license,
allowed-tools with the rupu tools they map to
(informational, never a grant), metadata, files, body, content hash and
install record. Installing and updating skills stay CLI operations
(rupu skill install / update).
Settings & project config
You can edit rupu's configuration from the browser. A global
Settings page writes the global
~/.rupu/config.toml, and a Config
tab on each Project detail view writes that project's
.rupu/config.toml. Config is layered
global → customer → project (a locked key in a higher layer wins; see Configuration), and the control plane
shows the effective value for every key together with its
provenance — which layer won.
Each surface is hybrid: a typed Form tab with per-field
validation, plus a raw TOML tab with validate-on-save, both
writing the same file. Alongside them sit a policy view
(what's locked) and a runtime status view — the latter
shows restart-required settings such as bind and
token read-only (the token masked). Saving the
global config reports which of the start-time-only keys you changed
(the [cp] background-loop toggles and
intervals, agentiflow.reaper_enabled,
netflow.asn_auto_refresh): those apply after
cp serve restarts, everything else at once.
Policy enforcement. An admin can lock
specific settings at the global level so projects — and the UI — can't
override them. Locked keys are pinned to the global value wherever config is
resolved (CLI, agent runs, cron, and the control plane alike), and appear
read-only on the project Config tab. Locks are declared as dotted key paths
in a [policy] section of the global config:
# ~/.rupu/config.toml — lock keys so projects can't override them [policy] lock = ["permission_mode", "autoflow.max_active", "cp.max_workspace_bytes"] # control-plane runtime settings live in their own section [cp] max_workspace_bytes = 104857600
Writes go through validated config endpoints — GET the effective config with
provenance, and PUT the global config, a project's config, or the policy
lock list. Every write is validated, backed up, and written atomically;
project-config paths are confined under the project's
.rupu/ (no path traversal), and writes require
the full rupu cp serve runtime.
Models. Settings has a Models tab listing every
model each configured provider offers, with its input limit, output cap and
where each number came from (your
[[providers.X.models]] entry or the provider's
own model list, cached for an hour). Refetch pulls a fresh list from
one provider or all of them. Runs resolve their limits from the same
catalog — an agent's own pins win — so the context gauge and compaction use
the numbers you see here.
Customers
If you work for several clients, a customer scope picker in the header narrows the whole UI to one of them — all, no customer, or a named customer. Runs, sessions, workflows, agents, findings, projects and usage follow it, and the choice is remembered in the browser. A page that can't be filtered says so rather than quietly showing everything. The Customers page manages customers and their project assignments, shows per-customer rollups priced with that customer's own pricing, and edits each customer's config layer. Before a launch, the launcher shows which customer the run will be attributed to and which provider, fallback and SCM accounts it will authenticate as — resolved exactly the way the launch itself resolves them. See Customers.
Multi-host
One control plane can drive rupu running on other machines, not just the one it's on. Register a remote host — over HTTP, SSH, a dial-home tunnel, or an object-store bucket — and launch runs, agents, and sessions on a chosen host, observe and control them, all from one browser, with a Fleet → Hosts surface and host filtering on run lists. See Multi-host for setup and the transport roadmap.
The MCP endpoint
rupu cp serve also serves an
MCP endpoint at /mcp
(streamable HTTP), behind the same token as /api.
An MCP client such as Claude Code, Cursor or Claude Desktop connects to it
and operates rupu — lists and launches workflows and agents, watches runs,
approves gates, drives sessions and messages a live run — with one operator
tool per control-plane route, a wait tool that
long-polls for a run to settle, and rupu's own tool catalog (files, shell,
SCM, issues, findings) as plain tools. It adds no permission layer of its
own: what runs is decided by the MCP client's harness, so treat the bearer
token like a key to the whole control plane and narrow what is served
with --tools. The old
rupu mcp serve is deprecated and now runs
rupu cp serve --no-web. Connecting a client,
the tools, resources and prompts are in
MCP & tools.
# connect Claude Code (loopback, no token) $ claude mcp add --transport http rupu http://127.0.0.1:7878/mcp
UI tour
A page-by-page map of the web app, with the route each page lives at. Every route can be bookmarked or shared, and the ⌘K command palette jumps to any page, run or session.
Two navigation shells
The app ships two sidebars over the same pages. The default (v1)
shell lists every page in grouped sections — Runs, Security, Build, Fleet.
The newer v2 shell folds them into seven destinations
(/overview, /activity,
/projects, /security,
/library, /fleet, plus
Customers), each a tab bar whose active tab sits in
?tab=. Old links keep working: under v2,
/agents redirects to
/library?tab=agents,
/hosts to /fleet,
/findings?q=… to
/security?tab=findings&q=…, and so
on. Pick the shell in the global config; it is read on every page load, so
a browser refresh applies it, no restart needed:
# opt into the v2 navigation shell $ rupu config set ui.cp.shell v2
| v2 destination | Tabs |
|---|---|
/overview | The dashboard. |
/activity | agents · workflows · autoflows · sessions (run lists) |
/security | findings · coverage · catalog |
/library | agents · workflows · autoflows · skills (definitions) |
/fleet | hosts · workers |
Runs and transcripts
| Route | What it shows |
|---|---|
/runs/agents, /runs/workflows, /runs/autoflows, /runs/agentiflows | One run list per kind, on the query line (host, status, time window, customer). /runs redirects. |
/runs/<id> | The live run view: graph, Token flow, the focused step's transcript, and the Events, Findings, Messages and Network tabs, with the run's controls. |
/transcript?path=… | A transcript on its own page, for agent runs, sessions and sub-agents that have no workflow graph. It streams while the run is live, and resolves a remote host's transcript when the link carries host and run. |
/sessions, /sessions/<id> | Session list and chat view (Conversation and Messages tabs). |
/agentiflows/<id> | Agentiflow detail (see Observe above). |
/events | Live Events: the vitals strip, the Needs-you deck and the event rail. |
/usage | Spend over time and the outlier panel. |
/netflow | The global network explorer. With no since: or until: in its query it opens on the shorter of the last hour and the latest run's span. |
Library: agents, workflows, autoflows, skills
| Route | What it shows |
|---|---|
/agents | Every agent definition the control plane can see, global or project-scoped, with its last run. New agent opens the builder. |
/agents/new | The Agent Builder (see Author above). Saving opens the new agent's page. |
/agents/<name> | Provider, model and limits up top, the highlighted definition file below, with Edit, Delete, and Run (which opens the launcher), plus the agent's recent runs. |
/workflows | Workflow definitions. New workflow starts one from YAML, or has a model generate it from a description. |
/workflows/<name> | The visual workflow editor (see Author above), with a validity badge and Save, Revert, Delete and Run. |
/autoflows | Workflows that declare autoflow triggers, each with a toggle to enable or disable it. |
/skills, /skills/<layer>/<name> | The installed skills (global and project layers) and a skill's page (see Library above). |
[cp].agent_authoring_ui
and [cp].workflow_editor_ui keys picked between
a classic and a newer UI; the classic UIs are gone, and the keys are now
ignored. rupu logs a warning if your config still sets them. Delete them.
Projects
/projects lists every project this control plane
tracks, with its customer. Each project's page has an identity header with
rollup tiles and a tab per route:
| Route | Tab |
|---|---|
/projects/<id> | Overview: a usage timeline, recent runs, coverage, sessions, and a summary of the project's definitions. |
/projects/<id>/runs | Runs started in this project, on the query line. |
/projects/<id>/findings | Findings filed against the project. |
/projects/<id>/code | Code: a file tree and syntax-highlighted source viewer for the project's checkout, with findings shown inline at their lines and a link to the repository. |
/projects/<id>/sessions | Sessions in this project, on the query line. |
/projects/<id>/coverage | Coverage for the project's targets. |
/projects/<id>/network | Network: the netflow explorer scoped to this project. |
/projects/<id>/config | Config: the project's .rupu/config.toml (see Settings & project config). |
/projects/<id>/definitions | The agents, workflows and autoflows this project can use: global definitions merged with its own .rupu/ ones, each badged project or global. Linked from the Overview tab. |
Security: findings, coverage, assets
| Route | What it shows |
|---|---|
/findings, /findings/<id> | The findings table and report page (see Findings above). |
/coverage | Coverage status for every target. |
/coverage/<target> | Overview: findings, files touched (by kind of touch), and how many concerns were assessed clean, with a finding, examined, or not applicable. |
/coverage/<target>/catalog | The concern catalog that applied to the target. |
/coverage/<target>/audit | Per-concern coverage: which in-scope files were assessed and which weren't, plus cross-model and serendipitous findings. |
/coverage/<target>/gap | Only the concerns with unassessed files, filterable by severity and file. |
/coverage/<target>/diff | Two runs' contributions to the target side by side (previous vs latest by default). |
/coverage/templates | The bundled concern templates, independent of any target. |
/assets | Security → Assets: the asset inventory across every project: hosts and services for network engagements, sites and routes for web, files for code. |
Fleet: hosts and workers
| Route | What it shows |
|---|---|
/hosts | Registered hosts and their health. |
/hosts/<id> | One host's details and health, plus the workflow runs on it. |
/workers | Read-only list of workers: the machine-and-identity pairs that have run something (a rupu workflow run or a session turn registers one), with their capabilities, recent activity and when each was last seen. A worker is not a running process; one that hasn't checked in for a while shows as stale. |
Settings
/settings has typed tabs for
General, Providers, Models, Autoflow,
SCM / Issues, Pricing and CP-Runtime, then
Raw (the global config.toml, read-only until
you click Edit), Policy (every key with a lock checkbox), and
Runtime status. Each typed field shows its effective value, which layer
it came from, and a lock toggle. The control plane's own token is never
shown or editable, only whether one is set.
Customers
/customers and
/customers/<slug> (with a tab per
sub-route) manage customers; see Customers above.