From 8d20cd9c6f42f25abf9710648f102548f9dd36ad Mon Sep 17 00:00:00 2001 From: Chetan Raghuvanshi <145042127+chhhee10@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:52:05 +0530 Subject: [PATCH 01/10] Add the pr-autopilot approve workflow Approves hermes-exosphere's own PRs as github-actions[bot] once pr-autopilot has reviewed them; see the file for what it checks. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/pr-autopilot-approve.yml diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml new file mode 100644 index 000000000..e62335802 --- /dev/null +++ b/.github/workflows/pr-autopilot-approve.yml @@ -0,0 +1,71 @@ +# Approve a PR that pr-autopilot reviewed but cannot approve itself. +# +# Copy to .github/workflows/pr-autopilot-approve.yml in every repo +# pr-autopilot reviews, on the default branch (workflow_dispatch runs the +# file from there). +# +# pr-autopilot acts as hermes-exosphere, the account that also opens Kiln's +# and the other agents' PRs. GitHub lets no one approve their own PR, so for +# those pr-autopilot posts its review as a comment and starts this workflow, +# which approves as github-actions[bot]. The org ruleset counts that approval. +# +# It approves only when all of these hold: +# * hermes-exosphere started it; +# * the PR is hermes-exosphere's own, still open, and its head is the +# commit that was reviewed; +# * hermes-exosphere posted a review on that commit carrying the approve +# marker (pr-autopilot's report::approve_marker). +# It merges nothing (a person does), never checks out the PR, and takes its +# inputs through env only. A later push drops the approval: the ruleset +# dismisses stale reviews. +# +# Needs Settings → Actions → General → "Allow GitHub Actions to create and +# approve pull requests". + +name: pr-autopilot approve + +on: + workflow_dispatch: + inputs: + pr: + description: PR number + required: true + sha: + description: The head commit pr-autopilot reviewed + required: true + +permissions: + pull-requests: write + +concurrency: pr-autopilot-approve-${{ inputs.pr }} + +jobs: + approve: + name: Approve pr-autopilot's verdict + if: github.actor == 'hermes-exosphere' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Approve + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR: ${{ inputs.pr }} + SHA: ${{ inputs.sha }} + BOT: hermes-exosphere + run: | + set -euo pipefail + case "$PR" in ''|*[!0-9]*) echo "not a PR number: $PR"; exit 1;; esac + case "$SHA" in ''|*[!0-9a-f]*) echo "not a commit: $SHA"; exit 1;; esac + pr="$(gh api "repos/$REPO/pulls/$PR")" + [ "$(jq -r .state <<<"$pr")" = open ] || { echo "PR #$PR is not open"; exit 1; } + [ "$(jq -r .user.login <<<"$pr")" = "$BOT" ] || { echo "only $BOT's own PRs"; exit 1; } + [ "$(jq -r .head.sha <<<"$pr")" = "$SHA" ] || { echo "head moved since the review"; exit 1; } + marker="" + n="$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ + --jq "[.[] | select(.user.login == env.BOT and .commit_id == env.SHA and (.body | contains(\"$marker\")))] | length" \ + | awk '{s += $1} END {print s + 0}')" + [ "$n" -gt 0 ] || { echo "no pr-autopilot approval on $SHA"; exit 1; } + gh api -X POST "repos/$REPO/pulls/$PR/reviews" \ + -f commit_id="$SHA" -f event=APPROVE \ + -f body="Approved for pr-autopilot, which reviewed commit \`${SHA:0:7}\` (review above) but cannot approve its own account's PR. A person merges." From 646f51cd3f25f70e814b1e9f9569ca9a528398db Mon Sep 17 00:00:00 2001 From: Chetan Raghuvanshi <145042127+chhhee10@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:12:40 +0530 Subject: [PATCH 02/10] Approve only on pr-autopilot's latest review of the commit An older approving review no longer counts once a newer one on the same commit says otherwise (found by pr-autopilot's review of FailproofAI/canary-doctor#1). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index e62335802..527e5f4e5 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -13,8 +13,9 @@ # * hermes-exosphere started it; # * the PR is hermes-exosphere's own, still open, and its head is the # commit that was reviewed; -# * hermes-exosphere posted a review on that commit carrying the approve -# marker (pr-autopilot's report::approve_marker). +# * hermes-exosphere's LATEST review on that commit carries the approve +# marker (pr-autopilot's report::approve_marker). An older approving +# review does not count once a newer one on the commit says otherwise. # It merges nothing (a person does), never checks out the PR, and takes its # inputs through env only. A later push drops the approval: the ruleset # dismisses stale reviews. @@ -61,11 +62,14 @@ jobs: [ "$(jq -r .state <<<"$pr")" = open ] || { echo "PR #$PR is not open"; exit 1; } [ "$(jq -r .user.login <<<"$pr")" = "$BOT" ] || { echo "only $BOT's own PRs"; exit 1; } [ "$(jq -r .head.sha <<<"$pr")" = "$SHA" ] || { echo "head moved since the review"; exit 1; } - marker="" - n="$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ - --jq "[.[] | select(.user.login == env.BOT and .commit_id == env.SHA and (.body | contains(\"$marker\")))] | length" \ - | awk '{s += $1} END {print s + 0}')" - [ "$n" -gt 0 ] || { echo "no pr-autopilot approval on $SHA"; exit 1; } + # Every page, then the newest of BOT's reviews on this commit. + latest="$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ + --jq '.[] | select(.user.login == env.BOT and .commit_id == env.SHA) + | [.submitted_at, (.body | contains(""))] + | @tsv' \ + | sort | tail -n 1)" + [ -n "$latest" ] || { echo "no pr-autopilot review of $SHA"; exit 1; } + [ "${latest##* }" = true ] || { echo "pr-autopilot's latest review of $SHA does not approve"; exit 1; } gh api -X POST "repos/$REPO/pulls/$PR/reviews" \ -f commit_id="$SHA" -f event=APPROVE \ -f body="Approved for pr-autopilot, which reviewed commit \`${SHA:0:7}\` (review above) but cannot approve its own account's PR. A person merges." From aea31cd12c2ed060e44191180f433932f9cecc44 Mon Sep 17 00:00:00 2001 From: Chetan Raghuvanshi <145042127+chhhee10@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:17:57 +0530 Subject: [PATCH 03/10] approve workflow: newest review by id, not by timestamp submitted_at has one-second ties, and on a tie the approving review sorted last. Review ids only grow (found by pr-autopilot's review of FailproofAI/canary-doctor#1). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index 527e5f4e5..994520b69 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -62,12 +62,13 @@ jobs: [ "$(jq -r .state <<<"$pr")" = open ] || { echo "PR #$PR is not open"; exit 1; } [ "$(jq -r .user.login <<<"$pr")" = "$BOT" ] || { echo "only $BOT's own PRs"; exit 1; } [ "$(jq -r .head.sha <<<"$pr")" = "$SHA" ] || { echo "head moved since the review"; exit 1; } - # Every page, then the newest of BOT's reviews on this commit. + # Every page, then the newest of BOT's reviews on this commit. Review + # ids only grow; submitted_at has one-second ties. latest="$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ --jq '.[] | select(.user.login == env.BOT and .commit_id == env.SHA) - | [.submitted_at, (.body | contains(""))] + | [.id, (.body | contains(""))] | @tsv' \ - | sort | tail -n 1)" + | sort -n -k 1,1 | tail -n 1)" [ -n "$latest" ] || { echo "no pr-autopilot review of $SHA"; exit 1; } [ "${latest##* }" = true ] || { echo "pr-autopilot's latest review of $SHA does not approve"; exit 1; } gh api -X POST "repos/$REPO/pulls/$PR/reviews" \ From 90f76c70c422f6c4a9b70d2ce00c8b8be78acbe8 Mon Sep 17 00:00:00 2001 From: Chetan Raghuvanshi <145042127+chhhee10@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:37:20 +0530 Subject: [PATCH 04/10] approve workflow: newest submitted review, by submission time A pending review's id predates its submission, so the highest id is not always the latest verdict (found by pr-autopilot's review of FailproofAI/pr-autopilot#2). Order by submitted_at, the id breaking one-second ties, and skip unsubmitted reviews. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index 994520b69..4746b9ed2 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -62,13 +62,15 @@ jobs: [ "$(jq -r .state <<<"$pr")" = open ] || { echo "PR #$PR is not open"; exit 1; } [ "$(jq -r .user.login <<<"$pr")" = "$BOT" ] || { echo "only $BOT's own PRs"; exit 1; } [ "$(jq -r .head.sha <<<"$pr")" = "$SHA" ] || { echo "head moved since the review"; exit 1; } - # Every page, then the newest of BOT's reviews on this commit. Review - # ids only grow; submitted_at has one-second ties. + # Every page, then the newest of BOT's submitted reviews on this + # commit: by submitted_at (a pending review's id predates its + # submission), the id breaking one-second ties. latest="$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ - --jq '.[] | select(.user.login == env.BOT and .commit_id == env.SHA) - | [.id, (.body | contains(""))] + --jq '.[] | select(.user.login == env.BOT and .commit_id == env.SHA + and .state != "PENDING" and .submitted_at != null) + | [.submitted_at, .id, (.body | contains(""))] | @tsv' \ - | sort -n -k 1,1 | tail -n 1)" + | sort -k 1,1 -k 2,2n | tail -n 1)" [ -n "$latest" ] || { echo "no pr-autopilot review of $SHA"; exit 1; } [ "${latest##* }" = true ] || { echo "pr-autopilot's latest review of $SHA does not approve"; exit 1; } gh api -X POST "repos/$REPO/pulls/$PR/reviews" \ From 6b4f73d800669dadef52432f1643c21789716a5c Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 29 Oct 2026 17:14:24 +0530 Subject: [PATCH 05/10] ci: approve workflow withdraws on a negative verdict, rechecks the head A newer non-approving hermes review of the same commit dismisses the bot approval; an approval posted after the head moved is dismissed; the job runs only from the default branch. Tested on agents-sandbox#10. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 52 +++++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index 4746b9ed2..a9c44e565 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -1,4 +1,5 @@ -# Approve a PR that pr-autopilot reviewed but cannot approve itself. +# Approve a PR that pr-autopilot reviewed but cannot approve itself, and +# withdraw that approval when pr-autopilot's verdict turns. # # Copy to .github/workflows/pr-autopilot-approve.yml in every repo # pr-autopilot reviews, on the default branch (workflow_dispatch runs the @@ -16,10 +17,21 @@ # * hermes-exosphere's LATEST review on that commit carries the approve # marker (pr-autopilot's report::approve_marker). An older approving # review does not count once a newer one on the commit says otherwise. +# When the latest review does not approve, it dismisses github-actions[bot]'s +# approvals instead, so a newer negative verdict on the same commit revokes +# an earlier approval (pr-autopilot dispatches it for both verdicts). If the +# head moves while the approval is posted, the new approval is dismissed. # It merges nothing (a person does), never checks out the PR, and takes its # inputs through env only. A later push drops the approval: the ruleset # dismisses stale reviews. # +# What this does not stop: whoever holds hermes-exosphere's token can write +# the marker review and dispatch this, or dispatch a modified copy from +# another branch (the ref check below is only a guard against mistakes). It +# keeps pr-autopilot's verdict honest; it is not a boundary against that +# token, and no in-repo workflow can be one while "Allow GitHub Actions to +# approve pull requests" is on. +# # Needs Settings → Actions → General → "Allow GitHub Actions to create and # approve pull requests". @@ -43,11 +55,13 @@ concurrency: pr-autopilot-approve-${{ inputs.pr }} jobs: approve: name: Approve pr-autopilot's verdict - if: github.actor == 'hermes-exosphere' + if: >- + github.actor == 'hermes-exosphere' && + github.ref == format('refs/heads/{0}', github.event.repository.default_branch) runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Approve + - name: Approve or withdraw env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} @@ -58,6 +72,16 @@ jobs: set -euo pipefail case "$PR" in ''|*[!0-9]*) echo "not a PR number: $PR"; exit 1;; esac case "$SHA" in ''|*[!0-9a-f]*) echo "not a commit: $SHA"; exit 1;; esac + # Dismiss github-actions[bot]'s standing approvals on this PR. + withdraw() { + gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ + --jq '.[] | select(.user.login == "github-actions[bot]" and .state == "APPROVED") | .id' | + while read -r id; do + gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$id/dismissals" \ + -f event=DISMISS -f message="$1" >/dev/null + echo "dismissed approval $id: $1" + done + } pr="$(gh api "repos/$REPO/pulls/$PR")" [ "$(jq -r .state <<<"$pr")" = open ] || { echo "PR #$PR is not open"; exit 1; } [ "$(jq -r .user.login <<<"$pr")" = "$BOT" ] || { echo "only $BOT's own PRs"; exit 1; } @@ -72,7 +96,23 @@ jobs: | @tsv' \ | sort -k 1,1 -k 2,2n | tail -n 1)" [ -n "$latest" ] || { echo "no pr-autopilot review of $SHA"; exit 1; } - [ "${latest##* }" = true ] || { echo "pr-autopilot's latest review of $SHA does not approve"; exit 1; } - gh api -X POST "repos/$REPO/pulls/$PR/reviews" \ + if [ "${latest##* }" != true ]; then + withdraw "pr-autopilot's latest review of ${SHA:0:7} does not approve" + exit 0 + fi + have="$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ + --jq '.[] | select(.user.login == "github-actions[bot]" and .state == "APPROVED" and .commit_id == env.SHA) | .id' | head -n 1)" + [ -z "$have" ] || { echo "already approved ($have)"; exit 0; } + id="$(gh api -X POST "repos/$REPO/pulls/$PR/reviews" \ -f commit_id="$SHA" -f event=APPROVE \ - -f body="Approved for pr-autopilot, which reviewed commit \`${SHA:0:7}\` (review above) but cannot approve its own account's PR. A person merges." + -f body="Approved for pr-autopilot, which reviewed commit \`${SHA:0:7}\` (review above) but cannot approve its own account's PR. A person merges." \ + --jq .id)" + # A push between the head check and the POST would leave this + # approval on an old commit, after the ruleset's stale dismissal ran. + head="$(gh api "repos/$REPO/pulls/$PR" --jq .head.sha)" + if [ "$head" != "$SHA" ]; then + gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$id/dismissals" \ + -f event=DISMISS -f message="head moved to ${head:0:7} while ${SHA:0:7} was approved" >/dev/null + echo "head moved while approving; approval $id dismissed"; exit 1 + fi + echo "approved ${SHA:0:7} ($id)" From 5330aebd3c593e9417461cfb1284fd15cfb4c642 Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 29 Oct 2026 18:20:01 +0530 Subject: [PATCH 06/10] ci: approve workflow on repository_dispatch repository_dispatch always runs the default branch's copy of the file, so no dispatcher can pick an edited copy on another branch (SEC-001). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 38 ++++++++-------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index a9c44e565..2f81ed373 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -2,8 +2,9 @@ # withdraw that approval when pr-autopilot's verdict turns. # # Copy to .github/workflows/pr-autopilot-approve.yml in every repo -# pr-autopilot reviews, on the default branch (workflow_dispatch runs the -# file from there). +# pr-autopilot reviews, on the default branch. It runs on repository_dispatch, +# which GitHub always runs from the default branch's copy of this file: no +# dispatcher can pick an edited copy on another branch. # # pr-autopilot acts as hermes-exosphere, the account that also opens Kiln's # and the other agents' PRs. GitHub lets no one approve their own PR, so for @@ -26,38 +27,27 @@ # dismisses stale reviews. # # What this does not stop: whoever holds hermes-exosphere's token can write -# the marker review and dispatch this, or dispatch a modified copy from -# another branch (the ref check below is only a guard against mistakes). It -# keeps pr-autopilot's verdict honest; it is not a boundary against that -# token, and no in-repo workflow can be one while "Allow GitHub Actions to -# approve pull requests" is on. -# -# Needs Settings → Actions → General → "Allow GitHub Actions to create and -# approve pull requests". +# the marker review and send the dispatch. It keeps pr-autopilot's verdict +# honest and the code that approves fixed; it is not a boundary against +# that token. That would need a second identity (a GitHub App or account). name: pr-autopilot approve on: - workflow_dispatch: - inputs: - pr: - description: PR number - required: true - sha: - description: The head commit pr-autopilot reviewed - required: true + # POST /repos/{owner}/{repo}/dispatches with + # {"event_type": "pr-autopilot-approve", "client_payload": {"pr": "N", "sha": "..."}} + repository_dispatch: + types: [pr-autopilot-approve] permissions: pull-requests: write -concurrency: pr-autopilot-approve-${{ inputs.pr }} +concurrency: pr-autopilot-approve-${{ github.event.client_payload.pr }} jobs: approve: name: Approve pr-autopilot's verdict - if: >- - github.actor == 'hermes-exosphere' && - github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + if: github.actor == 'hermes-exosphere' runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -65,8 +55,8 @@ jobs: env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} - PR: ${{ inputs.pr }} - SHA: ${{ inputs.sha }} + PR: ${{ github.event.client_payload.pr }} + SHA: ${{ github.event.client_payload.sha }} BOT: hermes-exosphere run: | set -euo pipefail From cecc3d3c4436a5ae97746448f5921bacac181081 Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 29 Oct 2026 18:23:17 +0530 Subject: [PATCH 07/10] approve workflow: a failed head re-read counts as moved Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index 2f81ed373..1a4d5bbf3 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -99,7 +99,8 @@ jobs: --jq .id)" # A push between the head check and the POST would leave this # approval on an old commit, after the ruleset's stale dismissal ran. - head="$(gh api "repos/$REPO/pulls/$PR" --jq .head.sha)" + # A failed re-read counts as moved. + head="$(gh api "repos/$REPO/pulls/$PR" --jq .head.sha || true)" if [ "$head" != "$SHA" ]; then gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$id/dismissals" \ -f event=DISMISS -f message="head moved to ${head:0:7} while ${SHA:0:7} was approved" >/dev/null From 27747778e6c5b173d57da6dafeb46a0f6fffb65f Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 29 Oct 2026 18:30:14 +0530 Subject: [PATCH 08/10] approve workflow: re-check the verdict after posting Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 34 ++++++++++++++-------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index 1a4d5bbf3..b8968f15d 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -21,7 +21,8 @@ # When the latest review does not approve, it dismisses github-actions[bot]'s # approvals instead, so a newer negative verdict on the same commit revokes # an earlier approval (pr-autopilot dispatches it for both verdicts). If the -# head moves while the approval is posted, the new approval is dismissed. +# head moves, or the verdict turns, while the approval is posted, the new +# approval is dismissed. # It merges nothing (a person does), never checks out the PR, and takes its # inputs through env only. A later push drops the approval: the ruleset # dismisses stale reviews. @@ -78,15 +79,19 @@ jobs: [ "$(jq -r .head.sha <<<"$pr")" = "$SHA" ] || { echo "head moved since the review"; exit 1; } # Every page, then the newest of BOT's submitted reviews on this # commit: by submitted_at (a pending review's id predates its - # submission), the id breaking one-second ties. - latest="$(gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ - --jq '.[] | select(.user.login == env.BOT and .commit_id == env.SHA - and .state != "PENDING" and .submitted_at != null) - | [.submitted_at, .id, (.body | contains(""))] - | @tsv' \ - | sort -k 1,1 -k 2,2n | tail -n 1)" + # submission), the id breaking one-second ties. Prints whether it + # carries the approve marker (true/false), or nothing. + verdict() { + gh api --paginate "repos/$REPO/pulls/$PR/reviews" \ + --jq '.[] | select(.user.login == env.BOT and .commit_id == env.SHA + and .state != "PENDING" and .submitted_at != null) + | [.submitted_at, .id, (.body | contains(""))] + | @tsv' \ + | sort -k 1,1 -k 2,2n | tail -n 1 | cut -f 3 + } + latest="$(verdict)" [ -n "$latest" ] || { echo "no pr-autopilot review of $SHA"; exit 1; } - if [ "${latest##* }" != true ]; then + if [ "$latest" != true ]; then withdraw "pr-autopilot's latest review of ${SHA:0:7} does not approve" exit 0 fi @@ -97,13 +102,18 @@ jobs: -f commit_id="$SHA" -f event=APPROVE \ -f body="Approved for pr-autopilot, which reviewed commit \`${SHA:0:7}\` (review above) but cannot approve its own account's PR. A person merges." \ --jq .id)" - # A push between the head check and the POST would leave this - # approval on an old commit, after the ruleset's stale dismissal ran. - # A failed re-read counts as moved. + # Re-read both inputs of the decision after posting: a push, or a + # newer negative review, between the checks above and the POST + # would otherwise leave this approval standing until the next run. + # A failed re-read counts as changed. head="$(gh api "repos/$REPO/pulls/$PR" --jq .head.sha || true)" if [ "$head" != "$SHA" ]; then gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$id/dismissals" \ -f event=DISMISS -f message="head moved to ${head:0:7} while ${SHA:0:7} was approved" >/dev/null echo "head moved while approving; approval $id dismissed"; exit 1 fi + if [ "$(verdict || true)" != true ]; then + withdraw "pr-autopilot's verdict on ${SHA:0:7} changed while approving" + exit 0 + fi echo "approved ${SHA:0:7} ($id)" From 23e3cdcf6a866e719e6da6446be1e5dfcf9d5c17 Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 29 Oct 2026 18:52:45 +0530 Subject: [PATCH 09/10] approve workflow: a failed verdict re-read dismisses the approval it posted Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-autopilot-approve.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml index b8968f15d..c64002ad3 100644 --- a/.github/workflows/pr-autopilot-approve.yml +++ b/.github/workflows/pr-autopilot-approve.yml @@ -112,8 +112,11 @@ jobs: -f event=DISMISS -f message="head moved to ${head:0:7} while ${SHA:0:7} was approved" >/dev/null echo "head moved while approving; approval $id dismissed"; exit 1 fi - if [ "$(verdict || true)" != true ]; then - withdraw "pr-autopilot's verdict on ${SHA:0:7} changed while approving" - exit 0 + # Any failure of the re-read (pipefail: a page that errors after an + # approving one too) dismisses this approval by id, without listing. + if ! now="$(verdict)" || [ "$now" != true ]; then + gh api -X PUT "repos/$REPO/pulls/$PR/reviews/$id/dismissals" \ + -f event=DISMISS -f message="pr-autopilot's verdict on ${SHA:0:7} changed, or could not be re-read, while approving" >/dev/null + echo "verdict changed or unreadable while approving; approval $id dismissed"; exit 1 fi echo "approved ${SHA:0:7} ($id)" From 991e4fc25ef96b5c1776d1064a843977bef76bb0 Mon Sep 17 00:00:00 2001 From: chhhee10 Date: Thu, 29 Oct 2026 19:21:35 +0530 Subject: [PATCH 10/10] supply-chain: bump nltk to 3.10.4 OSV-Scanner went red on main from GHSA-j456-xh4h-cpf2 (nltk WekaClassifier model path bypasses nltk.pathsec), fixed in 3.10.4. nltk is transitive via llama-index-core in the SDK's dev/test lockfile; re-locked with `uv lock --upgrade-package nltk`. 3.10.4 also fixes GHSA-8mgp-746c-j5xp, so its IgnoredVulns entry is now unused and is removed. Co-Authored-By: Claude Opus 5.5 --- osv-scanner.toml | 24 ------------------------ sdk/python/uv.lock | 8 ++++---- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/osv-scanner.toml b/osv-scanner.toml index 1d441f022..11772c471 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -84,27 +84,3 @@ reason = "chromadb 1.5.9 (published 2026-06-12, surfaced 2026-08-24). No fixed v id = "GHSA-xph7-9rjv-w5fr" ignoreUntil = 2026-11-20 reason = "chromadb 1.5.9 (published 2026-06-12, surfaced 2026-08-24). No fixed version. Transitive via crewai, dev/test lockfile only; see the shared justification above. Re-review by 2026-11-20." - -# --------------------------------------------------------------------------- -# nltk 3.10.3 — one advisory, no fixed version. -# -# Surfaced 2026-09-03 against an already-approved PR, which is the case this -# file exists for: nothing on the branch introduced it, and nothing on the -# branch can resolve it. -# -# nltk is transitive via llama-index-core and appears only in -# sdk/python/uv.lock, the dev/test lockfile that pins every extra so CI can -# exercise the adapters. The SDK's own runtime dependency list is empty — the -# `llamaindex` extra is opt-in and the integration module imports it lazily — -# so nltk reaches no shipped code path. OSV reports "0 vulnerabilities can be -# fixed" and an empty FIXED VERSION, so there is nothing to bump to; the only -# alternative is dropping the llamaindex extra and the adapter tests with it. -# -# Same re-review date as the chromadb entries above, so the whole list is -# revisited in one pass. -# --------------------------------------------------------------------------- - -[[IgnoredVulns]] -id = "GHSA-8mgp-746c-j5xp" -ignoreUntil = 2026-11-20 -reason = "nltk 3.10.3 (surfaced 2026-09-03, CVSS 8.3). No fixed version. Transitive via llama-index-core, dev/test lockfile only; the SDK ships zero runtime dependencies. Re-review by 2026-11-20." diff --git a/sdk/python/uv.lock b/sdk/python/uv.lock index 373511ada..6b3b4e1c0 100644 --- a/sdk/python/uv.lock +++ b/sdk/python/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version == '3.13.*'", @@ -2534,7 +2534,7 @@ wheels = [ [[package]] name = "nltk" -version = "3.10.3" +version = "3.10.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2543,9 +2543,9 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/cb/bbaa9be58e95d21729c6b9194376610230e0755f6de0bf38b6acc0dce079/nltk-3.10.4.tar.gz", hash = "sha256:44679f4e9220abfd496d2c8cdb22f8425ccbe557604f9aa5cacb1c44003dabc1", size = 4300570, upload-time = "2026-10-21T11:34:35.851Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" }, + { url = "https://files.pythonhosted.org/packages/a2/04/e7816813e409ca874ba2fcc35455db0e4130b79eb277e1f9edd270ca3c69/nltk-3.10.4-py3-none-any.whl", hash = "sha256:769ba5413f265b7d3e87cd163cc28efa01e29eeef8bb6080bc907949d9fcfb51", size = 1722994, upload-time = "2026-10-21T11:34:27.975Z" }, ] [[package]]