ff-ruyijs
A AI Browser run any site JavaScript from Python with the real Firefox engine. Headless SpiderMonkey + Necko for automation and scraping.
翻译准备中
Curated first-party app catalog for Rasputin - tiles published on their own cadence, independent of control-plane releases.
翻译准备中
正在加载完整 README。
查看完整 README ↗The curated first-party app catalog for Rasputin — the set of tiles an owner installs from. One directory per tile, holding the metadata a cluster needs and the compose stack it runs.
This repo publishes on its own cadence. A new app does not require a Rasputin release.
The catalog used to be go:embed-ed into the control-plane binary. That meant adding an app cost
a lockstep version cut across three repos against a two-hour build floor — for content that
changes daily. Worse, it taught the operator that new apps require a firmware upgrade, which is
a NAS-vendor experience rather than a homelab one.
The full reasoning, and the contract that decoupling creates, is ADR-0006: App catalog delivery (internal wiki).
tiles/<id>/
tile.json # metadata: collection, arch, ports, RAM floor, exposure, status
docker-compose.yml # the stack (available tiles only; preview tiles are metadata-only)
<id> is the tile id and it is load-bearing: it is validated as a DNS-1123 label because it
appears verbatim in <app>.<cluster-id>.internal. Renaming a tile id breaks the URL of an app
somebody has already installed. Treat ids as frozen once published.
| Status | Means |
|---|---|
available | Pinned and installable. Intended to mean "benched on real Pi 5 + N100 hardware" — see the exception below. |
preview | Shown in the grid so the catalog reflects the roadmap; install is refused (409). May omit its compose entirely. |
A tile flips preview → available only after it clears the hardware bench: installed and
exercised on real Pi 5 and N100 nodes by a person. Desk research does not qualify.
Bench run, 2026-08-23/24. Catalog v10 flipped thirteen tiles to
availableahead of the bench, on Bryce's call, so the bench was owed for all of them. It has now run: every tile in v10 was installed on real hardware — N100 and Pi 5 compute nodes on clustere3bench— and its LAN URL fetched and checked for that app's expected first page. Fifteen of eighteen passed.paperless-ngxnever came up andpi-holefailed mid image-pull; both were pulled from the catalog in v11 rather than left broken in front of an owner.home-assistantdeployed cleanly but answered400 Bad Request, because it rejects a proxied request unless it is told to trust the proxy; its tile now seeds that configuration.v11 re-bench, 2026-08-24: Home Assistant passes. It deploys on an arm64 node and its LAN URL serves the onboarding screen rather than a
400. Getting there took a control-plane fix as well as the tile one: the seed is a one-shot container, and the agent read a container that exits by design as a crash, so the deploy failed outright untilrasputin-control-plane#185taught it to read the exit code alongside the state. All sixteen tiles in v11 have now cleared the bench.Correction, 2026-08-29: that Home Assistant pass was an artifact of checking too early. Home Assistant treats a changed
http:block as a five-minute trial and reverts it if no admin confirms it — and on a fresh install no account exists yet to confirm anything. So the tile served its onboarding screen for five minutes, then reverted to a config with no proxy trust in it and went back to400for good. Both earlier "passes" were checks that landed inside that window. v13 seeds the setting where Home Assistant now keeps it and re-benched with a check at T+19s and T+9m54s.Validated through the published catalog, 2026-08-29. That v13 re-bench ran from a hand-applied compose, because an unmerged tile cannot reach a cluster — so it proved the fix and not the delivery. Repeated from the signed release:
e3benchfetched catalog v13, and the compose that ran on an arm64 node hashes byte-identical to the one inside the signedcatalog.jsonasset (2069bc0e…), which itself matches the published artifact (e4c9c1fe…). The app served its onboarding screen at T+32s and again at T+8m51s, past the five-minute trial window, with the setting in Home Assistant'sstablestore, nothing pending, no revert in its log and zero container restarts. All sixteen tiles have now cleared the bench, and this one cleared it the way an owner would actually install it.
ramFloorMB today is upstream's documented minimum, cited — not a measured figure. The bench
proves a tile boots and serves its first page; it does not measure the tile's memory. Read the
badge as the vendor's floor, and not as a number we have observed.
Today: 17 tiles — 17 available, 0 preview, sixteen bench-passed. Those sixteen have each been
installed on real hardware and had their URL checked for the app's expected first page.
human-system was added as available on Bryce's call. No bench run for it is recorded here, and
its storage.md §4.4 restore round-trip, owed because two of its volumes are critical, has not run
yet either.
The repo root holds a VERSION file containing a single positive integer:
1
That number is the catalog version, and it goes inside the signed bundle. A cluster refuses any bundle whose version does not exceed the one it already holds (ADR-0006 Decision 5). This closes an attack that needs no forged signature: replay a genuinely-signed older bundle and the fleet rolls back to image digests with known CVEs. A signature check cannot tell that from a real update, so the version gate has to.
If your pull request changes anything under tiles/, raise VERSION by one. CI enforces it —
the version job compares your branch against the base and fails if the corpus moved and the number
did not. A pull request that touches only docs or workflows needs no bump, and the job skips itself.
Two consequences worth knowing before they surprise you:
Building the bundle by hand, if you want to see what gets published:
go run ./cmd/catalogbundle -version 1 -o catalog.json
-version is required and has no default — there is no safe guess for a monotonic counter.
Add -o - to write to your terminal instead of a file.
The tile contract lives in one place, the tileschema Go module in rasputin-control-plane, and
both sides import it: this repo validates a tile before publishing, and the control plane
validates it again before loading it into a running cluster. There is deliberately one
implementation — two hand-maintained lists drift, and the drift is silent because each side
stays internally green.
This repo requires tileschema at a contract tag (tileschema/v1.x.y), never a commit.
Renovate opens a bump for each new tag and merges it once the required checks pass. A new major is
a new import path and is changed by hand. The bump arrives on Renovate's weekly scheduled run
(Mondays at 08:17 UTC); a manual workflow_dispatch of the renovate workflow brings it in at once.
Two halves:
ValidateTile — authored metadata. Everything expressible in tile.json.ValidateTileSafety — the security-relevant properties of the compose stack: images must be
digest-pinned (a tag is mutable at the registry, however specific it looks), host devices only
when the tile declares needsHardware, and — since ADR-0006 Decision 12 — every other privilege
declared rather than refused.A tile that needs a password, key or token gets it from the platform: see Secrets in a tile.
The catalog used to refuse privileged, host networking, host PID/IPC, any cap_add and any bind
mount outside two roots. That blocked apps people actually want (Home Assistant needs three of the
five) while saying nothing about security_opt: seccomp=unconfined, which is the closest a stack
gets to privileged without spelling it that way.
The rule now is no undeclared privilege. A tile states what its stack takes; the validator derives the same thing from the compose and refuses the tile when the compose takes more. Under-declaration is the error — over-declaring is allowed, so a tile can be conservative.
"requires": ["privilege-tiers-v1"],
"privilege": {
"tier": "host-trusting",
"grants": ["privileged", "host-network", "device:/dev/ttyUSB0"],
"why": "controls the smart devices on your network, and talks to USB radios"
}
Three tiers — routine (nothing outside its own container), elevated (reaches past itself but is
not root-equivalent) and host-trusting (effectively root on the node). The container runtime
socket is flagged separately from the tier, because it is not merely root-equivalent: it is the
ability to escape any constraint added later.
Do not hand-write the grants. tilelint derives them and prints the exact block to paste.
One thing stays an absolute refusal, and no tier permits it: a path reaching the platform's own
trust chain — /etc/rasputin, /var/lib/rasputin and anything containing them, /var/lib/rasputin/apps/
excepted. Not because it is the riskiest thing, but because consent is only meaningful while the
thing asking for it is still trustworthy; an app that can rewrite the trust store authorises every
future update.
Those safety facts are derived here at publish time and carried in the signed bundle manifest. The control plane never parses compose — it validates the derived facts, which the bundle signature makes exactly as trustworthy as the compose they came from.
go run ./cmd/tilelint # validate every tile, offline
go run ./cmd/tilelint -arch # also ask the registries what each image publishes
go run ./cmd/tilelint -tile jellyfin
The offline run gates every PR. The -arch probe runs weekly and on main rather than on
PRs — it talks to Docker Hub and ghcr, and a contributor cannot fix a registry outage.
Images must be digest-pinned. A tag is mutable at the registry however specific it looks,
so jellyfin/jellyfin:10.9.11 does not describe a fixed stack. Keep the tag for readability
and add the digest — jellyfin/jellyfin:10.9.11@sha256:… — which is the form the linter wants.
Open an issue with the Request an app template. A bot checks it is complete, that the id is usable as a hostname, that the tag is pinned rather than floating, and that the registry really publishes the architectures the request claims — asking the registry rather than believing the tag.
Passing that is not acceptance. It means the request is worth a human reading. The catalog is curated rather than comprehensive, so a complete, technically sound request can still be declined because it does not fit the set.
A single-architecture app is fine. Rasputin runs arm64 (Pi 5) and amd64 (N100) nodes; a tile declares what it supports and install offers only the nodes that can run it.
issue ──▶ triage bot ──▶ agent drafts a PR ──▶ gates ──▶ human merges ──▶ bench
complete? digest-pinned tile tiles curation preview
arch real? + its evidence supply-chain judgement → available
The agent stage is manually dispatched, not automatic: a human names the issue number, which means a human has read it. Nothing starts an agent because a stranger opened an issue.
Every stage after the agent is deterministic code the agent cannot edit. It proposes a tile
and the evidence behind it; the gates decide whether that proposal is safe, and a person
decides whether it belongs. The last step is hardware — a tile ships as preview until it
clears the bench, and no part of this pipeline can grant available.
This repo is being stood up incrementally. Still to land:
human-system. It is already available and no bench run for it is
recorded, so this is a debt against a shipped tile rather than a gate in front of it. The bench
runs recorded for the other tiles, and what each one checked, are under Tile status above.
The bench is a person's job and stays that way; the E19 pipeline will not be automating it.
Proving a published arm64 image really executes on a Pi is the one piece worth automating
later; that is deferred, not dropped.dongle tiles were removed rather than authored: ADS-B Ultrafeeder, AIS-catcher,
rtl_433 and WeeWX all need a USB SDR passed into a container, and that passthrough design is
still an open question. They come back when it is answered.The signed bundle and its publish pipeline are live. A merge to main publishes a new signed
catalog release when it raises VERSION, which every change under tiles/ must do (see
Catalog version above). A merge that touches neither tiles/ nor VERSION, such as a docs or
workflow change, publishes nothing: the publish job logs "Nothing to publish" and passes.
A new release goes out as a GitHub prerelease, so it reaches dev clusters only. catalog-v18,
promoted on 2026-09-11, was the first release on the stable channel. Promotion is a flag flip on a
release that already exists. It turns the release into a full release, which both channels take, so
stable receives the same signed bytes dev already had; it is not a new publish. A dev cluster takes
the highest catalog version of either kind, prerelease or full, from a control-plane build that
includes rasputin-control-plane#269
onward. On an older build, a dev cluster takes prereleases only. The procedure and its side effects
are in the header of .github/workflows/release.yml.
Bundles are signed with a dedicated app-catalog leaf under Geekdojo's IANA PEN 66587, carrying a catalog-only EKU — that leaf cannot sign an OS or firmware artifact even though it shares the trust root. The control plane fetches and verifies the newest bundle on its own channel on a 24-hour poll, and refuses anything older than the catalog it already holds. Until a cluster completes its first verified fetch, it runs the catalog embedded in its image: a published catalog release, pinned when that image was cut (ADR-0006 Decision 6). The app-request intake template and the agent-assisted drafting pipeline have landed too — see Requesting an app above.
This project is developed by a human maintainer working with AI coding assistants;
AI-assisted commits carry Co-Authored-By trailers naming the model. Per-process levels:
AI-DECLARATION.md. Approach, accountability, and provenance:
AI_DISCLOSURE.md.
AGPL-3.0, matching the rest of the Rasputin source.