diff --git a/.github/workflows/pr-autopilot-approve.yml b/.github/workflows/pr-autopilot-approve.yml new file mode 100644 index 000000000..c64002ad3 --- /dev/null +++ b/.github/workflows/pr-autopilot-approve.yml @@ -0,0 +1,122 @@ +# 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. 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 +# 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'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, 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. +# +# What this does not stop: whoever holds hermes-exosphere's token can write +# 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: + # 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-${{ github.event.client_payload.pr }} + +jobs: + approve: + name: Approve pr-autopilot's verdict + if: github.actor == 'hermes-exosphere' + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Approve or withdraw + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR: ${{ github.event.client_payload.pr }} + SHA: ${{ github.event.client_payload.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 + # 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; } + [ "$(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. 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 + 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." \ + --jq .id)" + # 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 + # 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)" 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]]