Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d8e57e3
feat(runner): add declarative JSON gate policy
jesse-merhi Jul 27, 2026
34cc64d
feat(plugin): add OpenClaw install gate
jesse-merhi Jul 28, 2026
03e4fa6
fix(plugin): harden install gate delivery
jesse-merhi Jul 28, 2026
f19aa96
fix(plugin): bind gate checks to trusted config
jesse-merhi Jul 28, 2026
c6b7f3f
fix(plugin): degrade safely on Windows
jesse-merhi Jul 28, 2026
0964527
fix(release): make npm promotion reproducible
jesse-merhi Jul 28, 2026
7a1490c
fix(plugin): terminate scanner process trees
jesse-merhi Jul 28, 2026
d70241d
fix(release): publish plugin from version tags
jesse-merhi Jul 28, 2026
555782e
fix(plugin): harden runtime delivery
jesse-merhi Jul 28, 2026
4cb7f7a
fix(plugin): scan complete install candidates
jesse-merhi Jul 28, 2026
735c244
fix(plugin): align with declarative gate policy
jesse-merhi Jul 28, 2026
b7f8b1e
fix(plugin): normalize bundled config identity
jesse-merhi Jul 28, 2026
1d51fb9
fix(plugin): scan compatible bundle directories
jesse-merhi Jul 28, 2026
ecd8d74
feat(release): add Windows ARM64 binaries
jesse-merhi Jul 28, 2026
2954e46
feat(profiles): publish config schema
jesse-merhi Jul 29, 2026
373a534
fix(plugin): harden install gate integration
jesse-merhi Jul 29, 2026
5c5a000
fix(plugin): defer publishing until host contract ships
jesse-merhi Jul 30, 2026
787c28f
feat(policy): add OpenClaw install policy adapter
jesse-merhi Jul 30, 2026
c2d2cd7
feat(policy): support warning decisions
jesse-merhi Jul 30, 2026
324dcff
docs(policy): explain sandbox path sharing
jesse-merhi Jul 30, 2026
bd0a16e
fix(profiles): preserve ClawHub gate behavior
jesse-merhi Jul 31, 2026
f6f39fb
Merge remote-tracking branch 'origin/main' into jesse/openclaw-native…
jesse-merhi Jul 31, 2026
3c64e68
fix(gate): align alias and schema validation
jesse-merhi Jul 31, 2026
3d1ea00
Merge remote-tracking branch 'origin/jesse/openclaw-native-gate-polic…
jesse-merhi Jul 31, 2026
90db896
test(profiles): cover opt-in install policy gate
jesse-merhi Jul 31, 2026
3c457a9
fix(policy): scan dependency symlink targets safely
jesse-merhi Jul 31, 2026
cecb304
fix(policy): fail closed on malformed npm preflight
jesse-merhi Jul 31, 2026
fc31957
fix(policy): reject unmapped judge profiles
jesse-merhi Jul 31, 2026
8f5a9a1
fix(release): verify npm dist tags on retry
jesse-merhi Jul 31, 2026
28b3b0f
Merge remote-tracking branch 'origin/main' into jesse/openclaw-instal…
jesse-merhi Jul 31, 2026
f91194e
test(profiles): retain install policy gate coverage
jesse-merhi Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 91 additions & 14 deletions .github/workflows/npm-release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: NPM Release
name: ClawScan Binary NPM Promotion

on:
workflow_dispatch:
Expand Down Expand Up @@ -138,7 +138,7 @@ jobs:
set -euo pipefail
RUN_JSON="$(gh run view "$PREFLIGHT_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,conclusion,url)"
# shellcheck disable=SC2016
printf '%s' "$RUN_JSON" | node --input-type=module -e 'const chunks=[]; process.stdin.on("data", (chunk) => chunks.push(chunk)); process.stdin.on("end", () => { const run = JSON.parse(Buffer.concat(chunks).toString("utf8")); const checks = [["workflowName", "NPM Release"], ["headBranch", "main"], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID}: ${run.url}`); });'
printf '%s' "$RUN_JSON" | node --input-type=module -e 'const chunks=[]; process.stdin.on("data", (chunk) => chunks.push(chunk)); process.stdin.on("end", () => { const run = JSON.parse(Buffer.concat(chunks).toString("utf8")); const checks = [["workflowName", "ClawScan Binary NPM Promotion"], ["headBranch", "main"], ["event", "workflow_dispatch"], ["conclusion", "success"]]; for (const [key, expected] of checks) { if (run[key] !== expected) { console.error(`Referenced npm preflight run ${process.env.PREFLIGHT_RUN_ID} must have ${key}=${expected}, got ${run[key] ?? "<missing>"}.`); process.exit(1); } } console.log(`Using npm preflight run ${process.env.PREFLIGHT_RUN_ID}: ${run.url}`); });'

