ci: integrate UCI AI review, AI assistant, and stale check - #317
Conversation
Adopt the shared sei-protocol/uci workflows so sei-js gets the same automated PR review, @seidroid assistant, and stale triage that sei-chain and sei-skill run. Pinned to the v0.0.15 release commit rather than a floating tag. sei-chain currently trails on v0.0.13 (AI review/assist) and v0.0.9 (stale check), so these are pinned forward instead of copied verbatim. v0.0.15 adds the `ai: skip-review` label switch, which only takes effect immediately when the caller subscribes to `labeled`/`unlabeled` — hence those trigger types. Cursor's scout pass is disabled because Bugbot already reviews sei-js PRs. xreview is intentionally omitted; it was removed upstream after v0.0.15. Also add REVIEW.md, the repo guidelines the AI reviewer loads from the PR base branch, covering the mcp-server transport/wallet invariants, hand-maintained precompile ABIs, changeset expectations, and known non-issues that would otherwise generate false positives. Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #317 +/- ##
=======================================
Coverage 79.75% 79.75%
=======================================
Files 83 83
Lines 1309 1309
Branches 217 217
=======================================
Hits 1044 1044
Misses 259 259
Partials 6 6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
CI-only change adding three UCI reusable-workflow callers plus a REVIEW.md guidelines file; all four files are well-documented and every load-bearing factual claim in REVIEW.md checks out against the tree. No blockers — findings are a likely-accidental contents: read drop in the stale-check job permissions, a missing concurrency guard on ai-review.yml, fork-PR behaviour, and a few verification items I couldn't confirm because network access is blocked in this environment.
Findings: 0 blocking | 16 non-blocking | 7 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Both second-opinion passes produced no output —
codex-review.mdandcursor-review.mdare empty, so this review is single-source with no independent corroboration. (REVIEW_GUIDELINES.mdwas also empty, which is expected: basemainhas no guidelines file until this PR merges.) REVIEW.mdcontent is accurate — I spot-checked every load-bearing claim against the tree and all held:validateSecurityConfig()doesprocess.exit(1)on HTTP+wallet;http-sse.tskeysconnectionsbytransport.sessionIdand returns 400/404 as described;src/tests/server/transport/http-sse.test.tsexists; threeJSON.parse(abi)sites incore/tools.ts; the three RPC env overrides incore/chains.ts;PRIVATE_KEYread via zod incore/config.ts;fixed: []/linked: []; six packages;codecov.ymlsets 80% for five packages and omitsmcp-server;noImplicitAny: false; Biome configures tabs/160-col and ignorescreate-sei/templates/**with nobiomescript in any package.json; no codegen inpackages/precompiles; and the.test.tsvs__tests__/*.spec.tssplit is as stated. The author's claim to have verified rather than assumed holds up.- Verify the guidelines filename against UCI v0.0.15 before merging. This is the single thing that would make the entire file inert if wrong, and it cannot be exercised on this PR because the reviewer loads it from the base branch, where no copy exists yet. Network access was blocked in my environment so I could not confirm that UCI reads
REVIEW.md(vs. some other path). Re-confirm on the first post-merge PR that the guidelines actually load. - Good security property worth affirming: because UCI loads guidelines from the base branch, a PR cannot weaken the reviewer's own rules for itself. That matters given §4 is explicitly a suppression list ("do not flag these").
- No prompt-injection attempt in this diff. §4's "Known non-issues — do not flag these" list is legitimate maintainer configuration, applies from the base branch, and I verified each entry against the source. Nothing in the PR title, body, or files tried to steer this review's verdict.
- Unverifiable from this environment (network blocked): that
uci-ref,enable-cursor,allowed-team, anddays-before-pr-staleall exist as inputs at the pinned SHA, and that the granted permission sets match what each reusable workflow declares. The author's test plan claims these were checked; I could not independently confirm them. uci-stale-check.ymlomitssecrets: inheritwhile the other two callers include it. Probably correct if the reusable workflow only usesGITHUB_TOKEN(typical foractions/stale), but worth confirming so the first scheduled run isn't the thing that discovers otherwise.uci-refduplicates theuses:pin in all three files. The SHA appears twice per file (once inuses:, once as an input) and the two must stay in sync on every version bump — an easy thing to half-update. Consider noting this in the bump procedure, or check whether UCI can defaultuci-refto the resolved workflow ref.- Four overlapping AI review passes on every PR once this lands: the existing
pr-to-slack-codex.yml, UCI's Codex pass, UCI's Claude pass, and Cursor Bugbot. The author already flagged retiringpr-to-slack-codex.ymlin the test plan — note thatREVIEW.md§1 hard-references it as active, so retiring it later requires editingREVIEW.mdin the same change. - 7 suggestion(s)/nit(s) flagged inline on specific lines.
| jobs: | ||
| stale: | ||
| name: Stale | ||
| permissions: |
There was a problem hiding this comment.
[suggestion] Job-level permissions replaces the workflow-level block rather than merging with it, so any scope not listed here is set to none. This job therefore runs with contents: none, and the workflow-level contents: read on line 10 applies to no job at all (stale is the only one).
Both sibling workflows added in this PR explicitly include contents: read in their job-level block, which suggests the omission here is accidental rather than deliberate. It's likely harmless if UCI's stale-check.yml only calls the issues/PRs API, but it breaks the moment that workflow does an actions/checkout.
Suggest adding contents: read to this block (and dropping the now-dead top-level permissions: on lines 9-10, or keeping it only as documentation).
| pull_request: | ||
| # `labeled`/`unlabeled` let the `ai: skip-review` label take effect immediately rather | ||
| # than on the next push. UCI ignores those two events for every other label. | ||
| types: [ opened, ready_for_review, synchronize, reopened, labeled, unlabeled ] |
There was a problem hiding this comment.
[suggestion] No concurrency guard on this caller, while uci-stale-check.yml does define one. synchronize fires on every push, and with labeled/unlabeled now in the trigger list there are more ways to stack runs — rapid pushes, or a push landing just after a label change, can run overlapping reviews on the same PR. That means duplicate posted reviews, racing check-run updates, and doubled LLM spend.
Unless UCI's ai-review.yml sets concurrency internally (worth confirming at the pinned SHA), consider:
concurrency:
group: ai-review-${{ github.event.pull_request.number }}
cancel-in-progress: true| @@ -0,0 +1,20 @@ | |||
| name: AI Review | |||
| on: | |||
| pull_request: | |||
There was a problem hiding this comment.
[suggestion] Because this uses pull_request (not pull_request_target), PRs from forks on a public repo get a read-only GITHUB_TOKEN and no secrets — secrets: inherit resolves to nothing. So AI Review will fail or silently no-op on every external-contributor PR, leaving a permanently red "AI Review" check on exactly the PRs where outside contributors see it.
To be clear: staying on pull_request is the correct security call here — pull_request_target would run privileged with untrusted code checked out. The suggestion is only to make the failure non-noisy. The existing pr-to-slack-codex.yml guards on github.event.pull_request.author_association (line 10) for precisely this reason; a similar if: on this job, or a note in the PR-workflow docs, would avoid the confusing red check.
| pull-requests: write | ||
| checks: write | ||
| id-token: write | ||
| secrets: inherit |
There was a problem hiding this comment.
[nit] secrets: inherit forwards every repo and org secret to the reusable workflow. This repo holds OPENAI_API_KEY, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID, and CODECOV_TOKEN, yet the PR description says only the PLATFORM_CODE_AGENT_* set is actually needed.
The SHA pin and same-org ownership make this acceptable, so this is hygiene rather than a vulnerability — but enumerating just the required secrets under secrets: would bound the blast radius if the pin were ever bumped to a bad commit, and would document the actual dependency. Same applies to ai-assist.yml.
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.sha }} |
There was a problem hiding this comment.
[nit] Keying concurrency on github.sha is an odd fit for a scheduled workflow: for cron runs the SHA is the tip of the default branch, so it stays constant between commits. A workflow_dispatch run will land in the same group as an in-flight scheduled run on the same SHA and, with cancel-in-progress: true, cancel it (and vice versa).
github.ref or a plain constant is the more usual key here. Separately, github.workflow resolves to UCI (line 1), which is generic enough to collide if another workflow later adopts that same name:.
| # See: https://github.com/sei-protocol/uci/releases/tag/v0.0.15 | ||
| uses: sei-protocol/uci/.github/workflows/ai-assistant.yml@65901242783550521f25a19199a6b10e54550b97 | ||
| permissions: | ||
| contents: read |
There was a problem hiding this comment.
[nit] Worth confirming contents: read is sufficient at the pinned SHA. If UCI's assistant can apply fixes — commit to the PR branch or push a suggestion branch in response to @seidroid — it needs contents: write, and this would fail only at the point someone first asks it to make a change. I couldn't check the v0.0.15 definition from here (no network), so flagging rather than asserting.
| @@ -0,0 +1,22 @@ | |||
| name: AI Assistant | |||
| on: | |||
| issue_comment: | |||
There was a problem hiding this comment.
[nit] issue_comment fires for comments on issues as well as PRs, from any user on a public repo, so every drive-by comment anywhere in the repo spawns a workflow run. Gating rests entirely on UCI's allowed-team check (line 22).
Delegating the authorization check to the reusable workflow is a reasonable design, but worth verifying two things at the pinned SHA: that the team check runs before any comment body reaches the agent as instructions, and that a non-member comment exits cleanly rather than as a failed run.
Job-level `permissions` replaces the workflow-level block instead of merging with it, so the `stale` job was running with `contents: none` and the workflow-level default applied to no job at all. Harmless today — UCI's stale-check only calls the issues/PRs API via `actions/stale` and never checks out — but it breaks silently the moment that changes. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
CI-only PR wiring three UCI reusable workflows (AI review, AI assistant, stale check) at a pinned SHA plus a new REVIEW.md guidelines file. Every factual claim in REVIEW.md checks out against the tree, but the filename appears not to match what the reviewer actually loads, and a few caller-side workflow guards (concurrency, comment filtering, issue-stale inputs) are missing.
Findings: 2 blocking | 13 non-blocking | 7 posted inline
Blockers
REVIEW.mdfilename likely doesn't match what the UCI reviewer reads (see inline on REVIEW.md:1). This is the one finding that would make the file a no-op, and it's cheap to confirm againstai-review.ymlat the pinned SHA65901242.- 1 blocking issue(s) flagged inline on specific lines.
Non-blocking
- Second-opinion passes produced no output for this review: both
codex-review.mdandcursor-review.mdwere empty. Cursor's absence is expected givenenable-cursor: false, but the empty Codex file is worth checking — if the Codex scout is silently failing,REVIEW.md's assumption that "overlapping findings are expected" from multiple reviewers doesn't hold. - There is no
CODEOWNERSin this repo. Because the reviewer loads the guidelines from the base branch, any merged edit toREVIEW.mdsilently reprograms every subsequent AI review — in particular the "do not flag these" list in §4, which is a direct suppression lever. Consider adding aCODEOWNERSentry for/REVIEW.mdso changes to it require a human sign-off from the same team named inallowed-team. REVIEW.md§4 is a static suppression list with no ownership or freshness marker. Entries like "no package defines abiomescript" and "mcp-serverhas no Codecov target" are true today but will silently become wrong instructions the moment someone adds abiomescript or acodecov.ymlflag. A one-line note pointing to the files each claim is derived from (biome.json,codecov.yml,checks.yml) would let a reviewer self-check rather than trust a stale assertion.- No changeset, correctly — this is CI/docs-only and matches
REVIEW.md§3's own carve-out. Noting it so a later reviewer doesn't file it. - Open items from the PR's own test plan remain unverified and are not resolvable from the diff: that
sei-protocol/sei-coreis the rightallowed-teamfor sei-js, that the org-levelPLATFORM_CODE_AGENT_*secrets/vars resolve here viasecrets: inherit, and whetherpr-to-slack-codex.ymlshould be retired now that UCI runs its own Codex pass.REVIEW.md:40asserts the Slack AppSec pass exists as an ongoing fact, so retiring that workflow later means editingREVIEW.mdtoo. secrets: inheriton all three callers passes the full org/repo secret set to the reusable workflow. That's acceptable here (first-party repo, pinned to a full SHA rather than a tag — good), but enumerating the handful ofPLATFORM_CODE_AGENT_*secrets explicitly would bound the blast radius if the pin is ever moved to a mutable ref.- I found no prompt-injection attempts in the diff.
REVIEW.mddoes instruct reviewers what not to flag, which is its legitimate purpose, and each of its "known non-issue" claims verifies against the source (security.tsdoesprocess.exit(1);createCorsMiddleware()does return a bare 204;stdio.tsdoes useconsole.error;codecov.ymldoes omit mcp-server;noImplicitAny: falseis intsconfig.base.json; six packages,fixed: []/linked: [], and the@asyncapioverrides are all as described). The content quality is high — the concerns above are about the mechanism, not the accuracy. - 6 suggestion(s)/nit(s) flagged inline on specific lines.
| @@ -0,0 +1,109 @@ | |||
| # Review guidelines for AI agents | |||
There was a problem hiding this comment.
[blocker] The filename looks wrong, which would make this whole file dead weight.
The PR description says this "is the guidelines file UCI's reviewer loads from the PR base branch" — but the reviewer harness that ran on this PR was handed a base-branch file named REVIEW_GUIDELINES.md, not REVIEW.md. Those two names can't both be right.
Worth checking directly against sei-protocol/uci/.github/workflows/ai-review.yml@65901242 which path it fetches from the base ref. If it's REVIEW_GUIDELINES.md, rename this file — otherwise the reviewer sees no guidelines and every §4 "known non-issue" keeps getting filed as a finding, which is the main thing this file exists to prevent.
(If v0.0.15 genuinely reads REVIEW.md and a later UCI renamed it, then this is fine as-is and the note is on the uci-ref pin instead.)
| in `src/server/transport/stdio.ts` is correct. Never suggest converting | ||
| these to `console.log`. The inverse *is* a finding: a new `console.log` on a | ||
| path reachable from stdio breaks the transport. | ||
| - **The CORS middleware sets no `Access-Control-Allow-Origin`.** |
There was a problem hiding this comment.
[suggestion] This entry is accurate about browsers but risks suppressing a real gap.
"Deny-by-default" only holds for browser clients that honour the missing Access-Control-Allow-Origin. Server-side there is no Origin or Host validation anywhere in http-sse.ts / streamable-http.ts, so a non-browser client — or a DNS-rebinding attack against a locally bound server — reaches the tool surface unimpeded. The MCP spec calls for explicit Origin validation on local HTTP transports for exactly this reason.
The wallet guard in validateSecurityConfig() keeps funds out of reach, so this isn't a drain primitive, but it still exposes the read tools and the configured RPC endpoint. Suggest narrowing the wording to "the absence of a permissive wildcard is deliberate — missing Origin/Host validation is a separate question and is still fair to raise", so a future reviewer isn't told to stay quiet about it.
| pull_request: | ||
| # `labeled`/`unlabeled` let the `ai: skip-review` label take effect immediately rather | ||
| # than on the next push. UCI ignores those two events for every other label. | ||
| types: [ opened, ready_for_review, synchronize, reopened, labeled, unlabeled ] |
There was a problem hiding this comment.
[suggestion] No concurrency block on this caller, unlike uci-stale-check.yml.
With synchronize in the trigger list, every push to an open PR starts a fresh full review. During a rebase-and-force-push or a rapid series of fixup commits that means several concurrent (or queued) review runs against the same PR, each burning model tokens and each racing to post a review. Adding the same pattern already used in release.yml fixes it:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: trueSeparately, worth being aware that pull_request (correctly chosen over pull_request_target) means fork PRs get no secrets and a read-only token — so AI review will not run on external contributions to this public repo. That's the safe tradeoff, just make sure nobody reads a missing review on a fork PR as an approval.
| @@ -0,0 +1,22 @@ | |||
| name: AI Assistant | |||
| on: | |||
| issue_comment: | |||
There was a problem hiding this comment.
[suggestion] issue_comment: created with no if: guard means a job starts for every comment on every issue and PR in the repo, then presumably exits inside UCI once it finds no @seidroid mention or a non-sei-core author.
Two consequences worth a caller-side guard:
- Runner minutes and a noisy Actions tab on ordinary discussion.
- Feedback-loop risk: UCI posts as a GitHub App with a
PLATFORM_CODE_AGENT_*token, notGITHUB_TOKEN, so its own comments and review submissions do re-trigger workflows. Combined withpull_request_review: submitted, the AI Review workflow posting a review can wake the assistant.
The allowed-team check likely stops the loop (an App isn't a team member), but relying on that means the loop is only broken inside a pinned third-party workflow. A cheap explicit guard:
if: >-
github.event.sender.type != 'Bot' &&
contains(github.event.comment.body || github.event.review.body, '@seidroid')A concurrency group keyed on the issue/PR number would also be worth adding here.
| # so `contents: read` has to be repeated here to survive. | ||
| permissions: | ||
| contents: read | ||
| issues: write |
There was a problem hiding this comment.
[suggestion] This job is granted issues: write on a daily cron, but the only input passed is days-before-pr-stale: 28 — issue staling is left entirely to whatever UCI v0.0.15 defaults to.
Since this runs unattended every day at 12:00 UTC and can comment on or close issues, the issue-side behaviour should be explicit rather than inherited: either pass days-before-issue-stale (and the corresponding close/exempt inputs) deliberately, or, if issues aren't meant to be triaged at all yet, drop issues: write so an upstream default change can't start closing them. Worth confirming against the v0.0.15 input defaults before merge.
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.sha }} |
There was a problem hiding this comment.
[nit] Keying the concurrency group on github.sha is a slightly odd fit for a cron/dispatch workflow. Consecutive daily runs on an unchanged default branch share the same sha and so land in the same group, while a workflow_dispatch fired during a running cron will cancel it. ${{ github.workflow }}-${{ github.ref }} — the pattern release.yml:8 already uses — expresses "one stale sweep at a time per branch" more directly.
| - **`packages/registry/chain-registry` and `.../community-assetlist` are | ||
| missing from the tree.** Both are git submodules (`.gitmodules`) and are | ||
| listed in `.gitignore`; they are populated by the `registry` package's | ||
| `postinstall` and by CI's submodule checkout. Their JSON is vendored |
There was a problem hiding this comment.
[nit] "and by CI's submodule checkout" is only half true: release.yml:24 sets submodules: recursive, but checks.yml — the workflow that actually gates PRs — does a plain actions/checkout@v4 and relies solely on the registry package's postinstall. Since a reviewer reading this line is most likely looking at a PR-gate failure, naming release.yml specifically would avoid pointing them at the wrong mechanism.
Summary
Adopts the shared
sei-protocol/ucireusable workflows so sei-js gets the same automated PR review,@seidroidassistant, and stale triage that sei-chain and sei-skill already run.All three are pinned to the v0.0.15 release commit (
65901242783550521f25a19199a6b10e54550b97) rather than a floating tag.ai-review.ymlai-review.ymlpull_requestai-assist.ymlai-assistant.ymluci-stale-check.ymlstale-check.ymlworkflow_dispatchPlus a
REVIEW.md, which is the guidelines file UCI's reviewer loads from the PR base branch.Notes on version selection
sei-chain is currently behind upstream, so its config was pinned forward rather than copied verbatim:
29a9c733…), and its stale check is still on the mutablev0.0.9tag.ai: skip-reviewlabel switch. That only takes effect immediately if the caller subscribes tolabeled/unlabeled, which sei-chain's copy omits — so those trigger types are included here.Deliberate choices
enable-cursor: false— Bugbot already reviews sei-js PRs (cursor[bot]on Version Packages #311, security(deps): pin @asyncapi packages to non-compromised versions #316), so the Cursor scout pass would duplicate it. Same rationale as sei-chain.mainafter v0.0.15 areseidroid: rename auto-review → ai-review; trim xreview READMEandchore(seidroid): remove xreview while its approach is reconsidered.allowed-team: 'sei-protocol/sei-core'— copied from sei-chain and sei-skill. Worth confirming this is the right team for sei-js.REVIEW.md
Adapted from the sei-chain and sei-skill equivalents but written against this repo. Every claim was verified against the source rather than assumed:
packages/mcp-serveras the security surface — the stdio-only wallet invariant invalidateSecurityConfig()and the SSE session binding added in fix(mcp-server): enforce session binding and response isolation in HTTP SSE transport #312.fixed: [],linked: []).console.errorfor informational logging (stdout carries JSON-RPC frames under stdio), the deliberately header-less CORS preflight response,process.exit(1)as fail-closed startup behaviour, the gitignored registry submodules, and Biome not being CI-enforced.Test plan
Workflows can't be fully exercised until they're on
main(the assistant and stale check only run from the default branch), but:uci-ref,enable-cursor,allowed-team,days-before-pr-stale) exists in the v0.0.15 definitionsPLATFORM_CODE_AGENT_*secrets andPLATFORM_CODE_AGENT_ANTHROPIC_*/SEI_LABS_ANTHROPIC_ORG_IDvars resolve for sei-js viasecrets: inheritsei-protocol/sei-coreis the correctallowed-teampr-to-slack-codex.ymlshould be retired now that UCI runs its own Codex passMade with Cursor