From 3e1c0f3088f12d73e475ba12cdb03e1ae9612b3d Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Thu, 6 Aug 2026 14:34:40 -0400 Subject: [PATCH 1/7] chore: block dependency install scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm has no allow-list model for lifecycle scripts, so ignore-scripts is the blunt equivalent of pnpm's onlyBuiltDependencies: a trojanized dependency cannot execute on `npm ci` in CI or on a laptop, where live credentials sit next to the install. A full install and test pass under the setting confirms nothing here needs it — the one dependency with an install hook is esbuild, whose platform binary arrives through optionalDependencies and is resolved by the JS shim at runtime. The setting also suppresses this package's own prepublishOnly hook, so the release workflow now documents that the explicit build step is what produces dist/ and must stay ahead of any publish. --- .github/workflows/release.yml | 9 +++++++++ .npmrc | 8 ++++++++ 2 files changed, 17 insertions(+) create mode 100644 .npmrc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79febe5..0364f6d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,15 @@ jobs: # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # # To enable: add NPM_TOKEN to repository secrets, # # and add `registry-url: https://registry.npmjs.org` to setup-node above. + # # + # # Heads up: .npmrc sets `ignore-scripts=true` so no dependency install + # # script ever runs. That setting is not limited to dependencies — it also + # # suppresses this package's own `prepublishOnly` hook + # # (`npm run clean && npm run build`), so `npm publish` will NOT build + # # dist/ for you. The explicit `npm run build` step above is what produces + # # the artifact; keep it before any publish step (or run + # # `npm run clean && npm run build` here) so a stale or empty dist/ is + # # never published. # - name: Create GitHub Release # run: gh release create "v${{ steps.version.outputs.version }}" --generate-notes diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9f27ac1 --- /dev/null +++ b/.npmrc @@ -0,0 +1,8 @@ +# Supply-chain hardening: never run a dependency's install lifecycle scripts. +# npm has no allow-list model (pnpm's onlyBuiltDependencies), so this is the +# blunt equivalent — it applies to CI (`npm ci`) and developer laptops alike. +# +# Caveat: this also suppresses THIS project's own lifecycle scripts, including +# `prepublishOnly` (clean + build). Any publish path must build explicitly +# first — see the note in .github/workflows/release.yml. +ignore-scripts=true From 2f39da324861a96ee5ef214cfd6273e8d61cc7e8 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Thu, 6 Aug 2026 15:08:20 -0400 Subject: [PATCH 2/7] ci: pin claude-code-action to a commit SHA This job holds ANTHROPIC_API_KEY, and the action it consumed was referenced by a tag that its maintainers can move. The action's own latest release is the floating v1 tag with immutable releases not enabled, so a retag would have reached this workflow without anything in the file changing. Pinned to the commit v1 resolves to today, so behavior is unchanged. --- .github/workflows/claude-pr-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index b624cbf..1030b40 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -24,7 +24,7 @@ jobs: mkdir -p .claude/skills cp -r /tmp/prose/skills/open-prose .claude/skills/open-prose - - uses: anthropics/claude-code-action@v1 + - uses: anthropics/claude-code-action@c038e4dcdedfbbca18dfb17df35a17e40ded4ddc # v1.0.186 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} prompt: "prose run pr-review.prose" From 2166bdd9b8c7ae808e3b40548fdf197743b144d8 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Fri, 7 Aug 2026 12:09:49 -0400 Subject: [PATCH 3/7] ci: gate on registry signatures and production advisories Both checks block here, which this repo can afford: all 55 installed packages carry valid registry signatures, and its production dependencies have no known advisories. Holding that at zero is far cheaper than reclaiming it later. Dependabot's github-actions entry keeps the pinned action SHAs current; without it they silently rot. --- .github/dependabot.yml | 24 ++++++++++++++++++++++++ .github/workflows/ci.yml | 7 +++++++ 2 files changed, 31 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bbd8bb2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 + +# Third-party actions are pinned to immutable commit SHAs. SHAs never move on +# their own, so this entry is what keeps them current — without it the pins rot. +# +# Only the github-actions ecosystem is enabled. npm version-update PRs are +# deliberately off: `npm ci` installs strictly from the lockfile and `.npmrc` +# blocks dependency install scripts, so drift is already contained, and a PR +# per release is noise nobody reads. Dependabot *alerts* are enabled in repo +# settings and are the signal layer we do want; automated *security-update* PRs +# are off for the same reason — advisories get triaged against real exposure, +# not auto-patched. +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + # One PR for all action bumps instead of one per action. + groups: + actions: + patterns: + - "*" + commit-message: + prefix: "ci" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30425c5..85cbc47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,13 @@ jobs: node-version: ${{ matrix.node-version }} cache: npm - run: npm ci + # Blocking. Every installed dependency must carry a valid npm registry + # signature, so a tampered or unsigned tarball fails the build. + - run: npm audit signatures + # Blocking. This tree has no known advisories in its production + # dependencies today, so keep it that way: fix, replace, or drop the + # dependency rather than loosening the gate. + - run: npm audit --omit=dev - run: npx tsc --noEmit - run: npx biome check . - run: npx vitest --run From 3f165dab4f245d215ae2a58fa6293cc4517af8f2 Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 11 Aug 2026 09:40:21 -0400 Subject: [PATCH 4/7] chore: run CI tools from the lockfile, not the registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every job invoked its tools through npx. npx falls back to downloading a package when the name is not in the lockfile, so tools that were never declared arrived unpinned and unverified at run time — after the signature and advisory gates had already passed on the installed tree, and in jobs holding a provider key or a write token. tsx was one of those, and is now a declared devDependency so the eval jobs run the version the lockfile records. Every invocation takes --no-install so a tool that goes missing fails the job instead of quietly fetching a replacement. The remaining case was 'npx biome check .'. The Biome CLI is not a dependency of this project and never has been, so that line resolved an unrelated package of the same short name and ran its no-op CLI: the lint gate reported success without reading a single file, while pulling an unreviewed dependency tree into CI and release. Removing it costs no coverage that existed. Adopting @biomejs/biome properly, and fixing the diagnostics it reports, is a change of its own. --- .github/workflows/ci.yml | 12 +- .github/workflows/press-eval.yml | 4 +- package-lock.json | 504 +++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 517 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85cbc47..9cfc00c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,12 @@ jobs: # dependencies today, so keep it that way: fix, replace, or drop the # dependency rather than loosening the gate. - run: npm audit --omit=dev - - run: npx tsc --noEmit - - run: npx biome check . - - run: npx vitest --run + # `--no-install` keeps these on the binaries `npm ci` just installed and + # verified. Without it, npx silently fetches an unpinned package from the + # registry whenever a tool is missing from the lockfile, which lands + # unreviewed code in the job right after the gates above cleared it. + - run: npx --no-install tsc --noEmit + - run: npx --no-install vitest --run + # No lint step: the Biome CLI is not a declared dependency here, so + # invoking it would download an unreviewed binary on every run. Wiring up + # @biomejs/biome, and fixing what it reports, is a separate change. diff --git a/.github/workflows/press-eval.yml b/.github/workflows/press-eval.yml index 0dd144a..c1f9eb5 100644 --- a/.github/workflows/press-eval.yml +++ b/.github/workflows/press-eval.yml @@ -27,7 +27,9 @@ jobs: - run: npm ci - name: Run Press evals (quick tier) - run: npx tsx src/eval-pipeline.ts --tier quick --concurrency 3 + # `--no-install` so this runs the lockfile's tsx rather than fetching an + # unpinned one from the registry into a job holding a provider key. + run: npx --no-install tsx src/eval-pipeline.ts --tier quick --concurrency 3 env: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} timeout-minutes: 8 diff --git a/package-lock.json b/package-lock.json index a9f993b..0bb4ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ }, "devDependencies": { "@types/node": "^24.3.0", + "tsx": "^4.23.12", "typescript": "^5.7.3", "vitest": "^3.2.4" }, @@ -1427,6 +1428,509 @@ "node": ">=14.0.0" } }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index 4574e81..2aa4c2f 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ }, "devDependencies": { "@types/node": "^24.3.0", + "tsx": "^4.23.12", "typescript": "^5.7.3", "vitest": "^3.2.4" } From 2a87970b741c633804471964c5ff29f2743bb6cc Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 11 Aug 2026 09:42:55 -0400 Subject: [PATCH 5/7] ci: pin the OpenProse skill checkout to a commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both jobs that install the OpenProse skill took it from another repository's default branch. That skill becomes the instructions the model follows, and both jobs hold a credential — an API key with pull-request write access in one, a provider key in the other. A commit pushed to that repository would therefore change what runs here, with nothing reviewed on this side. Both now fetch a fixed commit and leave no git credential on disk. Bump the ref deliberately, the same way the action pins are maintained. --- .github/workflows/claude-pr-review.yml | 13 ++++++++++++- .github/workflows/press-eval-full.yml | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/claude-pr-review.yml b/.github/workflows/claude-pr-review.yml index 1030b40..c5f442d 100644 --- a/.github/workflows/claude-pr-review.yml +++ b/.github/workflows/claude-pr-review.yml @@ -19,8 +19,19 @@ jobs: - uses: actions/checkout@v4 - name: Install OpenProse skill + # Pinned to a commit rather than tracking the default branch. This skill + # becomes the instructions Claude follows in a job that holds an API key + # and can write to pull requests, so a commit pushed to that repository + # would otherwise change behaviour here with nothing reviewed on this + # side. Bump deliberately. + env: + PROSE_REF: f7fa6770c4bf46d8af23215734ac5f16e5c3ee96 run: | - git clone --depth 1 https://github.com/openprose/prose.git /tmp/prose + set -euo pipefail + git init --quiet /tmp/prose + git -C /tmp/prose fetch --depth 1 --quiet \ + https://github.com/openprose/prose.git "${PROSE_REF}" + git -C /tmp/prose checkout --quiet FETCH_HEAD mkdir -p .claude/skills cp -r /tmp/prose/skills/open-prose .claude/skills/open-prose diff --git a/.github/workflows/press-eval-full.yml b/.github/workflows/press-eval-full.yml index 3b2ddb8..1eced2a 100644 --- a/.github/workflows/press-eval-full.yml +++ b/.github/workflows/press-eval-full.yml @@ -26,6 +26,12 @@ jobs: - uses: actions/checkout@v4 with: repository: openprose/prose + # Pinned: without a ref this tracks that repository's default branch, + # so a commit made there would change what runs in this job — which + # holds a provider key — with no change reviewed here. Bump + # deliberately. + ref: f7fa6770c4bf46d8af23215734ac5f16e5c3ee96 + persist-credentials: false path: prose - uses: actions/setup-node@v4 From bc3cd194575c9f71a3862eaa87c69b921010af1c Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 11 Aug 2026 09:42:55 -0400 Subject: [PATCH 6/7] ci: stop eval dispatch inputs from reaching the shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full eval workflow interpolated its three dispatch inputs straight into a bash script running with a provider key in the environment. GitHub substitutes those values before bash parses the line, so an input carrying command substitution or a statement separator executed as code — a path to the key that needs no change to this repository. The inputs now travel through the step environment and are read as quoted variables, and each is checked before use: tier is a choice, concurrency must be a small positive integer, and a model override must look like a provider/name pair. --- .github/workflows/press-eval-full.yml | 42 ++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/press-eval-full.yml b/.github/workflows/press-eval-full.yml index 1eced2a..85190ed 100644 --- a/.github/workflows/press-eval-full.yml +++ b/.github/workflows/press-eval-full.yml @@ -4,14 +4,16 @@ on: workflow_dispatch: inputs: tier: - description: "Eval tier: quick (3 cheap), standard (6 default), full (all)" + description: "Eval tier" required: false default: "standard" + type: choice + options: [quick, standard, full] model: - description: "Override model for all evals" + description: "Override model for all evals (provider/name form)" required: false concurrency: - description: "Max parallel evals" + description: "Max parallel evals (1-10)" required: false default: "3" @@ -42,13 +44,39 @@ jobs: - run: npm ci - name: Run Press evals + # Dispatch inputs travel through the environment and are validated before + # use. Interpolating them straight into this script would let a dispatch + # value containing shell syntax execute as code, in a job that holds a + # provider key. `--no-install` keeps tsx on the lockfile's copy. run: | - npx tsx src/eval-pipeline.ts \ - --tier ${{ inputs.tier || 'standard' }} \ - --concurrency ${{ inputs.concurrency || '3' }} \ - ${{ inputs.model && format('--model {0}', inputs.model) || '' }} + set -euo pipefail + + case "${TIER}" in + quick|standard|full) ;; + *) echo "::error::Invalid tier: ${TIER}" >&2; exit 1 ;; + esac + + if ! printf '%s' "${CONCURRENCY}" | grep -qE '^([1-9]|10)$'; then + echo "::error::concurrency must be an integer from 1 to 10" >&2 + exit 1 + fi + + args=(--tier "${TIER}" --concurrency "${CONCURRENCY}") + + if [ -n "${MODEL}" ]; then + if ! printf '%s' "${MODEL}" | grep -qE '^[A-Za-z0-9._-]+/[A-Za-z0-9._:-]+$'; then + echo "::error::model must look like provider/name" >&2 + exit 1 + fi + args+=(--model "${MODEL}") + fi + + npx --no-install tsx src/eval-pipeline.ts "${args[@]}" env: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + TIER: ${{ inputs.tier || 'standard' }} + CONCURRENCY: ${{ inputs.concurrency || '3' }} + MODEL: ${{ inputs.model }} timeout-minutes: 25 - name: Upload eval results From 2af7f38ae1b16c5a7361382cdd25b6691f9e0f1c Mon Sep 17 00:00:00 2001 From: Jose Montes de Oca Date: Tue, 11 Aug 2026 09:40:52 -0400 Subject: [PATCH 7/7] ci: narrow the release job and gate it on the same checks The release job runs on push to main, which is the path a change takes when it lands without a pull request. It ran none of the supply-chain checks that pull requests get, so the branch with the least review had the least verification. It now runs the same blocking signature and production advisory gates. It also held contents: write for the whole job, with a git credential left on disk, while every step in it only reads and builds. The write scope belongs to a release step that does not exist yet; when one is added, give it its own job rather than widening this one. --- .github/workflows/release.yml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0364f6d..cc15d16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,21 +5,30 @@ on: branches: [main] permissions: - contents: write + contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + # Nothing here writes to the repository, so leave no git credential on + # disk for the build and test steps to reach. + persist-credentials: false - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - - run: npx tsc --noEmit - - run: npx biome check . - - run: npx vitest --run + # Blocking, and repeated from pull-request CI on purpose: this workflow + # runs on push, so it is the only check a change merged without a pull + # request ever sees. + - run: npm audit signatures + - run: npm audit --omit=dev + # See ci.yml for why these carry `--no-install` and why there is no lint step. + - run: npx --no-install tsc --noEmit + - run: npx --no-install vitest --run - name: Get version id: version @@ -47,3 +56,7 @@ jobs: # run: gh release create "v${{ steps.version.outputs.version }}" --generate-notes # env: # GH_TOKEN: ${{ github.token }} + # # To enable: this needs `contents: write`, which the workflow no longer + # # grants. Give it to a separate release job rather than widening the + # # build job — the build runs the whole dev dependency graph, and the + # # release step needs nothing from it but the finished dist/.