From f6d92391d3294f28efc3c643ebc03c71a8f8ccbe Mon Sep 17 00:00:00 2001 From: snapsynapse <57973674+snapsynapse@users.noreply.github.com> Date: Fri, 10 Jul 2026 16:50:28 -0600 Subject: [PATCH] Harden manifest validation and action input handling --- .github/scripts/action-security-check.sh | 44 +++++++++++ .github/scripts/test-validate.sh | 66 ++++++++++++++++ .github/workflows/validate.yml | 6 ++ AGENTIC_SURFACES.md | 1 + AGENTS.md | 4 +- CHANGELOG.md | 25 ++++++ CLAUDE.md | 4 +- README.md | 10 ++- action.yml | 4 +- index.html | 12 +-- skill-provenance.skill | Bin 44832 -> 46637 bytes skill-provenance/CHANGELOG.md | 30 ++++---- skill-provenance/MANIFEST.yaml | 38 ++++----- skill-provenance/README.md | 30 +++++--- skill-provenance/SKILL.md | 17 +++-- skill-provenance/evals-distribution.json | 19 +++++ skill-provenance/evals.json | 58 ++++++++++++++ skill-provenance/validate.sh | 93 +++++++++++++++++------ 18 files changed, 373 insertions(+), 88 deletions(-) create mode 100755 .github/scripts/action-security-check.sh create mode 100755 .github/scripts/test-validate.sh diff --git a/.github/scripts/action-security-check.sh b/.github/scripts/action-security-check.sh new file mode 100755 index 0000000..6e570bd --- /dev/null +++ b/.github/scripts/action-security-check.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +action_file="$repo_root/action.yml" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/skill-provenance-action-test.XXXXXX")" +trap 'rm -rf "$test_root"' EXIT + +input_reference_count="$(grep -Ec '\$\{\{[[:space:]]*inputs\.bundle-path[[:space:]]*\}\}' "$action_file" || true)" +if [[ "$input_reference_count" -ne 1 ]]; then + echo "FAIL: bundle-path must have exactly one expression reference, in the environment mapping" >&2 + exit 1 +fi + +if ! grep -Eq '^[[:space:]]*BUNDLE_PATH:[[:space:]]+\$\{\{[[:space:]]*inputs\.bundle-path[[:space:]]*\}\}[[:space:]]*$' "$action_file"; then + echo "FAIL: bundle-path is not transported through the BUNDLE_PATH environment variable" >&2 + exit 1 +fi + +if ! grep -Eq '^[[:space:]]*run:.*"\$BUNDLE_PATH"' "$action_file"; then + echo "FAIL: the validation command does not pass BUNDLE_PATH as a quoted argument" >&2 + exit 1 +fi + +mkdir -p "$test_root/path with spaces" +cp -R "$repo_root/skill-provenance" "$test_root/path with spaces/bundle" +env GITHUB_ACTION_PATH="$repo_root" \ + BUNDLE_PATH="$test_root/path with spaces/bundle" \ + bash -c '"$GITHUB_ACTION_PATH/skill-provenance/validate.sh" "$BUNDLE_PATH"' >/dev/null + +sentinel="$test_root/injected" +adversarial_path="\"; touch $sentinel; #" +if env GITHUB_ACTION_PATH="$repo_root" \ + BUNDLE_PATH="$adversarial_path" \ + bash -c '"$GITHUB_ACTION_PATH/skill-provenance/validate.sh" "$BUNDLE_PATH"' >/dev/null 2>&1; then + echo "FAIL: adversarial bundle path unexpectedly validated" >&2 + exit 1 +fi +if [ -e "$sentinel" ]; then + echo "FAIL: bundle-path shell payload executed" >&2 + exit 1 +fi + +echo "Action input transport is shell-safe" diff --git a/.github/scripts/test-validate.sh b/.github/scripts/test-validate.sh new file mode 100755 index 0000000..773044e --- /dev/null +++ b/.github/scripts/test-validate.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Focused regression tests for validate.sh manifest hash handling. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +VALIDATOR="$ROOT_DIR/skill-provenance/validate.sh" +TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/skill-provenance-validate-test.XXXXXX")" +trap 'rm -rf "$TEST_DIR"' EXIT + +if command -v shasum >/dev/null 2>&1; then + hash_file() { shasum -a 256 "$1" | awk '{print $1}'; } +else + hash_file() { sha256sum "$1" | awk '{print $1}'; } +fi + +write_manifest() { + printf 'bundle: test\nfiles:\n - path: payload.txt\n role: reference\n%s\n' "$1" > "$TEST_DIR/MANIFEST.yaml" +} + +expect_pass() { + if ! "$@" >/dev/null 2>&1; then + echo "FAIL: expected success: $*" >&2 + exit 1 + fi +} + +expect_fail() { + if "$@" >/dev/null 2>&1; then + echo "FAIL: expected failure: $*" >&2 + exit 1 + fi +} + +printf 'payload\n' > "$TEST_DIR/payload.txt" +valid_hash="$(hash_file "$TEST_DIR/payload.txt")" + +write_manifest " hash: sha256:$valid_hash" +expect_pass "$VALIDATOR" "$TEST_DIR" + +write_manifest " hash: null" +expect_pass "$VALIDATOR" "$TEST_DIR" +rm "$TEST_DIR/payload.txt" +expect_fail "$VALIDATOR" "$TEST_DIR" +printf 'payload\n' > "$TEST_DIR/payload.txt" +rm "$TEST_DIR/payload.txt" +expect_fail "$VALIDATOR" "$TEST_DIR" +printf 'payload\n' > "$TEST_DIR/payload.txt" + +for hash_line in \ + "" \ + " hash: sha256:not-a-hash" \ + " hash: sha256:${valid_hash%?}" \ + " hash: md5:$valid_hash"; do + write_manifest "$hash_line" + expect_fail "$VALIDATOR" "$TEST_DIR" + expect_pass "$VALIDATOR" --update "$TEST_DIR" + expect_pass "$VALIDATOR" "$TEST_DIR" +done + +printf 'bundle: test\nfiles:\n - path: payload.txt\n role: reference\n hash: sha256:%s\n hash: sha256:%s\n' \ + "$valid_hash" "$valid_hash" > "$TEST_DIR/MANIFEST.yaml" +expect_fail "$VALIDATOR" "$TEST_DIR" +expect_fail "$VALIDATOR" --update "$TEST_DIR" + +echo "validate.sh hash-state regression tests passed" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9da6e7b..c3ac17c 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -33,3 +33,9 @@ jobs: - name: Verify release surfaces run: ./.github/scripts/release-surface-check.sh + + - name: Verify action input transport + run: ./.github/scripts/action-security-check.sh + + - name: Test validator hash states + run: ./.github/scripts/test-validate.sh diff --git a/AGENTIC_SURFACES.md b/AGENTIC_SURFACES.md index c4b7d65..6628c7c 100644 --- a/AGENTIC_SURFACES.md +++ b/AGENTIC_SURFACES.md @@ -28,6 +28,7 @@ the action. | `.github/workflows/validate.yml` | CI checker for manifest validation, derived package builds, and release-surface drift checks. | Release-confidence automation. Passing CI supports review but does not certify safety, author identity, or runtime behavior. | | `action.yml` | GitHub Actions Marketplace metadata and composite action wrapper for `skill-provenance/validate.sh`. | Code-executing CI entrypoint. It validates a bundle manifest in the workflow workspace but does not certify trust or safety. | | `.github/scripts/release-surface-check.sh` | CI and local release-surface drift checker for eval counts, GuideCheck sidecar metadata, and `.skill` ZIP freshness. | Code-executing release-confidence automation. It verifies declared release surfaces against local files but is not a trust anchor or safety certification. | +| `.github/scripts/action-security-check.sh` and `.github/scripts/test-validate.sh` | Regression checks for composite-action input transport and validator hash-state behavior. | Code-executing test helpers. They exercise bounded local fixtures and do not certify unrelated runtime safety. | | `skill-provenance.skill` | Claude Settings ZIP wrapper around the canonical bundle. | Release artifact. Verify against release provenance before installing. | ## Maintenance rule When adding or changing an assistant-facing surface, update this file if diff --git a/AGENTS.md b/AGENTS.md index 657087e..1f6785b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ skill-provenance/ ← Canonical source bundle (DO NOT rename) ├── README.md ← Human-facing user guide (has internal version header) ├── MANIFEST.yaml ← File inventory: roles, versions, SHA-256 hashes ├── CHANGELOG.md ← Recent in-bundle change history (last 5 entries) -├── evals.json ← 30 core evaluation scenarios -├── evals-distribution.json ← 16 supplemental distribution evals +├── evals.json ← 33 core evaluation scenarios +├── evals-distribution.json ← 17 supplemental distribution evals ├── validate.sh ← Bash script for local hash verification └── package.sh ← Bash script for derived strict/ClawHub outputs action.yml ← GitHub Actions Marketplace wrapper for validate.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 3395e12..449b81b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ The in-bundle file at `skill-provenance/CHANGELOG.md` is the active changelog that travels with the skill bundle and keeps only the five most recent entries to limit package weight. Older history remains here in the repo root. +## 5.0.0 - 2026-07-10 +- skill-provenance/validate.sh: Made manifest verification fail closed on + missing, malformed, or duplicate hash fields; added explicit + `hash: null` opt-outs; repaired missing or malformed hashes in update + mode; and kept inventory checks active for opted-out files. +- action.yml: Transported caller-controlled `bundle-path` through an + environment variable instead of interpolating it into Bash source. +- .github/scripts/action-security-check.sh, + .github/scripts/test-validate.sh, .github/workflows/validate.yml: Added + and enabled regression coverage for action input transport and validator + hash-state behavior. +- skill-provenance/SKILL.md and skill-provenance/README.md: Defined and + documented the explicit hash contract and breaking fail-closed behavior. +- skill-provenance/evals.json: Added 3 core security and recovery evals. +- skill-provenance/evals-distribution.json: Added a composite-action shell + safety eval. Coverage is now 33 core and 17 supplemental, 50 total. +- skill-provenance/MANIFEST.yaml and skill-provenance/CHANGELOG.md: Bumped + the canonical bundle to 5.0.0, advanced per-file versions, updated the + rolling history, and refreshed inventory metadata. +- README.md, AGENTS.md, CLAUDE.md, AGENTIC_SURFACES.md, and index.html: + Updated eval counts, executable-surface disclosure, release version, and + public modified date. +- skill-provenance.skill: Rebuilt the Claude Settings ZIP from the + canonical 5.0.0 bundle. + ## 4.13.1 - 2026-06-23 - action.yml: Added a root GitHub Actions Marketplace composite action wrapper that runs `skill-provenance/validate.sh` against a configurable diff --git a/CLAUDE.md b/CLAUDE.md index 5171b26..78589bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,8 +7,8 @@ A metaskill for version tracking across Agent Skills sessions, surfaces, and pla - `skill-provenance/SKILL.md` -- the skill definition (what agents read) - `skill-provenance/MANIFEST.yaml` -- file inventory with roles, versions, SHA-256 hashes - `skill-provenance/CHANGELOG.md` -- rolling recent history (last 5 entries) -- `skill-provenance/evals.json` -- 30 core evaluation scenarios -- `skill-provenance/evals-distribution.json` -- 16 supplemental distribution evals +- `skill-provenance/evals.json` -- 33 core evaluation scenarios +- `skill-provenance/evals-distribution.json` -- 17 supplemental distribution evals - `skill-provenance/validate.sh` -- local hash verification script - `skill-provenance/package.sh` -- derived copy generator (strict/ClawHub) - `action.yml` -- GitHub Actions Marketplace wrapper for bundle validation diff --git a/README.md b/README.md index 5c8f47c..0275bdd 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,8 @@ tells you whether a specific copy matches. Together they answer both .claude-plugin/plugin.json <- Claude Code plugin manifest action.yml <- GitHub Actions Marketplace wrapper .github/scripts/release-surface-check.sh <- Release-surface drift check +.github/scripts/action-security-check.sh <- Composite-action input safety regression +.github/scripts/test-validate.sh <- Validator hash-state regression suite skills/open/SKILL.md <- /skill-provenance:open (verify bundle on session start) skills/validate/SKILL.md <- /skill-provenance:validate (hash/inventory check only) skills/close/SKILL.md <- /skill-provenance:close (update versions on session end) @@ -283,8 +285,8 @@ skill-provenance/ <- Canonical source bundle (metadata mode) README.md <- User guide: workflows, worked example, troubleshooting MANIFEST.yaml <- File inventory with roles, versions, hashes CHANGELOG.md <- Recent in-bundle history (last 5 entries) - evals.json <- 30 core evaluation scenarios - evals-distribution.json <- 16 supplemental packaging/deployment/integrity evals + evals.json <- 33 core evaluation scenarios + evals-distribution.json <- 17 supplemental packaging/deployment/integrity evals validate.sh <- Local hash verification script package.sh <- Zero-dependency helper for derived copies CHANGELOG.md <- Full append-only repo history @@ -297,8 +299,8 @@ The directory is the canonical cross-platform source bundle. The `.skill` file i ## Evals -46 evaluation scenarios across two files: 30 core workflow evals in -[evals.json](skill-provenance/evals.json) and 16 supplemental +50 evaluation scenarios across two files: 33 core workflow evals in +[evals.json](skill-provenance/evals.json) and 17 supplemental distribution/package/integrity evals in [evals-distribution.json](skill-provenance/evals-distribution.json). diff --git a/action.yml b/action.yml index 6d5bdb6..98531c3 100644 --- a/action.yml +++ b/action.yml @@ -14,4 +14,6 @@ runs: steps: - name: Validate Skill Provenance manifest shell: bash - run: '"$GITHUB_ACTION_PATH/skill-provenance/validate.sh" "${{ inputs.bundle-path }}"' + env: + BUNDLE_PATH: ${{ inputs.bundle-path }} + run: '"$GITHUB_ACTION_PATH/skill-provenance/validate.sh" "$BUNDLE_PATH"' diff --git a/index.html b/index.html index 7f5ee95..658ee83 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@ - + @@ -47,7 +47,7 @@ "headline": "Skill Provenance - Portable provenance for Agent Skills", "description": "Portable provenance, integrity, and drift control for Agent Skills across local folders, registries, platform uploads, and multi-agent sessions.", "datePublished": "2026-01-31", - "dateModified": "2026-06-23", + "dateModified": "2026-07-10", "author": { "@type": "Person", "name": "Sam Rogers", @@ -524,9 +524,9 @@