- name: Download prepared npm tarball
uses: actions/download-artifact@v4
Expand All @@ -160,6 +160,11 @@ jobs:
process.stdout.write(normalizePackageVersion(process.env.RELEASE_TAG));
EOF
)"
EXPECTED_DIST_TAG="$(node --input-type=module <<'EOF'
import { npmDistTagForVersion } from "./scripts/build-npm-package.mjs";
process.stdout.write(npmDistTagForVersion(process.env.RELEASE_TAG));
EOF
)"
TAG_FILE="dist/npm/release-tag.txt"
SHA_FILE="dist/npm/release-sha.txt"
VERSION_FILE="dist/npm/package-version.txt"
Expand All @@ -184,32 +189,73 @@ jobs:
exit 1
fi
echo "PACKAGE_VERSION=$EXPECTED_PACKAGE_VERSION" >> "$GITHUB_ENV"
echo "NPM_DIST_TAG=$EXPECTED_DIST_TAG" >> "$GITHUB_ENV"

- name: Resolve publish tarball
- name: Resolve ClawScan publish tarball
id: publish_tarball
run: |
set -euo pipefail
TARBALL_PATH="$(find dist/npm -type f -name 'openclaw-clawscan-*.tgz' -print | sort | tail -n 1)"
if [[ -z "$TARBALL_PATH" ]]; then
echo "Prepared preflight tarball not found." >&2
CLAWSCAN_TARBALL="dist/npm/openclaw-clawscan-${PACKAGE_VERSION}.tgz"
if [[ ! -f "$CLAWSCAN_TARBALL" ]]; then
echo "Prepared ClawScan preflight tarball was not present." >&2
ls -la dist/npm >&2 || true
exit 1
fi
echo "path=$TARBALL_PATH" >> "$GITHUB_OUTPUT"
echo "path=$CLAWSCAN_TARBALL" >> "$GITHUB_OUTPUT"

- name: Ensure version is not already published
- name: Inspect ClawScan npm publish state
id: publish_state
run: |
set -euo pipefail
if npm view "@openclaw/clawscan@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
echo "@openclaw/clawscan@${PACKAGE_VERSION} is already published on npm."
package_name="@openclaw/clawscan"
tarball_path="${{ steps.publish_tarball.outputs.path }}"
published_version=""
view_output=""
view_status=0
set +e
view_output="$(npm view "${package_name}@${PACKAGE_VERSION}" version 2>&1)"
view_status=$?
set -e
if [[ "$view_status" -eq 0 && -n "$view_output" ]]; then
published_version="$view_output"
if [[ "$published_version" != "$PACKAGE_VERSION" ]]; then
echo "${package_name}@${PACKAGE_VERSION} reported unexpected version ${published_version}." >&2
exit 1
fi
remote_integrity=""
local_integrity=""
remote_integrity="$(npm view "${package_name}@${PACKAGE_VERSION}" dist.integrity)"
# shellcheck disable=SC2016
local_integrity="$(node --input-type=module -e '
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
const digest = createHash("sha512").update(readFileSync(process.argv[1])).digest("base64");
process.stdout.write(`sha512-${digest}`);
' "$tarball_path")"
if [[ "$remote_integrity" != "$local_integrity" ]]; then
echo "${package_name}@${PACKAGE_VERSION} does not match the prepared release tarball." >&2
exit 1
fi
echo "${package_name}@${PACKAGE_VERSION} is already published with valid release metadata; skipping publish."
echo "clawscan_needed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "$view_status" -eq 0 ]]; then
echo "${package_name}@${PACKAGE_VERSION} returned an empty publish-state response." >&2
exit 1
fi
echo "Publishing @openclaw/clawscan@${PACKAGE_VERSION}"
if ! printf '%s\n' "$view_output" | grep -q "E404"; then
printf '%s\n' "$view_output" >&2
exit "$view_status"
fi
echo "${package_name}@${PACKAGE_VERSION} is not published yet."
echo "clawscan_needed=true" >> "$GITHUB_OUTPUT"

