Review feedback + follow-up on the review-stamps feature: - The 409 trust gate now distinguishes reason 'unreviewed' (community code not vetted) from 'unverified' (no pinned checksum, possible even for first_party), each with an accurate message; the risk dialog carries the reason and matches its title/body to the actual case instead of always claiming 'unreviewed community extension' - Unreviewed registry entries are now developer-stage content: hidden from the Marketplace catalog and refused at the install endpoint (404) unless the panel runs in a development context (Flask debug / testing config, or the dev_mode setting). Hidden means not installable; the 409 acknowledgment flow only exists where they list - dev_mode setting description mentions the Marketplace effect - tests: reason assertions on both 409 cases; catalog visibility and 404-on-hidden-install coverage (38 passing)
12 KiB
Extension Registry & Publishing
The registry is how third-party (and non-bundled first-party) extensions
become browsable in the Marketplace without any per-panel seeding. It is a single
curated index.json; panels fetch it, merge its entries into Browse labeled
"Registry", and install from it with checksum verification.
This document is the format spec (task #16) and the publisher guide (task #21).
How a panel consumes the registry
- The panel fetches the index from
SERVERKIT_REGISTRY_URL(env var). When unset it defaults to the curated public registry via serverkit.ai (https://serverkit.ai/ext/index.json, which proxies and caches the rawserverkit-extensionsindex and serves its logo art); set it empty to disable remote fetching entirely (air-gapped panels). When the fetch fails (offline), it falls back to the last good cache, then to a bundled copy shipped atbackend/app/data/registry_index.json. The Marketplace never blanks. - Results are cached in-memory for
SERVERKIT_REGISTRY_TTLseconds (default 3600). - Discovery is read-only — nothing in the registry is ever auto-installed.
- Installing a registry entry downloads its
source, verifiessha256(when present) before extraction, and checks the panel version againstmin_panel_version/max_panel_version. - Installing straight from GitHub (Marketplace → Install manually → URL)
works with a repo URL, a release URL, a direct
.zip, orowner/repo/owner/repo@tagshorthand. The panel first previews the extension — resolving the download, readingplugin.json, and showing the version, declared permissions, panel-version compatibility, and any warnings — then installs the exact previewed bytes (checksum-pinned). Set the optionalSERVERKIT_GITHUB_TOKENenv var to lift GitHub's 60/hr anonymous API rate limit and enable private-repo installs; the token is attached only to GitHub hosts and is never logged or returned by the API.
Relevant endpoints:
| Endpoint | Purpose |
|---|---|
GET /api/v1/marketplace/registry |
list registry entries + live install state |
POST /api/v1/marketplace/registry/<slug>/install |
checksum-verified install (admin) |
GET /api/v1/plugins/updates |
installed plugins with a newer registry version |
POST /api/v1/plugins/<id>/update |
update a plugin to the registry version (admin) |
index.json format (schema_version 2)
schema_version is now 1 or 2. Version 2 is fully additive — v1 entries
stay valid unchanged; the new fields (logo, repo, bundled) are optional.
{
"schema_version": 2,
"updated": "2026-07-09",
"extensions": [
{
"slug": "serverkit-gui", // required — matches the manifest name
"display_name": "ServerKit Agent GUI", // required
"description": "…",
"version": "0.1.0", // required (unless bundled) — the published version
"category": "monitoring", // ai|monitoring|security|deployment|integration|ui|utility
"author": "Juan Denis",
"first_party": false, // true only for ServerKit-authored entries
"bundled": false, // v2 — true = ships inside the panel (catalog listing only)
"permissions": ["network"], // declared host permissions (honesty is reviewed)
"min_panel_version": "1.7.0", // optional compat gate (inclusive)
"max_panel_version": null, // optional
"sdk_version": "^1.0.0", // optional (additive) — frontend SDK range the bundle targets
"source": "https://github.com/owner/repo", // repo URL (latest release), release URL, or direct .zip
"sha256": "…", // sha256 of the release zip — STRONGLY recommended
"review": { // optional — hash-bound review stamp (see below)
"reviewer": "jhd3197",
"date": "2026-07-23",
"sha256": "…", // sha256 of the exact zip the reviewer inspected
"commit": "…", // optional git sha
"notes": "…" // optional
},
"repo": "https://github.com/owner/repo", // v2 — https URL of the source repo (shown as "Source repo")
"logo": "assets/serverkit-gui/logo.svg", // v2 — https URL OR repo-relative assets/<slug>/<file>
"release_notes": "…", // optional (additive) — shown in the update-diff modal
"homepage": "https://…",
"icon": "<svg-inner-markup/>", // rendered on the detail view
"screenshots": ["https://…/1.png"] // rendered on the detail view
}
]
}
Notes:
sourceaccepts the same forms as a URL install: a GitHub repo URL (resolves the latest release asset), a release-tag URL, or a direct.zipURL.sha256is the digest of the exact zipsourceresolves to. When present it is enforced — a mismatch is a hard failure with no partial install. Omit only while prototyping.min_panel_version/max_panel_versionare compared against the panel'sVERSION. An incompatible entry can be listed but not installed/updated.sdk_version(additive) is the semver range of the frontend SDK a runtime bundle targets — see EXTENSIONS.md → The SDK contract. Checked at install and at load. Older panels ignore it; older indexes omit it.release_notes(additive) is free text shown in the Marketplace update-diff modal alongside the version + permissions delta. Old panels ignore it.
v2 fields (logo, repo, bundled)
logo— an extension logo, shown first in the Marketplace art fallback chain (before brand marks / generated covers). Either an absolutehttps://URL or a repo-relative pathassets/<slug>/<file>(.svgor.png, ≤ 200 KB) committed to theserverkit-extensionsrepo alongside the entry. Repo-relative paths are resolved to absolute URLs by the panel (against the index URL it fetched) and rewritten by the serverkit.ai/extendpoint, so a single relative path works whether the panel reads the raw-GitHub index or the domain.repo— the https URL of the extension's source repository. The detail modal renders it as a "Source repo" link.bundled—truemarks an entry as one of the panel's builtin extensions. Bundled entries are catalog listings only (they ship inside the panel, sosource/sha256are optional) and are excluded from the Browse merge by default to avoid duplicating the builtin cards; the API exposes them viaGET /api/v1/marketplace/registry?include_bundled=true. Generate them from the panel repo — never hand-type:node scripts/export-registry-entries.mjs # emits index-v2 bundled entries
Review stamps (review) & trust levels
An entry may carry an optional review stamp recording that a reviewer
inspected the published artifact:
"review": {
"reviewer": "jhd3197", // who reviewed it
"date": "2026-07-23", // when
"sha256": "…", // sha256 of the EXACT zip that was reviewed
"commit": "…", // optional git sha of the reviewed source
"notes": "…" // optional free text
}
The stamp is hash-bound: the reviewer vouched for the bytes whose digest is
review.sha256, not for the slug. If the artifact ever changes (new release,
replaced asset), the entry's sha256 moves on and no longer matches
review.sha256 — the review is stale and stops counting. A stamp only
survives normalization when it is an object whose sha256 is a 64-character
lowercase hex digest; anything malformed is dropped.
From this the panel derives a per-entry trust level, surfaced on the
catalog API and in the Marketplace:
trust |
Meaning |
|---|---|
first_party |
first_party: true — authored by ServerKit (always wins). |
reviewed |
A valid review stamp exists and review.sha256 equals the entry's sha256. |
unreviewed |
Everything else — no stamp, a stale stamp (hash mismatch), a malformed stamp, or no sha256 to bind to. |
The Marketplace renders Reviewed (with a Reviewed by <reviewer> on <date> tooltip) and Unreviewed badges accordingly.
Install-time acknowledgment. Installing an unreviewed entry — or any
entry with no sha256 to verify against — requires explicit consent:
POST /api/v1/marketplace/registry/<slug>/install without
{"acknowledge_risk": true} in the JSON body returns 409 with
{"error": …, "trust": …, "reason": …, "requires_acknowledgment": true},
where reason is unreviewed (community code not vetted) or unverified
(no pinned checksum, possible even for first_party). The Marketplace catches
this, shows a confirmation dialog whose copy matches the reason, and resends
with acknowledge_risk: true only if the operator confirms (the
acknowledgment is recorded in the audit log). first_party and reviewed
entries install unchanged, with no extra prompt.
Unreviewed entries are developer-stage content. On a production panel
(Flask debug off, dev_mode setting off) unreviewed entries do not appear
in the Marketplace at all, and the install endpoint answers 404 for them —
hiding is the default, the 409 acknowledgment flow only exists where they are
visible. They list and install only in development contexts: Flask debug mode
(development/testing config) or the dev_mode setting toggled on under
Settings → Site. first_party and reviewed entries are unaffected.
Publishing an extension
-
Structure the repo per
docs/EXTENSIONS.md: aplugin.jsonat the archive root, plusbackend/and/orfrontend/. A third-party extension that ships a prebuilt runtime bundle (frontend_entry: dist/index.mjs, externalized shared libs) now renders on a prebuilt panel with no rebuild — seedocs/EXTENSIONS_CI.mdfor the build→hash→publish recipe. (Backend-only extensions need no frontend at all.) -
Cut a release. Tag a version and attach a plugin
.zipas a release asset (the installer prefers a.zipasset over the source zipball). Record the asset'ssha256:sha256sum my-extension-0.1.0.zip -
Submit the index PR. Open a PR against the
serverkit-extensionsrepo adding (or bumping) your entry inindex.json. Bumpingversionis what surfaces the "Update available" badge on installed panels.
Review checklist (what a maintainer verifies)
- Permissions honesty — declared
permissionsmatch what the code actually uses (docker|filesystem|shell|network|db, oragent.command:*). Over-broad or undeclared permissions are rejected. Enforcement (Phase 3 #25) makes an undeclared capability raise at runtime — declare accurately. - Checksum —
sha256present and matches the release asset. - License — a real OSS license (
licensefield + a LICENSE in the repo). - Compat —
min_panel_versionreflects the oldest panel actually tested. - Frontend bundle — if the extension ships UI, it's a runtime ESM bundle
(
frontend_entry: dist/index.mjs) that passes--validate(no embedded React, shared libs externalized). Raw.jsxfrontends don't render on prebuilt panels. - SDK range —
sdk_versionpresent for any extension shipping a runtime bundle and reflects the SDK it was actually built + tested against. - Brand-neutral — no competitor names in the name/description (project policy).
Free/OSS project: there are no paid extensions, quotas, or billing — ever.