Wheelhouse docs

Catalogue schema#

The app catalogue is one JSON document, agent/catalog.json, embedded in the agent binary at compile time. The agent can serve a signed catalogue index fetched from the download host in place of it — a detached ed25519 signature checked against a key compiled into the binary, with anything that does not verify refused and the reason shown on the Apps page. No catalogue signing key exists yet, so today every agent serves the embedded copy, GET /api/apps/catalog reports source.state: "embedded", and a corrected entry still needs an agent release.

bash
curl -sk "$R/api/apps/catalog" -H "Authorization: Bearer $T"

The document#

FieldTypeMeaning
versionstringThe catalogue's own format version. Currently 1.
networkstringThe container network name every non-host-network app joins. Currently apps.
prefixstringThat network's prefix. Currently 10.99.0.0/24.
categoriesarrayThe category names, in display order. Entries are sorted by this order, then case-insensitively by name within a category.
appsarrayThe entries.

Duplicate id values are a fatal error at startup: the agent exits 1 with app catalog is invalid.

An entry#

FieldTypeMeaning
idstringUnique. What every API call names.
namestringThe display name.
categorystringOne of categories.
blurbstringOne line, for the card.
descriptionstringThe longer text on the detail page.
kindstringcontainer (the default, and what an absent field means) or builtin.
builtinobjectRequired when kind is builtin. See below.
imagestringThe container image reference.
homepagestringThe project's own site.
tagsarraySearch keywords.
host_networkboolRun on the router's own interfaces instead of the container network.
portsarraySee below.
volumesarraySee below.
environmentarraySee below.
capabilityarrayLinux capabilities to grant, each becoming set container name <n> capability <c>.
privilegedboolAdvisory only — it produces a warning in the install plan, not a configuration node.
memorystringBecomes set container name <n> memory <value>.
integrationobjectWhat the UI does with the app once it runs. See below.
notesarrayWarnings to read before committing, in the entry's own words.
closesstringNames a documented platform gap this app fills.

ports#

FieldMeaning
nameLabel.
containerThe port inside the container.
hostThe port on the router, when the app publishes one.
protocoltcp or udp.
descriptionWhat it is for.

volumes#

FieldMeaning
nameThe volume's name in the configuration tree.
sourceThe default path on the router. The operator may override it at install time.
destinationThe path inside the container.
read_onlyAdds mode ro.
fileThis bind mount is a single file, not a directory.
descriptionWhat lives there.

file tells POST /api/apps/prepare to create a file rather than a directory, which is what the router requires before it will commit the mount. A socket is the daemon's to create, never the agent's.

Each volume becomes three or four operations:

set container name <n> volume <name> source <path>
set container name <n> volume <name> destination <path>
set container name <n> volume <name> mode ro          # only when read_only

environment#

FieldMeaning
nameThe variable.
valueThe catalogue's default.
descriptionWhat it does.
requiredAn empty value produces a warning in the plan.
secretThe UI masks it.

A key present in the install request wins even when empty — that is how an operator clears a catalogue default. An absent key falls back to the catalogue value. An empty final value stages nothing, and warns if the variable was required.

integration#

FieldMeaning
web_ui{port, scheme, path} — where the app's own interface is, so the UI can link to it.
health{port, path} — the probe.
widget{label, port, path, json} — one number to show on the app's card. Optional, and most entries have none.
nav_label, nav_iconA navigation entry for the running app.
dashboardCarried for the landing page. The Overview does not list apps, so nothing in the web UI reads it.
hintsIntegration offers surfaced on other pages. See Hint placeholders.

The health probe is a TCP connect, plus one HTTP GET when a path is given. It is not an app-specific API call. A 5xx is unhealthy; anything else that answered is healthy; a port that opens and does not speak HTTP reports port open, HTTP did not answer.

Off-router — when the agent is not running on the router it manages — a failed probe says nothing about the app, and the agent reports it that way. It decides by whether --api-url points at loopback.

widget#

json
"widget": {
  "label": "Series in memory",
  "port": 9090,
  "path": "/api/v1/status/tsdb",
  "json": "data.headStats.numSeries"
}
FieldMeaning
labelWhat to call the number. It carries its own unit — there is no unit field, because a unit kept separately from the label is a second thing to keep true.
portThe port the statistics are on. Named separately from health's, because a stats URL is not always on the port the UI links to.
pathThe path to GET. Must start with /.
jsonA dotted path of object keys to a single number in the response.

All four are required; an incomplete declaration is ignored rather than half-honoured. The agent reads the URL over plain HTTP with no credentials, caches the reading per app, and shows it on the card with its age, because a number on a screen with no age on it reads as "now".

This is the whole of the boundary. There is no authentication, no second request, no app-specific client, and no unit conversion — anything an app will not tell an anonymous caller in one number stays untold. Two of the catalogue's entries declare a widget; the rest show what they always showed, and that is the design rather than a backlog.

builtin#

A builtin entry is not a container at all. It is a native platform feature whose page in the web UI is enabled by "installing" it.

FieldMeaning
pageThe UI page the entry enables.
config_pathThe configuration path whose presence means the feature is in use.

config_path is what stops a page being hidden while its configuration exists: a feature whose subtree is present shows regardless of the enabled list. Planning an install for a builtin entry stages nothing and says so:

This is a built-in feature, not a container — installing it enables its page instantly
and stages nothing.

Enabled builtins are remembered in the agent's own state.json, under features, not in the router's configuration — it is a UI-surface preference, not router behaviour.

What an install actually stages#

set container network apps prefix 10.99.0.0/24        # only if the network is absent
set container name <n> image <image>
set container name <n> network apps address <chosen address>
set container name <n> volume <v> source <path>
set container name <n> volume <v> destination <path>
set container name <n> environment <VAR> value <value>
set container name <n> capability <cap>
set container name <n> memory <memory>

With host networking, the address line is replaced by set container name <n> allow-host-networks and the plan warns that the app shares the router's interfaces and any port it binds is exposed on the router itself.

Nothing is applied by planning. The operations go to the Commit Bar like every other change.

The three checked fields#

CheckedRuleWhy
The container name^[-a-zA-Z0-9]+$It becomes an element of a configuration path and of an op-mode path on the router.
A volume sourceMust be absolute, and may not contain a space or any of '"`$;&|<>()\On-router the agent creates it as root; off-router it renders into a shell line for the operator to paste.
An image referenceThe OCI reference grammar, at most 255 charactersThe router hands an image name to its container runtime through a shell, so an unvalidated reference would be a command line on the router.

Refusals happen where the message can name the field. The router would only say Set failed at commit.

Image pinning#

Every container entry is pinned by digest: 36 of 36 are host/repo:tag@sha256:…. check-images.py --pin is what rewrites an image to that form, and CI runs the same script with --require-digest, so an entry that loses its digest cannot merge. The tag stays for humans; the digest is what the runtime resolves. A pin does not move on its own — an upstream fix reaches a router only when somebody re-pins and ships a release.

Re-deriving the catalogue#

bash
python3 -c '
import json
c = json.load(open("agent/catalog.json"))
print(c["version"], c["network"], c["prefix"])
for a in c["apps"]:
    print(a["id"], a.get("kind","container"), a.get("image",""))
'

See also#

Updated 2026-09-04 catalogue apps containers schema