From 20c2f867444a5c3333af78480f84e71a24813aca Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:20:57 -0400 Subject: [PATCH 1/3] fix(ci): check enabledPlugins deltas against the catalog, not a deleted fleet list melodic-software/standards#663 deleted components/cloud-environment/fleet-plugins.json and now derives the cloud plugin list from this catalog's defaultEnabled. The gate still fetched the deleted file, so lint-2 and ci-status failed on every PR. With the list derived from the catalog, every catalogued plugin is either on by default or recorded off there, so the per-plugin coverage check is dropped. The gate keeps the orphaned-key, key-order and bootstrap marketplace checks and reads no network. docs/cloud-sessions.md now says plainly that plugins marked defaultEnabled: false are not installed in this repo's cloud sessions. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 17 +- docs/cloud-sessions.md | 85 +++++----- scripts/check-plugin-catalog-enablement.sh | 146 +++++------------- .../check-plugin-catalog-enablement.test.sh | 146 ++---------------- 4 files changed, 90 insertions(+), 304 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c47668380b..9fc432eee9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1047,18 +1047,17 @@ jobs: continue-on-error: true run: scripts/check-plugin-manifest-presence.sh - # docs/cloud-sessions.md promises `enabledPlugins` "turns on the whole - # catalog, so this repo dogfoods everything it publishes"; nothing - # enforced it, and ai-slop (#2892), context-budget (#2932) and - # improvement (#2985) each reached main catalogued but never enabled. The - # failure is silent by construction: .claude/cloud-bootstrap.sh computes - # its install set from that same enabledPlugins map. Repo-wide and - # static, so it always runs. The self-test runs first so a broken - # detector cannot mask a regression behind a green gate. + # The cloud plugin list is derived from the catalog's defaultEnabled, and + # .claude/settings.json carries this repo's deltas over it. A delta that + # names no catalogued plugin silently no-ops, and .claude/cloud- + # bootstrap.sh installs only the marketplace it hardcodes, so the gate + # holds the deltas to the catalog, their order, and that marketplace + # name. Repo-wide and static, so it always runs. The self-test runs first + # so a broken detector cannot mask a regression behind a green gate. - name: Test the plugin-catalog-enablement gate if: needs.changes.outputs.run_shell == 'true' run: bash scripts/check-plugin-catalog-enablement.test.sh - - name: Check every catalogued plugin carries an enabledPlugins key + - name: Check every enabledPlugins key names a catalogued plugin id: plugin_catalog_enablement continue-on-error: true run: scripts/check-plugin-catalog-enablement.sh diff --git a/docs/cloud-sessions.md b/docs/cloud-sessions.md index 6a77bc0ff9..60072ce517 100644 --- a/docs/cloud-sessions.md +++ b/docs/cloud-sessions.md @@ -376,55 +376,42 @@ catalog on, and the cloud bootstrap installs from the two together (see plugin changes. Declaring it is necessary but, per the trust gate above, not sufficient in a cloud session; verify in a fresh session and add the same bootstrap-plus-hook setup if the catalog does not load. -- The whole catalog is installed here, so this repo dogfoods everything it publishes and a - regression in any plugin surfaces here first, bar what a repo delta opts out of. Catalog - entries that ship `defaultEnabled: false` install disabled on a raw `claude plugin install`; - this repo's cloud bootstrap still treats fleet-list `true` as wanted (below). The enabling - list is the fleet cloud plugin list in standards - ([`components/cloud-environment/fleet-plugins.json`](https://github.com/melodic-software/standards/blob/main/components/cloud-environment/fleet-plugins.json)), - which the shared environment fetches at cache build, writes into the snapshot at - `/opt/melodic-fleet-plugins.json`, and installs at user scope; `cloud-bootstrap.sh` reads that - snapshot copy overlaid with `.claude/settings.json`, so the committed `enabledPlugins` block - carries only this repo's deltas (an explicit `false` opts out of a fleet entry; an entry beyond - the fleet adds one). The block used to mirror the catalog, and a local session start in every - checkout wrote one project-scope install record per mirrored entry into the user's - `installed_plugins.json` (#3688); the deltas-only block writes none. The trade is context: every - enabled plugin adds per-turn cost, so a *consumer* repo should opt out of what it does not need - rather than copying anything wholesale. The cloud bootstrap provisions every tool the - format/lint-on-edit hooks (`markdown-format`, `bash-format`, `biome-format`, `typos-format`, - `actionlint`, `eol-normalizer`) shell out to. -- The `plugin-catalog-enablement-gate` CI lane holds that whole-catalog claim to the files, in - both directions: every `.claude-plugin/marketplace.json` entry must be enabled by the fleet list - (fetched from its published URL at gate time; unreachable is a usage error, not a pass) or carry - an explicit `enabledPlugins` key, and every key for this marketplace must name a catalogued - plugin. It also checks that - `cloud-bootstrap.sh`'s hardcoded `marketplace_name` still names the marketplace the settings file - declares. The bootstrap selects what it installs with `endswith("@" + $n)`, so a rename that - updated the settings and the catalog but not that constant would leave its install set empty - while the parity lane stayed green over it. It exists because the claim was - prose for three plugin releases that shipped catalogued but never enabled, a silent failure, - since the bootstrap computes its install set from the same map and a session simply comes up - without those skills. `harness-config`'s `check-plugin-drift.sh` cannot cover it: it reads this - repo's relative `directory` catalog, but it diffs it only against the settings file's - `enabledPlugins` keys, reports a plugin with no key as NEW (report only), never reads the fleet - list, and checks no key order. -- Entries are sorted alphabetically, one per line, so a single plugin can be flipped to `false` - without disturbing the rest, a state the gate accepts, since an explicit `false` is a recorded - decision where an absent key is drift. The one opt-out recorded today is `playgrounds`: its - skill is a wrapper over the first-party `playground` plugin on `claude-plugins-official`, which - the cloud bootstrap does not install, so enabled here it could only ever print install commands. - The entries that should not start on their own are not - keyed here at all: the catalog ships them `defaultEnabled: false`, and `claude plugin install` - honors that flag, so a raw install leaves them disabled. They still appear as `true` on the - fleet list, so this repo's cloud bootstrap includes them in its wanted set and treats an - explicit JSON-`false` disabled install as the expected end state, not a verification failure. - A source-changing refresh does enable them: its chain is - `plugin uninstall --keep-data`, then `plugin install --scope user -y`, then - `plugin enable --scope user`, and that first step drops enabled state. - Operator opt-in outside that path is `/plugin enable`. That covers the two whose bundled MCP - servers need `userConfig` credentials this environment has no reason to hold, `miro` - (`miro_api_token`) and `dometrain-mcp` (`dometrain_api_key`), set with `/plugin configure`, - alongside `songwriting`, `kindle-dedrm`, and `ai-briefing`. +- This repo's cloud sessions install the plugins the catalog has on by default, plus this repo's + deltas. The shared environment's setup (`setup.sh` under `components/cloud-environment/` in + standards) derives the fleet list from this catalog at cache build, taking every entry whose + `defaultEnabled` is absent or `true`, writes it into the snapshot at + `/opt/melodic-fleet-plugins.json`, and installs it at user scope; `cloud-bootstrap.sh` reads + that snapshot copy overlaid with `.claude/settings.json`. **A plugin the catalog marks + `defaultEnabled: false` is not installed in this repo's cloud sessions** unless the committed + block opts it in, so a regression in one surfaces in CI and local use, not here. The committed + `enabledPlugins` block carries only deltas: an explicit `false` opts out of an on-by-default + plugin, and `true` opts in to an off-by-default one. The block is project scope, so a `true` + key also enables that plugin in every local session in this repo. The block used to mirror the + catalog, and a local session start in every checkout wrote one project-scope install record per + mirrored entry into the user's `installed_plugins.json` (#3688); the deltas-only block writes + none. The trade is context: every enabled plugin adds per-turn cost, so a *consumer* repo + should opt out of what it does not need rather than copying anything wholesale. The cloud + bootstrap provisions every tool the format/lint-on-edit hooks (`markdown-format`, + `bash-format`, `biome-format`, `typos-format`, `actionlint`, `eol-normalizer`) shell out to. +- The `plugin-catalog-enablement-gate` CI lane holds the deltas block to the catalog: every key + for this marketplace must name a catalogued plugin, since a key that names nothing silently + no-ops, and keys stay in byte order. It also checks that `cloud-bootstrap.sh`'s hardcoded + `marketplace_name` still names the marketplace the settings file declares. The bootstrap + selects what it installs with `endswith("@" + $n)`, so a rename that updated the settings and + the catalog but not that constant would leave its install set empty while the lane stayed green + over it. The lane does not require a key per plugin: with the fleet list derived from the + catalog, every catalogued plugin is either on by default or recorded off there. + `harness-config`'s `check-plugin-drift.sh` cannot cover this repo: it diffs the catalog only + against the settings file's `enabledPlugins` keys, reports a plugin with no key as NEW (report + only), and checks no key order. +- Entries are sorted alphabetically, one per line, so a single plugin can be flipped without + disturbing the rest. The one opt-out recorded today is `playgrounds`: its skill is a wrapper + over the first-party `playground` plugin on `claude-plugins-official`, which the cloud bootstrap + does not install, so enabled here it could only ever print install commands. Off-by-default + plugins are not keyed here; an operator who wants one turns it on with `/plugin enable`. Two + of them bundle MCP servers that need `userConfig` credentials this environment has no reason to + hold, `miro` (`miro_api_token`) and `dometrain-mcp` (`dometrain_api_key`), set with + `/plugin configure`. ### GitHub MCP tools vs the gh CLI diff --git a/scripts/check-plugin-catalog-enablement.sh b/scripts/check-plugin-catalog-enablement.sh index 20f228ae74..76d39b4fad 100755 --- a/scripts/check-plugin-catalog-enablement.sh +++ b/scripts/check-plugin-catalog-enablement.sh @@ -1,64 +1,45 @@ #!/usr/bin/env bash -# Gate: every plugin this repo's catalog publishes must be enabled somewhere -# a cloud session on this repo reads, and its own enabled-plugin set must name -# only catalogued plugins. +# Gate: this repo's committed enabled-plugin set must name only catalogued +# plugins, in byte order, and the cloud bootstrap must install the marketplace +# that set belongs to. # # scripts/check-plugin-catalog-enablement.sh run the gate (no flags) # -# WHY. docs/cloud-sessions.md states the property this repo depends on: this -# repo dogfoods everything it publishes, so a regression in any plugin -# surfaces here first. The failure is silent by construction: a plugin nothing enables is simply -# never installed by .claude/cloud-bootstrap.sh, so the session comes up green -# with the plugin's skills missing and no line of output naming what is -# absent. That is the docs/conventions/liveness-assertion/ shape -- a -# documented guarantee with no gate behind it -- and it costs exactly the -# dogfooding the directory-source marketplace exists to provide. -# -# WHERE ENABLEMENT LIVES. The fleet cloud plugin list in standards -# (components/cloud-environment/fleet-plugins.json) is what every cloud -# snapshot installs, and the bootstrap reads its snapshot copy overlaid with -# this repo's .claude/settings.json. So a catalogued plugin is covered when -# the fleet list enables it OR this file carries an explicit key for it, and -# this file does not mirror the whole catalog (a mirror writes one -# project-scope install record per plugin per checkout on every local session -# start). The fleet list is fetched from its published URL at gate time; an -# unreachable list is a usage error, never a pass. -# -# OFFLINE AND FORKS. The fleet list is required for correctness in the -# docs/plugin-philosophy.md "Prerequisites and failure behavior" sense: with no -# network, or from a fork whose standards repository lives elsewhere, the gate -# stops with exit 2 and names both overrides below rather than skipping. A -# visible skip would still read as a pass in the lane that runs this. +# WHAT A CLOUD SESSION HERE INSTALLS. The cloud environment's setup (setup.sh +# in the standards repository, components/cloud-environment/) derives the +# fleet list every snapshot installs from this catalog: each entry whose +# `defaultEnabled` is absent or `true`. .claude/cloud-bootstrap.sh reads that +# list overlaid with .claude/settings.json, whose `enabledPlugins` block +# carries only this repo's deltas: `false` opts out of an on-by-default +# plugin, `true` opts in to an off-by-default one. Because the list comes from +# the catalog, no catalogued plugin can go missing unannounced: it is on by +# default, or the catalog records it as off. So this gate checks the deltas +# block, not per-plugin coverage. The block does not mirror the catalog: a +# mirror writes one project-scope install record per plugin per checkout on +# every local session start. # # NOT COVERED ELSEWHERE. plugins/harness-config/skills/audit/scripts/ # check-plugin-drift.sh audits this same axis for CONSUMER repos. It reads this # repo's catalog too (a relative `directory` source), but it diffs the catalog # only against the `enabledPlugins` keys of one settings file, reports a plugin -# with no key as NEW (report only, exit 0), never reads the fleet list, and -# checks no key order, so it cannot gate this repo. -# scripts/check-plugin-manifest-presence.sh holds the catalog against the -# filesystem (manifest present, name matches, no unregistered directory); it -# says nothing about whether a catalogued plugin is ever enabled. +# with no key as NEW (report only, exit 0), and checks no key order, so it +# cannot gate this repo. scripts/check-plugin-manifest-presence.sh holds the +# catalog against the filesystem (manifest present, name matches, no +# unregistered directory). # -# WHAT IS CHECKED (both directions): -# 1. UNENABLED PLUGIN -- a .claude-plugin/marketplace.json entry that the -# fleet list does not enable AND that has no `@` key -# in .claude/settings.json `enabledPlugins`. -# 2. ORPHANED ENTRY -- an `enabledPlugins` key for this marketplace that +# WHAT IS CHECKED: +# 1. ORPHANED ENTRY -- an `enabledPlugins` key for this marketplace that # names no catalog entry. What a plugin rename or removal leaves behind; # the id resolves to nothing and the install silently no-ops. -# 3. UNSORTED KEYS -- `enabledPlugins` keys out of byte order. The same -# doc calls the alphabetical one-per-line layout the reason a single -# plugin can be flipped to `false` without disturbing the rest, and an -# insertion at the wrong point is how a duplicate-looking near-miss hides. +# 2. UNSORTED KEYS -- `enabledPlugins` keys out of byte order. +# docs/cloud-sessions.md calls the alphabetical one-per-line layout the +# reason a single plugin can be flipped without disturbing the rest, and +# an insertion at the wrong point is how a duplicate-looking near-miss +# hides. +# 3. IDENTITY -- the bootstrap's `marketplace_name` must equal the +# marketplace the settings file declares (see the check below). # -# A key set to `false` PASSES. An explicit `false` is a recorded decision: -# the gate treats every matching settings key as coverage regardless of -# value, so a disable of a plugin the fleet list never enabled still -# passes. When the fleet list does enable that plugin, the bootstrap's -# overlay honors the `false` as an opt-out (settings-wins). An absent key -# is the drift this gate exists to name. The two are different states and -# only one of them is silent. +# A key's value is not judged: `true` and `false` are both recorded deltas. # # Exit codes: 0 clean, 1 drift, 2 fatal (inputs missing or unreadable). # @@ -66,19 +47,8 @@ # PLUGIN_CATALOG_ENABLEMENT_MARKETPLACE -- path to marketplace.json # PLUGIN_CATALOG_ENABLEMENT_SETTINGS -- path to settings.json # PLUGIN_CATALOG_ENABLEMENT_BOOTSTRAP -- path to cloud-bootstrap.sh -# -# Env overrides (fleet list source): -# PLUGIN_CATALOG_ENABLEMENT_FLEET -- path to a local fleet list, -# instead of fetching the URL -# (offline, or the cloud -# snapshot's copy) -# PLUGIN_CATALOG_ENABLEMENT_FLEET_URL -- https URL to fetch it from -# (a fork or mirror); defaults -# to the standards repository set -euo pipefail -FLEET_URL="${PLUGIN_CATALOG_ENABLEMENT_FLEET_URL:-https://raw.githubusercontent.com/melodic-software/standards/main/components/cloud-environment/fleet-plugins.json}" - cd "$(dirname "${BASH_SOURCE[0]}")/.." if ! command -v jq >/dev/null 2>&1; then @@ -143,7 +113,7 @@ declared_keys="$(jq -r '.enabledPlugins // {} | keys_unsorted[]' "$SETTINGS" | t # file, so a regex metacharacter in it would change what the pattern matches # rather than what it says: a name like 'melodic.software' would make the '.' # match any character, silently accepting 'alpha@melodicXsoftware' as this -# marketplace's key and reporting the real plugin as unenabled. A gate whose +# marketplace's key and reporting it as an orphan. A gate whose # whole purpose is to catch silent drift must not have a silent-wrongness path # of its own. Flagged as informational (not a finding) by the security review # on #3235, on the grounds that the value is repo-controlled and crosses no @@ -156,51 +126,9 @@ enabled="$( done <<<"$declared_keys" | sort -u )" -# The fleet list: a local path from the test seam, else the published file. -# Fetch failure is fatal (exit 2): a gate that cannot read the list it judges -# by must not report green. -fleet_tmp='' -if [[ -n "${PLUGIN_CATALOG_ENABLEMENT_FLEET:-}" ]]; then - FLEET="$PLUGIN_CATALOG_ENABLEMENT_FLEET" -else - fleet_tmp="$(mktemp)" - trap 'rm -f "$fleet_tmp"' EXIT - if ! curl -fsSL --proto '=https' --connect-timeout 10 --max-time 30 --retry 2 --retry-delay 3 \ - "$FLEET_URL" -o "$fleet_tmp" 2>/dev/null; then - printf 'check-plugin-catalog-enablement: could not fetch the fleet list from %s\n' "$FLEET_URL" >&2 - echo ' The gate judges catalog coverage against that list; without it a green result would be a guess.' >&2 - echo ' Offline, point PLUGIN_CATALOG_ENABLEMENT_FLEET at a local copy; from a fork, set' >&2 - echo ' PLUGIN_CATALOG_ENABLEMENT_FLEET_URL to the https URL your standards repository publishes.' >&2 - exit 2 - fi - FLEET="$fleet_tmp" -fi -if ! jq -e 'type == "object" and ((.enabledPlugins // {}) | type == "object")' "$FLEET" >/dev/null 2>&1; then - printf 'check-plugin-catalog-enablement: fleet list %s is not a settings-shaped JSON object\n' "$FLEET" >&2 - exit 2 -fi -fleet_enabled="$( - jq -r --arg mp "$MARKET" '.enabledPlugins // {} | to_entries[] - | select(.value == true) | .key - | select(endswith("@" + $mp)) | .[:length - ($mp | length) - 1]' "$FLEET" | - tr -d '\r' | sort -u -)" -covered="$(printf '%s\n%s\n' "$enabled" "$fleet_enabled" | grep . | sort -u || true)" - errors=0 -# 1. FORWARD -- catalogued but enabled nowhere. -while IFS= read -r name; do - [[ -n "$name" ]] || continue - printf "UNENABLED PLUGIN: %s catalogs '%s', but the fleet list does not enable '%s@%s' and %s enabledPlugins has no key for it.\n" \ - "$MARKETPLACE" "$name" "$name" "$MARKET" "$SETTINGS" >&2 - printf " .claude/cloud-bootstrap.sh installs the fleet list overlaid with that file, so '%s' never loads in a session here.\n" \ - "$name" >&2 - printf " Add it to the fleet list in standards (components/cloud-environment/fleet-plugins.json), or carry an explicit key here.\n" >&2 - errors=$((errors + 1)) -done < <(comm -23 <(printf '%s\n' "$catalog") <(printf '%s\n' "$covered")) - -# 2. INVERSE -- enabled but no longer catalogued. +# 1. ORPHANS -- enabled but no longer catalogued. while IFS= read -r name; do [[ -n "$name" ]] || continue printf "ORPHANED ENABLED ENTRY: %s enables '%s@%s', but %s catalogs no such plugin.\n" \ @@ -209,7 +137,7 @@ while IFS= read -r name; do errors=$((errors + 1)) done < <(comm -13 <(printf '%s\n' "$catalog") <(printf '%s\n' "$enabled")) -# 3. LAYOUT -- keys must stay in byte order, one per line. +# 2. LAYOUT -- keys must stay in byte order, one per line. if [[ -n "$declared_keys" ]]; then if ! diff <(printf '%s\n' "$declared_keys") <(printf '%s\n' "$declared_keys" | LC_ALL=C sort) >/dev/null; then printf 'UNSORTED enabledPlugins: keys in %s are not in byte order.\n' "$SETTINGS" >&2 @@ -221,7 +149,7 @@ if [[ -n "$declared_keys" ]]; then fi fi -# 4. IDENTITY -- the bootstrap must agree on which marketplace this is. +# 3. IDENTITY -- the bootstrap must agree on which marketplace this is. # This gate derives the suffix from extraKnownMarketplaces; .claude/cloud- # bootstrap.sh hardcodes `marketplace_name` and selects its install set with # endswith("@" + $n). Rename the marketplace and update both settings keys and @@ -251,10 +179,8 @@ fi if ((errors > 0)); then { echo - echo "Every $MARKETPLACE entry must be enabled by the fleet list or carry an" - echo "enabledPlugins key in $SETTINGS, and every enabledPlugins key for the" - echo "'$MARKET' marketplace must name a catalogued plugin. A key set to false" - echo "is a recorded decision and passes; a plugin nothing enables is drift." + echo "Every enabledPlugins key in $SETTINGS for the '$MARKET' marketplace" + echo "must name a $MARKETPLACE entry, with keys in byte order, and" echo "$BOOTSTRAP must name that same marketplace, or what it installs and" echo "what this gate checks are two different sets." } >&2 @@ -262,4 +188,4 @@ if ((errors > 0)); then fi catalog_count="$(printf '%s\n' "$catalog" | grep -c . || true)" -echo "Every one of the $catalog_count catalogued plugins is enabled by the fleet list or carries an enabledPlugins key for '$MARKET'; none orphaned; keys sorted; $BOOTSTRAP installs that same marketplace." +echo "Every enabledPlugins key for '$MARKET' names one of the $catalog_count catalogued plugins; none orphaned; keys sorted; $BOOTSTRAP installs that same marketplace." diff --git a/scripts/check-plugin-catalog-enablement.test.sh b/scripts/check-plugin-catalog-enablement.test.sh index 28366c8813..41af308ff5 100755 --- a/scripts/check-plugin-catalog-enablement.test.sh +++ b/scripts/check-plugin-catalog-enablement.test.sh @@ -8,10 +8,10 @@ # # A green run on the current repo tree proves nothing about the gate -- the # tree is green by construction once the drift is fixed. These fixtures prove -# it goes RED on a catalogued plugin with no enabledPlugins key, on the inverse -# (an enabled id no catalog entry backs), and on unsorted keys; and that an -# explicit `false` still passes, because an off switch a gate rejects is an -# off switch nobody can use. +# it goes RED on an enabled id no catalog entry backs, on unsorted keys and on +# a bootstrap naming another marketplace; and that an explicit `false` and a +# catalogued plugin with no key both pass, because the settings block carries +# deltas only. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -76,44 +76,20 @@ write_bootstrap() { printf '#!/usr/bin/env bash\nmarketplace_name="%s"\n' "$1" >"$TMP/.claude/cloud-bootstrap.sh" } -FLEET="$TMP/fleet.json" -write_fleet() { - # "@" ids the fixture fleet list enables, one per - # argument; no arguments writes an empty list. - { - echo '{' - echo ' "extraKnownMarketplaces": {' - echo ' "fixture": {"source": {"source": "github", "repo": "o/r"}}' - echo ' },' - echo ' "enabledPlugins": {' - local first=1 id - for id in "$@"; do - if ((first)); then first=0; else echo ','; fi - printf ' "%s": true' "$id" - done - echo - echo ' }' - echo '}' - } >"$FLEET" -} - run() { ( cd "$TMP" && PLUGIN_CATALOG_ENABLEMENT_MARKETPLACE=".claude-plugin/marketplace.json" \ PLUGIN_CATALOG_ENABLEMENT_SETTINGS=".claude/settings.json" \ PLUGIN_CATALOG_ENABLEMENT_BOOTSTRAP=".claude/cloud-bootstrap.sh" \ - PLUGIN_CATALOG_ENABLEMENT_FLEET="${FLEET_OVERRIDE:-$FLEET}" \ bash "$SUT" 2>&1 ) } # Every fixture below declares the "fixture" marketplace unless it overrides # this, so the identity check agrees by default and each case exercises only -# the drift class it names. The fleet list is empty unless a case says -# otherwise, so the settings file alone has to cover the catalog there. +# the drift class it names. write_bootstrap fixture -write_fleet # --- 1. Happy path: catalog and enabledPlugins name the same set. ----------- write_marketplace alpha beta gamma @@ -126,55 +102,18 @@ else fail "happy path should pass (rc=$rc): $out" fi -# --- 2. Catalogued, enabled nowhere. ---------------------------------------- +# --- 2. Settings carries deltas only. --------------------------------------- +# The cloud plugin list is derived from the catalog, so a catalogued plugin +# with no key is not drift: it is on by default, or the catalog records it off. write_marketplace alpha beta gamma -printf 'alpha true\ngamma true\n' | write_settings -out="$(run)" -rc=$? -if [[ $rc -eq 1 ]] && grep -q 'UNENABLED PLUGIN' <<<"$out" && grep -q "'beta'" <<<"$out"; then - ok "a catalogued plugin enabled neither by the fleet list nor by settings fails the gate" -else - fail "plugin enabled nowhere should fail (rc=$rc): $out" -fi - -# --- 2b. The fleet list covers what settings does not mirror. --------------- -# Settings carries only deltas (here one opt-out) and the fleet list enables -# the rest of the catalog. -write_marketplace alpha beta gamma -write_fleet alpha@fixture beta@fixture gamma@fixture printf 'beta false\n' | write_settings out="$(run)" rc=$? if [[ $rc -eq 0 ]]; then - ok "a catalog the fleet list enables passes with a deltas-only settings block" -else - fail "fleet-covered catalog should pass (rc=$rc): $out" -fi - -# A fleet entry for another marketplace does not cover this catalog. -write_marketplace alpha beta -write_fleet alpha@fixture beta@other-market -printf 'alpha true\n' | write_settings -out="$(run)" -rc=$? -if [[ $rc -eq 1 ]] && grep -q "'beta'" <<<"$out"; then - ok "a fleet entry under another marketplace does not cover a catalogued plugin" -else - fail "foreign-marketplace fleet entry must not count as coverage (rc=$rc): $out" -fi - -# An unreadable or absent fleet list is fatal, never a pass. -write_marketplace alpha -printf 'alpha true\n' | write_settings -printf 'not json\n' >"$FLEET" -out="$(run)" -rc=$? -if [[ $rc -eq 2 ]] && grep -q 'not a settings-shaped JSON object' <<<"$out"; then - ok "a malformed fleet list exits 2" + ok "catalogued plugins with no enabledPlugins key pass" else - fail "malformed fleet list should exit 2 (rc=$rc): $out" + fail "a deltas-only settings block should pass (rc=$rc): $out" fi -write_fleet # --- 3. An explicit false is a decision, not drift. ------------------------- write_marketplace alpha beta gamma @@ -344,69 +283,4 @@ else fail "missing marketplace.json should exit 2 (rc=$rc): $out" fi -# --- 9. The fetch URL is overridable, and a failed fetch names the routes. ---- -# A curl shim records the URL it was handed and either serves the fixture fleet -# list or fails the way an offline host does, so no case touches the network. -SHIM_BIN="$TMP/shim-bin" -CURL_LOG="$TMP/curl-url.log" -mkdir -p "$SHIM_BIN" -cat >"$SHIM_BIN/curl" <<'EOF' -#!/usr/bin/env bash -out="" url="" -while (($#)); do - case "$1" in - -o) out="$2"; shift ;; - https://*) url="$1" ;; - esac - shift -done -printf '%s\n' "$url" >"$CURL_LOG" -[[ "$SHIM_CURL_MODE" == "ok" ]] || exit 6 -cp "$SHIM_FLEET" "$out" -EOF -chmod +x "$SHIM_BIN/curl" -run_fetch() { # [fleet-url-override] - : >"$CURL_LOG" - ( - cd "$TMP" && - PATH="$SHIM_BIN:$PATH" SHIM_CURL_MODE="$1" SHIM_FLEET="$FLEET" CURL_LOG="$CURL_LOG" \ - PLUGIN_CATALOG_ENABLEMENT_MARKETPLACE=".claude-plugin/marketplace.json" \ - PLUGIN_CATALOG_ENABLEMENT_SETTINGS=".claude/settings.json" \ - PLUGIN_CATALOG_ENABLEMENT_BOOTSTRAP=".claude/cloud-bootstrap.sh" \ - PLUGIN_CATALOG_ENABLEMENT_FLEET_URL="${2:-}" \ - bash "$SUT" 2>&1 - ) -} -write_marketplace alpha -printf 'alpha true\n' | write_settings -write_fleet -default_url='https://raw.githubusercontent.com/melodic-software/standards/main/components/cloud-environment/fleet-plugins.json' -out="$(run_fetch ok)" -rc=$? -if [[ $rc -eq 0 ]] && [[ "$(cat "$CURL_LOG")" == "$default_url" ]]; then - ok "with no override the fleet list is fetched from the standards repository" -else - fail "default fetch URL (rc=$rc, fetched '$(cat "$CURL_LOG")'): $out" -fi - -fork_url='https://raw.githubusercontent.com/example-fork/standards/main/fleet-plugins.json' -out="$(run_fetch ok "$fork_url")" -rc=$? -if [[ $rc -eq 0 ]] && [[ "$(cat "$CURL_LOG")" == "$fork_url" ]]; then - ok "PLUGIN_CATALOG_ENABLEMENT_FLEET_URL replaces the fetch URL" -else - fail "fetch URL override not honored (rc=$rc, fetched '$(cat "$CURL_LOG")'): $out" -fi - -out="$(run_fetch fail "$fork_url")" -rc=$? -if [[ $rc -eq 2 ]] && grep -Fq "could not fetch the fleet list from $fork_url" <<<"$out" && - grep -Fq 'PLUGIN_CATALOG_ENABLEMENT_FLEET at a local copy' <<<"$out" && - grep -Fq 'PLUGIN_CATALOG_ENABLEMENT_FLEET_URL' <<<"$out" && - ! grep -q 'none orphaned' <<<"$out"; then - ok "an unreachable fleet list exits 2 and names both overrides" -else - fail "unreachable fleet list should exit 2 with the override routes (rc=$rc): $out" -fi - test_harness::report From 7bb3279a7cad10b11d6ef128d51e70448cda69d1 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:35:51 -0400 Subject: [PATCH 2/3] docs: stop describing enabledPlugins as deltas only The block also carries keys that match the catalog default: they change nothing in the cloud and pin the plugin's state for local sessions in this repo. The doc, the gate header and the CI comment now say so. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 4 ++-- docs/cloud-sessions.md | 20 +++++++++++--------- scripts/check-plugin-catalog-enablement.sh | 13 +++++++------ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fc432eee9..14bdb0d705 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1048,10 +1048,10 @@ jobs: run: scripts/check-plugin-manifest-presence.sh # The cloud plugin list is derived from the catalog's defaultEnabled, and - # .claude/settings.json carries this repo's deltas over it. A delta that + # .claude/settings.json overlays its enabledPlugins keys on it. A key that # names no catalogued plugin silently no-ops, and .claude/cloud- # bootstrap.sh installs only the marketplace it hardcodes, so the gate - # holds the deltas to the catalog, their order, and that marketplace + # holds the keys to the catalog, their order, and that marketplace # name. Repo-wide and static, so it always runs. The self-test runs first # so a broken detector cannot mask a regression behind a green gate. - name: Test the plugin-catalog-enablement gate diff --git a/docs/cloud-sessions.md b/docs/cloud-sessions.md index 60072ce517..87fad3bc12 100644 --- a/docs/cloud-sessions.md +++ b/docs/cloud-sessions.md @@ -383,18 +383,20 @@ catalog on, and the cloud bootstrap installs from the two together (see `/opt/melodic-fleet-plugins.json`, and installs it at user scope; `cloud-bootstrap.sh` reads that snapshot copy overlaid with `.claude/settings.json`. **A plugin the catalog marks `defaultEnabled: false` is not installed in this repo's cloud sessions** unless the committed - block opts it in, so a regression in one surfaces in CI and local use, not here. The committed - `enabledPlugins` block carries only deltas: an explicit `false` opts out of an on-by-default - plugin, and `true` opts in to an off-by-default one. The block is project scope, so a `true` - key also enables that plugin in every local session in this repo. The block used to mirror the + block opts it in, so a regression in one surfaces in CI and local use, not here. In the + committed `enabledPlugins` block, an explicit `false` opts out of an on-by-default plugin, and + `true` opts in to an off-by-default one. The block is project scope, so every key also sets that + plugin's state in every local session in this repo, whatever the user's own scope says; a key + that matches the catalog default changes nothing in the cloud and is there for that local + effect. The block used to mirror the catalog, and a local session start in every checkout wrote one project-scope install record per - mirrored entry into the user's `installed_plugins.json` (#3688); the deltas-only block writes + mirrored entry into the user's `installed_plugins.json` (#3688); the current block writes none. The trade is context: every enabled plugin adds per-turn cost, so a *consumer* repo should opt out of what it does not need rather than copying anything wholesale. The cloud bootstrap provisions every tool the format/lint-on-edit hooks (`markdown-format`, `bash-format`, `biome-format`, `typos-format`, `actionlint`, `eol-normalizer`) shell out to. -- The `plugin-catalog-enablement-gate` CI lane holds the deltas block to the catalog: every key - for this marketplace must name a catalogued plugin, since a key that names nothing silently +- The `plugin-catalog-enablement-gate` CI lane holds the `enabledPlugins` block to the catalog: + every key for this marketplace must name a catalogued plugin, since a key that names nothing silently no-ops, and keys stay in byte order. It also checks that `cloud-bootstrap.sh`'s hardcoded `marketplace_name` still names the marketplace the settings file declares. The bootstrap selects what it installs with `endswith("@" + $n)`, so a rename that updated the settings and @@ -407,8 +409,8 @@ catalog on, and the cloud bootstrap installs from the two together (see - Entries are sorted alphabetically, one per line, so a single plugin can be flipped without disturbing the rest. The one opt-out recorded today is `playgrounds`: its skill is a wrapper over the first-party `playground` plugin on `claude-plugins-official`, which the cloud bootstrap - does not install, so enabled here it could only ever print install commands. Off-by-default - plugins are not keyed here; an operator who wants one turns it on with `/plugin enable`. Two + does not install, so enabled here it could only ever print install commands. An off-by-default + plugin with no key here stays off; an operator who wants one turns it on with `/plugin enable`. Two of them bundle MCP servers that need `userConfig` credentials this environment has no reason to hold, `miro` (`miro_api_token`) and `dometrain-mcp` (`dometrain_api_key`), set with `/plugin configure`. diff --git a/scripts/check-plugin-catalog-enablement.sh b/scripts/check-plugin-catalog-enablement.sh index 76d39b4fad..8805bbbede 100755 --- a/scripts/check-plugin-catalog-enablement.sh +++ b/scripts/check-plugin-catalog-enablement.sh @@ -9,12 +9,13 @@ # in the standards repository, components/cloud-environment/) derives the # fleet list every snapshot installs from this catalog: each entry whose # `defaultEnabled` is absent or `true`. .claude/cloud-bootstrap.sh reads that -# list overlaid with .claude/settings.json, whose `enabledPlugins` block -# carries only this repo's deltas: `false` opts out of an on-by-default -# plugin, `true` opts in to an off-by-default one. Because the list comes from -# the catalog, no catalogued plugin can go missing unannounced: it is on by -# default, or the catalog records it as off. So this gate checks the deltas -# block, not per-plugin coverage. The block does not mirror the catalog: a +# list overlaid with .claude/settings.json, whose `enabledPlugins` block can +# opt out (`false`) or opt in (`true`); a key matching the catalog default +# changes nothing in the cloud and pins the plugin's state for local sessions. +# Because the list comes from the catalog, no catalogued plugin can go missing +# unannounced: it is on by default, or the catalog records it as off. So this +# gate checks the block's keys, not per-plugin coverage. The block does not +# mirror the catalog: a # mirror writes one project-scope install record per plugin per checkout on # every local session start. # From 4b150dbd4d6a5ba4d4efff019aae191a5236be84 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:50:44 -0400 Subject: [PATCH 3/3] fix: cover the multi-agent config-root copy and correct the dometrain-mcp note scripts/sync-config-root.test.sh failed on main (PASS=5 FAIL=3) because #5890 added plugins/multi-agent/lib/config-root.sh to the sync cluster but not to the test's extra copies. docs/cloud-sessions.md implied dometrain-mcp has no enabledPlugins key; it has a default-matching false key. Co-Authored-By: Claude Opus 5.5 --- docs/cloud-sessions.md | 7 ++++--- scripts/sync-config-root.test.sh | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/cloud-sessions.md b/docs/cloud-sessions.md index 87fad3bc12..cbeedba97b 100644 --- a/docs/cloud-sessions.md +++ b/docs/cloud-sessions.md @@ -410,9 +410,10 @@ catalog on, and the cloud bootstrap installs from the two together (see disturbing the rest. The one opt-out recorded today is `playgrounds`: its skill is a wrapper over the first-party `playground` plugin on `claude-plugins-official`, which the cloud bootstrap does not install, so enabled here it could only ever print install commands. An off-by-default - plugin with no key here stays off; an operator who wants one turns it on with `/plugin enable`. Two - of them bundle MCP servers that need `userConfig` credentials this environment has no reason to - hold, `miro` (`miro_api_token`) and `dometrain-mcp` (`dometrain_api_key`), set with + plugin with no key here stays off; an operator who wants one turns it on with `/plugin enable`. + Two off-by-default plugins bundle MCP servers that need `userConfig` credentials this + environment has no reason to hold, `miro` (`miro_api_token`, no key here) and `dometrain-mcp` + (`dometrain_api_key`, pinned off by a default-matching `false` key), set with `/plugin configure`. ### GitHub MCP tools vs the gh CLI diff --git a/scripts/sync-config-root.test.sh b/scripts/sync-config-root.test.sh index dd5f23fff7..7b14a2badd 100755 --- a/scripts/sync-config-root.test.sh +++ b/scripts/sync-config-root.test.sh @@ -18,6 +18,7 @@ sync_cluster_suite::run \ --copy 'plugins/ai-slop/lib/config-root.sh' \ --extra-copy 'plugins/attribution/lib/config-root.sh' \ --extra-copy 'plugins/docs-naming/lib/config-root.sh' \ + --extra-copy 'plugins/multi-agent/lib/config-root.sh' \ --v1 'config_root_classify() { echo repo; }\n' \ --v2 'config_root_classify() { echo repo; }\nconfig_root_resolve() { echo /r; }\n' \ --drift 'config_root_classify() { echo home; }\n' \