mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-23 18:31:21 +03:00
fix(release): update ClawHub slug pipeline deps (#277)
* fix(release): update clawhub slug pipeline deps * fix(release): bump clawhub cli undici lock * fix(release): guard clawhub publish slug * fix(release): keep suite slug releasable * fix(release): harden clawhub slug guard * fix(release): pass clawhub registry to slug guard
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <target-clawhub-slug>" >&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}."
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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/,
|
||||
|
||||
Reference in New Issue
Block a user