From e2e93eaeaed39f09c55df742b4dc41081252014e Mon Sep 17 00:00:00 2001 From: bdchatham Date: Mon, 10 Aug 2026 15:07:10 -0700 Subject: [PATCH] Run seidroid xreview from the released Go driver Restores the reusable workflow, which was removed from main while sei-chain and platform still pin the v0.0.15 commit that carries it. What they pin runs a Python driver that dies at `command -v python3` on the runner, so nothing has been reviewing. The review is now sei-agent-driver, installed from sei-protocol/sei-internal-skills, which is public, so there is no credential and no GOPRIVATE. That replaces four steps with two: the sparse checkout of this repo, the python venv and httpx install, and the curl exchange for a bearer all go away. The driver mints its own bearer from the M2M client, so the token never lands in a step output where every later step in the job can read it. driver-version pins the CLI and travels with the `uses:` ref, so one ref fixes both the workflow and the binary rather than letting them skew -- which is the failure these callers are in today. The server is reachable over https at its gateway hostname, so the job no longer has to be in-cluster and runs-on defaults to a hosted runner. XREVIEW_ALLOW_TOOLS is set, without which the driver declines every permission prompt, the agent is refused the shell it reads the diff with, and the review reports on nothing. Matched on tool_name rather than policy_name, because the deployment sends one policy name for every native request. The guard, the per-PR concurrency and the sticky upsert are kept as they were. Go is pinned rather than taken from the runner image, which only satisfies the module's floor by luck. Callers must drop the now-removed uci-ref input when they bump. Verified with actionlint and shellcheck. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/seidroid-xreview.yml | 168 +++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 .github/workflows/seidroid-xreview.yml diff --git a/.github/workflows/seidroid-xreview.yml b/.github/workflows/seidroid-xreview.yml new file mode 100644 index 0000000..d5fb4cc --- /dev/null +++ b/.github/workflows/seidroid-xreview.yml @@ -0,0 +1,168 @@ +name: seidroid xreview +run-name: UCI / seidroid xreview + +# Reusable, comment-triggered agentic PR review. A thin caller in the reviewed repo +# wires the issue_comment trigger and calls this with `uses:`. Flow: comment +# `seidroid xreview` on a PR -> trusted-commenter gate -> drive one managed sei-droid +# omnigent session over the PR -> post one sticky verdict. +# +# The review itself is sei-agent-driver, installed at `driver-version` from +# sei-protocol/sei-internal-skills, which is public. A caller updates by bumping the +# pinned `uses:` ref; the driver version travels with it, so one ref fixes both the +# workflow and the binary it runs. + +on: + workflow_call: + inputs: + driver-version: + description: "sei-agent-driver release to install. Bump with the pinned uses: ref." + required: false + type: string + default: 'v0.1.0' + runs-on: + description: "Runner label for the review job." + required: false + type: string + default: 'ubuntu-latest' + omnigent-base-url: + description: "omnigent base URL. Reached over https, so no in-cluster runner is needed." + required: false + type: string + default: 'https://seigent.dev.platform.sei.io' + secrets: + OMNIGENT_M2M_CLIENT_SECRET: + description: "omnigent client-credentials secret. The driver exchanges it for a session bearer." + required: true + +permissions: {} + +jobs: + guard: + # Cheap allowlist and command parse on a hosted runner, no secrets, before any + # review work spins up. The author_association gate is the trust boundary: only + # OWNER/MEMBER/COLLABORATOR can fire it, so an untrusted PR author cannot. + name: Guard + runs-on: ubuntu-latest + permissions: {} + if: >- + ${{ github.event.issue.pull_request != null && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) }} + outputs: + should_run: ${{ steps.parse.outputs.should_run }} + pr_number: ${{ steps.parse.outputs.pr_number }} + comment_id: ${{ steps.parse.outputs.comment_id }} + steps: + - id: parse + env: + BODY: ${{ github.event.comment.body }} + run: | + set -euo pipefail + cmd="$(printf '%s' "$BODY" | tr -d '\r')" + # Require a LINE reading exactly `seidroid xreview`. Anchoring the match to a + # whole line is what keeps a comment that merely quotes or discusses the + # command from triggering a review. + cmdline="$(printf '%s\n' "$cmd" | grep -m1 -E '^[[:space:]]*seidroid[[:space:]]+xreview[[:space:]]*$' || true)" + if [ -z "$cmdline" ]; then + echo "should_run=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + # The comment id becomes the driver's per-trigger id, so a re-delivered + # comment adopts the same session rather than driving a new turn. + { + echo "should_run=true" + echo "pr_number=${{ github.event.issue.number }}" + echo "comment_id=${{ github.event.comment.id }}" + } >> "$GITHUB_OUTPUT" + + xreview: + name: Review + needs: guard + if: needs.guard.outputs.should_run == 'true' + # Exactly one review per PR: a newer `seidroid xreview` cancels an in-flight one, + # so there are never two posters. Job-level, so the group is entered only when a + # real command runs. + concurrency: + group: seidroid-xreview-${{ github.event.issue.number }} + cancel-in-progress: true + runs-on: ${{ inputs.runs-on }} + # Above the driver's own run deadline, so a slow-but-valid review is never + # hard-killed mid-turn, which would end it with no annotation saying why. + timeout-minutes: 40 + permissions: + pull-requests: write # upsert the one sticky verdict comment + contents: read # read PR metadata + env: + # Installed here rather than left to the runner image's GOPATH, so both steps + # name one path and neither shells out to `go env`. + GOBIN: ${{ github.workspace }}/.driver-bin + steps: + # Pinned rather than taking the runner image's Go, which moves under us and + # only has to satisfy the module's floor by luck. + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + # Public module, so no credential and no GOPRIVATE. The version reference is + # plain: the driver is a nested module, and its path-prefixed tag is what + # resolution consumes rather than what is typed here. + - name: Install the driver + env: + DRIVER_VERSION: ${{ inputs.driver-version }} + run: | + set -euo pipefail + go install "github.com/sei-protocol/sei-internal-skills/sei-agent-driver/cmd/sei-agent-driver@${DRIVER_VERSION}" + "$GOBIN/sei-agent-driver" --version + + - name: Review + id: drive + env: + OMNIGENT_BASE_URL: ${{ inputs.omnigent-base-url }} + OMNIGENT_M2M_CLIENT_ID: sei-droid + OMNIGENT_M2M_CLIENT_SECRET: ${{ secrets.OMNIGENT_M2M_CLIENT_SECRET }} + # The driver declines every permission prompt it is not told to accept, so + # without this the agent is refused the shell it needs to read the diff and + # reviews nothing. Matched on tool_name, not policy_name: the deployment + # sends one policy name for every native request, so allowing that would + # accept every tool call the agent asks for. + XREVIEW_ALLOW_TOOLS: Bash,Read + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR: ${{ needs.guard.outputs.pr_number }} + TRIGGER_ID: ${{ needs.guard.outputs.comment_id }} + run: | + set -uo pipefail + "$GOBIN/sei-agent-driver" xreview "$REPO" "$PR" \ + --out verdict.md --trigger-id "$TRIGGER_ID" + rc=$? + # Gates on whether a verdict was PRODUCED, not on the exit code: a + # teardown-only failure still publishes, and a no-verdict run never posts a + # placeholder. + if [ -s verdict.md ]; then + echo "verdict_produced=true" >> "$GITHUB_OUTPUT" + if [ "$rc" -ne 0 ]; then + echo "::warning::driver exited $rc but produced a verdict; see the logs" + fi + else + echo "verdict_produced=false" >> "$GITHUB_OUTPUT" + echo "::error::driver produced no verdict (exit $rc)" + exit "$rc" + fi + + - name: Post verdict (sticky upsert) + if: ${{ steps.drive.outputs.verdict_produced == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + MARKER: "" + REPO: ${{ github.repository }} + PR: ${{ needs.guard.outputs.pr_number }} + run: | + set -euo pipefail + body="$MARKER"$'\n'"$(cat verdict.md)" + # One bot comment per PR: find by marker, then PATCH, else POST. + id="$(gh api "repos/$REPO/issues/$PR/comments" --paginate \ + --jq "map(select(.body | startswith(\"$MARKER\"))) | .[0].id // empty")" + if [ -n "$id" ]; then + gh api -X PATCH "repos/$REPO/issues/comments/$id" -f body="$body" >/dev/null + else + gh api -X POST "repos/$REPO/issues/$PR/comments" -f body="$body" >/dev/null + fi