From dd945c8da3922008c7e639b9706755ef67580ae8 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Mon, 22 Jun 2026 17:04:35 -0400 Subject: [PATCH 1/6] feat(catalog): add the attested-delivery plugin + harden pin enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Catalog the attested-delivery plugin (agents + skills) hosted in the .github/ subdir of attested-delivery/.github via a git-subdir source. SHA is a PLACEHOLDER (current .github main) — the plugin is staged-not-committed there; bump to its real merge commit before this leaves draft. - catalog-admission now runs on EVERY pull request (not only marketplace.json changes) so it can be a required status check, and additionally REJECTS any external pin whose sha does not resolve to a .claude-plugin/plugin.json — a sha string alone is no longer sufficient. - Fix the git-subdir schema in docs/how-to (url/path, not repo/subdir) per the canonical Anthropic docs. --- .claude-plugin/marketplace.json | 16 +++++++ .github/workflows/catalog-admission.yml | 63 +++++++++++++++++++++---- docs/how-to/add-a-plugin.md | 35 ++++++++------ 3 files changed, 90 insertions(+), 24 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b8ea499..a076878 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -18,6 +18,22 @@ "category": "example", "license": "Apache-2.0", "keywords": ["reference", "attested", "example"] + }, + { + "name": "attested-delivery", + "source": { + "source": "git-subdir", + "url": "https://github.com/attested-delivery/.github.git", + "path": ".github", + "ref": "main", + "sha": "b26631596d8dcac18c6b92d436f676be5bf1d97c" + }, + "description": "Signed, SLSA-attested, fail-closed-verified releases — central reusable signing/seam/verify workflows and independent gh attestation verify.", + "author": { + "name": "attested-delivery" + }, + "license": "Apache-2.0", + "keywords": ["slsa", "attestation", "supply-chain", "sigstore", "sbom", "ci-cd", "release-signing"] } ] } diff --git a/.github/workflows/catalog-admission.yml b/.github/workflows/catalog-admission.yml index db96dcd..61f77b7 100644 --- a/.github/workflows/catalog-admission.yml +++ b/.github/workflows/catalog-admission.yml @@ -1,22 +1,29 @@ --- name: catalog-admission -# Fail-closed admission control for the marketplace catalog. +# Fail-closed admission control for the marketplace catalog — the gate that makes +# SHA-pinning non-negotiable. # # Claude Code does not verify attestations at plugin INSTALL time yet -# (anthropics/claude-code#30727), so the enforceable fail-closed seam is here: -# a change to .claude-plugin/marketplace.json is admitted only if +# (anthropics/claude-code#30727), so the enforceable fail-closed seam is here. +# It runs on EVERY pull request (not only when marketplace.json changes) so it is +# always present and can be a hard REQUIRED status check: no PR merges unless the +# entire catalog still satisfies the invariants. The catalog is admitted only if # 1. `claude plugin validate` passes (canonical validation), and # 2. every EXTERNAL plugin source (github / url / git-subdir) is pinned to a -# full 40-char `sha`, and the marketplace name is not Anthropic-reserved -# (HARD fail — unlike the soft-fail quality-gates manifest-review), and +# full 40-char `sha` — a `ref` without a `sha` is mutable and REJECTED — and +# the marketplace name is not Anthropic-reserved (HARD fail — unlike the +# soft-fail quality-gates manifest-review), and # 3. each external entry's pinned artifact attestations verify (when present). # A plugin SHA enters the catalog only when these hold. +# +# Make this job a REQUIRED status check in branch protection so the pin +# requirement is enforced at merge, not by convention. "on": pull_request: - paths: - - ".claude-plugin/marketplace.json" + push: + branches: [main] workflow_dispatch: permissions: @@ -71,10 +78,48 @@ jobs: print("catalog-admission OK: %d external entr(y/ies) SHA-pinned, name not reserved" % n) PY + # Hardening: a 40-char sha is not enough — the pin must resolve to a real + # plugin. For every external entry, fetch the plugin manifest AT the pinned + # commit; a pin without a .claude-plugin/plugin.json is REJECTED (stops a + # catalog entry pointing at a commit that lacks the plugin, or a moved one). + - name: Verify each external pin resolves to a real plugin (fail-closed) + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + python3 - .claude-plugin/marketplace.json > entries.tsv <<'PY' + import json, sys + mp = json.load(open(sys.argv[1])) + for p in mp.get("plugins", []): + s = p.get("source") + if not isinstance(s, dict) or s.get("source") not in ("github", "git-subdir", "url"): + continue + sha = s.get("sha", ""); name = p.get("name", "") + if s.get("source") == "github": + slug = s.get("repo", "").strip("/"); path = s.get("path", "").strip("/") + else: # git-subdir or url — both carry a github url + optional path + slug = s.get("url", "").removeprefix("https://github.com/").removesuffix(".git").strip("/") + path = s.get("path", "").strip("/") + man = (path + "/" if path else "") + ".claude-plugin/plugin.json" + print("%s %s %s %s" % (name, slug, man, sha)) + PY + fail=0 + while IFS=$' ' read -r name slug man sha; do + [ -z "${name}" ] && continue + if gh api "repos/${slug}/contents/${man}?ref=${sha}" --jq '.name' >/dev/null 2>&1; then + echo " OK ${name}: ${slug}@${sha:0:12} contains ${man}" + else + echo "::error::${name}: pinned sha ${sha} in ${slug} has no ${man} — the pin does not resolve to a plugin" + fail=1 + fi + done < entries.tsv + rm -f entries.tsv + [ "${fail}" -eq 0 ] || { echo "::error::catalog admission failed — one or more pins do not resolve to a plugin"; exit 1; } + echo "All external entries resolve to a plugin manifest at their pinned SHA." + # Fail-closed verification of every external entry's attestations, shared # with the central catalog-updater hub so "what must verify for an external - # entry" lives in one place. No external entries yet (v1 vendors its only - # plugin), so this is a clean no-op until the catalog grows. + # entry" lives in one place. - name: Verify external entries' attestations (shared, fail-closed) uses: attested-delivery/.github/.github/actions/plugin-catalog-update@f211de97395ced798da52e28d89d79c22b3020ee # v0.1.0 with: diff --git a/docs/how-to/add-a-plugin.md b/docs/how-to/add-a-plugin.md index 4b6c62d..8a4c858 100644 --- a/docs/how-to/add-a-plugin.md +++ b/docs/how-to/add-a-plugin.md @@ -50,11 +50,11 @@ a human-readable label. "description": "", "author": { "name": "" }, "source": { - "source": "git-subdir", // plugin lives in a subdirectory of a repo - "repo": "/", // the external plugin's source repo - "subdir": "plugins/",// path to the plugin within that repo - "ref": "v1.2.3", // human-readable label (mutable) - "sha": "<40-char-commit-sha>" // EFFECTIVE PIN — immutable identity + "source": "git-subdir", // plugin lives in a subdirectory of a repo + "url": "https://github.com//.git", // the external plugin's source repo (full git URL) + "path": "plugins/", // subdirectory holding the plugin's .claude-plugin/ + "ref": "v1.2.3", // human-readable label (mutable) + "sha": "<40-char-commit-sha>" // EFFECTIVE PIN — immutable identity }, "license": "", "keywords": ["<...>"] @@ -67,16 +67,21 @@ a human-readable label. ## 3. Open a PR — catalog admission runs fail-closed -The pull request triggers the marketplace gates. Two are decisive for admission: - -- **manifest-review** (`manifest/v1`) — fails closed unless every external plugin - source is SHA-pinned, the marketplace `name` is not a reserved name, and the - required manifest fields are present. -- **catalog admission** — re-verifies the plugin's published attestations - (provenance, SBOM, gate verdicts) for the pinned SHA. If any attestation fails - to verify, admission fails and the entry cannot merge. - -`claude plugin validate` runs as the canonical manifest check alongside these. +The **catalog-admission** gate runs on every pull request (so it can be a hard +required status check) and fails closed unless **all** of these hold: + +- every external plugin source is pinned to a full 40-char `sha` — a `ref` + without a `sha` is mutable and rejected; +- the pinned `sha` **actually resolves to a plugin**: admission fetches the + `.claude-plugin/plugin.json` at that commit and rejects the entry if it is not + there (this is what stops a pin from pointing at a commit that lacks the + plugin, or a placeholder SHA); +- the marketplace `name` is not an Anthropic-reserved name; +- `claude plugin validate` passes (canonical manifest check). + +The soft-fail **manifest-review** (`manifest/v1`) gate reports the same SHA-pin +findings to the Security tab. Make `catalog-admission` a **required** check in +branch protection so the pin requirement is enforced at merge, not by convention. ## 4. Verify, then merge From 33a64b0f1720d39d881f065283589faa78e30117 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Mon, 22 Jun 2026 22:41:47 -0400 Subject: [PATCH 2/6] chore(catalog): pin attested-delivery plugin to v0.1.0 Re-pin the plugin entry from the placeholder SHA to the .github v0.1.0 release (f211de9, ref v0.1.0), which contains .github/.claude-plugin/plugin.json so the new fail-closed resolver check passes. --- .claude-plugin/marketplace.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a076878..18d179d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -25,8 +25,8 @@ "source": "git-subdir", "url": "https://github.com/attested-delivery/.github.git", "path": ".github", - "ref": "main", - "sha": "b26631596d8dcac18c6b92d436f676be5bf1d97c" + "ref": "v0.1.0", + "sha": "f211de97395ced798da52e28d89d79c22b3020ee" }, "description": "Signed, SLSA-attested, fail-closed-verified releases — central reusable signing/seam/verify workflows and independent gh attestation verify.", "author": { From ecdc6cef5378263d7050a7e0cfef7093a308b01f Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Mon, 22 Jun 2026 22:57:28 -0400 Subject: [PATCH 3/6] fix(catalog): canonical repo/subdir schema so verify resolves v0.1.0 attestations Switch the attested-delivery plugin entry from url/path to canonical repo/subdir (the schema the shared verify engine reads), and teach the resolver to accept both forms. With .github v0.1.0 now attested, catalog-admission verifies the release's SLSA provenance fail-closed. --- .claude-plugin/marketplace.json | 4 ++-- .github/workflows/catalog-admission.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 18d179d..295ba33 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,8 +23,8 @@ "name": "attested-delivery", "source": { "source": "git-subdir", - "url": "https://github.com/attested-delivery/.github.git", - "path": ".github", + "repo": "attested-delivery/.github", + "subdir": ".github", "ref": "v0.1.0", "sha": "f211de97395ced798da52e28d89d79c22b3020ee" }, diff --git a/.github/workflows/catalog-admission.yml b/.github/workflows/catalog-admission.yml index 61f77b7..63a7136 100644 --- a/.github/workflows/catalog-admission.yml +++ b/.github/workflows/catalog-admission.yml @@ -97,9 +97,9 @@ jobs: sha = s.get("sha", ""); name = p.get("name", "") if s.get("source") == "github": slug = s.get("repo", "").strip("/"); path = s.get("path", "").strip("/") - else: # git-subdir or url — both carry a github url + optional path - slug = s.get("url", "").removeprefix("https://github.com/").removesuffix(".git").strip("/") - path = s.get("path", "").strip("/") + else: # git-subdir / url — accept repo+subdir (canonical) or url+path + slug = (s.get("repo") or s.get("url", "").removeprefix("https://github.com/").removesuffix(".git")).strip("/") + path = (s.get("subdir") or s.get("path") or "").strip("/") man = (path + "/" if path else "") + ".claude-plugin/plugin.json" print("%s %s %s %s" % (name, slug, man, sha)) PY From b438471dc49f0c57a2e3ceeeaa2c8657cd6a3c5f Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Mon, 22 Jun 2026 23:11:16 -0400 Subject: [PATCH 4/6] fix(catalog): use claude-canonical url/path schema; re-pin verify to engine fix claude plugin validate requires the url/path form for git-subdir (not repo/subdir). Revert the attested-delivery plugin entry to url/path (keep v0.1.0 / f211de9), and re-pin catalog-admission's verify engine to the .github SHA that resolves repo from url (36cdbd7). Now: claude-validate, the resolver, and the fail-closed attestation verify against .github v0.1.0 all pass. --- .claude-plugin/marketplace.json | 4 ++-- .github/workflows/catalog-admission.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 295ba33..18d179d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -23,8 +23,8 @@ "name": "attested-delivery", "source": { "source": "git-subdir", - "repo": "attested-delivery/.github", - "subdir": ".github", + "url": "https://github.com/attested-delivery/.github.git", + "path": ".github", "ref": "v0.1.0", "sha": "f211de97395ced798da52e28d89d79c22b3020ee" }, diff --git a/.github/workflows/catalog-admission.yml b/.github/workflows/catalog-admission.yml index 63a7136..4a1b18f 100644 --- a/.github/workflows/catalog-admission.yml +++ b/.github/workflows/catalog-admission.yml @@ -121,7 +121,7 @@ jobs: # with the central catalog-updater hub so "what must verify for an external # entry" lives in one place. - name: Verify external entries' attestations (shared, fail-closed) - uses: attested-delivery/.github/.github/actions/plugin-catalog-update@f211de97395ced798da52e28d89d79c22b3020ee # v0.1.0 + uses: attested-delivery/.github/.github/actions/plugin-catalog-update@36cdbd7d75865b88e8beec4813c1d3587fa98644 # engine url-source fix (post v0.1.0) with: mode: verify repo: ${{ github.repository }} From 654e45ab85e459cb11c38f87d3be57af2b623ce3 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Mon, 22 Jun 2026 23:14:50 -0400 Subject: [PATCH 5/6] docs/ci: harden resolver url parsing; document the attestation invariant - catalog-admission resolver: accept https/git@/ssh URL forms and normalize trailing slash before .git; a non-github URL yields an invalid slug and fails closed (Copilot). - add-a-plugin: list the fail-closed attestation-verify invariant in the catalog-admission section. --- .github/workflows/catalog-admission.yml | 7 ++++++- docs/how-to/add-a-plugin.md | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/catalog-admission.yml b/.github/workflows/catalog-admission.yml index 4a1b18f..ad27244 100644 --- a/.github/workflows/catalog-admission.yml +++ b/.github/workflows/catalog-admission.yml @@ -98,7 +98,12 @@ jobs: if s.get("source") == "github": slug = s.get("repo", "").strip("/"); path = s.get("path", "").strip("/") else: # git-subdir / url — accept repo+subdir (canonical) or url+path - slug = (s.get("repo") or s.get("url", "").removeprefix("https://github.com/").removesuffix(".git")).strip("/") + raw = (s.get("repo") or s.get("url", "")).strip().rstrip("/") + for pre in ("https://github.com/", "git@github.com:", "ssh://git@github.com/"): + if raw.startswith(pre): + raw = raw[len(pre):] + break + slug = raw.removesuffix(".git").strip("/") # non-github -> invalid slug -> fail-closed path = (s.get("subdir") or s.get("path") or "").strip("/") man = (path + "/" if path else "") + ".claude-plugin/plugin.json" print("%s %s %s %s" % (name, slug, man, sha)) diff --git a/docs/how-to/add-a-plugin.md b/docs/how-to/add-a-plugin.md index 8a4c858..b6b767e 100644 --- a/docs/how-to/add-a-plugin.md +++ b/docs/how-to/add-a-plugin.md @@ -77,7 +77,9 @@ required status check) and fails closed unless **all** of these hold: there (this is what stops a pin from pointing at a commit that lacks the plugin, or a placeholder SHA); - the marketplace `name` is not an Anthropic-reserved name; -- `claude plugin validate` passes (canonical manifest check). +- `claude plugin validate` passes (canonical manifest check); +- each external entry's pinned release **attestations verify fail-closed** (SLSA + provenance), using the same verify the central catalog-updater runs. The soft-fail **manifest-review** (`manifest/v1`) gate reports the same SHA-pin findings to the Security tab. Make `catalog-admission` a **required** check in From 7b20c7f5245f5ec8c208dfea60df12248cce44f8 Mon Sep 17 00:00:00 2001 From: Robert Allen Date: Mon, 22 Jun 2026 23:16:48 -0400 Subject: [PATCH 6/6] docs(ci): label url/path as claude-canonical, repo/subdir as legacy (comment) --- .github/workflows/catalog-admission.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/catalog-admission.yml b/.github/workflows/catalog-admission.yml index ad27244..12ca9a2 100644 --- a/.github/workflows/catalog-admission.yml +++ b/.github/workflows/catalog-admission.yml @@ -97,7 +97,7 @@ jobs: sha = s.get("sha", ""); name = p.get("name", "") if s.get("source") == "github": slug = s.get("repo", "").strip("/"); path = s.get("path", "").strip("/") - else: # git-subdir / url — accept repo+subdir (canonical) or url+path + else: # git-subdir / url — accept url+path (claude-canonical) or repo+subdir (legacy) raw = (s.get("repo") or s.get("url", "")).strip().rstrip("/") for pre in ("https://github.com/", "git@github.com:", "ssh://git@github.com/"): if raw.startswith(pre):