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

@ -14,6 +14,8 @@
//!
//! The operator is the `Remote-User` header the gateway sets, as for
//! `agent_input`; the classifier names them in its journal line.
//!
//! [`review`] runs the same rules on a review request and posts the result.
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Write as _;
@ -35,6 +37,8 @@ use utoipa::{IntoParams, ToSchema};
use crate::forge::{self, DefaultBranchFiles, PullForReview};
use crate::{AppState, error_problem};
pub(crate) mod review;
/// How long a finished run's result stays readable.
pub(crate) const RESULT_TTL: Duration = Duration::from_hours(1);
@ -707,6 +711,12 @@ pub(crate) mod tests {
files: Vec<(&'static str, String)>,
}
impl Fake {
pub(crate) fn new(reply: Reply, files: Vec<(&'static str, String)>) -> Self {
Self { reply, files }
}
}
impl PullSource for Fake {
async fn pull(
&self,
@ -1017,6 +1027,15 @@ pub(crate) mod tests {
})
}
/// A JSON rules file holding the one `datetime` classification.
pub(crate) fn rules_json() -> (&'static str, String) {
let [_, json] = RULES_PATHS;
(
json,
rules_value(&serde_json::json!([datetime()]), &[]).to_string(),
)
}
/// `file` as both formats, each with its path.
fn both_formats(file: &serde_json::Value) -> [(&'static str, String); 2] {
let [yml, json] = RULES_PATHS;