Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-secret-client/src/review_classifier.rs
atlas a669cd96b6 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
2026-10-12 16:11:50 +02:00

99 lines
3.4 KiB
Rust

//! The review classifier agreement: where its inference key, its queue
//! client secret and its forge token live in the store, and what each object
//! holds.
//!
//! The operator writes the key by hand in the store's UI; the classifier
//! reads the key and the client secret under its own certificate, the key
//! once per run. Its read stanza in `swarm-bao.nix` names exactly these two
//! paths. The forge token is `swarm-controller`'s alone: it mints it, and
//! posts the classifier's reviews with it.
use serde::{Deserialize, Serialize};
use crate::{
Error,
path::{Kind, principal_prefix},
};
/// The classifier's name segment under [`Kind::Service`].
const SERVICE: &str = "review-classifier";
/// The path holding the Anthropic API key the classifier runs `claude` with.
///
/// # Errors
/// Never in practice: the name segment is a constant. The `Result` is
/// [`principal_prefix`]'s.
pub fn api_key_path() -> Result<String, Error> {
let prefix = principal_prefix(Kind::Service, SERVICE)?;
Ok(format!("{prefix}/anthropic-api-key"))
}
/// What [`api_key_path`] holds.
///
/// No `Debug`: `api_key` is a live provider credential.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApiKey {
/// The key itself, under the field name [`crate::acp::ProviderKey`] uses.
/// The operator enters this field name in the store's UI.
pub api_key: String,
}
/// The path holding the token of the classifier's forge account, as a
/// [`crate::forge::Credential`].
///
/// # Errors
/// Never in practice: the name segment is a constant. The `Result` is
/// [`principal_prefix`]'s.
pub fn forge_token_path() -> Result<String, Error> {
let prefix = principal_prefix(Kind::Service, SERVICE)?;
Ok(format!("{prefix}/forge/token"))
}
/// The path holding the queue client secret of the classifier's OIDC client
/// `client_id`, where `swarm-secret-publisher.nix` puts every swarm service's.
///
/// # Errors
/// [`Error::PathSegment`] when `client_id` is not a single path segment.
pub fn client_secret_path(client_id: &str) -> Result<String, Error> {
let prefix = principal_prefix(Kind::Service, client_id)?;
Ok(format!("{prefix}/oidc/client"))
}
/// What [`client_secret_path`] holds.
///
/// No `Debug`: `value` authenticates the classifier at the queue.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientSecret {
/// The secret, under the field name the publisher writes.
pub value: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_paths_are_the_ones_the_store_grants() {
// Spelled out: the operator types the first into the store's UI, and
// `swarm-bao.nix` grants each literal.
assert_eq!(
api_key_path().expect("a constant segment is legal"),
"swarm/services/review-classifier/anthropic-api-key"
);
assert_eq!(
forge_token_path().expect("a constant segment is legal"),
"swarm/services/review-classifier/forge/token"
);
assert_eq!(
client_secret_path("swarm-review-classifier").expect("a plain id is legal"),
"swarm/services/swarm-review-classifier/oidc/client"
);
}
#[test]
fn the_stored_key_field_is_api_key() {
let stored: ApiKey =
serde_json::from_str(r#"{"api_key":"k"}"#).expect("the documented shape decodes");
assert_eq!(stored.api_key, "k");
}
}