diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3100ef7..acc27ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -166,6 +166,10 @@ jobs: template/ \ -x '*.git*' + - name: Build per-skill zips + if: steps.skip.outputs.skip != 'true' + run: scripts/package_skills.sh + - name: Create GitHub release with plugin zip if: steps.skip.outputs.skip != 'true' env: @@ -178,4 +182,5 @@ jobs: --title "${NEW_TAG}" \ --generate-notes \ --notes-start-tag "${PREV_TAG}" \ - secure-agent-playbook.zip + secure-agent-playbook.zip \ + dist/skills/*.zip diff --git a/.gitignore b/.gitignore index 2cf4794..fecb89f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ Thumbs.db .env.local .env.*.local +# Build output (scripts/package_skills.sh) +dist/ + # Node (if tooling added later) node_modules/ npm-debug.log* diff --git a/README.md b/README.md index 7f8e0a5..c6d3058 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,14 @@ Claude will automatically activate the relevant skill based on context. See [Ski > **Note:** Do not use GitHub's "Download ZIP" button — it nests files in a subdirectory that the plugin validator rejects. Always use the release asset zip. +**Organization skills** — For Claude organization admins uploading individual skills at [Organization settings > Skills](https://claude.ai/admin-settings/skills): + +1. Go to the [latest release](https://github.com/OWASP/secure-agent-playbook/releases/latest) +2. Download the zip for each skill you want (e.g. `web-security-review.zip`) +3. Upload it at **Organization settings > Skills > Upload a skill** + +> **Note:** The skills upload rejects zips with more than 200 files, so the full `secure-agent-playbook.zip` cannot be uploaded there — use the per-skill zips. To build them from a clone, run `scripts/package_skills.sh` and upload from `dist/skills/`. + **Local development** — To test from a local clone instead of GitHub: ``` /plugin marketplace add /path/to/agent-security-playbook diff --git a/scripts/package_skills.sh b/scripts/package_skills.sh new file mode 100755 index 0000000..760d333 --- /dev/null +++ b/scripts/package_skills.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Package each skill as a self-contained zip uploadable at +# https://claude.ai/admin-settings/skills, which rejects zips with more +# than 200 files (see issue #23 — the whole-repo zip cannot be used there). +# Skills reference plays/, templates/, and data/ relative to the plugin +# root, so those files are vendored into each zip at the same relative +# paths. +set -euo pipefail +cd "$(dirname "$0")/.." +OUT="$PWD/dist/skills" +rm -rf "$OUT" +mkdir -p "$OUT" + +for skill in plugins/*/skills/*/; do + plugin=$(dirname "$(dirname "$skill")") + name=$(basename "$skill") + stagedir="$OUT/.staging" + stage="$stagedir/$name" + rm -rf "$stagedir" + mkdir -p "$stage" + cp "$skill/SKILL.md" "$stage/" + + # Plays: only the specific play files the skill references. + for play in $(grep -ohE 'plays/[A-Za-z0-9._-]+\.md' "$skill/SKILL.md" | sort -u); do + mkdir -p "$stage/plays" + cp "$plugin/$play" "$stage/plays/" + done + + # Templates: the whole dir when referenced (two small files). + if grep -q 'templates/' "$skill/SKILL.md"; then + cp -R "$plugin/templates" "$stage/templates" + fi + + # Data: each referenced dataset, minus READMEs (never loaded at runtime). + for d in $(grep -ohE 'data/[a-z-]+' "$skill/SKILL.md" | sort -u); do + mkdir -p "$stage/$d" + find "$plugin/$d" -name '*.md' ! -name 'README.md' -exec cp {} "$stage/$d/" \; + done + + # ponytail: MASTG tests are only ever loaded via the mastg_tests lists in + # the MASVS files, so unreferenced tests are pruned to fit the 200-file + # limit; revisit if skills ever load mastg files directly. + if [ -d "$stage/data/mastg" ] && [ -d "$stage/data/masvs" ]; then + referenced=$(grep -rhoE 'MASTG-TEST-[0-9]+' "$stage/data/masvs" | sort -u) + for f in "$stage"/data/mastg/*.md; do + grep -qx "$(basename "$f" .md)" <<<"$referenced" || rm "$f" + done + fi + + count=$(find "$stage" -type f | wc -l | tr -d ' ') + if [ "$count" -gt 200 ]; then + echo "ERROR: $name stages $count files, exceeding the 200-file upload limit" >&2 + exit 1 + fi + (cd "$stagedir" && zip -qr "$OUT/$name.zip" "$name") + rm -rf "$stagedir" + echo "$name.zip ($count files)" +done