White-label walkthrough
This page builds, start to finish, a distributable console for your team:
one wheel — call it mycorp-console — that an operator installs from your
private index and launches into a console that is branded as yours and
pre-configured: your LLM gateway active, hosts, groups, profiles,
filters, and starter runbooks already in place, and your theme on the
window. Nothing on this page modifies runspec-console itself — it's all
standard packaging around it.
The moving parts:
| Mechanism | What it does |
|---|---|
A custom LLM adapter entry point (runspec_console.adapters) |
Makes your gateway a selectable provider — and, when active, the console brands its window title, docs link, and update check from your package's metadata |
A config seed entry point (runspec_console.config_seed) |
Ships baseline config files as package data, applied on first launch and re-applied on upgrade — works fully offline |
[console.theme] (delivered by the seed) |
Accent colour, brand name, icon, watermark, title-bar tint |
[project.scripts] |
A branded launcher command |
A Config Sync repo can layer on top of the seed for day-to-day config changes; the seed is the offline floor underneath it (seed < sync < the operator's local edits).
1. Project skeleton
Start from the example plugin in the runspec repo —
packages/python/runspec-console/examples/console-adapter-plugin/ — and
rename. Target layout:
mycorp-console/
├── pyproject.toml
└── mycorp_console/
├── __init__.py
├── adapter.py # your LLM provider
├── app.ico # your icon (package data)
└── config_seed/ # baseline config (package data)
├── config.toml
├── runspec_hosts.toml
├── runspec_groups.toml
├── runspec_profiles.toml
├── runspec_filters.toml
└── runspec_runbooks.toml
The pyproject.toml wires everything:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mycorp-console"
version = "1.0.0"
description = "MyCorp operations console"
requires-python = ">=3.11"
dependencies = [
"runspec-console[credentials,git-sync,schedule,notifications]>=0.174.0",
"httpx>=0.27", # your gateway client's deps
]
# Branding is derived from THIS package's metadata when your provider is active:
# window title from the distribution name, docs link from these URLs, and the
# update check tracks this package against your index.
[project.urls]
Documentation = "https://docs.mycorp.example/console"
Homepage = "https://mycorp.example"
# A branded launcher alongside `runspec-console` — same console underneath.
[project.scripts]
mycorp-console = "runspec_console.app:main"
# Your gateway becomes a selectable provider: `provider = "mycorp"`.
[project.entry-points."runspec_console.adapters"]
mycorp = "mycorp_console.adapter:MyCorpAdapter"
# Your bundled baseline config: a package with a config_seed/ data directory.
[project.entry-points."runspec_console.config_seed"]
mycorp = "mycorp_console"
[tool.hatch.build.targets.wheel]
packages = ["mycorp_console"]
Make sure the data files ship in the wheel (hatchling includes package
directories by default; verify config_seed/ and app.ico appear in the
built wheel).
2. The adapter
The adapter is what makes the console yours: branding follows the active
[llm] provider, and an entry-point provider resolves to the distribution
that advertises it. Two routes:
- Your gateway speaks a LangServe-ish
/invokedialect (most corporate LLM gateways): subclass the bundledLangServeAdapterand override the request/response mapping — often just a few lines. - Anything else: implement the
ModelAdapterbase class directly.
The example plugin's adapter.py shows both, and the console ships a
conformance test kit — wire it into your CI:
from runspec_console.adapters.testing import (
assert_adapter_contract, assert_chat_response, assert_tool_turn,
)
The full adapter API, streaming contract, and the [llm] keys your adapter
receives are documented in Custom model adapters.
3. The config seed
Everything in config_seed/ must be one of the known config files (the same
set Config Sync manages). What to put in each:
config.toml — the heart of the seed:
[llm]
provider = "mycorp" # your adapter, active out of the box
base_url = "https://llm-gw.mycorp.example"
models = ["mycorp-sonnet", "mycorp-haiku"]
model = "mycorp-sonnet"
# No api_key here — secrets are never seeded. Use api_key_command, or have
# operators paste their key once in Settings → Model / API.
api_key_command = "mycorp-auth token --scope llm"
api_key_ttl_ms = 300000
[console]
docs_url = "https://docs.mycorp.example/console"
[console.theme]
brand_name = "MyCorp Console"
accent_color = "#b3172a"
icon = "pkg:mycorp_console:app.ico" # your bundled icon
title_bar_color = "#7a0f1d" # Windows 11 caption tint
watermark_text = "MyCorp internal"
watermark_style = "banner" # tiled | banner | center
# Bootstrap the live config layer: consoles pull day-to-day config from git.
[config_sync]
repo = "https://git.mycorp.example/ops/console-config.git"
tag = "v1"
# Keys you expect each operator to own — a seed/pull re-assert never overwrites
# these; the local value always wins. "<file>:<dotted.path>", * = any entry.
preserve = [
"runspec_credentials.toml:*.username", # every credential's username stays local
"config.toml:llm.api_key_command", # a machine-specific key command wins
]
[ssh]
proxy = "http://proxy.mycorp.example:8080" # your network's realities
The whole preserve list lives here, in [config_sync] — it is not a field on
the entries it protects. You never add anything to a [[credential]] or
[[host]] table; each preserve string names its target by path:
"<file>:<entry>.<field>", where <entry> is a specific id/name or * for
every entry in that file. So one line —
"runspec_credentials.toml:*.username" — pins the username across all seeded
credentials; "runspec_credentials.toml:jira-prod.username" would pin just that
one. (For config.toml, which isn't a list of entries, the path is simply the
dotted key: "config.toml:llm.api_key_command".)
api_key_command resolves relative to the install. A seeded key helper is
commonly a console-script you ship in the same wheel
([project.scripts] mycorp-auth = "mycorp_console.auth:main"), but the venv can
be created anywhere — so api_key_command runs with this venv's script
directory prepended to PATH, and seeding the bare command
(api_key_command = "mycorp-auth token --scope llm") resolves wherever the venv
lives (a hardcoded Scripts//bin/ path would not). Two env vars are also
exported for a module-style helper: RUNSPEC_PYTHON (this venv's
interpreter) and RUNSPEC_VENV (its root) —
api_key_command = '"$RUNSPEC_PYTHON" -m mycorp_console.auth token'
(%RUNSPEC_PYTHON% under cmd). The secret itself is never seeded — only the
command that fetches it. Because a venv-relative command is correct on every
machine, no operator ever needs to override it locally, so re-assert-on-upgrade
(below) never fights you over this key.
runspec_hosts.toml — the fleet, ready in the sidebar on first launch:
[[host]]
name = "app-01"
ssh = "svc-ops@app-01.mycorp.example"
runspec_paths = ["/opt/venvs/ops/bin/runspec"]
group = "app"
profiles = ["production"]
runspec_groups.toml / runspec_profiles.toml / runspec_filters.toml
— the group vocabulary, host working-sets, and output filters your runbooks
and docs assume.
runspec_runbooks.toml — starter procedures, so the agent has your
playbook from day one:
[[runbook]]
id = "morning-checks"
title = "Morning fleet checks"
symptom = "Start-of-day verification"
steps = """
1. Run health-check across the `app` group and summarise failures.
2. Check overnight backup logs on nas-01 and flag anything unusual.
"""
You can also seed runspec_triggers.toml, runspec_repos.toml,
runspec_credentials.toml (metadata only — secret values always come
from each operator's own keychain), and runspec_self_service.toml.
Seeded credentials pair especially well with the
platform-credential naming convention:
seed entries labelled Windows and Linux, have every runnable read
WINDOWS_USERNAME/WINDOWS_PASSWORD and LINUX_USERNAME/LINUX_PASSWORD
via env fallbacks, and each operator supplies two secrets once to cover the
whole catalogue.
How the seed is applied
On every startup the console compares the installed seed packages' versions
against a machine-local stamp: unchanged means no-op; new or upgraded means
the seed re-folds onto the local config through the same non-destructive
merge Config Sync uses — collections merge by name/id, config.toml
deep-overlays, and the operator's local edits and preserve-pinned keys win.
So shipping mycorp-console 1.1.0 with an updated seed rolls the baseline
forward on the next launch, without stomping local changes.
The one thing to brief operators on: a re-assert does overlay the seed's
value on top of local for any seed-shipped key it declares — so an operator
who edited a seeded key in place (rather than adding their own entry or
preserve-ing it) sees it revert on the next upgrade. That's the point of a
package-controlled baseline, but it shouldn't surprise anyone. Settings →
Config Sync → Config seed makes it visible: alongside the seed's status and a
Re-apply seed button, it lists any local values a re-assert would
overwrite — with their local → seed values and a hint to add the key to
Preserve — and, separately, the ones a preserve rule already protects. So an
operator can spot and pin a machine-specific edit before an upgrade reverts it.
Design a key you expect operators to own (a machine-local path, a personal
account name) to be preserved from the start — via the [config_sync].preserve
list in the seed's config.toml above — or seed it as a separate entry they can
override. A common one is a credential's username: seed the credential's id /
label / kind / arg-bindings so the entry exists, but preserve
runspec_credentials.toml:*.username so each operator's own account name (and
the keychain secret, which is never seeded regardless) survives every upgrade.
Declare preserve in the seed's own config.toml (as shown above) — the
seed apply reads the preserve list from both the seed it's applying and the
local config, so a seed-declared preserve protects the operator's local value
from the very first re-assert that ships it. (Preserve read only from local
config would be one upgrade behind: the upgrade that introduces the preserve
runs against the old local config, clears the value, then writes the preserve
— which looks exactly like preserve being ignored.) Two caveats still hold: a
preserve can only keep a value that is already in the local file at upgrade
time (it restores the operator's current value, it can't bring back one an
earlier upgrade already cleared — so if usernames were blanked before you added
the preserve, operators re-enter them once and they stick thereafter); and the
secret behind a credential is never seeded regardless, so an operator always
sets that once in Settings → Credentials.
4. Theme reference
All keys live under [console.theme], delivered by the seed (there's no
Settings editor — deliberate, so the brand is package-controlled). Invalid
values are dropped, never fatal; with none set the console is a stock
install, pixel-identical.
| Key | Accepts | Effect |
|---|---|---|
brand_name |
text | Sidebar title (unset ⇒ "runspec") |
accent_color |
#rgb / #rrggbb |
UI primary colour and the generated app icon |
icon |
file path or pkg:<module>:<relpath> |
Explicit .ico/.png app icon (bundle it as package data with the pkg: form) |
title_bar_color |
hex | Native Windows 11 caption tint — independent of accent_color, so a blue-accent UI can carry a red title bar; no-op elsewhere |
watermark_text |
text | Faint watermark behind content |
watermark_logo |
http(s)/data: image URL |
Image watermark |
watermark_style |
tiled / banner / center |
Repeated marks, one large upper-third mark, or one centred mark |
The window title, in-app documentation link, and update-check target come from your package metadata (§1), not the theme.
5. Build, publish, install
python -m build
twine upload --repository-url https://pypi.mycorp.example/simple dist/*
Operator install — this is the entire rollout instruction:
pip install mycorp-console --index-url https://pypi.mycorp.example/simple
mycorp-console
The footer's update badge now tracks mycorp-console against your index, so
publishing 1.1.0 prompts every operator. For fleet-side venvs behind the
same private index, the configure-pip runnable in runspec-linux writes
the pip.conf — drivable by the agent during
host bootstrap.
6. Verify
Launch on a clean machine (or a fresh %APPDATA% by temporarily renaming
runspec-console/) and check:
- [ ] Window and sidebar show MyCorp Console, your icon, your accent — and the title bar tint on Windows 11.
- [ ] Settings → Model / API shows
mycorpactive; a chat turn answers through your gateway. - [ ] The sidebar lists your seeded hosts and groups; the profile switcher shows your profiles.
- [ ] The agent knows the seeded runbooks: ask "what runbooks do you have?".
- [ ] Settings → Config Sync shows the seed stamped with your package version, and the git layer configured.
- [ ] The in-app Documentation link opens your docs; the footer update check names your package.