- name: Publish
run: npm publish "${{ steps.publish_tarball.outputs.path }}" --access public --provenance
- name: Publish ClawScan binary package
if: steps.publish_state.outputs.clawscan_needed == 'true'
run: npm publish "${{ steps.publish_tarball.outputs.path }}" --access public --provenance --tag "$NPM_DIST_TAG"

- name: Verify npm release metadata
- name: Verify ClawScan npm release metadata
run: |
set -euo pipefail
NPM_DIST_JSON=""
Expand All @@ -224,3 +270,34 @@ jobs:
sleep 5
done
printf '%s\n' "$NPM_DIST_JSON"

TAGGED_VERSION=""
for attempt in {1..12}; do
if TAGGED_VERSION="$(npm view "@openclaw/clawscan@${NPM_DIST_TAG}" version 2>/tmp/npm-tag-view-error)" &&
[[ "$TAGGED_VERSION" == "$PACKAGE_VERSION" ]]; then
break
fi
if [[ "$attempt" == "12" ]]; then
cat /tmp/npm-tag-view-error >&2 || true
echo "Expected dist-tag ${NPM_DIST_TAG} to resolve to ${PACKAGE_VERSION}, got ${TAGGED_VERSION:-<missing>}." >&2
exit 1
fi
sleep 5
done

if [[ "$NPM_DIST_TAG" == "next" ]]; then
LATEST_VERSION=""
LATEST_STATUS=0
set +e
LATEST_VERSION="$(npm view "@openclaw/clawscan@latest" version 2>/tmp/npm-latest-view-error)"
LATEST_STATUS=$?
set -e
if [[ "$LATEST_STATUS" -eq 0 && "$LATEST_VERSION" == "$PACKAGE_VERSION" ]]; then
echo "Prerelease ${PACKAGE_VERSION} must not be assigned to the latest dist-tag." >&2
exit 1
fi
if [[ "$LATEST_STATUS" -ne 0 ]] && ! grep -q "E404" /tmp/npm-latest-view-error; then
cat /tmp/npm-latest-view-error >&2
exit "$LATEST_STATUS"
fi
fi
100 changes: 75 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
--generate-notes

publish-npm:
name: Publish @openclaw/clawscan
name: Publish ClawScan npm package
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' || inputs.publish
Expand Down Expand Up @@ -182,37 +182,87 @@ jobs:
execFileSync("git", ["merge-base", "--is-ancestor", releaseSha, "origin/main"]);
NODE

- name: Ensure version is unpublished
env:
RELEASE_TAG: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
package_name="$(node -p "require('./npm/clawscan/package.json').name")"
package_version="$(node --input-type=module -e 'import { normalizePackageVersion } from "./scripts/build-npm-package.mjs"; process.stdout.write(normalizePackageVersion(process.env.RELEASE_TAG));')"
set +e
output="$(npm view "${package_name}@${package_version}" version 2>&1)"
status=$?
set -e
if [ "${status}" -eq 0 ]; then
echo "${package_name}@${package_version} is already published."
exit 1
fi
if ! printf '%s\n' "${output}" | grep -q "E404"; then
printf '%s\n' "${output}" >&2
exit "${status}"
fi

