Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller: post the review classifier's verdict on a review request

Requesting the forge account `review-classifier` as a reviewer now runs the
repo's rules against the PR and posts the result as that account's review,
on the head commit the run read. A push starts no run; only a fresh review
request does.

- An instance-wide `pull_request_review_request` system hook delivers to
  `/webhook/forge/review-request`. A request for any other reviewer, or a
  removed request, does nothing.
- The controller reads the PR, loads the rules with the same reader as
  `GET /api/review-classifier/rules`, sends one request on the try-out
  subject, and posts APPROVED or REQUEST_CHANGES with the table as the body.
- With no clean verdict (PR or rules don't load, the run fails, or any
  classification has no answer) it posts a COMMENT saying why, never an
  approval. The event is one function, `no_clean_verdict_event`.
- Table cells are flattened, markup-escaped, and every `@` gets a
  zero-width space, so a model's reason can't break the table or mention
  anyone.
- At most two requested reviews run at once; the rest wait in memory.
- The account is minted the way hive accounts are: a five-minute pass
  queues a `MintClassifierForgeToken` node when the token at
  `swarm/services/review-classifier/forge/token` is missing or stale. The
  token is `write:repository`. The controller's bao policy gains that leaf
  (create/read/update), pinned in module-eval bao-grants. The classifier
  never sees the token.

The account gets no repo access automatically; a repo admin adds it by
hand. A re-request retires the account's earlier review: Forgejo clears
`official` on a reviewer's earlier reviews when it is requested again
(models/issues/review.go AddReviewRequest, v16.0.5), and approvals and
rejection blocks count official reviews only.

Refs #5137
This commit is contained in:
atlas 2026-10-12 14:28:42 +02:00
commit a669cd96b6
17 changed files with 1156 additions and 50 deletions

View file

@ -318,10 +318,14 @@ the agent runs on, or on a forge container there.
### Swarm-wide forge webhooks
At startup the controller registers two Forgejo hooks pointing at
itself — a `push` hook on `internal/knowledge` and a `pull_request` hook
on the `agent-configs` org, both under
`https://<swarm.domain>/webhook/forge/`.
At startup the controller registers its Forgejo hooks pointing at
itself, all under `https://<swarm.domain>/webhook/forge/`:
- a `push` hook on `internal/knowledge`
- a `pull_request` hook on the `agent-configs` org
- an instance-wide `push` system hook, for commit metrics
- an instance-wide `pull_request_review_request` system hook, which starts
the [review classifier](review-classifier.md#trigger-and-lifecycle)
The controller **interprets** a delivery and sends hives a specific
message — _the knowledge repo changed_, _deploy agent `foo` at rev

View file

@ -74,6 +74,7 @@ of the cell says how.
| `swarm/services/<clientId>/oidc/client` | authelia | that service's own reader, under its own certificate — the queue responder's under `swarm-nats-auth`'s, the homeserver's under `swarm-matrix-ctl`'s, the forge's under `swarm-forge-publisher`'s, the review classifier's under `swarm-review-classifier`'s; the bao UI's by the store's granter | must be stated | must be stated |
| `swarm/services/ci-runner/registration-token` | `swarm-controller`, from the forge's admin API, at start and in a five-minute pass; its grant on the path is write-only | `swarm-bao-ci-runner` on the runner's host, under the runner's own certificate, into a file only `swarm-ci-runner-register` in the runner's container loads; no other principal's policy reads it | ❌ the forge hands out one token until an admin resets it; the controller writes a reset token within five minutes | ❌ `swarm-bao-ci-runner` reads it when it starts, at boot or deploy, and restarts the runner when the value changed |
| `swarm/services/review-classifier/anthropic-api-key` | the operator, by hand in the store's UI, field `api_key` | `swarm-review-classifier` in its container, under its own certificate, at the start of every run; held for that run and handed to `claude`'s environment only; no other principal's policy reads it | ❌ an operator's key; replaced by writing a new one | ✅ read on every run, so a written or removed key takes effect on the next run |
| `swarm/services/review-classifier/forge/token` | `swarm-controller`, with the forge's admin API, for its `review-classifier` account, at start and in a five-minute pass | `swarm-controller` under its own certificate, before it decides whether to mint, and on every review it posts as the account; the classifier service's policy doesn't read it | ✅ the controller's pass re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes) | ✅ the controller reads the store on every review it posts |
| _(not in the store)_ a hive's mTLS leaf | the store's own PKI, or an operator placing it by hand | its own client, off disk — the exception above, because it's what makes every other row's pull possible | must be stated | must be stated |
<!-- vale write-good.Passive = YES -->

View file

