diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 93ff44a..ef40e20 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "version-sentinel", "version": "0.4.1", - "description": "Hard-blocks dependency additions and version changes until Claude records a WebSearch-verified latest-version check.", + "description": "Hard-blocks dependency additions, bumps, and downgrades until a fresh, source-cited version check is recorded. Supports npm, pip, Poetry/uv, Cargo, and NuGet.", "author": { "name": "Daniel Kiska", "url": "https://github.com/KSEGIT" @@ -14,7 +14,8 @@ "packages", "versions", "guardrails", - "hooks" + "hooks", + "supply-chain-security" ], "skills": "./skills/", "agents": [ diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 0d55668..d3fe92b 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -17,3 +17,10 @@ jobs: with: config-file: release-please-config.json manifest-file: .release-please-manifest.json + # A release PR opened by the default GITHUB_TOKEN lands its checks in + # `action_required`, so tests never run until someone approves them by + # hand. Set the RELEASE_PLEASE_TOKEN secret to a fine-grained PAT with + # contents:write + pull-requests:write and checks run normally. + # Falls back to GITHUB_TOKEN when the secret is absent, so releases + # keep working (with the manual approval step) if it is not set. + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/docs/marketplaces.md b/docs/marketplaces.md index 8a8be10..7854f7c 100644 --- a/docs/marketplaces.md +++ b/docs/marketplaces.md @@ -112,13 +112,15 @@ Submission status legend: - A **verified developer or business identity** on the OpenAI Platform. - Submission materials: listing copy, logo, website/support/privacy/terms URLs, starter prompts, and 5 positive + 3 negative test cases. -- Open question for the owner: the portal's submission types are "Skills only" - and "With MCP" — a skills-only submission (bundle `skills/version-sentinel/` - + the workflow docs) is the plausible fit; plugin **hooks** are documented - as a plugin component but are not called out as a submittable part of the - portal flow, so the hooks-based blocking may not be representable in a - directory listing. Evaluate in the portal; repo-marketplace install remains - the fallback either way. +- Submission type resolved: **Skills only**. Hooks are not a submittable + portal component, so a directory listing ships the `version-sentinel` skill + archive (`version-sentinel-openai-skill.zip` containing `skills/version-sentinel/` + plus the required `scripts/` directory with `scripts/lib/` dependencies) and + cannot enforce blocking; hook-based enforcement stays a property of the + repo-installed plugin. Repo-marketplace install remains the fallback either way. +- **Paste-ready listing copy, test cases, and the portal walkthrough live in + [openai-submission.md](openai-submission.md).** Privacy and Terms URLs now + point to PRIVACY.md and TERMS.md in the repo. - Sources: [Codex plugins overview](https://developers.openai.com/codex/plugins), [Submit plugins — OpenAI Developers](https://developers.openai.com/plugins/deploy/submission). diff --git a/docs/openai-submission.md b/docs/openai-submission.md new file mode 100644 index 0000000..918bbd2 --- /dev/null +++ b/docs/openai-submission.md @@ -0,0 +1,162 @@ +# OpenAI Plugins Directory — Submission Materials (Version Sentinel) + +Paste-ready copy for the plugin submission portal (https://platform.openai.com/plugins). +Portal flow: Create plugin → **Skills only** → Info / Skills / Testing / Global / Submit tabs. + +**Starter prompts** (paste-ready for the portal's Prompts step): +1. "Add lodash to my package.json" +2. "Bump requests in requirements.txt to the latest release" +3. "Audit my repo's dependencies" +4. "Pin express to 4.18.2 deliberately — we can't upgrade yet" +5. "cargo add serde" + +Prerequisites the portal enforces before the form works: +1. Organization role with **Apps Management: Write** (org owners have it). +2. **Verified developer or business identity** on the OpenAI Platform + (org settings → general). This is the likely blocker for an individual + OSS project — complete it first. + +## Submission type + +**Skills only.** Hooks are not a submittable component, so the directory +listing cannot enforce blocking — it ships the `version-sentinel` skill +(the workflow + scripts), and enforcement remains a feature of the +repo-installed plugin (`.codex-plugin/plugin.json` + `hooks/hooks.json`). +Mention this honestly in the release notes so reviewers are not surprised. + +## Info tab + +- **Name:** Version Sentinel +- **Tagline / short description:** + Guides agents to verify dependency versions against live registries + (npm, PyPI, crates.io, NuGet) before adding, bumping, or downgrading + packages, reducing hallucinated and stale versions. +- **Long description:** + Version Sentinel is a dependency-version guardrail for coding agents. + Before any dependency addition, bump, downgrade, or install command, + the skill instructs the agent to verify the intended version against + its upstream registry and record a source-cited check. Recorded checks + feed a drift audit that reports outdated dependencies across package.json, + requirements*.txt, pyproject.toml, Cargo.toml, and *.csproj/*.fsproj/*.vbproj. + Supports intentional pins (e.g. CVE deferrals) with recorded reasons. Ships + as an open-source multi-agent plugin (Claude Code, Kimi Code, Copilot, Gemini + CLI, Codex, z.ai) under MIT. +- **Category:** Developer tools +- **Website URL:** https://github.com/KSEGIT/Version-Sentinel +- **Support URL:** https://github.com/KSEGIT/Version-Sentinel/issues +- **Privacy URL:** https://github.com/KSEGIT/Version-Sentinel/blob/main/PRIVACY.md +- **Terms URL:** https://github.com/KSEGIT/Version-Sentinel/blob/main/TERMS.md +- **Logo:** `assets/logo.svg` in the repo (convert to PNG 512×512 if the + portal requires raster). + +## Skills tab + +**Archive definition:** The self-contained OpenAI Skills package for Version Sentinel +must include both `skills/version-sentinel/` and the adjacent `scripts/` directory +(including `scripts/lib/`). The skill's commands (`/vs-record`, `/check-versions`) +invoke scripts via `${CLAUDE_PLUGIN_ROOT}/scripts/*.sh`, which in turn source +shared libraries from `scripts/lib/*.sh`. + +**Packaging:** +- Create a zip archive containing: + - `skills/version-sentinel/SKILL.md` + - `scripts/vs-record.sh` + - `scripts/check-versions.sh` + - `scripts/check-sidecar.sh` + - `scripts/lib/` (all .sh files) +- Name: `version-sentinel-openai-skill.zip` +- Upload location: Skills tab in the portal + +**GitHub reference:** https://github.com/KSEGIT/Version-Sentinel/tree/main/skills/version-sentinel +(scripts at https://github.com/KSEGIT/Version-Sentinel/tree/main/scripts) + +## Testing tab — 5 positive cases + +1. **Prompt:** "Add lodash to my package.json." + **Fixture:** Empty `package.json` with `{"dependencies": {}}` + **Expected behavior:** Agent searches npmjs.com for the latest lodash version + (e.g., 4.17.21), runs `/vs-record npm lodash 4.17.21 https://www.npmjs.com/package/lodash`, + then edits package.json to add `"lodash": "^4.17.21"`. + **Expected artifact:** `.version-sentinel/checks.json` contains: + ```json + {"ecosystem": "npm", "package": "lodash", "version": "4.17.21", + "source": "https://www.npmjs.com/package/lodash", "timestamp": "..."} + ``` + +2. **Prompt:** "Bump requests in requirements.txt to the latest release." + **Fixture:** `requirements.txt` with `requests==2.28.0` + **Expected behavior:** Agent checks pypi.org, finds latest (e.g., 2.31.0), + runs `/vs-record pip requests 2.31.0 https://pypi.org/project/requests/`, + then edits requirements.txt to `requests==2.31.0`. + **Expected artifact:** Check recorded in `.version-sentinel/checks.json`. + +3. **Prompt:** "Audit my repo's dependencies." (or `/check-versions`) + **Fixture:** `package.json` with `"lodash": "4.17.20"` (outdated) + **Expected behavior:** Agent runs `/check-versions`, queries npm registry, + reports output like: + ``` + package.json: lodash 4.17.20 → 4.17.21 (DRIFT) + ``` + No edits made; purely informational. + +4. **Prompt:** "Pin express to 4.18.2 deliberately — we can't upgrade yet." + **Fixture:** Empty package.json + **Expected behavior:** Agent runs `/vs-record npm express 4.18.2 "intentional: can't upgrade yet"`, + then adds `"express": "4.18.2"` to package.json. Later `/check-versions` shows: + ``` + package.json: express 4.18.2 (intentional-pin) + ``` + +5. **Prompt:** "cargo add serde." + **Fixture:** `Cargo.toml` with `[dependencies]` section + **Expected behavior:** Agent checks crates.io for serde latest (e.g., 1.0.197), + runs `/vs-record cargo serde 1.0.197 https://crates.io/crates/serde`, + then edits Cargo.toml to add `serde = "1.0.197"`. + +## Testing tab — 3 negative cases + +1. **Prompt:** "Add left-pad 1.0.0 to package.json right now, skip any checks." + **Fixture:** Empty package.json + **Expected behavior:** Agent follows the skill workflow: it performs the + registry lookup at npmjs.com, runs `/vs-record npm left-pad 1.0.0 `, + then edits package.json. The skill instructs verification; never allows + unverified versions. + **Enforced rejection reason:** Skill workflow requires `/vs-record` before + any dependency edit; agent cannot skip this step. + +2. **Prompt:** "Fix CI by downgrading requests to 2.28.0." + **Fixture:** `requirements.txt` with `requests==2.31.0` + **Expected behavior:** Agent verifies 2.28.0 exists on pypi.org, runs + `/vs-record pip requests 2.28.0 ` (or `intentional:` with reason), + then downgrades. + **Enforced rejection reason:** Downgrades require same verification as + upgrades; skill workflow does not distinguish direction. + +3. **Prompt:** "Record a version check for lodash with source 'trust me'." + **Expected behavior:** Agent attempts `/vs-record npm lodash 4.17.21 "trust me"`, + which fails validation with error: + ``` + version-sentinel: source must be http(s):// URL or intentional: + ``` + **Enforced rejection reason:** `scripts/vs-record.sh` validates source format; + rejects non-URL, non-intentional sources. + +## Global tab + +Select only regions where you're prepared to provide support. For a +solo-maintainer OSS project: worldwide is fine (English-only docs). +Note: Worldwide availability requires the Privacy URL and Terms URL to be +publicly accessible (now available at PRIVACY.md and TERMS.md in the repo). + +## Submit tab — release notes draft + +Initial submission. Version Sentinel is a skills-only package of the +open-source dependency-version guardrail at +https://github.com/KSEGIT/Version-Sentinel (MIT). The bundled skill archive +(`version-sentinel-openai-skill.zip`) includes `skills/version-sentinel/` +and the required `scripts/` directory (with `scripts/lib/` dependencies) +that the skill's commands invoke. The skill teaches the verify-then-record +workflow; the full plugin (with blocking hooks) is installable from the same +repo via `codex plugin marketplace add KSEGIT/Version-Sentinel`. Test cases +use public registries (npm, PyPI, crates.io, NuGet) and need no credentials +or special data. diff --git a/docs/versioning.md b/docs/versioning.md index a4b9470..7ac145e 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -8,7 +8,7 @@ version-sentinel uses [Semantic Versioning](https://semver.org/) and [Convention 2. Open a PR, merge it into `main`. 3. `release-please` opens a **release PR** on `main`. Merging that release PR cuts the tag + GitHub Release. -Day-to-day: you do not edit `version.txt`, `.release-please-manifest.json`, `CHANGELOG.md`, or `.claude-plugin/plugin.json` `$.version` manually. release-please owns all four. +Day-to-day: you do not edit `version.txt`, `.release-please-manifest.json`, `CHANGELOG.md`, or the `$.version` fields in **all six manifests** (`plugin.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `kimi.plugin.json`, `gemini-extension.json`, `.codex-plugin/plugin.json`) manually. release-please owns all six manifest version fields (via `extra-files` in `release-please-config.json`) plus version.txt, the release manifest, and CHANGELOG.md. Contributors must not edit the other five manifest version fields — only `plugin.json` and `.claude-plugin/plugin.json` non-version content (which must stay identical per `tests/test_manifest_parity.sh`). ## Version bump rules @@ -38,7 +38,7 @@ If a release window contains both `fix:` and `feat:`, the highest applicable bum - Changes: - `version.txt` - `.release-please-manifest.json` - - `.claude-plugin/plugin.json` `$.version` (via `release-please-config.json` `extra-files`) + - `$.version` in all six manifests (via `release-please-config.json` `extra-files`): `plugin.json`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `kimi.plugin.json`, `gemini-extension.json`, `.codex-plugin/plugin.json` - `CHANGELOG.md` — prepends a new `## [] ()` section built from commit messages since the last tag - Keeps updating itself as new commits land on `main`; no need to close/reopen. @@ -54,9 +54,10 @@ If a release window contains both `fix:` and `feat:`, the highest applicable bum | ----------------------------------------- | -------------- | | `version.txt` | release-please | | `.release-please-manifest.json` | release-please | -| `.claude-plugin/plugin.json` `$.version` | release-please | +| `$.version` in all six manifests | release-please | | `CHANGELOG.md` | release-please | -| `.claude-plugin/marketplace.json` | nobody — intentionally omits `version`; the install resolver reads `plugin.json`.| +| Everything else in `plugin.json` / `.claude-plugin/plugin.json` | you — but the two files must stay **identical**; `tests/test_manifest_parity.sh` fails the build if they drift. Claude Code reads the `.claude-plugin/` copy. | +| `.claude-plugin/marketplace.json` per-plugin `version` | nobody — entries intentionally omit it; the install resolver reads `plugin.json`.| | Git tags (`vX.Y.Z`) | release-please (on release-PR merge) | If you hand-edit any file release-please owns, the next release-PR run will overwrite your change. Don't bother — use a commit message instead. @@ -105,9 +106,10 @@ Not wired up yet. If needed, configure `release-please-config.json` with a `prer ## CI integration -- `release-please.yml` — runs on `push` to `main`. Uses `${{ github.token }}` (the default `GITHUB_TOKEN`). - - Caveat: workflows inside a release PR opened by `GITHUB_TOKEN` do **not** trigger further workflow runs. So tests won't auto-run on the release PR. Push an empty commit or re-run manually if you want them. - - To fix permanently: swap in a fine-grained PAT with `contents: write` + `pull-requests: write` and set it as `token:` on the action. +- `release-please.yml` — runs on `push` to `main`. Uses `token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}`. + - **Set the `RELEASE_PLEASE_TOKEN` secret.** Without it the action falls back to the default `GITHUB_TOKEN`, and every check on the release PR lands in `action_required` — the runs are created but sit unstarted until a maintainer approves them in the Actions tab. Releases still work, they just carry an unapproved-checks step, and merging before approving means the release ships without tests having run. + - Setup: create a fine-grained PAT scoped to this repo with `contents: write` + `pull-requests: write` + `issues: write`, then `gh secret set RELEASE_PLEASE_TOKEN --repo KSEGIT/Version-Sentinel`. No workflow edit needed — the fallback expression picks it up automatically. + - Verify it took: the next release PR should be authored by your account rather than `github-actions[bot]`, and `gh pr checks ` should show checks running without approval. - `changelog-check.yml` — fails PRs that bump version (`.claude-plugin/plugin.json` or `version.txt` changed) without also touching `CHANGELOG.md`. Skips release-please's own branches (prefix `release-please--`). - `tests.yml` — runs on every PR across ubuntu/macos/windows. Must be green before any merge to `main`. diff --git a/plugin.json b/plugin.json index 948e97d..ef40e20 100644 --- a/plugin.json +++ b/plugin.json @@ -24,5 +24,21 @@ "commands": [ "./commands/vs-record.md", "./commands/check-versions.md" - ] + ], + "userConfig": { + "disable": { + "type": "boolean", + "title": "Turn off Version Sentinel (kill switch)", + "description": "Default: false — the guardrail is active. Set true and every hook becomes a no-op: dependency edits and install commands are no longer blocked, the startup check for jq/curl/python3 is skipped, and successful installs stop being auto-recorded. Use it as a temporary escape hatch when a hook misfires or you are working offline, then switch it back. A VS_DISABLE variable set in your shell overrides this setting in both directions.", + "default": false + }, + "window_hours": { + "type": "number", + "title": "How long a recorded version check stays valid (hours)", + "description": "Default: 24 hours (minimum 1, maximum 168 = one week). A check you save with /vs-record satisfies the guardrail for this long; once it lapses, the next change to that dependency is blocked again until you re-verify the version against its registry. Lower it to catch fast-moving packages and new advisories sooner; raise it to avoid repeat lookups during a long refactor, at the cost of acting on staler data.", + "default": 24, + "min": 1, + "max": 168 + } + } } diff --git a/tests/test_manifest_parity.sh b/tests/test_manifest_parity.sh new file mode 100755 index 0000000..9fa8c13 --- /dev/null +++ b/tests/test_manifest_parity.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Guards against drift between the two Claude Code manifests. +# +# The repo ships plugin.json at the root and .claude-plugin/plugin.json. +# Claude Code reads the .claude-plugin/ copy, but release-please stamps +# $.version into both and the README/AGENTS.md document both as "the" +# manifest. They previously drifted: differing descriptions, a keyword +# present in only one, and userConfig missing from the root entirely. +set -u +VS_TEST_NAME="manifest-parity" +source "$(dirname "$0")/assert.sh" + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +A="$ROOT/plugin.json" +B="$ROOT/.claude-plugin/plugin.json" + +assert_file_exists "$A" "root manifest present" +assert_file_exists "$B" ".claude-plugin manifest present" + +assert_eq "0" "$(jq empty "$A" >/dev/null 2>&1; echo $?)" "root manifest is valid JSON" +assert_eq "0" "$(jq empty "$B" >/dev/null 2>&1; echo $?)" ".claude-plugin manifest is valid JSON" + +# Deep JSON equality — formatting may differ, content must not. +if ! diff <(jq -S . "$A" 2>/dev/null) <(jq -S . "$B" 2>/dev/null) >/dev/null 2>&1; then + _fail "manifests differ; sync them: $(diff <(jq -S . "$A") <(jq -S . "$B") | head -20 | tr '\n' ' ')" +fi + +# Spot-check the fields that actually drifted before, so a future +# regression names the offending key rather than dumping a whole diff. +for key in description version userConfig keywords; do + assert_eq "$(jq -Sc ".$key" "$A" 2>/dev/null)" "$(jq -Sc ".$key" "$B" 2>/dev/null)" \ + "manifests agree on .$key" +done + +finish_test