diff --git a/.github/workflows/plugin-catalog-ci.yml b/.github/workflows/plugin-catalog-ci.yml index 84efbc0761..66f7ad8e8e 100644 --- a/.github/workflows/plugin-catalog-ci.yml +++ b/.github/workflows/plugin-catalog-ci.yml @@ -126,12 +126,23 @@ jobs: fi # Manifest schema + declared-vs-registered capability check. - if hermes plugins validate "$PLUGIN_DIR"; then - echo "✅ PASS: $entry" - else + if ! hermes plugins validate "$PLUGIN_DIR"; then echo "::error file=$entry::hermes plugins validate failed" - FAILED=1 + FAILED=1; echo "::endgroup::"; continue fi + + # SELF-UPDATER GATE (README rule 3): the pin is the only update path. + # A desktop bundle that both fetches from GitHub AND writes/renames + # plugin files is a self-updater; either half alone is fine (a + # plugin may read the API, or manage its own data files). + SELF_UPDATE=$(grep -rlE 'releases/latest|raw\.githubusercontent\.com' \ + --include='*.js' --include='*.mjs' --include='*.cjs' --include='*.ts' "$PLUGIN_DIR" \ + | xargs -r grep -lE 'writeTextFile|renamePath|writeFile\(' || true) + if [ -n "$SELF_UPDATE" ]; then + echo "::error file=$entry::self-updating code in catalog build (fetches GitHub AND writes plugin files): $SELF_UPDATE" + FAILED=1; echo "::endgroup::"; continue + fi + echo "✅ PASS: $entry" echo "::endgroup::" done <<< "$CHANGED_FILES" diff --git a/plugin-catalog/README.md b/plugin-catalog/README.md index 6a44c3a6e6..6ea912ad5b 100644 --- a/plugin-catalog/README.md +++ b/plugin-catalog/README.md @@ -16,10 +16,13 @@ meaningful: 2. **Exact SHA pins are mandatory.** Every entry pins a full 40-character commit SHA. Branches, tags, and short SHAs are rejected by the loader. Installs clone the repository and check out exactly that commit. -3. **Pin maturity.** The pinned release should be **at least 2 weeks old** - at pin time, mirroring the supply-chain policy used for `optional-mcps/` - and pyproject dependencies. This gives the community time to notice a - compromised release before Hermes ships a pointer to it. +3. **No self-updating code.** A listed plugin must not fetch and replace + its own files (in-app "check for updates", signed release downloaders, + remote `plugin.js` loaders). The exact SHA pin *is* the trust model; a + self-updater lets an installed copy move to a commit nobody reviewed. + Updates reach users only through a SHA-bump PR here plus + `hermes plugins update `. Keep the updater in the standalone + distribution if you want one; strip it from the catalog build. 4. **SHA bumps are new PRs.** Updating an entry's pin is a new PR whose diff (old SHA → new SHA) is re-reviewed like any other change — reviewers are expected to look at the upstream commit range being adopted. diff --git a/website/docs/user-guide/features/plugin-catalog.md b/website/docs/user-guide/features/plugin-catalog.md index bf6fd831e2..89913653d8 100644 --- a/website/docs/user-guide/features/plugin-catalog.md +++ b/website/docs/user-guide/features/plugin-catalog.md @@ -150,8 +150,9 @@ in short, an entry must be: 3. **Released** — the repo has real releases/tags, not just a default branch. 4. **Passing validation** — the catalog validation GitHub Action is green on the PR (schema, SHA format, reachability). -5. **Pinned to settled code** — the pinned SHA is at least **2 weeks old**, so - the catalog never points at code pushed moments before review. +5. **Not self-updating** — the catalog build must not download and replace + its own files; the pinned SHA is the only update path (a SHA-bump PR plus + `hermes plugins update `). Pin updates (bumping `sha` to a newer commit) follow the same PR + review process.