diff --git a/.github/clawhub-cli/package-lock.json b/.github/clawhub-cli/package-lock.json index 7f4615b..3c42acf 100644 --- a/.github/clawhub-cli/package-lock.json +++ b/.github/clawhub-cli/package-lock.json @@ -384,9 +384,9 @@ } }, "node_modules/undici": { - "version": "7.27.2", - "resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/undici/-/undici-7.27.2.tgz", - "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "version": "7.28.0", + "resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "engines": { "node": ">=20.18.1" } diff --git a/.github/workflows/skill-release.yml b/.github/workflows/skill-release.yml index d2afadf..9c5b21c 100644 --- a/.github/workflows/skill-release.yml +++ b/.github/workflows/skill-release.yml @@ -1717,7 +1717,7 @@ jobs: run: bash scripts/ci/install_clawhub_cli.sh - name: Patch clawhub publish payload workaround - # Temporary: clawhub@0.7.0 publish payload is missing acceptLicenseTerms. + # Idempotent compatibility guard: older clawhub@0.7.0 builds omitted acceptLicenseTerms. if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != '' run: node scripts/ci/patch_clawhub_publish_payload.mjs @@ -1732,6 +1732,18 @@ jobs: CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \ clawhub login --token "$CLAWHUB_TOKEN" --site "$SITE" --no-input + - name: Guard ClawHub slug ownership + if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != '' + run: | + set -euo pipefail + SITE=${CLAWHUB_SITE:-https://clawhub.ai} + REGISTRY=${CLAWHUB_REGISTRY:-$SITE} + export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json" + export CLAWHUB_SITE="$SITE" + export CLAWHUB_REGISTRY="$REGISTRY" + bash scripts/ci/guard_clawhub_slug_owner.sh \ + "${{ needs.release-tag.outputs.clawhub_slug }}" + - name: Guard duplicate ClawHub version if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != '' run: | @@ -1872,7 +1884,7 @@ jobs: run: bash scripts/ci/install_clawhub_cli.sh - name: Patch clawhub publish payload workaround - # Temporary: clawhub@0.7.0 publish payload is missing acceptLicenseTerms. + # Idempotent compatibility guard: older clawhub@0.7.0 builds omitted acceptLicenseTerms. run: node scripts/ci/patch_clawhub_publish_payload.mjs - name: Login to ClawHub @@ -1890,6 +1902,17 @@ jobs: CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \ clawhub login --token "$CLAWHUB_TOKEN" --site "$SITE" --no-input + - name: Guard ClawHub slug ownership + run: | + set -euo pipefail + SITE=${CLAWHUB_SITE:-https://clawhub.ai} + REGISTRY=${CLAWHUB_REGISTRY:-$SITE} + export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json" + export CLAWHUB_SITE="$SITE" + export CLAWHUB_REGISTRY="$REGISTRY" + bash scripts/ci/guard_clawhub_slug_owner.sh \ + "${{ steps.publishable.outputs.clawhub_slug }}" + - name: Publish to ClawHub run: | set -euo pipefail diff --git a/package-lock.json b/package-lock.json index 08d97d5..fba6f62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,11 +31,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -44,27 +45,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.29.0", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -89,12 +92,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.0", - "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "version": "7.29.7", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -104,12 +108,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -127,33 +132,36 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -163,79 +171,84 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.7", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@babel/types": "^7.29.7" }, + "bin": "./bin/babel-parser.js", "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/template": { - "version": "7.28.6", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -243,12 +256,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -3092,10 +3106,20 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" diff --git a/scripts/ci/guard_clawhub_slug_owner.sh b/scripts/ci/guard_clawhub_slug_owner.sh new file mode 100644 index 0000000..14cf404 --- /dev/null +++ b/scripts/ci/guard_clawhub_slug_owner.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 " >&2 +} + +if [ "$#" -ne 1 ]; then + usage + exit 2 +fi + +TARGET_SLUG="$1" +SITE="${CLAWHUB_SITE:-https://clawhub.ai}" +REGISTRY="${CLAWHUB_REGISTRY:-$SITE}" +CONFIG_PATH="${CLAWHUB_CONFIG_PATH:-$HOME/.clawhub-ci/config.json}" + +if [[ ! "$TARGET_SLUG" =~ ^[a-z0-9-]+$ ]]; then + echo "::error::Invalid ClawHub slug for ownership guard: ${TARGET_SLUG}" + exit 1 +fi + +if [ ! -f "$CONFIG_PATH" ]; then + echo "::error::ClawHub config not found at ${CONFIG_PATH}. Run clawhub login before ownership guard." + exit 1 +fi + +TOKEN="$(jq -r '.token // empty' "$CONFIG_PATH")" +if [ -z "$TOKEN" ]; then + echo "::error::ClawHub token missing from ${CONFIG_PATH}. Run clawhub login before ownership guard." + exit 1 +fi + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +api_get() { + local path="$1" + local output_path="$2" + local url="${REGISTRY%/}${path}" + local http_status + local curl_status + + set +e + http_status="$( + curl --silent --show-error --location --max-time 15 \ + --header "Accept: application/json" \ + --header "Authorization: Bearer ${TOKEN}" \ + --output "$output_path" \ + --write-out "%{http_code}" \ + "$url" + )" + curl_status=$? + set -e + + if [ "$curl_status" -ne 0 ]; then + echo "::error::Failed to call ClawHub API: ${url}" + return 1 + fi + + printf '%s\n' "$http_status" +} + +whoami_json="$TMP_DIR/whoami.json" +whoami_status="$(api_get "/api/v1/whoami" "$whoami_json")" +if [ "$whoami_status" != "200" ]; then + echo "::error::Failed to verify authenticated ClawHub publisher. HTTP ${whoami_status}." + cat "$whoami_json" + exit 1 +fi + +publisher_handle="$(jq -r '.user.handle // empty' "$whoami_json")" +if [ -z "$publisher_handle" ]; then + echo "::error::Could not determine authenticated ClawHub publisher handle." + cat "$whoami_json" + exit 1 +fi + +target_json="$TMP_DIR/target.json" +target_status="$(api_get "/api/v1/skills/${TARGET_SLUG}" "$target_json")" +if [ "$target_status" = "404" ]; then + echo "Target ClawHub slug ${TARGET_SLUG} is not currently published; authenticated publisher ${publisher_handle} may create it." + exit 0 +fi + +if [ "$target_status" != "200" ]; then + echo "::error::Failed to inspect target ClawHub slug ${TARGET_SLUG}. HTTP ${target_status}." + cat "$target_json" + exit 1 +fi + +target_owner="$(jq -r '.owner.handle // .owner.displayName // empty' "$target_json")" +if [ -z "$target_owner" ]; then + echo "::error::Could not determine owner for existing ClawHub slug ${TARGET_SLUG}." + echo "target owner: ${target_owner:-unknown}" + exit 1 +fi + +if [ "$target_owner" != "$publisher_handle" ]; then + echo "::error::Resolved ClawHub slug ${TARGET_SLUG} is already owned by ${target_owner}, but the authenticated publisher is ${publisher_handle}. Transfer or alias the registry slug before publishing." + exit 1 +fi + +echo "ClawHub slug ownership guard passed: ${TARGET_SLUG} owned by authenticated publisher ${publisher_handle}." diff --git a/scripts/ci/resolve_clawhub_slug.mjs b/scripts/ci/resolve_clawhub_slug.mjs index 3be1c43..2b3cf64 100644 --- a/scripts/ci/resolve_clawhub_slug.mjs +++ b/scripts/ci/resolve_clawhub_slug.mjs @@ -43,14 +43,14 @@ export function resolveClawHubSlug({ name, platforms = [] }) { throw new Error(`Invalid skill name for ClawHub slug mapping: ${name}`); } - if (name.startsWith("clawsec-")) { - return name; - } - if (EXPLICIT_SLUGS.has(name)) { return EXPLICIT_SLUGS.get(name); } + if (name.startsWith("clawsec-")) { + return name; + } + if (PLATFORM_KEYS.some((platform) => name.startsWith(`${platform}-`))) { return `clawsec-${name}`; } diff --git a/scripts/test-skill-release-workflow.mjs b/scripts/test-skill-release-workflow.mjs index f87276a..d72a81a 100644 --- a/scripts/test-skill-release-workflow.mjs +++ b/scripts/test-skill-release-workflow.mjs @@ -6,11 +6,13 @@ const ciWorkflowPath = new URL('../.github/workflows/ci.yml', import.meta.url); const validateSkillInstallDocsPath = new URL('./ci/validate_skill_install_docs.mjs', import.meta.url); const installClawhubCliPath = new URL('./ci/install_clawhub_cli.sh', import.meta.url); const patchClawhubPayloadPath = new URL('./ci/patch_clawhub_publish_payload.mjs', import.meta.url); +const guardClawhubSlugOwnerPath = new URL('./ci/guard_clawhub_slug_owner.sh', import.meta.url); const workflow = await readFile(workflowPath, 'utf8'); const ciWorkflow = await readFile(ciWorkflowPath, 'utf8'); const validateSkillInstallDocs = await readFile(validateSkillInstallDocsPath, 'utf8'); const installClawhubCli = await readFile(installClawhubCliPath, 'utf8'); const patchClawhubPayload = await readFile(patchClawhubPayloadPath, 'utf8'); +const guardClawhubSlugOwner = await readFile(guardClawhubSlugOwnerPath, 'utf8'); assert.match( workflow, @@ -341,6 +343,12 @@ assert.match( 'ClawHub publish must use the resolved ClawHub slug', ); +assert.match( + workflow, + /clawhub publish "\$SKILL_PATH"[\s\S]*--slug "\$CLAWHUB_SLUG"/, + 'ClawHub publish must use the resolved ClawHub slug', +); + assert.equal( workflow.match(/bash scripts\/ci\/install_clawhub_cli\.sh/g)?.length, 2, @@ -353,6 +361,12 @@ assert.equal( 'ClawHub publish and republish jobs must share the same payload patch helper', ); +assert.equal( + workflow.match(/bash scripts\/ci\/guard_clawhub_slug_owner\.sh/g)?.length, + 2, + 'ClawHub publish and republish jobs must guard mapped slug ownership before publishing', +); + assert.doesNotMatch( workflow, /npm ci --prefix \.github\/clawhub-cli/, @@ -403,6 +417,54 @@ assert.match( 'ClawHub payload patch helper must preserve the acceptLicenseTerms workaround', ); +assert.match( + patchClawhubPayload, + /Already patched/, + 'ClawHub payload patch helper must stay idempotent when the pinned CLI already includes acceptLicenseTerms', +); + +assert.match( + guardClawhubSlugOwner, + /api_get "\/api\/v1\/whoami" "\$whoami_json"/, + 'ClawHub slug ownership guard must verify the authenticated publisher through the ClawHub API', +); + +assert.match( + guardClawhubSlugOwner, + /api_get "\/api\/v1\/skills\/\$\{TARGET_SLUG\}" "\$target_json"/, + 'ClawHub slug ownership guard must inspect the resolved publish slug through the ClawHub API', +); + +assert.match( + guardClawhubSlugOwner, + /\[ "\$target_status" = "404" \]/, + 'ClawHub slug ownership guard must treat HTTP 404 as the structured unpublished-slug signal', +); + +assert.match( + guardClawhubSlugOwner, + /\[ "\$target_owner" != "\$publisher_handle" \]/, + 'ClawHub slug ownership guard must reject slugs owned by a different authenticated registry publisher', +); + +assert.doesNotMatch( + guardClawhubSlugOwner, + /SOURCE_SLUG|source_owner|grep -Eqi[\s\S]*Skill not found/, + 'ClawHub slug ownership guard must not inspect raw source names or depend on stderr wording', +); + +assert.match( + workflow, + /SITE=\$\{CLAWHUB_SITE:-https:\/\/clawhub\.ai\}[\s\S]*REGISTRY=\$\{CLAWHUB_REGISTRY:-\$SITE\}[\s\S]*export CLAWHUB_CONFIG_PATH="\$HOME\/\.clawhub-ci\/config\.json"[\s\S]*export CLAWHUB_SITE="\$SITE"[\s\S]*export CLAWHUB_REGISTRY="\$REGISTRY"[\s\S]*bash scripts\/ci\/guard_clawhub_slug_owner\.sh[\s\S]*\$\{\{ needs\.release-tag\.outputs\.clawhub_slug \}\}/, + 'ClawHub publish job must guard the resolved publish slug with the authenticated ClawHub config path', +); + +assert.match( + workflow, + /SITE=\$\{CLAWHUB_SITE:-https:\/\/clawhub\.ai\}[\s\S]*REGISTRY=\$\{CLAWHUB_REGISTRY:-\$SITE\}[\s\S]*export CLAWHUB_CONFIG_PATH="\$HOME\/\.clawhub-ci\/config\.json"[\s\S]*export CLAWHUB_SITE="\$SITE"[\s\S]*export CLAWHUB_REGISTRY="\$REGISTRY"[\s\S]*bash scripts\/ci\/guard_clawhub_slug_owner\.sh[\s\S]*\$\{\{ steps\.publishable\.outputs\.clawhub_slug \}\}/, + 'ClawHub republish job must guard the resolved publish slug with the authenticated ClawHub config path', +); + assert.doesNotMatch( workflow, /clawhub inspect "\$SKILL_NAME" --version "\$VERSION" --json/,