Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-secret-client/src/review_classifier.rs
atlas 1b4e764e5c review classifier: run on ACP when the swarm settings choose it
The swarm settings gain `runtime` (`api-key`, the default, or `acp`) and
an `acp` provider record: an https base URL, the provider's model, and
its prices per million tokens. A PUT now changes only the fields its
body names, so the settings form that sends model, effort and limit
leaves the runtime alone; `null` clears `effort` or `acp`. The body stays
strict, and the stored document keeps #5192's lenient read.

The controller sends the runtime in each run request. On `acp` the
classifier runs one prompt on a new `opencode acp` session per
classification through hive-runtime's AcpRuntime, in a scratch
directory, with a config rendered from the settings. opencode runs
`read` and other tools without asking, so OPENCODE_PERMISSION
{"*":"deny"} takes every tool away from the model, and a deny-all policy
refuses any request on top. The title call, which resends the prompt,
is turned off. The deadline cancels the turn, and a turn that does not
end within 15s of that is dropped, which also covers an agent stuck
before its prompt. The answer is the final assistant text, as ACP has
no result event.

The provider key is read per run from
swarm/services/review-classifier/acp-provider, which the bao UI's
admins login may create and update, and reaches opencode through
AcpRuntime::with_provider_key and the trampoline's allowlist.

Each ACP classification's cost is computed from the configured prices
and the reported tokens, and added to
hyperhive.review_classifier.cost.usage labelled repo, pr, model and
currency.

Part of #5096
2026-10-14 10:14:36 +02:00

113 lines
3.9 KiB
Rust

//! The review classifier agreement: where its inference keys, its queue
//! client secret and its forge token live in the store, and what each object
//! holds.
//!
//! The operator writes the keys by hand in the store's UI; the classifier
//! reads them and the client secret under its own certificate, a key once per
//! run. Its read stanza in `swarm-bao.nix` names exactly these three 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"))
}
/// The path holding the provider key the classifier runs an ACP agent with,
/// as a [`crate::acp::ProviderKey`].
///
/// # Errors
/// Never in practice: the name segment is a constant. The `Result` is
/// [`principal_prefix`]'s.
pub fn acp_provider_key_path() -> Result<String, Error> {
let prefix = principal_prefix(Kind::Service, SERVICE)?;
Ok(format!("{prefix}/acp-provider"))
}
/// 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!(
acp_provider_key_path().expect("a constant segment is legal"),
"swarm/services/review-classifier/acp-provider"
);
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");
}
}