From 4c0ec3bd7637f1938394cb069981cb5f2d440e12 Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Fri, 28 Aug 2026 11:19:34 +0100 Subject: [PATCH 1/2] Add option to allow bots reviewing bot code Concrete usecase for this is the backport PR AI review authored by bots. --- .github/seidroid/ai-review/README.md | 7 +++- .github/workflows/ai-review.yml | 61 ++++++++++++++++++---------- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/.github/seidroid/ai-review/README.md b/.github/seidroid/ai-review/README.md index 5b8d265..39d4498 100644 --- a/.github/seidroid/ai-review/README.md +++ b/.github/seidroid/ai-review/README.md @@ -54,6 +54,7 @@ jobs: with: uci-ref: v1 # allowed-team: my-org/my-team # default: sei-protocol/sei-core + # allowed-bots: 'dependabot[bot],renovate[bot]' # exact logins; default: deny all bots # extra-instructions: "Flag added allocations in the hot path." # prebuild-script: "go mod download" # warm Codex's offline sandbox # re-review-on-push: true # review again after every PR push @@ -72,6 +73,7 @@ jobs: | `nitpick-label` | `ai: nitpick` | Include nit-level findings only when this label is present. | | `trigger-phrase` | `@seidroid` | Exact ` review` command used to request another review. | | `allowed-team` | `sei-protocol/sei-core` | Active members may request another review. Empty denies everyone. | +| `allowed-bots` | `''` | Comma- or whitespace-separated exact GitHub bot logins allowed to request another review. Empty denies all bots. | | `runs-on` | `ubuntu-latest` | Runner label. | | `claude-model` | `''` | Optional Claude model override. | | `approve-on-success` | `true` | If true, APPROVE on a clean verdict; else COMMENT. | @@ -135,8 +137,9 @@ jobs: and a read-only token, so the workflow degrades gracefully for forks; do not switch to `pull_request_target` to "fix" forks. - An exact `@seidroid review` comment is reserved for the review workflow, so the assistant - ignores it. Re-review requests are accepted only from active members of `allowed-team`; - membership lookup errors fail closed. + ignores it. Human re-review requests are accepted only from active members of + `allowed-team`; bot requests require an exact, case-insensitive login match in + `allowed-bots`. Membership lookup errors and empty allowlists fail closed. - The assistant is gated to active members of `allowed-team` (checked before any model runs) and ignores bot-authored comments. It is read-only unless `allow-write` is set. - Untrusted PR/comment content is passed to the models as **data**, never interpolated diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 94f5f28..d7104fc 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -78,6 +78,11 @@ on: required: false type: string default: 'sei-protocol/sei-core' + allowed-bots: + description: "Comma- or whitespace-separated GitHub bot logins allowed to request re-reviews. Empty denies all bots." + required: false + type: string + default: '' runs-on: description: "Runner label." required: false @@ -120,27 +125,25 @@ jobs: preflight: name: Preflight runs-on: ${{ inputs.runs-on }} - # Comment events are cheap-filtered here, then exact command parsing and authorization - # happen in `resolve`. Non-command comments never reach a model. + # Comment events are cheap-filtered here, then exact command parsing and human-team + # or bot-allowlist authorization happen in `resolve`. Non-command comments never + # reach a model. if: >- ${{ github.event_name == 'pull_request' || ( github.event_name == 'issue_comment' && github.event.issue.pull_request && - github.event.comment.user.type != 'Bot' && contains(github.event.comment.body, inputs.trigger-phrase) && contains(github.event.comment.body, 'review') ) || ( github.event_name == 'pull_request_review_comment' && - github.event.comment.user.type != 'Bot' && contains(github.event.comment.body, inputs.trigger-phrase) && contains(github.event.comment.body, 'review') ) || ( github.event_name == 'pull_request_review' && - github.event.review.user.type != 'Bot' && github.event.review.body && contains(github.event.review.body, inputs.trigger-phrase) && contains(github.event.review.body, 'review') @@ -203,6 +206,7 @@ jobs: env: TRIGGER_PHRASE: ${{ inputs.trigger-phrase }} ALLOWED_TEAM: ${{ inputs.allowed-team }} + ALLOWED_BOTS: ${{ inputs.allowed-bots }} SKIP_REVIEW_LABEL: ${{ inputs.skip-review-label }} NITPICK_LABEL: ${{ inputs.nitpick-label }} SEIDROID_USER_ID: ${{ vars.PLATFORM_CODE_AGENT_USER_ID }} @@ -291,9 +295,11 @@ jobs: const body = eventName === "pull_request_review" ? context.payload.review?.body : context.payload.comment?.body; - const actor = eventName === "pull_request_review" - ? context.payload.review?.user?.login - : context.payload.comment?.user?.login; + const author = eventName === "pull_request_review" + ? context.payload.review?.user + : context.payload.comment?.user; + const actor = author?.login; + const actorType = author?.type; const normalize = value => String(value || "").trim().replace(/\s+/g, " ").toLowerCase(); const command = `${normalize(process.env.TRIGGER_PHRASE)} review`; if (normalize(body) !== command || !actor) { @@ -302,21 +308,34 @@ jobs: } let authorized = false; - const team = String(process.env.ALLOWED_TEAM || ""); - const slash = team.indexOf("/"); - if (slash > 0 && slash < team.length - 1) { - const org = team.slice(0, slash); - const team_slug = team.slice(slash + 1); - try { - const membership = await github.rest.teams.getMembershipForUserInOrg({ - org, team_slug, username: actor, - }); - authorized = membership.data.state === "active"; - } catch (error) { - core.notice(`Could not verify active membership for ${actor} in ${team}; denying request.`); + if (actorType === "Bot") { + const allowedBots = new Set( + String(process.env.ALLOWED_BOTS || "") + .split(/[\s,]+/) + .map(login => login.trim().toLowerCase()) + .filter(Boolean) + ); + authorized = allowedBots.has(actor.toLowerCase()); + if (!authorized) { + core.notice(`${actor} is not in allowed-bots; denying request.`); } } else { - core.notice("allowed-team is empty or invalid; denying request."); + const team = String(process.env.ALLOWED_TEAM || ""); + const slash = team.indexOf("/"); + if (slash > 0 && slash < team.length - 1) { + const org = team.slice(0, slash); + const team_slug = team.slice(slash + 1); + try { + const membership = await github.rest.teams.getMembershipForUserInOrg({ + org, team_slug, username: actor, + }); + authorized = membership.data.state === "active"; + } catch (error) { + core.notice(`Could not verify active membership for ${actor} in ${team}; denying request.`); + } + } else { + core.notice("allowed-team is empty or invalid; denying request."); + } } core.setOutput("should_run", String(authorized)); From 67d7fbf816a79966492c13ee8a9707663b1b732c Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Fri, 28 Aug 2026 11:52:24 +0100 Subject: [PATCH 2/2] Address AI preflight comment --- .github/seidroid/ai-review/README.md | 7 ++++--- .github/workflows/ai-review.yml | 24 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/seidroid/ai-review/README.md b/.github/seidroid/ai-review/README.md index 39d4498..5ac1496 100644 --- a/.github/seidroid/ai-review/README.md +++ b/.github/seidroid/ai-review/README.md @@ -54,7 +54,7 @@ jobs: with: uci-ref: v1 # allowed-team: my-org/my-team # default: sei-protocol/sei-core - # allowed-bots: 'dependabot[bot],renovate[bot]' # exact logins; default: deny all bots + # allowed-bots: '["dependabot[bot]", "renovate[bot]"]' # exact logins; default: [] # extra-instructions: "Flag added allocations in the hot path." # prebuild-script: "go mod download" # warm Codex's offline sandbox # re-review-on-push: true # review again after every PR push @@ -73,7 +73,7 @@ jobs: | `nitpick-label` | `ai: nitpick` | Include nit-level findings only when this label is present. | | `trigger-phrase` | `@seidroid` | Exact ` review` command used to request another review. | | `allowed-team` | `sei-protocol/sei-core` | Active members may request another review. Empty denies everyone. | -| `allowed-bots` | `''` | Comma- or whitespace-separated exact GitHub bot logins allowed to request another review. Empty denies all bots. | +| `allowed-bots` | `'[]'` | JSON array of exact GitHub bot logins allowed to request another review. An empty array denies all bots. | | `runs-on` | `ubuntu-latest` | Runner label. | | `claude-model` | `''` | Optional Claude model override. | | `approve-on-success` | `true` | If true, APPROVE on a clean verdict; else COMMENT. | @@ -139,7 +139,8 @@ jobs: - An exact `@seidroid review` comment is reserved for the review workflow, so the assistant ignores it. Human re-review requests are accepted only from active members of `allowed-team`; bot requests require an exact, case-insensitive login match in - `allowed-bots`. Membership lookup errors and empty allowlists fail closed. + the `allowed-bots` JSON array. Non-allowlisted bots are rejected before a runner starts. + Membership lookup errors and empty allowlists fail closed. - The assistant is gated to active members of `allowed-team` (checked before any model runs) and ignores bot-authored comments. It is read-only unless `allow-write` is set. - Untrusted PR/comment content is passed to the models as **data**, never interpolated diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index d7104fc..89cee91 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -79,10 +79,10 @@ on: type: string default: 'sei-protocol/sei-core' allowed-bots: - description: "Comma- or whitespace-separated GitHub bot logins allowed to request re-reviews. Empty denies all bots." + description: "JSON array of GitHub bot logins allowed to request re-reviews. An empty array denies all bots." required: false type: string - default: '' + default: '[]' runs-on: description: "Runner label." required: false @@ -128,22 +128,35 @@ jobs: # Comment events are cheap-filtered here, then exact command parsing and human-team # or bot-allowlist authorization happen in `resolve`. Non-command comments never # reach a model. + # `allowed-bots` is JSON so `contains` checks array membership rather than substrings. if: >- ${{ github.event_name == 'pull_request' || ( github.event_name == 'issue_comment' && github.event.issue.pull_request && + ( + github.event.comment.user.type != 'Bot' || + contains(fromJSON(inputs.allowed-bots), github.event.comment.user.login) + ) && contains(github.event.comment.body, inputs.trigger-phrase) && contains(github.event.comment.body, 'review') ) || ( github.event_name == 'pull_request_review_comment' && + ( + github.event.comment.user.type != 'Bot' || + contains(fromJSON(inputs.allowed-bots), github.event.comment.user.login) + ) && contains(github.event.comment.body, inputs.trigger-phrase) && contains(github.event.comment.body, 'review') ) || ( github.event_name == 'pull_request_review' && + ( + github.event.review.user.type != 'Bot' || + contains(fromJSON(inputs.allowed-bots), github.event.review.user.login) + ) && github.event.review.body && contains(github.event.review.body, inputs.trigger-phrase) && contains(github.event.review.body, 'review') @@ -309,11 +322,10 @@ jobs: let authorized = false; if (actorType === "Bot") { + const configuredBots = JSON.parse(process.env.ALLOWED_BOTS || "[]"); const allowedBots = new Set( - String(process.env.ALLOWED_BOTS || "") - .split(/[\s,]+/) - .map(login => login.trim().toLowerCase()) - .filter(Boolean) + (Array.isArray(configuredBots) ? configuredBots : []) + .map(login => String(login).toLowerCase()) ); authorized = allowedBots.has(actor.toLowerCase()); if (!authorized) {