- name: Check npm package
run: |
node --test npm/clawscan/test/*.test.mjs
node --test scripts/build-npm-package.test.mjs
node scripts/build-npm-package.mjs --version "${{ needs.build.outputs.version }}" --pack --smoke

- name: Stage npm package
run: node scripts/build-npm-package.mjs --version "${{ needs.build.outputs.version }}"
- name: Inspect npm publish state
id: publish_state
env:
RELEASE_TAG: ${{ needs.build.outputs.version }}
run: |
set -euo pipefail
package_version="$(node --input-type=module -e 'import { normalizePackageVersion } from "./scripts/build-npm-package.mjs"; process.stdout.write(normalizePackageVersion(process.env.RELEASE_TAG));')"
inspect_package() {
local package_name="$1"
local tarball_path="$2"
local output_name="$3"
local published_version=""
local view_output=""
local view_status=0
set +e
view_output="$(npm view "${package_name}@${package_version}" version 2>&1)"
view_status=$?
set -e
if [[ "$view_status" -eq 0 && -n "$view_output" ]]; then
published_version="$view_output"
local remote_integrity=""
local local_integrity=""
remote_integrity="$(npm view "${package_name}@${package_version}" dist.integrity)"
# shellcheck disable=SC2016
local_integrity="$(node --input-type=module -e '
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
const digest = createHash("sha512").update(readFileSync(process.argv[1])).digest("base64");
process.stdout.write(`sha512-${digest}`);
' "$tarball_path")"
if [[ "$published_version" != "$package_version" || "$remote_integrity" != "$local_integrity" ]]; then
echo "${package_name}@${package_version} does not match the prepared release tarball." >&2
exit 1
fi
echo "${package_name}@${package_version} already matches the prepared release tarball; skipping publish."
echo "${output_name}=false" >> "$GITHUB_OUTPUT"
return
fi
if [[ "$view_status" -eq 0 ]]; then
echo "${package_name}@${package_version} returned an empty publish-state response." >&2
exit 1
fi
if ! printf '%s\n' "$view_output" | grep -q "E404"; then
printf '%s\n' "$view_output" >&2
return "$view_status"
fi
echo "${output_name}=true" >> "$GITHUB_OUTPUT"
}
inspect_package \
"@openclaw/clawscan" \
"dist/npm/openclaw-clawscan-${package_version}.tgz" \
"clawscan_needed"
echo "package_version=$package_version" >> "$GITHUB_OUTPUT"

- name: Publish ClawScan binary package
if: steps.publish_state.outputs.clawscan_needed == 'true'
run: npm publish "dist/npm/openclaw-clawscan-${{ steps.publish_state.outputs.package_version }}.tgz" --access public --provenance

- name: Publish with npm trusted publishing
run: npm publish dist/npm/package --access public --provenance
- name: Verify npm release metadata
run: |
set -euo pipefail
for package_name in "@openclaw/clawscan"; do
npm_dist_json=""
for attempt in {1..12}; do
if npm_dist_json="$(npm view "${package_name}@${{ steps.publish_state.outputs.package_version }}" dist.tarball dist.integrity --json 2>/tmp/npm-view-error)" && [[ -n "$npm_dist_json" ]]; then
break
fi
if [[ "$attempt" == "12" ]]; then
cat /tmp/npm-view-error >&2 || true
exit 1
fi
sleep 5
done
printf '%s\n' "$npm_dist_json"
done

update-homebrew-tap:
name: Update Homebrew tap
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ ClawScan turns that approach into a repeatable CLI. It includes a built-in `claw
| `clawscan profiles [-v]` | Inspect built-in profiles; `-v` prints the catalog as YAML. |
| `clawscan benchmark [list\|<benchmark-id>]` | Discover or run supported benchmarks through a selected scanner/profile/judge setup. |
| `clawscan install <scanner-id> [...]` | Install or verify local scanner dependencies where ClawScan has registry-backed install plans. |
| `clawscan openclaw-install-policy` | Act as an external OpenClaw `security.installPolicy.exec` command. Reads the staged install request from stdin and returns allow/warn/block JSON. |

## OpenClaw install policy

ClawScan integrates with OpenClaw at the operator-owned
`security.installPolicy` boundary. It does not register an install hook or
depend on plugin activation. The policy command scans the staged `sourcePath`
for both skills and plugins before OpenClaw commits a supported install or
update.

See [docs/openclaw-install-policy.md](docs/openclaw-install-policy.md) for the
trusted executable setup, configuration, payload contract, and scope.

## Scanners

Expand Down Expand Up @@ -187,6 +199,7 @@ clawscan profiles -v
| Profile | Scanners | Judge |
| --- | --- | --- |
| `clawhub` | `skillspector`, `clawscan-static` | Codex `gpt-5.5`, high reasoning, bundled ClawHub prompt/schema |
| `openclaw-install-policy` | `skillspector`, `clawscan-static` | none |

### Build a custom profile with `.clawscan.yml`

Expand Down
Loading
Loading