@ -68,30 +68,36 @@ classifications:
## Trigger and lifecycle
The classifier has a dedicated forge service account. Requesting that
account as a reviewer on a PR starts a run:
The classifier has a dedicated forge service account, `review-classifier`
(see [The service account](#the-service-account)). Requesting that account
as a reviewer on a PR starts a run:
1. The forge sends the review-request webhook to
1. The forge sends the `pull_request_review_request` delivery to
[`swarm-controller`](../../swarm-controller/README.md), the swarm's one
webhook owner.
2. The controller hands the run to the classifier service as an async
message.
3. The classifier runs every classification in the repo's config, read from
the default branch, against the PR's commits and diff.
4. The service account posts the result on the PR as a review.
webhook owner, on its instance-wide hook `/webhook/forge/review-request`.
A delivery for any other reviewer, or a removed request, does nothing.
2. The controller reads the PR and the repo's rules from the default branch,
and hands the run to the classifier service as one queue request on
`$SWARM.review-classifier.try-out`, the subject a try-out uses.
3. The classifier runs every classification against the PR's commits and
diff.
4. The controller posts the result on the PR as the service account's
review, on the commit it read.
A verdict belongs to the commit it ran on. Once the PR's head moves,
requesting the service account as a reviewer again starts a fresh run on the
new head.
A verdict belongs to the commit it ran on. A push starts no run. Once the
PR's head moves, requesting the service account as a reviewer again starts a
fresh run on the new head.
That only holds if the bot's review on an older commit stops counting once
the head moves. Either the repo enables Forgejo's stale-review dismissal
(see [PR review gate](../process/pr-review-gate.md)), or the service
dismisses its own earlier review when it posts a new one. Where neither
applies, an old approval keeps counting and an old request-changes keeps
blocking.
The re-request also retires the account's earlier review. The forge clears
the `official` flag on a reviewer's earlier reviews when it's requested
again, and only official reviews count toward the required approvals or
block a merge. Removing the request before the new review lands restores
the earlier one. A repo whose branch protection also dismisses stale approvals (see
[PR review gate](../process/pr-review-gate.md)) stops counting an approval
on an older commit even without a re-request.
Runs go in parallel, up to a maximum set at swarm level.
At most two requested reviews run at once. More wait in the controller's
memory, so a controller restart drops them, and nothing gets posted for them.
## Components
@ -137,6 +143,31 @@ and from every agent's account. It's the reviewer someone requests, it
authors every classifier review, and it pushes the branches the try-out page
creates.
`swarm-controller` creates it as `review-classifier`, with the email
`review-classifier@services.hyperhive.local` and no repo creation, and mints
its one token, `swarm-review-classifier` with scope `write:repository`, into
the store at `swarm/services/review-classifier/forge/token`. A pass at start
and every five minutes re-mints when the token is missing or no longer
matches the forge. The controller refuses an existing account by that name
with any other email. The controller posts the reviews with that token, and
the classifier service never sees it.
**Giving a repo access is a repo admin's step, by hand.** Nothing grants
it automatically. In the repo's settings, add `review-classifier` as a
collaborator:
- with **read**, so someone can request it as a reviewer, and its reviews show,
but its approval doesn't count
- with **write**, so its approval counts toward the required approvals and
its request-changes blocks the merge where the branch protection blocks on
rejected reviews. Where the branch protection restricts approvals to an
allowlist, the account counts only once it's on that list, and read access
is then enough.
The repo also needs a rules file on its default branch (see
[Repo configuration](#repo-configuration)). Without one, a request posts a
comment saying so.
## The classification run
### Shared context
@ -209,6 +240,18 @@ like any other ([`pr-review-gate.md`](../process/pr-review-gate.md)). The
Repos that rely on it typically raise their required approval count to
account for it.
A run with no clean verdict never approves. When the PR or the rules don't
load, the run fails, or **any** classification has no answer, the account
posts a **comment** instead, starting with `**No verdict:**` and the reason,
followed by the table when there is one. A comment neither approves nor
blocks the merge: nine timeouts and one pass post a comment, not an
approval.
The controller flattens every cell to one line, with `|`, backticks, brackets, `<`, `>`,
`&`, `#` and other markup characters escaped, and a zero-width space after
each `@`, so a model's `reason` can't break the table, embed markup, or
mention anyone.
## Runtimes
The classifier runs on one of three runtimes, chosen at swarm level:
@ -302,9 +345,6 @@ These get settled, and filled in here, as the work reaches them:
and how the recent-runs page reads them.
- **Oversized diffs.** What happens when the diff doesn't fit the model's
context. Lockfiles already collapse to one line each, as in `hive-forge diff`.
- **Stale reviews.** Whether the repo's stale-review dismissal setting
retires the bot's review on an older commit, or the service dismisses its
own earlier review when it posts a new one.
- **Budget.** Any per-run or per-day spend limit on top of the concurrency
cap.
- **Diff-leaves-swarm policy.** On the API key and ACP runtimes the diff
@ -315,20 +355,17 @@ These get settled, and filled in here, as the work reaches them:
- **The "no tools" mechanism on ACP.** How the ACP runtime runs a session with
no tools at all. The claude runtimes pass `--tools ""`.
- **Credential paths.** Where in bao the ACP and subscription runtimes'
credentials and the service account's forge token live, and which component
holds the token. The API key is at
`secret/swarm/services/review-classifier/anthropic-api-key`, field `api_key`.
credentials live. The API key is at
`secret/swarm/services/review-classifier/anthropic-api-key`, field `api_key`,
and the service account's forge token at
`secret/swarm/services/review-classifier/forge/token`.
- **Model naming across runtimes.** How the swarm-level model setting
handles runtime-specific model names.
- **ACP cost labels.** How an ACP run's cost gets a repo and PR label, so
it reads from the metrics store like the claude runtimes' cost.
- **Async message medium for review runs.** Which queue subjects or streams
carry a webhook-started run. A try-out is one request on
`$SWARM.review-classifier.try-out`, answered on its reply subject.
- **Reviewer handoff.** How a later reviewer, human or agent, learns the
classifier passed a PR before it starts its own review.
- **Opt-in details.** Beyond the config file: the service account's access
to a repo, so someone can request it as reviewer and it can push try-out
branches.
- **Try-out branch access.** The service account's access for pushing try-out
branches, beyond what a repo admin gives it for reviews.
- **Commit author for try-out PRs.** How the classifier derives the commit
author's name and email from the swarm UI login.