From a5ca03d6ba2b25972621d3cd710cec5b5d790d19 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 1 Oct 2026 16:34:56 +0200 Subject: [PATCH 1/3] ci(pullfrog): pin the action and make the reviewer review-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes to the workflow the Pullfrog console generated, each for a reason that applies to this repository specifically. `push: disabled`. The input defaults to `enabled`, which lets the agent push branches and open pull requests of its own. Every commit and pull request here is authored by its maintainer, so the agent gets no write path at all: it reads the diff and comments. The input is declared in the action's own action.yml and is not mentioned in the getting-started guide, so the generated file leaves it at the permissive default. Pinned to a commit SHA instead of `@v0`. That tag moves — it has already advanced through ninety v0.1.x releases — and this is the one action here that runs an agent with repository access, so a mutable ref is the last place to accept one. Pullfrog's versioning page supports pinning and notes nothing is lost: the action fetches its steps from npm either way, so the agent still tracks patch releases without this file changing. `shell: restricted`, which scrubs *_TOKEN / *_KEY / *_SECRET / *_PASSWORD / *_CREDENTIAL from the environment the agent's shell sees. Already the default for a public repository; stated explicitly so that making this repository private some day cannot silently widen it, since private repos default to `enabled`. The provider `env:` block is left exactly as generated. Unset secrets resolve to empty strings, and the action's sanitizeSecret() leaves the variable untouched in that case rather than treating it as a key, so the block is inert until a key is actually added and it cannot shadow keys held in Pullfrog's own store or the Router. Nothing else is touched: no `run:` steps, so no shell interpolation surface, and no workflow expression reads untrusted event data. --- .github/workflows/pullfrog.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index 8b3491b..8aaa079 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -26,9 +26,30 @@ jobs: with: fetch-depth: 1 - name: Run agent - uses: pullfrog/pullfrog@v0 + # Pinned to a commit SHA rather than the documented `@v0`, which is a + # tag that MOVES — it has already advanced through ninety v0.1.x + # releases. This is the one action here that runs an agent with access + # to the repository, so a mutable ref is the last place to accept one. + # Pullfrog's versioning page supports pinning and notes nothing is + # lost: the action fetches its steps from npm either way, so the agent + # still tracks patch releases without this file changing. Bump the SHA + # to update. + uses: pullfrog/pullfrog@0d318bef8c7cf7ae3f193ef32b2bc74e1d94b4d1 # v0.1.90 with: prompt: ${{ inputs.prompt }} + # REVIEW ONLY. `push` defaults to `enabled`, which lets the agent + # push branches and open pull requests of its own. Every commit and + # pull request in this repository is authored by its maintainer, so + # the agent gets no write path: it reads the diff and comments. The + # input is declared in the action's action.yml and is not mentioned + # in the getting-started guide. + push: disabled + # Scrubs *_TOKEN / *_KEY / *_SECRET / *_PASSWORD / *_CREDENTIAL from + # the environment the agent's shell sees. Already the default for a + # public repository; set explicitly so that making this repository + # private some day cannot silently widen it — private repos default + # to `enabled`. + shell: restricted env: # add at least one provider API key ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} From 9f1f57f0fa5a6436ab74a7464e7cf3ba5616a279 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 1 Oct 2026 16:55:13 +0200 Subject: [PATCH 2/3] ci(pullfrog): stop the checkout persisting a push credential MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/checkout defaults to persist-credentials: true, which writes the job's GITHUB_TOKEN into .git/config. That leaves a working push credential inside the very checkout the agent then operates on, so it contradicts the `push: disabled` set alongside it: the input closes the action's own push path and the token reopens it from underneath. Caught by Pinakes' CI policy check — "pullfrog.yml has an actions/checkout step that persists GitHub credentials" — which refuses any checkout that persists credentials. It was right, and the same omission was in all of these repositories, so the fix goes everywhere rather than only where a check happened to be watching. Nothing needs the credential: the agent reaches GitHub through the App's installation token, not the local git remote, and these repositories are public, so an unauthenticated fetch works. --- .github/workflows/pullfrog.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index 8aaa079..c216702 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -25,6 +25,16 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 1 + # actions/checkout defaults to persist-credentials: true, which + # leaves the job's GITHUB_TOKEN in .git/config. That hands the agent + # a working push credential inside the very checkout it operates on, + # which contradicts `push: disabled` below — the token is a write + # path the input cannot close. Pinakes' own CI policy check refuses + # any checkout that persists credentials, and it was right to. + # Nothing here needs it: the agent reaches GitHub through the App's + # installation token, and these repositories are public, so an + # unauthenticated fetch works. + persist-credentials: false - name: Run agent # Pinned to a commit SHA rather than the documented `@v0`, which is a # tag that MOVES — it has already advanced through ninety v0.1.x From 459050103597b277b17f4fc6295792dc710354fb Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 1 Oct 2026 17:41:52 +0200 Subject: [PATCH 3/3] ci(pullfrog): satisfy the workflow security and lint gates The reviewer workflow was the only file in .github failing the Workflow, YAML and shell security gate. Four separate problems, all real: - zizmor (pedantic) wants every write permission documented, and it reads a trailing comment on the permission line, not a block above it. id-token: write now says on its own line what it is for: an OIDC identity for Pullfrog token service, which grants nothing on the repository. - The job had no name, so it showed up anonymously in the checks list. - The workflow had no concurrency group. It was left out on purpose, because every run is dispatched against the default branch and one shared group would make two reviews cancel each other. The group is keyed on the dispatched run name instead, so a fresh review of the same pull request supersedes a stale one while different pull requests never collide, and it falls back to the unique run id when no name is passed. - Two lines carried trailing whitespace, inherited from the generated template, which yamllint rejects. The gate had never reached yamllint because zizmor failed first. Also pins actions/checkout by commit SHA where it was still a floating v6 tag. Verified locally with the same three tools the gate runs, over the whole .github tree: zizmor --persona pedantic, actionlint and yamllint all exit 0. --- .github/workflows/pullfrog.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index c216702..dbfda92 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -11,18 +11,36 @@ on: type: string description: Run name +# Every run is dispatched against the default branch, so a single shared group +# would make two reviews cancel each other. The group is keyed on the run name +# the dispatcher passes — one group per pull request under review — so a fresh +# review of the SAME pull request supersedes a stale one, while reviews of +# different pull requests never collide. Without a run name the key falls back +# to the run id, which is unique, so the group degrades to "cancel nothing". +concurrency: + group: ${{ github.workflow }}-${{ inputs.name || github.run_id }} + cancel-in-progress: true + +# Read-only by default: no job may write to the repository unless it says so. permissions: contents: read jobs: pullfrog: + name: Pullfrog agent runs-on: ubuntu-latest permissions: - id-token: write + # The action mints a short-lived OIDC token to prove this run's identity + # to Pullfrog's own token service, which is how a Router or subscription + # plan authenticates without a long-lived key stored in this repository. + # It grants nothing on the repository itself. + id-token: write # OIDC identity for Pullfrog's token service only. + # Read the diff under review. Deliberately NOT `write`: the agent is + # review-only (see `push: disabled` below) and has no commit path. contents: read steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 1 # actions/checkout defaults to persist-credentials: true, which @@ -76,14 +94,14 @@ jobs: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }} OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - + # for Amazon Bedrock (https://docs.pullfrog.com/bedrock) # AWS_BEARER_TOKEN_BEDROCK: ${{ secrets.AWS_BEARER_TOKEN_BEDROCK }} # AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} # AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # AWS_REGION: us-east-1 # BEDROCK_MODEL_ID: - + # for Google Vertex AI (https://docs.pullfrog.com/vertex) # VERTEX_SERVICE_ACCOUNT_JSON: >- # ${{ secrets.VERTEX_SERVICE_ACCOUNT_JSON }}