diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index c02dd1f..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,327 +0,0 @@ -{ - "env": { - "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1", - "CLAUDE_CODE_COORDINATOR_MODE": "1", - "CLAUDE_CODE_EFFORT_LEVEL": "auto", - "BASH_MAX_OUTPUT_LENGTH": "30000", - "MAX_MCP_OUTPUT_TOKENS": "25000" - }, - - "sandbox": { - "enabled": true, - "filesystem": { - "denyWrite": ["~/.ssh", "~/.gnupg", "~/.aws", "~/.config/solana/id.json"] - } - }, - - "enableAllProjectMcpServers": true, - - "defaultMode": "default", - - "extraKnownMarketplaces": { - "safe-solana-ai": { - "source": { - "source": "github", - "repo": "solanabr/safe-solana-ai" - } - } - }, - - "enabledPlugins": { - "rust-analyzer-lsp@claude-plugins-official": true, - "typescript-lsp@claude-plugins-official": true, - "csharp-lsp@claude-plugins-official": true, - "safe-solana-ai@safe-solana-ai": true - }, - - "includeCoAuthoredBy": false, - - "permissions": { - "allow": [ - "Bash(anchor *)", - "Bash(cargo build*)", - "Bash(cargo test*)", - "Bash(cargo clippy *)", - "Bash(cargo fmt *)", - "Bash(cargo audit *)", - "Bash(cargo install *)", - "Bash(cargo geiger *)", - "Bash(cargo check *)", - "Bash(cargo doc *)", - "Bash(cargo run *)", - "Bash(cargo add *)", - "Bash(cargo remove *)", - "Bash(cargo update *)", - "Bash(cargo clean *)", - "Bash(cargo bench *)", - "Bash(cargo fix *)", - "Bash(cargo tree *)", - "Bash(cargo metadata *)", - "Bash(rustup *)", - "Bash(rustfmt *)", - "Bash(trident *)", - - "Bash(solana config *)", - "Bash(solana address *)", - "Bash(solana balance *)", - "Bash(solana airdrop *)", - "Bash(solana program show *)", - "Bash(solana program dump *)", - "Bash(solana logs *)", - "Bash(solana account *)", - "Bash(solana cluster-version *)", - "Bash(solana epoch-info *)", - "Bash(solana slot *)", - "Bash(solana rent *)", - "Bash(solana decode *)", - "Bash(solana transaction-history *)", - "Bash(solana confirm *)", - "Bash(solana-keygen *)", - "Bash(solana-test-validator *)", - "Bash(surfpool *)", - - "Bash(npm *)", - "Bash(yarn *)", - "Bash(pnpm *)", - "Bash(bun *)", - "Bash(npx *)", - "Bash(node *)", - "Bash(tsx *)", - "Bash(tsc *)", - "Bash(next *)", - "Bash(vite *)", - "Bash(vitest *)", - "Bash(prettier *)", - "Bash(eslint *)", - "Bash(create-solana-dapp *)", - - "Bash(git status *)", - "Bash(git diff *)", - "Bash(git log *)", - "Bash(git show *)", - "Bash(git add *)", - "Bash(git commit *)", - "Bash(git push *)", - "Bash(git pull *)", - "Bash(git fetch *)", - "Bash(git branch *)", - "Bash(git checkout *)", - "Bash(git switch *)", - "Bash(git merge *)", - "Bash(git rebase *)", - "Bash(git stash *)", - "Bash(git tag *)", - "Bash(git remote *)", - "Bash(git submodule *)", - "Bash(git blame *)", - "Bash(git cherry-pick *)", - "Bash(gh *)", - - "Bash(ls *)", - "Bash(cat *)", - "Bash(head *)", - "Bash(tail *)", - "Bash(grep *)", - "Bash(rg *)", - "Bash(find *)", - "Bash(fd *)", - "Bash(mkdir *)", - "Bash(touch *)", - "Bash(echo *)", - "Bash(printf *)", - "Bash(pwd)", - "Bash(cd *)", - "Bash(cp *)", - "Bash(mv *)", - "Bash(wc *)", - "Bash(sort *)", - "Bash(uniq *)", - "Bash(diff *)", - "Bash(tree *)", - "Bash(which *)", - "Bash(command *)", - "Bash(type *)", - "Bash(env *)", - "Bash(export *)", - "Bash(date *)", - "Bash(basename *)", - "Bash(dirname *)", - "Bash(realpath *)", - "Bash(readlink *)", - "Bash(xargs *)", - "Bash(sed *)", - "Bash(awk *)", - "Bash(cut *)", - "Bash(tr *)", - "Bash(tee *)", - "Bash(jq *)", - "Bash(curl *)", - "Bash(wget *)", - "Bash(tar *)", - "Bash(unzip *)", - "Bash(du *)", - "Bash(df *)", - "Bash(file *)", - "Bash(stat *)", - "Bash(sha256sum *)", - "Bash(md5sum *)", - "Bash(base64 *)", - "Bash(openssl *)", - - "Bash(docker build *)", - "Bash(docker run *)", - "Bash(docker compose *)", - "Bash(docker ps *)", - "Bash(docker logs *)", - "Bash(docker images *)", - "Bash(docker exec *)", - "Bash(docker stop *)", - "Bash(docker inspect *)", - - "Bash(unity-editor *)", - "Bash(dotnet *)", - "Bash(nuget *)", - - "Bash(python *)", - "Bash(python3 *)", - "Bash(pip *)", - "Bash(pip3 *)", - - "Bash(avm *)", - "Bash(solana-verify *)", - "Bash(mollusk *)", - "Bash(litesvm *)", - "Bash(spl-token *)", - "Bash(just *)", - "Bash(make *)", - "Bash(cmake *)", - "Bash(wasm-pack *)", - "Bash(cross *)", - "Bash(cargo-generate *)", - "Bash(solana-install *)", - - "Read(*)", - "Edit(*)", - "Write(*)", - "Glob(*)", - "Grep(*)", - "Agent(*)", - "WebFetch(*)", - "WebSearch(*)" - ], - "deny": [ - "Bash(rm -rf /*)", - "Bash(rm -rf ~*)", - "Bash(rm -rf .*)", - "Bash(sudo *)", - "Bash(chmod 777 *)", - "Bash(chmod -R 777 *)", - "Bash(chown *)", - "Bash(mkfs *)", - "Bash(dd *)", - "Bash(shutdown *)", - "Bash(reboot *)", - "Bash(kill -9 *)", - "Bash(killall *)", - "Bash(pkill *)", - "Bash(git push --force *)", - "Bash(git push -f *)", - "Bash(git reset --hard *)", - "Bash(git clean -fd *)", - "Bash(solana program deploy *mainnet*)", - "Bash(solana transfer *)", - "Bash(solana withdraw-from-nonce-account *)", - "Bash(docker rm *)", - "Bash(docker rmi *)", - "Bash(docker system prune *)", - "Bash(solana close *)", - "Bash(solana authorize *)", - "Bash(spl-token burn *)", - "Bash(spl-token close *)", - "Bash(git push * --force*)", - "Bash(git push * -f *)", - - "Read(**/*-keypair.json)", - "Read(**/.env)", - "Read(**/.superstack/config.json)" - ] - }, - - "hooks": { - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "echo '🤖 Solana Claude Config is ON 🚀🦾 powered by @SuperteamBR 🇧🇷'\necho ''\nif command -v solana >/dev/null 2>&1; then\n echo \"🔗 $(solana config get | grep 'RPC URL' | awk '{print $3}')\"\n echo \"👛 $(solana address 2>/dev/null || echo 'No wallet configured')\"\nelse\n echo '⚠️ Solana CLI not found'\nfi" - } - ] - } - ], - - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "for f in $(git diff --cached --name-only 2>/dev/null); do\n case \"$f\" in\n *.rs) command -v rustfmt >/dev/null 2>&1 && rustfmt \"$f\" 2>/dev/null || true ;;\n *.ts|*.tsx|*.js|*.jsx|*.json) command -v npx >/dev/null 2>&1 && npx prettier --write \"$f\" 2>/dev/null || true ;;\n esac\ndone" - } - ] - } - ], - - "SubagentStop": [ - { - "hooks": [ - { - "type": "command", - "command": "echo \"🤖 Agent completed: ${CLAUDE_SUBAGENT_NAME:-unknown}\"" - } - ] - } - ], - - "PostToolUse": [ - { - "matcher": "Write|Edit", - "hooks": [ - { - "type": "command", - "command": "if [ -z \"$CLAUDE_FILE_PATH\" ]; then exit 0; fi\n\ncase \"$CLAUDE_FILE_PATH\" in\n *.rs)\n command -v rustfmt >/dev/null 2>&1 && rustfmt \"$CLAUDE_FILE_PATH\" || true\n ;;\n *.ts|*.tsx|*.js|*.jsx|*.json|*.md)\n command -v npx >/dev/null 2>&1 && npx prettier --write \"$CLAUDE_FILE_PATH\" || true\n ;;\nesac" - } - ] - }, - - { - "matcher": "Bash", - "when": { - "command_matches": "cargo test|anchor test" - }, - "hooks": [ - { - "type": "command", - "command": "echo \"\"\necho \"📊 Test Summary:\";\nif [ \"$CLAUDE_TOOL_EXIT_CODE\" -eq 0 ]; then\n echo \"✅ All tests passed!\"\nelse\n echo \"❌ Some tests failed. Review output above.\"\nfi" - } - ] - }, - - { - "matcher": "Bash", - "when": { - "command_matches": "dotnet test|unity-editor.*-runTests" - }, - "hooks": [ - { - "type": "command", - "command": "echo \"\"\necho \"📊 .NET Test Summary:\";\nif [ \"$CLAUDE_TOOL_EXIT_CODE\" -eq 0 ]; then\n echo \"✅ All tests passed!\"\nelse\n echo \"❌ Some tests failed. Review output above.\"\nfi" - } - ] - } - ] - }, - - "modelDefaults": { - "agent": "opus", - "command": "sonnet" - } -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e6d485..803cb6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,8 +66,10 @@ jobs: include: - name: macos-arm64 os: macos-15 # GitHub-hosted M-series runner (aarch64-apple-darwin) + plat: darwin-arm64 - name: linux-x64 os: ubuntu-latest # x86_64-unknown-linux-gnu + plat: linux-x64 steps: - uses: actions/checkout@v4 @@ -94,6 +96,73 @@ jobs: - name: cargo test --verbose run: cargo test --manifest-path ${{ env.MANIFEST }} --verbose + # Prove the npm launcher stub (bin/cli.js) resolves and execs the binary. + # No network: we use the freshly built binary, not a release download. + - name: npm launcher smoke (no network) + shell: bash + run: | + set -euo pipefail + bin="crates/engine/target/debug/safe-ai-skill" + test -x "$bin" + mkdir -p npm/bin + cp "$bin" "npm/bin/safe-ai-skill-${{ matrix.plat }}" + node --check npm/bin/cli.js + out=$(node npm/bin/cli.js status) + echo "$out" + echo "$out" | grep -q '"command":"status"' + + # ── cross-build (the two non-native shipped targets, build-only) ────────── + # release.yml ships four binaries; the build-test matrix above only builds + + # tests the two NATIVE targets. Foreign-arch tests can't run on these runners, + # so here we BUILD darwin-x64 and linux-arm64 to catch compile regressions on + # those targets before a release tag — mirroring release.yml's target setup. + cross-build: + name: Cross-build / ${{ matrix.target }} + needs: lint + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-apple-darwin # darwin-x64 + os: macos-15 + use_cross: false + - target: aarch64-unknown-linux-gnu # linux-arm64 + os: ubuntu-latest + use_cross: true # cross handles the aarch64-linux sysroot + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust (${{ env.RUST_TOOLCHAIN }}) + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + targets: ${{ matrix.target }} + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + crates/engine/target + key: cargo-cross-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('crates/engine/Cargo.lock') }} + restore-keys: | + cargo-cross-${{ runner.os }}-${{ matrix.target }}- + + - name: Install cross (aarch64-linux only) + if: ${{ matrix.use_cross }} + run: cargo install cross --git https://github.com/cross-rs/cross --locked + + - name: Build (cargo) + if: ${{ !matrix.use_cross }} + run: cargo build --manifest-path ${{ env.MANIFEST }} --release --target ${{ matrix.target }} + + - name: Build (cross) + if: ${{ matrix.use_cross }} + run: cross build --manifest-path ${{ env.MANIFEST }} --release --target ${{ matrix.target }} + # ── plugin binary checksum + shim syntax ───────────────────────────────── verify-plugin-bin: name: Verify committed plugin binaries @@ -114,3 +183,26 @@ jobs: - name: Shim passes sh -n (syntax check) run: sh -n plugins/safe-ai-skill/bin/safe-ai-skill + + # ── package + script checks (npm wrapper, install.sh, version consistency) ─ + package-checks: + name: Package + script checks + runs-on: ubuntu-latest # ships shellcheck + node preinstalled + steps: + - uses: actions/checkout@v4 + + - name: node --check (npm wrapper) + run: | + node --check npm/bin/cli.js + node --check npm/scripts/postinstall.js + + - name: shellcheck (install.sh + scripts) + run: shellcheck install.sh scripts/check-versions.sh + + - name: sh -n (install.sh + scripts) + run: | + sh -n install.sh + sh -n scripts/check-versions.sh + + - name: Version consistency across manifests + run: sh scripts/check-versions.sh diff --git a/.github/workflows/npm-e2e.yml b/.github/workflows/npm-e2e.yml new file mode 100644 index 0000000..9df933a --- /dev/null +++ b/.github/workflows/npm-e2e.yml @@ -0,0 +1,54 @@ +# npm install e2e — safe-ai-skill +# +# Proves the FULL published install path end to end: `npm pack` the wrapper, +# install the tarball, let scripts/postinstall.js download the real release +# binary, verify its SHA-256 against the published SHA256SUMS, and exec it. +# +# This depends on published GitHub Release assets (the binaries + SHA256SUMS for +# the package's version tag), so it does NOT run on every PR. It runs: +# • on release publish (validate the artifacts we just shipped), +# • on a weekly schedule (catch release/asset drift), and +# • manually via workflow_dispatch. + +name: npm install e2e + +on: + release: + types: [published] + schedule: + - cron: "0 9 * * 1" # Mondays 09:00 UTC + workflow_dispatch: + +jobs: + npm-install: + name: npm install + verify + exec / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Pack the npm wrapper + working-directory: npm + run: | + tarball=$(npm pack | tail -n1) + echo "TARBALL=$PWD/$tarball" >> "$GITHUB_ENV" + + - name: Install globally (runs postinstall download + checksum verify) + run: | + prefix="$RUNNER_TEMP/npm-global" + mkdir -p "$prefix" + npm install -g --prefix "$prefix" "$TARBALL" + echo "$prefix/bin" >> "$GITHUB_PATH" + + - name: Smoke — exec the installed CLI + run: | + out=$(safe-ai-skill status) + echo "$out" + echo "$out" | grep -q '"command":"status"' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3c4d32..45a4288 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,12 +185,76 @@ jobs: dist/safe-ai-skill-linux-arm64 \ dist/SHA256SUMS - # Optionally update the committed plugin bin/ directory so the - # post-release state is self-consistent. This is a non-blocking step; - # it opens a PR or prints a reminder rather than committing directly. - - name: Reminder — update committed bin/ artifacts + # ── vendor the released binaries back into plugins/safe-ai-skill/bin/ (PR) ── + # The committed plugin binaries are what the Claude marketplace install ships; + # after each release we refresh all four + the SHA256SUMS so the repo state is + # self-consistent and CI's verify-plugin-bin stays green. Opens a PR (does NOT + # push to main) so a human reviews the binary update before it lands. + # + # NOTE: a PR opened with the default GITHUB_TOKEN will NOT trigger CI. To have + # CI (verify-plugin-bin) run on the vendor PR automatically, add a repo secret + # RELEASE_PAT (a fine-grained PAT with contents+pull_requests write); the job + # falls back to GITHUB_TOKEN when it is absent. + vendor-binaries: + name: Vendor binaries into plugin (PR) + needs: release + runs-on: ubuntu-latest + permissions: + contents: write # push the vendor branch + pull-requests: write # open the PR + steps: + - name: Checkout main + uses: actions/checkout@v4 + with: + ref: main + token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} + + - name: Download built binaries (from this run) + uses: actions/download-artifact@v4 + with: + path: dist/ + + - name: Stage binaries + regenerate SHA256SUMS + run: | + set -euo pipefail + find dist/ -mindepth 2 -type f -exec mv {} dist/ \; || true + bindir=plugins/safe-ai-skill/bin + for p in darwin-arm64 darwin-x64 linux-x64 linux-arm64; do + install -m 0755 "dist/safe-ai-skill-$p" "$bindir/safe-ai-skill-$p" + done + # SHA256SUMS covers the shim + all four binaries (what verify-plugin-bin checks). + ( cd "$bindir" && sha256sum \ + safe-ai-skill \ + safe-ai-skill-darwin-arm64 safe-ai-skill-darwin-x64 \ + safe-ai-skill-linux-x64 safe-ai-skill-linux-arm64 \ + > SHA256SUMS && sha256sum -c SHA256SUMS ) + + - name: Determine release tag + id: tag + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + fi + + - name: Open PR with refreshed binaries + env: + GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} run: | - echo "::notice::After verifying the release assets, copy the new" - echo "::notice::binaries into plugins/safe-ai-skill/bin/ and update" - echo "::notice::SHA256SUMS, then commit. The CI verify-plugin-bin" - echo "::notice::job will confirm the checksums on the next push." + set -euo pipefail + TAG="${{ steps.tag.outputs.tag }}" + BRANCH="release/vendor-${TAG}-binaries" + if git diff --quiet -- plugins/safe-ai-skill/bin; then + echo "::notice::Committed binaries already match the release; nothing to vendor." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -c "$BRANCH" + git add plugins/safe-ai-skill/bin + git commit -m "release: vendor ${TAG} platform binaries" + git push -f origin "$BRANCH" + BODY="Automated: refreshes plugins/safe-ai-skill/bin/ with the four ${TAG} release binaries + regenerated SHA256SUMS so marketplace installs work on every platform and verify-plugin-bin stays green. Built from the same run that created the GitHub Release. Review the binary diff, then merge." + gh pr create --base main --head "$BRANCH" --title "release: vendor ${TAG} platform binaries" --body "$BODY" || true + diff --git a/.gitignore b/.gitignore index 43b497f..e243da4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,40 @@ -# External Claude skill submodules -.claude/skills/ext/ +# ── Secrets — never commit ─────────────────────────────────────────────────── +.env +.env.* +.env.example + +# ── Local dev / dogfooding config ──────── +.claude/ CLAUDE.local.md +CLAUDE.md +.mcp.json +.gitmodules -# Secrets — never commit -.env +# ── Rust / Cargo ───────────────────────────────────────────────────────────── +target/ +**/*.rs.bk +*.pdb +# Keep Cargo.lock committed + +# ── Node / npm ─────────────────────────────────────────────────────────────── +node_modules/ +npm-debug.log* +*.tgz + +# ── OS clutter ─────────────────────────────────────────────────────────────── +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +desktop.ini + +# ── Editor / IDE ───────────────────────────────────────────────────────────── +*.swp +*.swo +*~ +.idea/ +.vscode/ +*.code-workspace diff --git a/CHANGELOG.md b/CHANGELOG.md index cffa175..fe7c5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,15 @@ All notable changes are documented in this file. Format follows [Keep a Changelo --- -## [1.0.0] — 2026-06-16 +## [1.0.0] — 2026-06-26 ### Summary -First stable release. Ships the safe-ai-skill engine, runtime action firewall, and supply-chain verifier as a standalone Claude Code plugin targeting `solanabr/solana-ai-kit` v2.0.0 as its primary integration hub. +First stable release. Ships the safe-ai-skill engine, runtime action firewall, and supply-chain verifier as a standalone, general-purpose Claude Code plugin — it secures any skills, MCPs, and agents. `solanabr/solana-ai-kit` v2.0.0 is the reference integration used to exercise and document the gates, not a requirement. ### Engine -- Static Rust binary (`safe-ai-skill` / `safe-ai-skill`) with synchronous I/O; no runtime dependency. Prebuilt for `darwin-arm64`, `darwin-x64`, `linux-x64` with `SHA256SUMS`. Falls back to `cargo build --release` if no prebuilt matches; shim exits code 2 (block) if neither path is available — never fails open. +- Static Rust binary (`safe-ai-skill` / `safe-ai-skill`) with synchronous I/O; no runtime dependency. Prebuilt for `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64` with `SHA256SUMS`. Falls back to `cargo build --release` if no prebuilt matches; shim exits code 2 (block) if neither path is available — never fails open. - Hook wiring: `gate-bash` (solana/spl-token/anchor CLI), `gate-bash-secrets` (unconditional secret and exfiltration patterns), `gate-read` (secret file globs), `gate-mcp` (value-moving MCP tools), `redact` (PostToolUse secret scrub), `prompt-guard` (UserPromptSubmit private key/seed block). - Append-only audit log (`audit.jsonl`), TOFU lockfile (`lockfile.json`), daily spend ledger (`spend.json`), time-boxed grant store (`grants.json`), session keypair dir (mode 0600). - Policy DSL (`default.policy.yaml`): full schema synced on-disk including `catalog`, `ext`, and `exec_install_scripts` fields. Deep-merge over project `.safe-ai-skill/policy.yaml`. Fail-closed on parse error. @@ -51,8 +51,19 @@ First stable release. Ships the safe-ai-skill engine, runtime action firewall, a ### Distribution -- Standalone plugin (`claude plugin marketplace add solanabr/safe-ai-skill`). Pre-enabled in repo `.claude/settings.json` for contributors. -- CI builds all three platform binaries and regenerates `SHA256SUMS` on every release tag. +- **Plugin** (always-on firewall): `claude plugin marketplace add solanabr/safe-ai-skill` → `claude plugin install safe-ai-skill@safe-ai-skill`. Ships committed platform binaries; the shim resolves the native prebuilt, else builds from `crates/engine`, else fails closed. +- **CLI** (`safe-ai-skill` Tier 1/2 commands) via three paths, all checksum-verified against the release `SHA256SUMS`: + - npm — `npm install -g safe-ai-skill` / `npx safe-ai-skill`. `postinstall.js` downloads the platform binary from the matching GitHub Release tag and aborts on checksum mismatch (no silent install). + - Standalone — `curl -fsSL …/install.sh | sh`. Downloads + verifies the binary before installing to `~/.local/bin` (overridable via `SAFE_AI_SKILL_BIN_DIR`); fails loud, installs nothing on mismatch. + - cargo — `cargo install safe-ai-skill` (builds from source on any Rust-capable platform). +- README Install section separates the always-on plugin (firewall) from the `safe-ai-skill` CLI. + +### Tooling & CI + +- `release.yml` builds all four platform binaries (`darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`) and regenerates `SHA256SUMS` on every `v*` tag. +- `ci.yml`: lint (fmt + clippy), native build+test matrix with an npm-launcher smoke, a build-only cross-build job covering `darwin-x64` + `linux-arm64`, committed-binary checksum verification, and a `package-checks` job (`node --check` on the npm wrapper, `shellcheck`/`sh -n` on shell scripts, and `scripts/check-versions.sh` asserting npm/Cargo/plugin/marketplace/README versions agree). +- `npm-e2e.yml` (release/schedule/dispatch): packs, installs, checksum-verifies, and execs the published CLI end to end. +- Repo hygiene: `.claude/` (local dogfooding config) is gitignored and the shipped plugin lives in `plugins/`; `.DS_Store`, OS/editor clutter, and install-time-downloaded `npm/bin/safe-ai-skill-*` binaries are ignored. - Folding safe-ai-skill into the `stbr` solana-ai-kit marketplace is deferred to a future release. ### Out of scope (deferred) diff --git a/README.md b/README.md index 9987c42..70132f9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ At `PreToolUse` it intercepts commands, MCP calls, and secret-file reads before ## Install +safe-ai-skill has two layers. The **plugin** is the always-on runtime firewall (SessionStart verifier + PreToolUse gates). The **CLI** is the `safe-ai-skill` binary that powers the Tier 1/2 commands (`registry`, `add skill`, `add mcp`, `install`, `verify`, `status`). Most users want the plugin; install the CLI too if you want to gate skill/MCP installs before they run. + +### Plugin (runtime firewall) + ```bash claude plugin marketplace add solanabr/safe-ai-skill claude plugin install safe-ai-skill@safe-ai-skill @@ -25,10 +29,37 @@ claude plugin install safe-ai-skill@safe-ai-skill Every Claude Code session is protected automatically from that point. No per-project configuration required. -**Contributors:** cloning this repo gives immediate protection — the plugin is already enabled in `.claude/settings.json`. - **Dev install:** `claude plugin marketplace add .` from the repo root, then `claude plugin install safe-ai-skill@safe-ai-skill`. +### CLI (`safe-ai-skill` command) + +The plugin runs the firewall; the Tier 1/2 commands need the `safe-ai-skill` binary on your `PATH`. Pick one: + +**npm** + +```bash +npm install -g safe-ai-skill # installs the CLI globally +npx safe-ai-skill add skill # or run without installing +``` + +`safe-ai-skill add skill ` is the headline command — it runs the full verification pipeline on the fetched content before any install touches your machine (see [Usage tiers](#usage-tiers)). + +**Standalone script** (no npm or cargo) + +```bash +curl -fsSL https://raw.githubusercontent.com/solanabr/safe-ai-skill/main/install.sh | sh +``` + +Downloads the prebuilt binary for your platform and verifies its SHA-256 against the release's published `SHA256SUMS` before installing — checksum-pinned, not an opaque pipe. Set `SAFE_AI_SKILL_BIN_DIR` to override the install location (default `~/.local/bin`). + +**cargo** (any platform with a Rust toolchain) + +```bash +cargo install safe-ai-skill +``` + +The npm and standalone paths download the binary matching their release tag and abort on any checksum mismatch. + ## What it protects **Runtime (PreToolUse firewall)** diff --git a/SECURITY.md b/SECURITY.md index 15a56e7..75b7613 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,7 +8,7 @@ **Prompt content.** `prompt-guard` (UserPromptSubmit hook) blocks prompts that contain raw private keys or seed phrases before they reach the model. -**Supply chain.** Every session start runs `verify session` over the installed skills directories, each of the 18 `ext/` git submodules independently, MCP server entries in `.claude/settings.json`, and all registry-installed catalog entries. The `@latest` MCP posture is flagged as INFORMATIONAL; safe-ai-skill does not treat it as an error because solana-ai-kit keeps MCPs `@latest` by design. Content that drifts from its pinned hash or git SHA is quarantined before skills are loaded. +**Supply chain.** Every session start runs `verify session` over the installed skills directories, each `ext/` git submodule independently, MCP server entries in `.claude/settings.json`, and all registry-installed catalog entries. The `@latest` MCP posture is flagged as INFORMATIONAL; safe-ai-skill does not treat it as an error when a hub keeps its MCPs `@latest` by design. Content that drifts from its pinned hash or git SHA is quarantined before skills are loaded. **Install-script execution.** Runtime `curl|bash`, `wget|bash`, and `curl|sh` patterns are intercepted by `gate-bash-secrets` (unconditional, microsecond matching) before the pipe executes. The default policy is `exec_install_scripts: ask` — the user sees the URL and script preview before approval. @@ -16,7 +16,9 @@ ### `ext/` submodule supply chain -solana-ai-kit ships 18 third-party `ext/` git submodules: third-party security tools, DeFi protocol SDKs, NFT tooling, and infrastructure integrations (including `ext/ghostsecurity`, `ext/trailofbits`, `ext/jupiter`, `ext/metaplex`, `ext/helius`, `ext/sendai`, `ext/vercel`, and others). Each is an independent supply-chain unit with a different origin, maintainer, and risk profile. +safe-ai-skill is general-purpose: it verifies whatever `ext/` submodules, skills, and MCPs a given setup ships — any hub, any count, or none. The walkthrough below uses `solanabr/solana-ai-kit` as a concrete worked example because it ships a large, heterogeneous set of submodules; the same verification applies unchanged to any other configuration. + +solana-ai-kit, for example, ships 18 third-party `ext/` git submodules: third-party security tools, DeFi protocol SDKs, NFT tooling, and infrastructure integrations (including `ext/ghostsecurity`, `ext/trailofbits`, `ext/jupiter`, `ext/metaplex`, `ext/helius`, `ext/sendai`, `ext/vercel`, and others). Each is an independent supply-chain unit with a different origin, maintainer, and risk profile. **What can go wrong per submodule:** - Telemetry preambles in SKILL.md files that POST skill-invocation metadata to third-party endpoints. diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..f523da7 --- /dev/null +++ b/install.sh @@ -0,0 +1,124 @@ +#!/bin/sh +# install.sh — standalone installer for the safe-ai-skill CLI +# +# Downloads the prebuilt safe-ai-skill binary for the current platform from the +# GitHub Release matching $SAFE_AI_SKILL_VERSION (default: latest pinned below), +# verifies its SHA-256 against the release's published SHA256SUMS, and installs +# it to $SAFE_AI_SKILL_BIN_DIR (default: ~/.local/bin). +# +# This mirrors the npm postinstall path (npm/scripts/postinstall.js). It is the +# checksum-verified alternative to `curl | bash`: it FAILS LOUDLY and +# installs nothing on a checksum mismatch or download error. A broken silent +# install is worse than a loud failing one for a security tool. +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/solanabr/safe-ai-skill/main/install.sh | sh +# SAFE_AI_SKILL_BIN_DIR=/usr/local/bin sh install.sh +# SAFE_AI_SKILL_VERSION=1.0.0 sh install.sh + +set -eu + +REPO="solanabr/safe-ai-skill" +# Default version — keep in sync with npm/package.json and crates/engine/Cargo.toml. +VERSION="${SAFE_AI_SKILL_VERSION:-1.0.0}" +TAG="v${VERSION}" +RELEASE_BASE="https://github.com/${REPO}/releases/download/${TAG}" +BIN_DIR="${SAFE_AI_SKILL_BIN_DIR:-${HOME}/.local/bin}" + +err() { + printf 'safe-ai-skill: %s\n' "$1" >&2 +} + +fatal() { + err "INSTALL FAILED — $1" + exit 1 +} + +# ── platform detection ────────────────────────────────────────────────────── + +detect_platform() { + os="$(uname -s)" + arch="$(uname -m)" + case "${os}" in + Darwin) + case "${arch}" in + arm64) echo "darwin-arm64" ;; + x86_64) echo "darwin-x64" ;; + *) return 1 ;; + esac + ;; + Linux) + case "${arch}" in + x86_64) echo "linux-x64" ;; + aarch64 | arm64) echo "linux-arm64" ;; + *) return 1 ;; + esac + ;; + *) + return 1 + ;; + esac +} + +# ── download (curl or wget) ───────────────────────────────────────────────── + +download() { + # download + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$1" -o "$2" + elif command -v wget >/dev/null 2>&1; then + wget -qO "$2" "$1" + else + fatal "neither curl nor wget is available." + fi +} + +# ── sha256 (sha256sum or shasum) ──────────────────────────────────────────── + +sha256_of() { + # sha256_of → prints lowercase hex hash + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + fatal "neither sha256sum nor shasum is available." + fi +} + +# ── main ──────────────────────────────────────────────────────────────────── + +plat="$(detect_platform)" || fatal "unsupported platform $(uname -s)/$(uname -m). Install via: cargo install safe-ai-skill" + +binary_name="safe-ai-skill-${plat}" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "${tmp_dir}"' EXIT INT TERM + +err "downloading ${binary_name} from release ${TAG}…" +download "${RELEASE_BASE}/${binary_name}" "${tmp_dir}/${binary_name}" \ + || fatal "could not download ${RELEASE_BASE}/${binary_name}. Verify release ${TAG} exists, or use: cargo install safe-ai-skill" +download "${RELEASE_BASE}/SHA256SUMS" "${tmp_dir}/SHA256SUMS" \ + || fatal "could not download SHA256SUMS for ${TAG}." + +# Expected hash for our binary from the published SHA256SUMS. +expected="$(grep " ${binary_name}\$" "${tmp_dir}/SHA256SUMS" | awk '{print $1}' | head -n1)" +[ -n "${expected}" ] || fatal "${binary_name} not found in SHA256SUMS." + +actual="$(sha256_of "${tmp_dir}/${binary_name}")" +if [ "${actual}" != "${expected}" ]; then + err " expected: ${expected}" + err " got: ${actual}" + fatal "checksum mismatch for ${binary_name}. Do NOT use this binary; the artifact may be tampered or the release out of date." +fi + +mkdir -p "${BIN_DIR}" +install_path="${BIN_DIR}/safe-ai-skill" +cp "${tmp_dir}/${binary_name}" "${install_path}" +chmod 0755 "${install_path}" + +err "installed safe-ai-skill ${VERSION} → ${install_path} (checksum verified)." + +case ":${PATH}:" in + *":${BIN_DIR}:"*) ;; + *) err "note: ${BIN_DIR} is not on your PATH. Add it, e.g.: export PATH=\"${BIN_DIR}:\$PATH\"" ;; +esac diff --git a/npm/.gitignore b/npm/.gitignore new file mode 100644 index 0000000..c2ed510 --- /dev/null +++ b/npm/.gitignore @@ -0,0 +1,6 @@ +# Downloaded at install time by scripts/postinstall.js — never commit the binary. +bin/safe-ai-skill-* +bin/SHA256SUMS + +# npm pack output +*.tgz diff --git a/npm/package.json b/npm/package.json index 7b866b2..4af4473 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "safe-ai-skill", - "version": "0.1.0", + "version": "1.0.0", "description": "Security firewall + supply-chain verifier for Solana AI dev — gates transactions, deploys, and secrets; verifies skills and MCPs.", "keywords": [ "solana", diff --git a/plugins/safe-ai-skill/bin/SHA256SUMS b/plugins/safe-ai-skill/bin/SHA256SUMS index fe0d90f..2a1a3a7 100644 --- a/plugins/safe-ai-skill/bin/SHA256SUMS +++ b/plugins/safe-ai-skill/bin/SHA256SUMS @@ -1,2 +1,2 @@ f195099489c7cca7069e8b6eba526a201c86f30317c259086a5ad9e9e2f53bbb safe-ai-skill-darwin-arm64 -0678c2ac03010286837f4d9999a5f45b8b06bd9b41584e37a88e11fe4d63a08d safe-ai-skill +8c2bd361ebb8a9d6b319ed1b488183b629c395368672e8f7085aee7816c4ba9d safe-ai-skill diff --git a/plugins/safe-ai-skill/bin/safe-ai-skill b/plugins/safe-ai-skill/bin/safe-ai-skill index 1f4aa60..b0efc32 100755 --- a/plugins/safe-ai-skill/bin/safe-ai-skill +++ b/plugins/safe-ai-skill/bin/safe-ai-skill @@ -31,6 +31,7 @@ case "$os" in Linux) case "$arch" in x86_64|amd64) plat="linux-x64" ;; + aarch64|arm64) plat="linux-arm64" ;; esac ;; esac diff --git a/plugins/safe-ai-skill/policy/default.policy.yaml b/plugins/safe-ai-skill/policy/default.policy.yaml index 480ed9d..eb8da91 100644 --- a/plugins/safe-ai-skill/policy/default.policy.yaml +++ b/plugins/safe-ai-skill/policy/default.policy.yaml @@ -62,7 +62,7 @@ catalog: supply_chain: verify_skills_dirs: [ "~/.claude/skills", ".claude/skills" ] flag_unpinned_mcp: true # @latest in .mcp.json — INFORMATIONAL/LOW (MCPs kept @latest), not blocking - flag_telemetry_curl: true # solana-new Convex preamble (generic telemetry detector) + flag_telemetry_curl: true # generic telemetry preamble detector (e.g. Convex /api/mutation) verify_ext_submodules: true # walk .claude/skills/ext/ as individual verifiable units ext_dir: ".claude/skills/ext" exec_install_scripts: ask # runtime curl … | sh/bash gate: allow | ask | deny diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..0106134 --- /dev/null +++ b/scripts/check-versions.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# check-versions.sh — assert the release version is identical across all +# distribution manifests. Run in CI and before tagging a release. +# +# Sources checked: +# npm/package.json "version": "X" +# crates/engine/Cargo.toml version = "X" +# plugins/safe-ai-skill/.claude-plugin/plugin.json "version": "X" +# .claude-plugin/marketplace.json "version": "X" +# README.md badge/version-X-blue +# +# Exits non-zero (and prints the offending values) if any disagree. + +set -eu + +cd "$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)" + +fail=0 + +# Extract the first "version" string value from a JSON file. +json_version() { + grep -m1 '"version"' "$1" | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' +} + +npm_v="$(json_version npm/package.json)" +cargo_v="$(grep -m1 '^version' crates/engine/Cargo.toml | sed 's/.*"\([^"]*\)".*/\1/')" +plugin_v="$(json_version plugins/safe-ai-skill/.claude-plugin/plugin.json)" +market_v="$(json_version .claude-plugin/marketplace.json)" +readme_v="$(grep -m1 'badge/version-' README.md | sed 's/.*badge\/version-\([0-9][^-]*\)-.*/\1/')" + +printf '%-12s %s\n' "npm:" "${npm_v}" +printf '%-12s %s\n' "cargo:" "${cargo_v}" +printf '%-12s %s\n' "plugin:" "${plugin_v}" +printf '%-12s %s\n' "marketplace:" "${market_v}" +printf '%-12s %s\n' "readme:" "${readme_v}" + +for v in "${cargo_v}" "${plugin_v}" "${market_v}" "${readme_v}"; do + if [ "${v}" != "${npm_v}" ]; then + fail=1 + fi +done + +if [ -z "${npm_v}" ]; then + echo "check-versions: could not parse npm version" >&2 + fail=1 +fi + +if [ "${fail}" -ne 0 ]; then + echo "check-versions: VERSION MISMATCH — all manifests must agree." >&2 + exit 1 +fi + +echo "check-versions: OK — all manifests at ${npm_v}."