mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
Enhance/skill release (#8)
* Refactor skill packaging and checksum generation process - Removed .skill package creation from the skill-release workflow and scripts, focusing on checksum generation only. - Updated README and SKILL.md files to reflect new installation methods using clawhub. - Simplified the skill checksums generator script to only generate checksums without packaging. - Adjusted installation instructions across various skills to promote clawhub for easier installation. - Enhanced error handling and verification steps in the installation scripts for individual files. * Add ext-docs to .gitignore to exclude documentation files from version control
This commit is contained in:
+362
-109
@@ -4,6 +4,10 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- '*-v[0-9]*.[0-9]*.[0-9]*'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'skills/*/skill.json'
|
||||
- 'skills/*/SKILL.md'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -15,7 +19,349 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
validate-pr-version-sync:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate version parity for bumped skills
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
get_md_version() {
|
||||
local md_file="$1"
|
||||
awk '
|
||||
NR == 1 && $0 == "---" { in_frontmatter = 1; next }
|
||||
in_frontmatter && $0 == "---" { exit }
|
||||
in_frontmatter && $0 ~ /^version:[[:space:]]*/ {
|
||||
sub(/^version:[[:space:]]*/, "", $0)
|
||||
gsub(/[[:space:]]+$/, "", $0)
|
||||
print $0
|
||||
exit
|
||||
}
|
||||
' "$md_file"
|
||||
}
|
||||
|
||||
get_md_version_from_git() {
|
||||
local sha="$1"
|
||||
local path="$2"
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
if git cat-file -e "${sha}:${path}" 2>/dev/null; then
|
||||
git show "${sha}:${path}" > "$tmp_file"
|
||||
get_md_version "$tmp_file"
|
||||
fi
|
||||
|
||||
rm -f "$tmp_file"
|
||||
}
|
||||
|
||||
touched_skills_file="$(mktemp)"
|
||||
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- 'skills/*/skill.json' 'skills/*/SKILL.md' \
|
||||
| awk -F/ 'NF >= 3 {print $1 "/" $2}' \
|
||||
| sort -u > "${touched_skills_file}"
|
||||
|
||||
if [ ! -s "${touched_skills_file}" ]; then
|
||||
echo "No skill metadata files changed in this PR."
|
||||
rm -f "${touched_skills_file}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
checked_skills=0
|
||||
failures=0
|
||||
|
||||
while IFS= read -r skill_dir; do
|
||||
json_path="${skill_dir}/skill.json"
|
||||
md_path="${skill_dir}/SKILL.md"
|
||||
|
||||
head_json_version=""
|
||||
if [ -f "${json_path}" ]; then
|
||||
head_json_version="$(jq -r '.version // empty' "${json_path}" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
head_md_version=""
|
||||
if [ -f "${md_path}" ]; then
|
||||
head_md_version="$(get_md_version "${md_path}")"
|
||||
fi
|
||||
|
||||
base_json_version=""
|
||||
if git cat-file -e "${BASE_SHA}:${json_path}" 2>/dev/null; then
|
||||
base_json_version="$(git show "${BASE_SHA}:${json_path}" | jq -r '.version // empty' 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
base_md_version="$(get_md_version_from_git "${BASE_SHA}" "${md_path}")"
|
||||
|
||||
json_version_changed=false
|
||||
md_version_changed=false
|
||||
|
||||
if [ "${head_json_version}" != "${base_json_version}" ]; then
|
||||
json_version_changed=true
|
||||
fi
|
||||
|
||||
if [ "${head_md_version}" != "${base_md_version}" ]; then
|
||||
md_version_changed=true
|
||||
fi
|
||||
|
||||
if [ "${json_version_changed}" != "true" ] && [ "${md_version_changed}" != "true" ]; then
|
||||
echo "No version bump detected for ${skill_dir}; skipping."
|
||||
continue
|
||||
fi
|
||||
|
||||
checked_skills=$((checked_skills + 1))
|
||||
echo "Version bump detected for ${skill_dir} (skill.json changed: ${json_version_changed}, SKILL.md changed: ${md_version_changed})"
|
||||
|
||||
if [ ! -f "${json_path}" ]; then
|
||||
echo "::error file=${json_path}::Missing skill.json after version bump."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ ! -f "${md_path}" ]; then
|
||||
echo "::error file=${md_path}::Missing SKILL.md after version bump."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${head_json_version}" ]; then
|
||||
echo "::error file=${json_path}::Missing .version in skill.json."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "${head_md_version}" ]; then
|
||||
echo "::error file=${md_path}::Missing version in SKILL.md frontmatter."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "${head_json_version}" != "${head_md_version}" ]; then
|
||||
echo "::error file=${json_path}::Version mismatch. skill.json=${head_json_version}, SKILL.md=${head_md_version}"
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Version parity OK for ${skill_dir}: ${head_json_version}"
|
||||
done < "${touched_skills_file}"
|
||||
|
||||
rm -f "${touched_skills_file}"
|
||||
|
||||
if [ "${checked_skills}" -eq 0 ]; then
|
||||
echo "No version bumps detected in changed skill metadata files."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${failures}" -gt 0 ]; then
|
||||
echo "::error::Found ${failures} version parity issue(s) across ${checked_skills} bumped skill(s)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validated ${checked_skills} bumped skill(s): skill.json and SKILL.md versions are present and equal."
|
||||
|
||||
release:
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: validate-pr-version-sync
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run release dry-run for changed skills
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
get_md_version() {
|
||||
local md_file="$1"
|
||||
awk '
|
||||
NR == 1 && $0 == "---" { in_frontmatter = 1; next }
|
||||
in_frontmatter && $0 == "---" { exit }
|
||||
in_frontmatter && $0 ~ /^version:[[:space:]]*/ {
|
||||
sub(/^version:[[:space:]]*/, "", $0)
|
||||
gsub(/[[:space:]]+$/, "", $0)
|
||||
print $0
|
||||
exit
|
||||
}
|
||||
' "$md_file"
|
||||
}
|
||||
|
||||
touched_skills_file="$(mktemp)"
|
||||
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- 'skills/*/skill.json' 'skills/*/SKILL.md' \
|
||||
| awk -F/ 'NF >= 3 {print $1 "/" $2}' \
|
||||
| sort -u > "${touched_skills_file}"
|
||||
|
||||
if [ ! -s "${touched_skills_file}" ]; then
|
||||
echo "No skill metadata files changed in this PR."
|
||||
rm -f "${touched_skills_file}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
dry_run_count=0
|
||||
failures=0
|
||||
mkdir -p dist/dry-run
|
||||
|
||||
while IFS= read -r skill_dir; do
|
||||
json_path="${skill_dir}/skill.json"
|
||||
md_path="${skill_dir}/SKILL.md"
|
||||
|
||||
head_json_version=""
|
||||
if [ -f "${json_path}" ]; then
|
||||
head_json_version="$(jq -r '.version // empty' "${json_path}" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
head_md_version=""
|
||||
if [ -f "${md_path}" ]; then
|
||||
head_md_version="$(get_md_version "${md_path}")"
|
||||
fi
|
||||
|
||||
if [ -z "${head_json_version}" ] || [ -z "${head_md_version}" ] || [ "${head_json_version}" != "${head_md_version}" ]; then
|
||||
echo "::error file=${skill_dir}::Version metadata is invalid for dry-run. Ensure validate-pr-version-sync passes."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ ! -f "${json_path}" ]; then
|
||||
echo "::error file=${json_path}::Missing skill.json."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ ! -f "${md_path}" ]; then
|
||||
echo "::error file=${md_path}::Missing SKILL.md."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! jq -e '.name and .version and .sbom and .sbom.files and (.sbom.files | type == "array")' "${json_path}" >/dev/null 2>&1; then
|
||||
echo "::error file=${json_path}::skill.json missing required release fields (name/version/sbom.files)."
|
||||
failures=$((failures + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
skill_name="$(basename "${skill_dir}")"
|
||||
version="${head_json_version}"
|
||||
tag="${skill_name}-v${version}"
|
||||
dry_run_count=$((dry_run_count + 1))
|
||||
|
||||
echo "::group::Dry-run release ${tag}"
|
||||
|
||||
out_root="dist/dry-run/${tag}"
|
||||
out_dist="${out_root}/dist"
|
||||
out_assets="${out_root}/release-assets"
|
||||
checksums_file="${out_dist}/checksums.json"
|
||||
mkdir -p "${out_dist}" "${out_assets}"
|
||||
|
||||
cat > "${checksums_file}" << EOF
|
||||
{
|
||||
"skill": "${skill_name}",
|
||||
"version": "${version}",
|
||||
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"repository": "${{ github.repository }}",
|
||||
"tag": "${tag}",
|
||||
"files": {
|
||||
EOF
|
||||
|
||||
first=true
|
||||
temp_sbom_file="$(mktemp)"
|
||||
jq -r '.sbom.files[].path' "${json_path}" > "${temp_sbom_file}"
|
||||
|
||||
while IFS= read -r file; do
|
||||
[ -z "${file}" ] && continue
|
||||
full_path="${skill_dir}/${file}"
|
||||
if [ -f "${full_path}" ]; then
|
||||
sha256="$(sha256sum "${full_path}" | awk '{print $1}')"
|
||||
size="$(stat -c%s "${full_path}" 2>/dev/null || stat -f%z "${full_path}")"
|
||||
filename="$(basename "${file}")"
|
||||
|
||||
if [ "${first}" = true ]; then
|
||||
first=false
|
||||
else
|
||||
echo " ," >> "${checksums_file}"
|
||||
fi
|
||||
|
||||
cat >> "${checksums_file}" << FILEENTRY
|
||||
"${filename}": {
|
||||
"sha256": "${sha256}",
|
||||
"size": ${size},
|
||||
"path": "${file}",
|
||||
"url": "https://github.com/${{ github.repository }}/releases/download/${tag}/${filename}"
|
||||
}
|
||||
FILEENTRY
|
||||
|
||||
cp "${full_path}" "${out_assets}/${filename}"
|
||||
else
|
||||
echo "::error file=${json_path}::SBOM references missing file: ${file}"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
done < "${temp_sbom_file}"
|
||||
|
||||
rm -f "${temp_sbom_file}"
|
||||
|
||||
skill_json_sha="$(sha256sum "${json_path}" | awk '{print $1}')"
|
||||
skill_json_size="$(stat -c%s "${json_path}" 2>/dev/null || stat -f%z "${json_path}")"
|
||||
|
||||
cat >> "${checksums_file}" << SKILLJSON
|
||||
,
|
||||
"skill.json": {
|
||||
"sha256": "${skill_json_sha}",
|
||||
"size": ${skill_json_size},
|
||||
"url": "https://github.com/${{ github.repository }}/releases/download/${tag}/skill.json"
|
||||
}
|
||||
SKILLJSON
|
||||
|
||||
cat >> "${checksums_file}" << EOF
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
if ! jq -e . "${checksums_file}" >/dev/null 2>&1; then
|
||||
echo "::error file=${checksums_file}::Generated checksums.json is invalid JSON."
|
||||
failures=$((failures + 1))
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
|
||||
cp "${json_path}" "${out_assets}/skill.json"
|
||||
if [ -f "${skill_dir}/README.md" ]; then
|
||||
cp "${skill_dir}/README.md" "${out_assets}/README.md"
|
||||
fi
|
||||
cp "${checksums_file}" "${out_assets}/checksums.json"
|
||||
|
||||
echo "Prepared dry-run assets for ${tag}:"
|
||||
ls -la "${out_assets}"
|
||||
echo "::endgroup::"
|
||||
done < "${touched_skills_file}"
|
||||
|
||||
rm -f "${touched_skills_file}"
|
||||
|
||||
if [ "${failures}" -gt 0 ]; then
|
||||
echo "::error::Release dry-run failed with ${failures} issue(s) across ${dry_run_count} skill(s)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${dry_run_count}" -eq 0 ]; then
|
||||
echo "No changed skills found for dry-run."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Release dry-run completed successfully for ${dry_run_count} changed skill(s)."
|
||||
|
||||
release-tag:
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
|
||||
@@ -191,96 +537,7 @@ jobs:
|
||||
}
|
||||
SKILLJSON
|
||||
|
||||
# Note: checksums.json is NOT closed here - will be finalized after .skill package is created
|
||||
|
||||
echo "=== Intermediate checksums.json (before .skill) ==="
|
||||
cat "dist/checksums.json"
|
||||
|
||||
- name: Bundle security skills into suite
|
||||
if: steps.parse.outputs.skill_name == 'clawsec-suite'
|
||||
run: |
|
||||
SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
|
||||
|
||||
echo "=== Bundling security skills into suite ==="
|
||||
|
||||
# Create bundled directory
|
||||
mkdir -p "$SKILL_PATH/bundled"
|
||||
|
||||
# List of skills to bundle (exclude clawtributor - opt-in only)
|
||||
BUNDLE_SKILLS=("clawsec-feed" "openclaw-audit-watchdog" "soul-guardian")
|
||||
|
||||
for skill in "${BUNDLE_SKILLS[@]}"; do
|
||||
if [ -d "skills/$skill" ]; then
|
||||
echo "Bundling $skill..."
|
||||
mkdir -p "$SKILL_PATH/bundled/$skill"
|
||||
cp -r "skills/$skill"/* "$SKILL_PATH/bundled/$skill/"
|
||||
|
||||
# Verify skill.json exists
|
||||
if [ -f "$SKILL_PATH/bundled/$skill/skill.json" ]; then
|
||||
SKILL_VERSION=$(jq -r '.version' "$SKILL_PATH/bundled/$skill/skill.json")
|
||||
echo "✓ Bundled $skill v${SKILL_VERSION}"
|
||||
else
|
||||
echo "ERROR: $skill/skill.json not found"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "WARNING: skills/$skill not found, skipping..."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Bundling complete"
|
||||
|
||||
- name: Create .skill package
|
||||
run: |
|
||||
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
|
||||
SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
|
||||
|
||||
cd "$SKILL_PATH"
|
||||
|
||||
# Create zip starting with skill.json
|
||||
zip -q "../../dist/${SKILL_NAME}.skill" skill.json
|
||||
|
||||
# Add each SBOM file individually to preserve directory structure
|
||||
while IFS= read -r file; do
|
||||
if [ -f "$file" ]; then
|
||||
zip -qu "../../dist/${SKILL_NAME}.skill" "$file"
|
||||
echo "Added: $file"
|
||||
else
|
||||
echo "Warning: SBOM file not found: $file"
|
||||
fi
|
||||
done < <(jq -r '.sbom.files[].path' skill.json)
|
||||
|
||||
# Add README if it exists
|
||||
if [ -f README.md ]; then
|
||||
zip -qu "../../dist/${SKILL_NAME}.skill" README.md
|
||||
echo "Added: README.md"
|
||||
fi
|
||||
|
||||
cd ../..
|
||||
|
||||
echo "=== Created ${SKILL_NAME}.skill ==="
|
||||
unzip -l "dist/${SKILL_NAME}.skill"
|
||||
|
||||
- name: Add .skill checksum and finalize checksums.json
|
||||
run: |
|
||||
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
|
||||
SKILL_PACKAGE="dist/${SKILL_NAME}.skill"
|
||||
|
||||
# Calculate .skill package checksum
|
||||
SKILL_PACKAGE_SHA=$(sha256sum "$SKILL_PACKAGE" | awk '{print $1}')
|
||||
SKILL_PACKAGE_SIZE=$(stat -c%s "$SKILL_PACKAGE" 2>/dev/null || stat -f%z "$SKILL_PACKAGE")
|
||||
|
||||
# Add .skill package entry to checksums
|
||||
cat >> "dist/checksums.json" << SKILLPACKAGE
|
||||
,
|
||||
"${SKILL_NAME}.skill": {
|
||||
"sha256": "${SKILL_PACKAGE_SHA}",
|
||||
"size": ${SKILL_PACKAGE_SIZE},
|
||||
"url": "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${SKILL_NAME}.skill"
|
||||
}
|
||||
SKILLPACKAGE
|
||||
|
||||
# Close JSON
|
||||
# Close checksums JSON
|
||||
cat >> "dist/checksums.json" << EOF
|
||||
}
|
||||
}
|
||||
@@ -301,12 +558,6 @@ jobs:
|
||||
jq -r '.sbom.files[].path' "$SKILL_PATH/skill.json" > "$TEMPFILE"
|
||||
|
||||
while IFS= read -r file; do
|
||||
# Skip bundled files - they're only for the .skill package
|
||||
if [[ "$file" == bundled/* ]]; then
|
||||
echo "Skipping bundled file: $file"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -f "$SKILL_PATH/$file" ]; then
|
||||
# Flatten directory structure for release assets
|
||||
cp "$SKILL_PATH/$file" "release-assets/$(basename "$file")"
|
||||
@@ -322,8 +573,7 @@ jobs:
|
||||
cp "$SKILL_PATH/README.md" release-assets/
|
||||
fi
|
||||
|
||||
# Copy package and checksums
|
||||
cp "dist/${SKILL_NAME}.skill" release-assets/
|
||||
# Copy checksums
|
||||
cp "dist/checksums.json" release-assets/
|
||||
|
||||
echo "=== Release assets ==="
|
||||
@@ -362,29 +612,32 @@ jobs:
|
||||
|
||||
### Quick Install
|
||||
|
||||
Download the complete skill package:
|
||||
**Via clawhub (recommended):**
|
||||
```bash
|
||||
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/${{ steps.parse.outputs.skill_name }}.skill
|
||||
npx clawhub@latest install ${{ steps.parse.outputs.skill_name }}
|
||||
```
|
||||
|
||||
Or fetch the main skill file directly:
|
||||
**Manual download with verification:**
|
||||
```bash
|
||||
curl -sL https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/SKILL.md
|
||||
# 1. Download checksums
|
||||
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.json
|
||||
|
||||
# 2. Download individual files
|
||||
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/SKILL.md
|
||||
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/skill.json
|
||||
|
||||
# 3. Verify checksums
|
||||
sha256sum SKILL.md
|
||||
# Compare with value in checksums.json
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
All files include SHA256 checksums. Download `checksums.json` and verify:
|
||||
All files include SHA256 checksums in `checksums.json`:
|
||||
```bash
|
||||
curl -sL https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.json | jq .
|
||||
```
|
||||
|
||||
Verify a file:
|
||||
```bash
|
||||
sha256sum SKILL.md
|
||||
# Compare with value in checksums.json
|
||||
```
|
||||
|
||||
### Files
|
||||
|
||||
See `checksums.json` for the complete file manifest with SHA256 hashes.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
.codex
|
||||
_bmad
|
||||
_bmad-output
|
||||
ext-docs
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
+59
-30
@@ -10,6 +10,7 @@ Thank you for your interest in contributing security skills to the ClawSec ecosy
|
||||
- [skill.json Reference](#skilljson-reference)
|
||||
- [Testing Your Skill](#testing-your-skill)
|
||||
- [Submission Process](#submission-process)
|
||||
- [Version Bump and Release Flow](#version-bump-and-release-flow)
|
||||
- [Review Criteria](#review-criteria)
|
||||
- [After Acceptance](#after-acceptance)
|
||||
- [Submitting Security Advisories](#submitting-security-advisories)
|
||||
@@ -49,7 +50,7 @@ git checkout -b skill/my-new-skill
|
||||
All skills distributed through ClawSec undergo security review and are hashed for agent verification. Trust is implicit:
|
||||
|
||||
- **Backend Verification**: Every skill is validated against checksums, SBOM manifests, and security policies
|
||||
- **Transparent Security**: SHA256 checksums, signature verification, and advisory feeds operate automatically
|
||||
- **Transparent Security**: SHA256 checksums, and advisory feeds operate automatically
|
||||
- **Contribution Flow**: Submit skills via PR → maintainer review → approval → release
|
||||
|
||||
|
||||
@@ -145,14 +146,22 @@ Create `skill.json` with the following structure:
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Start with version `0.0.1`
|
||||
- Start with version `0.0.1` in both `skill.json` and `SKILL.md` frontmatter
|
||||
- List ALL files your skill needs in the SBOM
|
||||
|
||||
### Step 3: Create SKILL.md
|
||||
|
||||
This is the main documentation for your skill. Use this template:
|
||||
This is the main documentation for your skill. Include YAML frontmatter with a `version` that matches `skill.json`:
|
||||
|
||||
````markdown
|
||||
```markdown
|
||||
---
|
||||
name: my-skill-name
|
||||
version: 0.0.1
|
||||
description: Brief description of what your skill does
|
||||
metadata: {"openclaw":{"emoji":"🔒","category":"security"}}
|
||||
---
|
||||
|
||||
# My Skill Name
|
||||
|
||||
## Overview
|
||||
@@ -161,11 +170,7 @@ Brief description of what this skill does and why it's useful for AI agent secur
|
||||
|
||||
## Usage
|
||||
|
||||
How to use the skill:
|
||||
|
||||
```bash
|
||||
# Example commands or usage patterns
|
||||
```
|
||||
How to use the skill.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -182,25 +187,8 @@ How to use the skill:
|
||||
## Security Considerations
|
||||
|
||||
Important security notes about this skill.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Basic Usage
|
||||
|
||||
Description and example output.
|
||||
|
||||
### Example 2: Advanced Usage
|
||||
|
||||
Description and example output.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions.
|
||||
|
||||
## Contributing
|
||||
|
||||
How others can improve this skill.
|
||||
```
|
||||
````
|
||||
|
||||
### Step 4: Add Supporting Files
|
||||
|
||||
@@ -314,7 +302,8 @@ If your skill includes executable scripts or requires testing:
|
||||
|
||||
- [ ] All SBOM files exist
|
||||
- [ ] skill.json is valid JSON
|
||||
- [ ] Version is 1.0.0 for new skills
|
||||
- [ ] Version is `0.0.1` for new skills
|
||||
- [ ] `skill.json` version matches `SKILL.md` frontmatter version
|
||||
- [ ] No hardcoded credentials or secrets
|
||||
- [ ] Trigger phrases are descriptive
|
||||
- [ ] Required binaries are documented
|
||||
@@ -380,6 +369,39 @@ Any special considerations for reviewers.
|
||||
|
||||
---
|
||||
|
||||
## Version Bump and Release Flow
|
||||
|
||||
This repository uses a branch-first workflow for skill versions:
|
||||
|
||||
1. Make skill changes on a branch (`skill/<name>-...`).
|
||||
2. Keep versions in sync:
|
||||
- `skills/<skill>/skill.json` -> `.version`
|
||||
- `skills/<skill>/SKILL.md` -> frontmatter `version`
|
||||
3. For existing skills, you can bump versions on your branch with:
|
||||
|
||||
```bash
|
||||
./scripts/release-skill.sh <skill-name> <new-version>
|
||||
```
|
||||
|
||||
4. Push your branch and open a PR. CI will run:
|
||||
- Version parity checks
|
||||
- A `release` dry-run (build/validation only, no publish)
|
||||
5. Do **not** push release tags from PR branches.
|
||||
- `scripts/release-skill.sh` creates a local tag. Keep it local during PR review.
|
||||
- If you need to remove that local tag: `git tag -d <skill-name>-v<version>`
|
||||
6. After merge, a maintainer creates and pushes the release tag from `main`:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull --ff-only origin main
|
||||
git tag -a <skill-name>-v<version> -m "<skill-name> version <version>"
|
||||
git push origin <skill-name>-v<version>
|
||||
```
|
||||
|
||||
7. Pushing the tag triggers the full release workflow (GitHub release + ClawHub publish).
|
||||
|
||||
---
|
||||
|
||||
## Review Criteria
|
||||
|
||||
Maintainers will review your skill based on:
|
||||
@@ -419,8 +441,8 @@ Once your skill is accepted:
|
||||
1. **Maintainers will:**
|
||||
- Review your PR (Prompt Security staff or designated maintainers)
|
||||
- Merge your PR after security review
|
||||
- Create the first release using `scripts/release-skill.sh`
|
||||
- Generate checksums and publish to GitHub Releases
|
||||
- Create and push a release tag from merged `main` (`<skill>-v<version>`)
|
||||
- Generate checksums and publish to GitHub Releases + ClawHub
|
||||
- Update the skills catalog website
|
||||
|
||||
2. **You'll be credited:**
|
||||
@@ -463,7 +485,7 @@ mkdir -p skills/simple-scanner
|
||||
cat > skills/simple-scanner/skill.json << 'EOF'
|
||||
{
|
||||
"name": "simple-scanner",
|
||||
"version": "0.0.1,
|
||||
"version": "0.0.1",
|
||||
"description": "Basic security scanner for AI agents",
|
||||
"author": "contributor-name",
|
||||
"license": "MIT",
|
||||
@@ -484,6 +506,13 @@ cat > skills/simple-scanner/skill.json << 'EOF'
|
||||
EOF
|
||||
|
||||
cat > skills/simple-scanner/SKILL.md << 'EOF'
|
||||
---
|
||||
name: simple-scanner
|
||||
version: 0.0.1
|
||||
description: Basic security scanner for AI agents
|
||||
metadata: {"openclaw":{"emoji":"🔍","category":"security"}}
|
||||
---
|
||||
|
||||
# Simple Scanner
|
||||
|
||||
A basic security scanner for AI agents.
|
||||
|
||||
@@ -41,7 +41,7 @@ ClawSec is a **complete security skill suite for the OpenClaw family of agents (
|
||||
- **🛡️ File Integrity Protection** - Drift detection and auto-restore for critical agent files (SOUL.md, IDENTITY.md, etc.)
|
||||
- **📡 Live Security Advisories** - Automated NVD CVE polling and community threat intelligence
|
||||
- **🔍 Security Audits** - Self-check scripts to detect prompt injection markers and vulnerabilities
|
||||
- **🔐 Checksum Verification** - SHA256 checksums for all skill artifacts via `.skill` packages
|
||||
- **🔐 Checksum Verification** - SHA256 checksums for all skill artifacts
|
||||
- **Health Checks** - Automated updates and integrity verification for all installed skills
|
||||
|
||||
---
|
||||
@@ -170,10 +170,9 @@ When a skill is tagged (e.g., `soul-guardian-v1.0.0`), the pipeline:
|
||||
|
||||
1. **Validates** - Checks `skill.json` version matches tag
|
||||
2. **Generates Checksums** - Creates `checksums.json` with SHA256 hashes for all SBOM files
|
||||
3. **Packages** - Creates `.skill` zip file with all required files
|
||||
4. **Releases** - Publishes to GitHub Releases with all artifacts
|
||||
5. **Supersedes Old Releases** - Marks older versions (same major) as pre-releases
|
||||
6. **Triggers Pages Update** - Refreshes the skills catalog on the website
|
||||
3. **Releases** - Publishes to GitHub Releases with all artifacts
|
||||
4. **Supersedes Old Releases** - Marks older versions (same major) as pre-releases
|
||||
5. **Triggers Pages Update** - Refreshes the skills catalog on the website
|
||||
|
||||
### Release Versioning & Superseding
|
||||
|
||||
@@ -194,7 +193,6 @@ When you release `skill-v0.0.2`, the previous `skill-v0.0.1` release is automati
|
||||
### Release Artifacts
|
||||
|
||||
Each skill release includes:
|
||||
- `<skill>.skill` - Packaged skill (zip format)
|
||||
- `checksums.json` - SHA256 hashes for integrity verification
|
||||
- `skill.json` - Skill metadata
|
||||
- `SKILL.md` - Main skill documentation
|
||||
@@ -220,16 +218,15 @@ Checks:
|
||||
- SBOM files exist and are readable
|
||||
- OpenClaw metadata is properly structured
|
||||
|
||||
### Skill Packager
|
||||
### Skill Checksums Generator
|
||||
|
||||
Creates a distributable `.skill` file with checksums:
|
||||
Generates `checksums.json` with SHA256 hashes for a skill:
|
||||
|
||||
```bash
|
||||
python utils/package_skill.py skills/clawsec-feed ./dist
|
||||
```
|
||||
|
||||
Outputs:
|
||||
- `clawsec-feed.skill` - Zip package with all SBOM files
|
||||
- `checksums.json` - SHA256 hashes for verification
|
||||
|
||||
---
|
||||
|
||||
@@ -106,7 +106,7 @@ export const SkillDetail: React.FC = () => {
|
||||
};
|
||||
|
||||
const installCommand = skillData
|
||||
? `curl -sLO https://clawsec.prompt.security/releases/download/${skillData.name}-v${skillData.version}/${skillData.name}.skill`
|
||||
? `npx clawhub@latest install ${skillData.name}`
|
||||
: '';
|
||||
|
||||
const releasePageUrl = useMemo(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# populate-local-skills.sh
|
||||
# Builds local skills index from skills/ directory for development preview.
|
||||
# This mirrors the skill-release.yml pipeline exactly - generates real checksums and .skill packages.
|
||||
# This mirrors the skill-release.yml pipeline exactly - generates real checksums.
|
||||
#
|
||||
# Usage: ./scripts/populate-local-skills.sh
|
||||
|
||||
@@ -159,50 +159,6 @@ FILEENTRY
|
||||
}
|
||||
SKILLJSON
|
||||
|
||||
# === Create .skill package BEFORE closing checksums JSON ===
|
||||
SKILL_PACKAGE="$PUBLIC_SKILLS_DIR/$SKILL_NAME/${SKILL_NAME}.skill"
|
||||
|
||||
# Get files from SBOM and create zip
|
||||
pushd "$SKILL_DIR" > /dev/null
|
||||
|
||||
FILES=$(jq -r '.sbom.files[].path' skill.json 2>/dev/null | tr '\n' ' ')
|
||||
|
||||
if [ -n "$FILES" ]; then
|
||||
# Create zip with SBOM files + skill.json
|
||||
zip -r "$SKILL_PACKAGE" $FILES skill.json 2>/dev/null || true
|
||||
|
||||
# Add README if exists
|
||||
if [ -f README.md ]; then
|
||||
zip -u "$SKILL_PACKAGE" README.md 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -f "$SKILL_PACKAGE" ]; then
|
||||
PACKAGE_SIZE=$(stat -f%z "$SKILL_PACKAGE" 2>/dev/null || stat -c%s "$SKILL_PACKAGE")
|
||||
echo " ✓ Created: ${SKILL_NAME}.skill ($(( PACKAGE_SIZE / 1024 ))KB)"
|
||||
|
||||
# Add .skill package checksum
|
||||
if command -v sha256sum &> /dev/null; then
|
||||
SKILL_PACKAGE_SHA=$(sha256sum "$SKILL_PACKAGE" | awk '{print $1}')
|
||||
else
|
||||
SKILL_PACKAGE_SHA=$(shasum -a 256 "$SKILL_PACKAGE" | awk '{print $1}')
|
||||
fi
|
||||
|
||||
echo "," >> "$CHECKSUMS_FILE"
|
||||
cat >> "$CHECKSUMS_FILE" << SKILLPACKAGE
|
||||
"${SKILL_NAME}.skill": {
|
||||
"sha256": "$SKILL_PACKAGE_SHA",
|
||||
"size": $PACKAGE_SIZE,
|
||||
"url": "https://clawsec.prompt.security/releases/download/$TAG/${SKILL_NAME}.skill"
|
||||
}
|
||||
SKILLPACKAGE
|
||||
echo " ✓ Checksum: ${SKILL_NAME}.skill ($SKILL_PACKAGE_SHA)"
|
||||
fi
|
||||
else
|
||||
echo " ⚠️ No SBOM files, skipping .skill package"
|
||||
fi
|
||||
|
||||
popd > /dev/null
|
||||
|
||||
# Close checksums JSON
|
||||
cat >> "$CHECKSUMS_FILE" << EOF
|
||||
}
|
||||
|
||||
@@ -8,18 +8,26 @@
|
||||
# 3. Committing the changes
|
||||
# 4. Creating the git tag
|
||||
#
|
||||
# After running, push with: git push && git push origin <tag>
|
||||
# After running, push your current branch and tag:
|
||||
# git push origin <branch>
|
||||
# git push origin <tag>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <skill-name> <version>"
|
||||
echo "Example: $0 clawsec-feed 1.1.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SKILL_NAME="$1"
|
||||
VERSION="$2"
|
||||
SKILL_PATH="skills/$SKILL_NAME"
|
||||
|
||||
# Validation
|
||||
if [ -z "$SKILL_NAME" ] || [ -z "$VERSION" ]; then
|
||||
echo "Usage: $0 <skill-name> <version>"
|
||||
echo "Example: $0 clawsec-feed 1.1.0"
|
||||
# Ensure we're on a branch (not detached HEAD) so release flow works from feature branches
|
||||
CURRENT_BRANCH="$(git symbolic-ref --quiet --short HEAD || true)"
|
||||
if [ -z "$CURRENT_BRANCH" ]; then
|
||||
echo "Error: Detached HEAD detected. Checkout a branch before running release." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -57,6 +65,7 @@ if ! git diff --quiet "$SKILL_PATH/" 2>/dev/null; then
|
||||
fi
|
||||
|
||||
echo "Releasing $SKILL_NAME version $VERSION"
|
||||
echo "Branch: $CURRENT_BRANCH"
|
||||
echo "======================================="
|
||||
|
||||
# Create a temporary directory for atomic operations
|
||||
@@ -215,7 +224,8 @@ fi
|
||||
|
||||
echo ""
|
||||
echo "Done! To release, push the commit and tag:"
|
||||
echo " git push && git push origin $TAG"
|
||||
echo " git push origin $CURRENT_BRANCH"
|
||||
echo " git push origin $TAG"
|
||||
echo ""
|
||||
echo "Or to undo:"
|
||||
echo " git reset --hard HEAD~1 && git tag -d $TAG"
|
||||
|
||||
@@ -45,8 +45,7 @@ get_release_assets() {
|
||||
# Always included
|
||||
assets+=("skill.json")
|
||||
assets+=("checksums.json")
|
||||
assets+=("${skill_name}.skill")
|
||||
|
||||
|
||||
# README if exists
|
||||
if [ -f "$skill_path/README.md" ]; then
|
||||
assets+=("README.md")
|
||||
@@ -151,12 +150,6 @@ validate_skill() {
|
||||
fi
|
||||
done < <(extract_all_referenced_files "$skill_path/SKILL.md")
|
||||
|
||||
# Check for common patterns that reference this skill
|
||||
if grep -qE "/${skill_name}\.skill" "$skill_path/SKILL.md"; then
|
||||
if printf '%s\n' "${RELEASE_ASSETS[@]}" | grep -q "^${skill_name}.skill$"; then
|
||||
echo -e " ${GREEN}✓${NC} ${skill_name}.skill reference found and will be created"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
@@ -199,7 +192,7 @@ validate_skill() {
|
||||
for doc in "$other_skill_dir"/*.md; do
|
||||
[ -f "$doc" ] || continue
|
||||
|
||||
if grep -qE "/${skill_name}\.skill|/${skill_name}-v" "$doc" 2>/dev/null; then
|
||||
if grep -qE "/${skill_name}-v" "$doc" 2>/dev/null; then
|
||||
echo -e " → Referenced by ${other_skill}/$(basename "$doc")"
|
||||
cross_refs_found=true
|
||||
fi
|
||||
|
||||
+14
-45
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill Packager - Creates a distributable .skill file and checksums
|
||||
Skill Checksums Generator - Generates checksums.json for a skill
|
||||
|
||||
Usage:
|
||||
python utils/package_skill.py <path/to/skill-folder> [output-directory]
|
||||
@@ -13,7 +13,6 @@ Example:
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
@@ -31,14 +30,14 @@ def calculate_sha256(file_path: Path) -> str:
|
||||
|
||||
def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None, Path | None]:
|
||||
"""
|
||||
Package a skill folder into a .skill file and generate checksums.
|
||||
Generate checksums for a skill folder.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill folder
|
||||
output_dir: Optional output directory (defaults to current directory)
|
||||
|
||||
Returns:
|
||||
Tuple of (skill_file_path, checksums_file_path) or (None, None) on error
|
||||
Tuple of (None, checksums_file_path) or (None, None) on error
|
||||
"""
|
||||
skill_path = Path(skill_path).resolve()
|
||||
|
||||
@@ -66,26 +65,25 @@ def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None,
|
||||
else:
|
||||
output_path = Path.cwd()
|
||||
|
||||
skill_filename = output_path / f"{skill_name}.skill"
|
||||
checksums_filename = output_path / "checksums.json"
|
||||
|
||||
# Collect files from SBOM
|
||||
files_to_package = []
|
||||
files_to_checksum = []
|
||||
sbom_files = skill_data.get("sbom", {}).get("files", [])
|
||||
|
||||
for file_entry in sbom_files:
|
||||
file_rel_path = file_entry["path"]
|
||||
full_path = skill_path / file_rel_path
|
||||
if full_path.exists():
|
||||
files_to_package.append((file_rel_path, full_path))
|
||||
files_to_checksum.append((file_rel_path, full_path))
|
||||
|
||||
# Always include skill.json
|
||||
files_to_package.append(("skill.json", skill_json_path))
|
||||
files_to_checksum.append(("skill.json", skill_json_path))
|
||||
|
||||
# Include README.md if it exists
|
||||
readme_path = skill_path / "README.md"
|
||||
if readme_path.exists():
|
||||
files_to_package.append(("README.md", readme_path))
|
||||
files_to_checksum.append(("README.md", readme_path))
|
||||
|
||||
# Generate checksums
|
||||
print("Generating checksums...")
|
||||
@@ -98,7 +96,7 @@ def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None,
|
||||
"files": {},
|
||||
}
|
||||
|
||||
for rel_path, full_path in files_to_package:
|
||||
for rel_path, full_path in files_to_checksum:
|
||||
filename = Path(rel_path).name
|
||||
sha256 = calculate_sha256(full_path)
|
||||
size = full_path.stat().st_size
|
||||
@@ -111,40 +109,12 @@ def package_skill(skill_path: str, output_dir: str = None) -> tuple[Path | None,
|
||||
}
|
||||
print(f" {filename}: {sha256[:16]}...")
|
||||
|
||||
# Create .skill package (zip file) first so we can include its checksum
|
||||
print("\nCreating .skill package...")
|
||||
try:
|
||||
with zipfile.ZipFile(skill_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for rel_path, full_path in files_to_package:
|
||||
# Use skill folder as root in archive
|
||||
arcname = f"{skill_name}/{rel_path}"
|
||||
zipf.write(full_path, arcname)
|
||||
print(f" Added: {arcname}")
|
||||
|
||||
print(f"\n[OK] Package created: {skill_filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to create package: {e}")
|
||||
return None, None
|
||||
|
||||
# Add .skill file to checksums now that it exists
|
||||
skill_file_name = f"{skill_name}.skill"
|
||||
skill_sha256 = calculate_sha256(skill_filename)
|
||||
skill_size = skill_filename.stat().st_size
|
||||
|
||||
checksums_data["files"][skill_file_name] = {
|
||||
"sha256": skill_sha256,
|
||||
"size": skill_size,
|
||||
"url": f"https://clawsec.prompt.security/releases/download/{skill_name}-v{version}/{skill_file_name}",
|
||||
}
|
||||
print(f" {skill_file_name}: {skill_sha256[:16]}...")
|
||||
|
||||
# Write checksums.json
|
||||
with open(checksums_filename, "w") as f:
|
||||
json.dump(checksums_data, f, indent=2)
|
||||
print(f"\n[OK] Checksums written to: {checksums_filename}")
|
||||
|
||||
return skill_filename, checksums_filename
|
||||
return None, checksums_filename
|
||||
|
||||
|
||||
def main():
|
||||
@@ -158,18 +128,17 @@ def main():
|
||||
skill_path = sys.argv[1]
|
||||
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
|
||||
print(f"Packaging skill: {skill_path}")
|
||||
print(f"Generating checksums for: {skill_path}")
|
||||
if output_dir:
|
||||
print(f" Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
skill_file, checksums_file = package_skill(skill_path, output_dir)
|
||||
_, checksums_file = package_skill(skill_path, output_dir)
|
||||
|
||||
if skill_file and checksums_file:
|
||||
if checksums_file:
|
||||
print("\n" + "=" * 50)
|
||||
print("Packaging complete!")
|
||||
print(f" Skill package: {skill_file}")
|
||||
print(f" Checksums: {checksums_file}")
|
||||
print("Checksums generation complete!")
|
||||
print(f" Checksums: {checksums_file}")
|
||||
print("=" * 50)
|
||||
sys.exit(0)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user