A pull-request reviewer with an approval before merging#
You will build pr-reviewer: given a pull request, it reads the change and
the code around it, posts a review, and — when it judges the change ready —
asks to merge it. The merge waits on the machine until an admin approves
it. Nothing else on GitHub is writable, and a guardrail keeps merges out of
every other agent's reach.
flowchart LR
I[Input: acme/api#7] --> A[pr-reviewer]
A -->|pull_request_read,<br/>get_file_contents| G[(GitHub MCP)]
A -->|pull_request_review_write| G
A -->|merge_pull_request| H{{Held: approval}}
H -->|admin approves| G
Before you begin#
- A machine that can run agents, and the editor role (the admin role for the guardrail and to approve).
- A model: an Anthropic key in a credential
anthropic-key(keyapi_key), or a strong coding model on Eos. - A GitHub fine-grained token for the repositories to review, with
Contents, Pull requests and Issues set to Read and write,
kept in a credential
github-tokenunder the keytoken. Or use Connect with GitHub (Add tools and connections).
1. Create the agent#
The policy permits reads and reviews, and permits merging only with an admin's approval, held up to an hour:
@id("github-reads")
permit (principal, action == Action::"tools/call", resource in Server::"github")
when {
["get_file_contents", "get_repository_tree", "list_commits", "get_commit", "search_code",
"pull_request_read", "list_pull_requests", "issue_read"].contains(resource.name)
};
@id("github-review")
permit (principal, action == Action::"tools/call", resource in Server::"github")
when { ["pull_request_review_write", "add_comment_to_pending_review"].contains(resource.name) };
@id("merge-needs-an-admin")
@approval("role:admin")
@approval_wait("1h")
permit (principal, action == Action::"tools/call", resource in Server::"github")
when { resource.name == "merge_pull_request" };
- Anemoi → Agents → New agent, name
pr-reviewer. - Start from: Pull request reviewer. It brings the Assistant, the instructions and the GitHub connection with Writes need approval.
- Model: Anthropic,
claude-sonnet-4-5, credentialanthropic-key. - Connections → GitHub: credential
github-token. - Switch to Advanced and replace the Tool policy with the text above. Add to the Instructions: When the change is correct, tested and approved by your review, merge it with merge_pull_request (squash). If merging is refused or not approved, say so and stop.
- Limits: 30 minutes, Stop at, per run
$3. - Create the agent.
$ jq -n --rawfile p pr-reviewer.cedar '{
metadata: {name: "pr-reviewer"},
spec: {
kind: "astralyx",
model: {provider: "anthropic", model: "claude-sonnet-4-5", credential: "anthropic-key"},
instructions: "You review pull requests on GitHub. Read the description, the changed files and, where needed, the surrounding code. Post one review: what the change does, what is wrong or risky, concrete suggestions. When the change is correct and tested, merge it with merge_pull_request (squash). If merging is refused or not approved, say so and stop.",
tools: [{name: "github", kind: "mcp", url: "https://api.githubcopilot.com/mcp/", credential: "github-token"}],
policy: {tools: $p},
budget: {max_seconds: 1800, max_cost_usd: 3}
}}' \
| curl -sS -X POST "$WS/agents" -H "Authorization: Bearer $ASTRA_TOKEN" -H "Content-Type: application/json" -d @-
2. Check the policy before it runs#
Under the policy, Test a call on github: pull_request_read →
allowed by github-reads; merge_pull_request → needs a person's
approval (merge-needs-an-admin); delete_file → denied: no policy
permits it.
$ jq -n --rawfile p pr-reviewer.cedar '{tools: $p, tests: [
{server: "github", tool: "pull_request_read"},
{server: "github", tool: "merge_pull_request"},
{server: "github", tool: "delete_file"}]}' \
| curl -sS -X POST "$WS/agent-policy-checks" -H "Authorization: Bearer $ASTRA_TOKEN" \
-H "Content-Type: application/json" -d @- | jq -c '.tests[] | {decision, reason}'
{"decision":"allow","reason":"permitted by github-reads"}
{"decision":"requires_approval","reason":"needs a person's approval (merge-needs-an-admin)"}
{"decision":"deny","reason":"no policy permits it"}
The tool names are those of GitHub's MCP server. The agent sees only the tools its policy allows when it lists them.
3. Keep merges for this agent alone#
Add a workspace guardrail so no other agent merges, whatever its policy:
@id("only-the-reviewer-merges")
@reason("only pr-reviewer merges, with an admin's approval")
forbid (principal, action == Action::"tools/call", resource)
when { resource.name like "*merge*" && principal.agent != "pr-reviewer" };
@id("no-rest-merges")
@reason("merging goes through pr-reviewer")
forbid (principal, action == Action::"http", resource)
when { resource.host == "api.github.com" && resource.method == "PUT" && resource.path like "/repos/*/pulls/*/merge" };
Anemoi → Guardrails → New guardrail, name
only-the-reviewer-merges, the text above, Create.
4. Review a pull request#
The run reads the pull request, posts its review, then calls
merge_pull_request. That call stops on the machine; the run shows
waiting for approval, and the workspace's admins get an email.
5. Approve the merge#
Anemoi → Approvals: github/merge_pull_request from pr-reviewer.
Open it: The call shows the arguments (owner, repo, pull number,
merge method), read from the machine. Read the review on GitHub, then
Approve — or Deny with a reason, which the agent is told.
$ A=$(curl -sS "$WS/approvals?state=Pending&run=pr-reviewer-5e01c2" -H "Authorization: Bearer $ASTRA_TOKEN" | jq -r '.items[0].metadata.name')
$ curl -sS "$WS/approvals/$A/details" -H "Authorization: Bearer $ASTRA_TOKEN"
$ curl -sS -X POST "$WS/approvals/$A/decision" -H "Authorization: Bearer $ASTRA_TOKEN" \
-H "Content-Type: application/json" -d '{"decision": "approve"}'
The held call goes ahead once; the run finishes and says the pull request was merged. If nobody decides within the hour, the agent is told the merge is still waiting and ends; the approval stays open for 24 hours, and the same call from a new run goes ahead once approved.
6. Check the record#
The run's Trace shows each call with its rule; its Receipt lists the approval used — its name, Used, who approved and when — and its policy digest covers the guardrail. Check it offline:
Run it on every pull request#
Anemoi has no GitHub trigger of its own. Start a run from your CI when a pull request is opened or marked ready, with a personal API token kept as a secret:
on:
pull_request:
types: [opened, ready_for_review]
jobs:
review:
runs-on: ubuntu-latest
steps:
- run: |
curl -fsS -X POST "$WS/agents/pr-reviewer/runs" -H "Authorization: Bearer $ASTRA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"input\": \"${{ github.repository }}#${{ github.event.pull_request.number }}\"}"
env:
WS: https://console.astralyx.cloud/api/v1/orgs/acme/workspaces/research/clusters/main/api
ASTRA_TOKEN: ${{ secrets.ASTRA_TOKEN }}