This is the wire shape, written down here because the hub-side fetcher does not exist yet and a format nobody has specified is a format two people will implement differently. Nothing in the hub reads any of this today.
Everything under index/ is generated from the submissions by
tools/registry/build_index.py and signed by CI. Do not edit it
by hand and do not commit it: it is published to the feed, not to the repository.
https://extensions.remaestro.app
This is the constant a hub bakes in, and it is the part that cannot change, because changing it
means an update every installed hub has to receive before it can find the registry again. The hosting
behind it can change freely — Pages today, a bucket or our own box later — and that is a DNS edit
rather than a release. Which is the whole reason it is a domain of ours and not the
redth.github.io/remaestro-extensions path Pages hands out.
The DNS record does not exist yet, and the Pages custom domain is not configured. Nothing resolves at that name today. It is written down first so the hub-side fetcher has a settled constant to compile in rather than a placeholder to guess at, and so the two are never out of step.
https://extensions.remaestro.app/plugins/<id>.json the install path — small, bounded, signed
https://extensions.remaestro.app/plugins/<id>.json.sig
https://extensions.remaestro.app/catalog.json browse and search only — allowed to grow
https://extensions.remaestro.app/catalog.json.sig
plugins/<id>.json is what a hub fetches to install or update. It carries the newest offered
version for each abi and nothing else. The hub’s release manifest refuses to be a growing list on
purpose — “a document that grows with every release is a document that eventually fails to parse on
the oldest box in the field — the one that most needs to be able to update” — and a marketplace
index is exactly the growing list that warning is about. Keying by abi rather than truncating to a
single “latest” keeps a hub that speaks an older protocol able to find something it can run.
catalog.json is browse and search. It may grow without limit, because the console is the only
thing that reads it and no install or update path may depend on it. That is the constraint that
keeps the other document small; if a hub ever needs the catalogue to install something, the split has
been undone.
Schema: schema/index-plugin.schema.json.
{
"schema": 1,
"id": "com.acme.lamp",
"name": "Acme Lamp",
"summary": "…",
"license": "MIT",
"source": "https://github.com/acme/remaestro-lamp",
"kind": "driver",
"publisher": {
"id": "com.acme",
"name": "Acme Ltd",
"contact": "https://github.com/acme",
"keys": [
{ "id": "2026-01", "algorithm": "ecdsa-p256-sha256", "publicKey": "…base64 SPKI…", "status": "active" }
]
},
// Keyed by abi, as a string. The lookup that matters is "what do you have that I can speak".
"latest": {
"1": {
"version": "1.1.0",
"abi": 1,
"runtime": "native",
"releasedAt": "2026-08-14",
"archives": {
"linux-arm64": { "url": "https://…", "sha256": "…", "size": 16252928, "signature": "…", "keyId": "2026-01" }
}
}
},
"withdrawn": [ { "version": "1.0.3", "at": "2026-08-20", "reason": "…" } ],
"generatedAt": "2026-08-15T00:00:00+00:00"
}
Revoked keys stay in publisher.keys. A hub that meets an archive signed by a revoked key has to be
able to say which key, not merely that something is wrong.
Written as expectations rather than as code, because the fetcher is a later piece of work and this is the half of it that is a contract.
latest whose abi this build speaks. No entry means “this plugin has
nothing for a hub like yours”, which is a sentence, not an error.size.publisher.keys with the matching keyId. Both, in that order, before
anything is unpacked.<data>/plugins/.staging — never in /tmp, which is a
256 MiB tmpfs in RAM on the appliance. Unpack to .partial, then move.A hub can install a plugin from a plugin.json URL with no registry involved at all, and that
path is built first. The registry is discovery; it is never a dependency. This is what keeps the
promise that the hub never needs a service of ours to do anything.
What install-by-URL gives up is exactly what the registry adds: nobody has checked the digest against a signed document, nobody has pinned the publisher’s key, and nobody has looked at the submission. It should say so at the point of install.
Detached, one .sig beside each document, holding base64 of a DER-encoded ECDSA P-256 signature
over SHA-256 of the document’s exact bytes. The same shape, the same encodings and the same
verification call the hub already uses for release manifests — deliberately, so there is one
implementation of “check a signature” on a path somebody watches rather than two.
We sign the index. We never sign a plugin. See signing.md.