swarm-review-classifier: answer a try-out with one claude run per classification
A new service crate. It reads its queue client secret from the store at start, takes try-outs on the review classifier subject in its queue group, and for each run reads the Anthropic API key from the store, builds the shared context (commits with full messages, diff with lockfiles collapsed), runs one claude --print per classification with no session, no tools, no MCP server and the key in the child's environment only, validates the JSON answer and aggregates the rows. A missing key or a run where no classification got an answer fails the run; one failed classification is an excluded row with its error. One journal line per run names the operator, repo, PR, head SHA, rows and overall verdict. swarm-secret-client gains the two paths the service reads.
This commit is contained in:
parent
98c873965b
commit
88a55f136e
11 changed files with 1235 additions and 1 deletions
|
|
@ -5,7 +5,8 @@
|
|||
//! the rules every path obeys ([`path`]), the translation from this
|
||||
//! deployment's environment into a logged-in client ([`client`]), and, per kind
|
||||
//! of secret, the path it lives at together with the fields it holds
|
||||
//! ([`matrix`], [`queue`], [`mtls`], [`forge`], [`github`], [`acp`], [`ci`]). Each of those is a thing the controller
|
||||
//! ([`matrix`], [`queue`], [`mtls`], [`forge`], [`github`], [`acp`], [`ci`],
|
||||
//! [`review_classifier`]). Each of those is a thing the controller
|
||||
//! and a hive must say identically, so it is said once here.
|
||||
//!
|
||||
//! [`policy`] is the same kind of agreement seen from the other side: which of
|
||||
|
|
@ -32,6 +33,7 @@ pub mod mtls;
|
|||
pub mod path;
|
||||
pub mod policy;
|
||||
pub mod queue;
|
||||
pub mod review_classifier;
|
||||
|
||||
pub use client::SecretStore;
|
||||
|
||||
|
|
|
|||
81
swarm-secret-client/src/review_classifier.rs
Normal file
81
swarm-secret-client/src/review_classifier.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//! The review classifier agreement: where its inference key and its queue
|
||||
//! client secret live in the store, and what each object holds.
|
||||
//!
|
||||
//! The operator writes the key by hand in the store's UI; the classifier
|
||||
//! reads both under its own certificate, the key once per run. Its read
|
||||
//! stanza in `swarm-bao.nix` names exactly these two paths.
|
||||
|
||||
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 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 both literals.
|
||||
assert_eq!(
|
||||
api_key_path().expect("a constant segment is legal"),
|
||||
"swarm/services/review-classifier/anthropic-api-key"
|
||||
);
|
||||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue