Watch
0
0
Fork
You've already forked hyperhive
0

swarm containers: run the claude-code the hive pins for agents

This commit is contained in:
damocles 2026-10-16 01:20:38 +02:00
commit a169947f15
6 changed files with 65 additions and 2 deletions

View file

@ -7,6 +7,9 @@ agent's harness installs them from there.
Where it runs and how it's enabled: [`services.md`](services.md#claude-login-minter).
The service runs the `claude-code` that `services.hyperhive.c0re.claudeCodePackage`
pins for agents, and its container's own nixpkgs' where the hive pins none.
## What happens on a login
1. The agent publishes `needs_login` in its turn-state header, and its card

View file

@ -135,6 +135,9 @@ config) and an empty directory of its own. The prompt reaches it on stdin as
one user message in `--input-format stream-json`, so that it can carry content
blocks (see [Shared context](#shared-context)).
The `claude` is the one `services.hyperhive.c0re.claudeCodePackage` pins for
agents, or its container's own nixpkgs' where the hive pins none.
On the ACP runtime, each classification is one prompt on a new `opencode acp`
session. Its config and opencode's state live in a directory of its own,
removed after it. Its working directory is one empty directory that every

View file

@ -158,6 +158,12 @@
`nix-collect-garbage` cannot reclaim an old `claude-code` until
every agent has been rebuilt past it and the old generations
are gone.
The swarm's `swarm-claude-login` and `swarm-review-classifier`
containers, where this host runs them, follow the same pin and
keep their own nixpkgs' `claude-code` when it is `null`. They are
not agents, so pinning an older build for an agent-side
regression pins them too.
'';
};
dashboardPort = lib.mkOption {

View file

@ -21,6 +21,16 @@
}:
let
hyperhiveCfg = config.services.hyperhive;
# The `claude-code` the agents run, so this container does not trail them on
# the CLI; `fallback`, the container's own nixpkgs', only where the hive pins
# none.
claudeCode =
fallback:
if hyperhiveCfg.c0re.claudeCodePackage != null then
hyperhiveCfg.c0re.claudeCodePackage
else
fallback;
cfg = hyperhiveCfg.deploy.claude-login;
baoCfg = hyperhiveCfg.swarm.bao;
baoDeploy = hyperhiveCfg.deploy.bao;
@ -175,7 +185,7 @@ in
SWARM_CLAUDE_LOGIN_NATS_URL = "tls://${natsCfg.domain}:${toString natsCfg.port}";
SWARM_CLAUDE_LOGIN_OIDC_TOKEN_ENDPOINT = "${autheliaCfg.url}/api/oidc/token";
SWARM_CLAUDE_LOGIN_OIDC_CLIENT_ID = clientId;
SWARM_CLAUDE_LOGIN_CLAUDE = "${pkgs.claude-code}/bin/claude";
SWARM_CLAUDE_LOGIN_CLAUDE = "${claudeCode pkgs.claude-code}/bin/claude";
}
// lib.optionalAttrs (storeFiles ? claude-login-bao-server-ca) {
BAO_CACERT = credential "claude-login-bao-server-ca";

View file

@ -22,6 +22,16 @@
}:
let
hyperhiveCfg = config.services.hyperhive;
# The `claude-code` the agents run, so this container does not trail them on
# the CLI; `fallback`, the container's own nixpkgs', only where the hive pins
# none.
claudeCode =
fallback:
if hyperhiveCfg.c0re.claudeCodePackage != null then
hyperhiveCfg.c0re.claudeCodePackage
else
fallback;
cfg = hyperhiveCfg.deploy.review-classifier;
baoCfg = hyperhiveCfg.swarm.bao;
baoDeploy = hyperhiveCfg.deploy.bao;
@ -195,7 +205,7 @@ in
SWARM_REVIEW_CLASSIFIER_NATS_URL = "tls://${natsCfg.domain}:${toString natsCfg.port}";
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_CLAUDE = "${claudeCode pkgs.claude-code}/bin/claude";
SWARM_REVIEW_CLASSIFIER_OPENCODE = "${pkgs.opencode}/bin/opencode";
}
// lib.optionalAttrs (storeFiles ? review-classifier-bao-server-ca) {

View file

@ -99,6 +99,13 @@ let
deploy.builder.enable = false;
};
# Same, with the claude-code build the agents run pinned on the hive. Any
# package serves: only the path the containers are pointed at is read.
swarmServicesPinnedClaude = hiveWithController {
deploy.allSwarmServices = true;
c0re.claudeCodePackage = pkgs.hello;
};
# Same, with one of those services placed on another host. Every derivation
# in that module is `mkDefault` so this stays expressible.
swarmServicesBaoElsewhere = hive {
@ -273,6 +280,30 @@ let
&& !(swarmServicesBaoElsewhere.containers ? swarm-claude-login)
&& swarmServicesBaoElsewhere.services.hyperhive.deploy.nats.enable;
}
{
# The CLI is the one the agents run, not the container's own nixpkgs'.
# Unpinned is the control: it falls back to the container's own `claude-code`.
name = "the claude login minter and the review classifier run the claude-code the hive pins";
ok =
let
claudeOf =
host: container: unit: var:
host.containers.${container}.config.systemd.services.${unit}.environment.${var};
pinned = "${pkgs.hello}/bin/claude";
in
claudeOf swarmServicesPinnedClaude "swarm-claude-login" "swarm-claude-login"
"SWARM_CLAUDE_LOGIN_CLAUDE" == pinned
&&
claudeOf swarmServicesPinnedClaude "swarm-review-classifier" "swarm-review-classifier"
"SWARM_REVIEW_CLASSIFIER_CLAUDE" == pinned
&& lib.hasInfix "claude-code" (
claudeOf swarmServicesHere "swarm-claude-login" "swarm-claude-login" "SWARM_CLAUDE_LOGIN_CLAUDE"
)
&& lib.hasInfix "claude-code" (
claudeOf swarmServicesHere "swarm-review-classifier" "swarm-review-classifier"
"SWARM_REVIEW_CLASSIFIER_CLAUDE"
);
}
{
# The switch fills in for an operator who has not spoken and yields to
# one who has — that is what keeps a shared service placeable on a host