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
This commit is contained in:
parent
38ee68c31d
commit
1b4e764e5c
24 changed files with 1622 additions and 262 deletions
|
|
@ -604,14 +604,19 @@ let
|
|||
# to fetch config repos.
|
||||
builderForgeTokenLeaf = "swarm/services/builder/forge/token";
|
||||
|
||||
# The review classifier's ACP provider key, the nix half of
|
||||
# `swarm_secret_client::review_classifier::acp_provider_key_path`. Written
|
||||
# and read like the API key above.
|
||||
classifierAcpKeyLeaf = "swarm/services/review-classifier/acp-provider";
|
||||
|
||||
# The UI's OIDC client secret. The publisher writes it; the granter reads it
|
||||
# into the `oidc` auth method's config, which is the only copy bao uses.
|
||||
baoUiClientLeaf = "swarm/services/${cfg.ui.oidc.clientId}/oidc/client";
|
||||
|
||||
# What an OIDC login through the UI gets: the key tree and each key's KV
|
||||
# metadata, and a write on the one leaf the operator fills by hand. Never a
|
||||
# value: KV v2 serves values under `data/`, and the one `data/` stanza has
|
||||
# no `read`. The UI needs no `sys/` grant on top:
|
||||
# metadata, and a write on the leaves the operator fills by hand. Never a
|
||||
# value: KV v2 serves values under `data/`, and no `data/` stanza has a
|
||||
# `read`. The UI needs no `sys/` grant on top:
|
||||
# `sys/internal/ui/mounts` lists any mount the token holds a capability
|
||||
# under, and the rest it calls is in the `default` policy.
|
||||
operatorViewerPolicyName = "swarm-operator-viewer";
|
||||
|
|
@ -622,6 +627,9 @@ let
|
|||
path "${credentialMountPath}/data/${classifierApiKeyLeaf}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "${credentialMountPath}/data/${classifierAcpKeyLeaf}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
'';
|
||||
|
||||
# authelia's operator group (`operatorGroup` in ./swarm-authelia.nix, the
|
||||
|
|
@ -1001,15 +1009,17 @@ let
|
|||
}
|
||||
];
|
||||
|
||||
# The review classifier: the API key the operator writes, read once per run,
|
||||
# and its own queue client secret, read at start. Both by the daemon inside
|
||||
# its container.
|
||||
# The review classifier: the API key and the ACP provider key the operator
|
||||
# writes, the one its run's runtime needs read once per run, and its own
|
||||
# queue client secret, read at start. All by the daemon inside its
|
||||
# container.
|
||||
reviewClassifierReaders = [
|
||||
{
|
||||
name = "swarm-review-classifier";
|
||||
cn = baoDeploy.reviewClassifierCommonName;
|
||||
policyText =
|
||||
readStanza "${credentialMountPath}/data/${classifierApiKeyLeaf}"
|
||||
+ readStanza "${credentialMountPath}/data/${classifierAcpKeyLeaf}"
|
||||
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.review-classifier.clientId}/oidc/client";
|
||||
}
|
||||
];
|
||||
|
|
@ -1868,9 +1878,10 @@ in
|
|||
description = ''
|
||||
Subject the store's `swarm-review-classifier` cert-auth role accepts:
|
||||
the identity the review classifier presents from inside its
|
||||
container. Its grant is two paths, read only:
|
||||
`swarm/services/review-classifier/anthropic-api-key` and its own
|
||||
queue client secret.
|
||||
container. Its grant is three paths, read only:
|
||||
`swarm/services/review-classifier/anthropic-api-key`,
|
||||
`swarm/services/review-classifier/acp-provider` and its own queue
|
||||
client secret.
|
||||
|
||||
⚠️ Reserved as a hive name by ./swarm.nix, like its siblings.
|
||||
'';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
# that sets `deploy.review-classifier.enable`, which `deploy.allSwarmServices`
|
||||
# defaults on where the host has the classifier's store identity. It answers the
|
||||
# swarm UI's review try-outs: swarm-controller sends a PR and the operator's classifications over
|
||||
# the queue, and the daemon runs `claude` once per classification and replies.
|
||||
# the queue, and the daemon runs `claude`, or `opencode` on the ACP runtime,
|
||||
# once per classification and replies.
|
||||
# docs/swarm/review-classifier.md has the design.
|
||||
#
|
||||
# ⚠️ Its own network namespace on the bridge, as ./swarm-ci-runner.nix has: a
|
||||
|
|
@ -11,8 +12,8 @@
|
|||
#
|
||||
# The container holds one credential, its own store leaf, which nspawn hands in
|
||||
# as a system credential with no copy on disk. The daemon reads everything else
|
||||
# from the store with it: its queue client secret at start and the API key on
|
||||
# every run.
|
||||
# from the store with it: its queue client secret at start and the run's
|
||||
# runtime's key on every run.
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
|
|
@ -62,9 +63,10 @@ let
|
|||
oneWord = path: builtins.match ".*[[:space:]].*" path == null;
|
||||
credential = id: "/run/credentials/${unit}.service/${id}";
|
||||
|
||||
# What `claude` exports its metrics with, where this host runs a hive-tier
|
||||
# collector on the bridge. The daemon adds the repo and the PR to the
|
||||
# resource attributes for each run.
|
||||
# What `claude` exports its metrics with, and the daemon its ACP runs' cost,
|
||||
# where this host runs a hive-tier collector on the bridge. The daemon adds
|
||||
# the repo and the PR to each `claude` run's resource attributes, and to
|
||||
# each cost record's attributes.
|
||||
otelEnv = lib.optionalAttrs otelCfg.enable {
|
||||
CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
||||
OTEL_METRICS_EXPORTER = "otlp";
|
||||
|
|
@ -109,7 +111,7 @@ in
|
|||
services.hyperhive.deploy.bao.reviewClassifierClientCertFile
|
||||
services.hyperhive.deploy.bao.reviewClassifierClientKeyFile
|
||||
|
||||
The classifier reads its queue client secret and the API key with it.
|
||||
The classifier reads its queue client secret and its runtime keys with it.
|
||||
|
||||
Both default to the pair glue-bao-tls.nix mints when
|
||||
services.hyperhive.deploy.bao.enable is true (the host that runs the
|
||||
|
|
@ -187,6 +189,7 @@ in
|
|||
SWARM_REVIEW_CLASSIFIER_OIDC_TOKEN_ENDPOINT = "${autheliaCfg.url}/api/oidc/token";
|
||||
SWARM_REVIEW_CLASSIFIER_OIDC_CLIENT_ID = clientId;
|
||||
SWARM_REVIEW_CLASSIFIER_CLAUDE = "${pkgs.claude-code}/bin/claude";
|
||||
SWARM_REVIEW_CLASSIFIER_OPENCODE = "${pkgs.opencode}/bin/opencode";
|
||||
}
|
||||
// lib.optionalAttrs (storeFiles ? review-classifier-bao-server-ca) {
|
||||
BAO_CACERT = credential "review-classifier-bao-server-ca";
|
||||
|
|
|
|||
Loading…
Reference in a new issue