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.
curl -sk "$R/api/apps/catalog" -H "Authorization: Bearer $T"The document#
| Field | Type | Meaning |
|---|---|---|
version | string | The catalogue's own format version. Currently 1. |
network | string | The container network name every non-host-network app joins. Currently apps. |
prefix | string | That network's prefix. Currently 10.99.0.0/24. |
categories | array | The category names, in display order. Entries are sorted by this order, then case-insensitively by name within a category. |
apps | array | The entries. |
Duplicate id values are a fatal error at startup: the agent exits 1 with
app catalog is invalid.
An entry#
| Field | Type | Meaning |
|---|---|---|
id | string | Unique. What every API call names. |
name | string | The display name. |
category | string | One of categories. |
blurb | string | One line, for the card. |
description | string | The longer text on the detail page. |
kind | string | container (the default, and what an absent field means) or builtin. |
builtin | object | Required when kind is builtin. See below. |
image | string | The container image reference. |
homepage | string | The project's own site. |
tags | array | Search keywords. |
host_network | bool | Run on the router's own interfaces instead of the container network. |
ports | array | See below. |
volumes | array | See below. |
environment | array | See below. |
capability | array | Linux capabilities to grant, each becoming set container name <n> capability <c>. |
privileged | bool | Advisory only — it produces a warning in the install plan, not a configuration node. |
memory | string | Becomes set container name <n> memory <value>. |
integration | object | What the UI does with the app once it runs. See below. |
notes | array | Warnings to read before committing, in the entry's own words. |
closes | string | Names a documented platform gap this app fills. |
ports#
| Field | Meaning |
|---|---|
name | Label. |
container | The port inside the container. |
host | The port on the router, when the app publishes one. |
protocol | tcp or udp. |
description | What it is for. |
volumes#
| Field | Meaning |
|---|---|
name | The volume's name in the configuration tree. |
source | The default path on the router. The operator may override it at install time. |
destination | The path inside the container. |
read_only | Adds mode ro. |
file | This bind mount is a single file, not a directory. |
description | What 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_onlyenvironment#
| Field | Meaning |
|---|---|
name | The variable. |
value | The catalogue's default. |
description | What it does. |
required | An empty value produces a warning in the plan. |
secret | The 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#
| Field | Meaning |
|---|---|
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_icon | A navigation entry for the running app. |
dashboard | Carried for the landing page. The Overview does not list apps, so nothing in the web UI reads it. |
hints | Integration 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#
"widget": {
"label": "Series in memory",
"port": 9090,
"path": "/api/v1/status/tsdb",
"json": "data.headStats.numSeries"
}| Field | Meaning |
|---|---|
label | What 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. |
port | The port the statistics are on. Named separately from health's, because a stats URL is not always on the port the UI links to. |
path | The path to GET. Must start with /. |
json | A 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.
| Field | Meaning |
|---|---|
page | The UI page the entry enables. |
config_path | The 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#
| Checked | Rule | Why |
|---|---|---|
| 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 source | Must 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 reference | The OCI reference grammar, at most 255 characters | The 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#
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#
- The catalogue — all 40 entries.
- Hint placeholders
check-images.py- Endpoint index — the ten
/api/apps/*routes. - Audit entries — what an app action records.
- Apps are configuration
- Install an app