A new swarm service, `swarm-claude-login`, in its own container on the host
that sets `deploy.claude-login.enable` (defaulted on by `allSwarmServices`
where the host has the minter's store leaf, like the review classifier).
- swarm-ui: an agent card showing "login needed" gets a "log in" button.
Its dialog starts the login, shows the URL claude printed, and takes the
pasted code.
- swarm-controller: `POST/DELETE /api/agents/{name}/claude-login` and
`POST /api/agents/{name}/claude-login/code`, behind the swarm UI's
authelia-gated `/api/`, relay the steps to the minter as requests on
`$SWARM.claude-login.request`.
- swarm-claude-login: runs `claude auth login` over piped stdio with HOME
and CLAUDE_CONFIG_DIR in a per-login scratch dir, scrapes the first
https URL, writes the code to stdin, and stores the `.credentials.json`
at `swarm/agents/<agent>/claude-credentials` with a KV v2 write, a new
version over any earlier one. The scratch dir goes on every exit path; a
login waits 15 minutes for its code.
- hive-agent: while parked in `wait_for_login`, reads that leaf every 30 s
under the agent's own cert and installs it only when its KV version is
newer than the one recorded in `<harness>/claude-credentials-version`.
An equal or older version is never put back, because claude rotates the
refresh token and the stored copy is stale after the first refresh.
- swarm-secret-client: `claude_login` paths and `SecretStore::read_versioned`.
- Credentials: a `claude-login` store leaf minted by glue-bao-tls, a
`swarm-claude-login` policy (create/update on
`swarm/agents/+/claude-credentials`, read on its own queue client
secret), an authelia machine client, and a `--claude-login-client`
grant in the queue's auth-callout responder. The unit runs as a static
system user.
The hive's own web-UI login is unchanged.
Refs #5268
137 lines
6.5 KiB
Nix
137 lines
6.5 KiB
Nix
# "The swarm-wide services run HERE."
|
|
#
|
|
# A swarm has one forge, one matrix, one SSO. This says this host is
|
|
# where they live, and asserts the per-service `enable`s that follow —
|
|
# the same mode-not-default shape as ./local-defaults.nix, one tier down.
|
|
#
|
|
# The forge derives from here like the rest: every swarm needs one, but
|
|
# only one host in it runs it.
|
|
{
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
deployCfg = config.services.hyperhive.deploy;
|
|
in
|
|
{
|
|
options.services.hyperhive.deploy.allSwarmServices = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = ''
|
|
Host the swarm's shared services on this hive. The services that
|
|
exist once per swarm rather than once per hive — the forge, the
|
|
matrix homeserver, the SSO provider, the queue, the metrics and log
|
|
stores — have their toggle asserted from this, so a swarm's service
|
|
host is declared in one place.
|
|
|
|
Every toggle it asserts is a {option}`services.hyperhive.deploy.*`
|
|
one, because "does THIS host run it" is a per-host decision — which
|
|
is the same reason this option is a `deploy.*` one itself. See
|
|
./deploy.nix.
|
|
|
|
`services.hyperhive.deploy.singleHostSwarm` turns this on as part
|
|
of the all-on-one-box mode. Set it directly to run the swarm's
|
|
services on a host that is not otherwise all-local — a dedicated
|
|
services box with hives elsewhere is exactly that shape.
|
|
|
|
With it off, this hive is a *client* of those services: it still
|
|
configures how to reach them, it just doesn't run them.
|
|
'';
|
|
};
|
|
|
|
# Same precedence reasoning as ./local-defaults.nix: fills in for an
|
|
# operator who hasn't spoken, yields to one who has.
|
|
#
|
|
# Everything derives under `deploy.*` now, because "does THIS host run
|
|
# it" is a per-host decision and `swarm.*` has to be identical on every
|
|
# host. Same switch, same rule, one attribute path.
|
|
config.services.hyperhive.deploy.matrix.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The collector that feeds the pair above (note: no `swarm.` prefix,
|
|
# this is ./otel.nix's existing per-hive option).
|
|
config.services.hyperhive.otel.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The rest of the shared services, from the same switch and for the same
|
|
# reason.
|
|
#
|
|
# authelia: a swarm has one SSO provider, and this says it lives here.
|
|
# With it off the hive is a *client* — `swarm.authelia.url` still points
|
|
# at whoever runs it.
|
|
config.services.hyperhive.deploy.authelia.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The queue. Same rule: once per swarm, optional.
|
|
config.services.hyperhive.deploy.nats.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The swarm collector that feeds the metrics pair, and the only tier
|
|
# holding the upstream credential. ⚠️ NOT the per-hive collector below,
|
|
# which every hive runs.
|
|
config.services.hyperhive.deploy.swarm-otel.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The metrics pair, deriving together on purpose: a store with no UI is
|
|
# unreadable and a UI with no store is empty, so there is no sensible
|
|
# deployment that takes one and not the other from this switch. An
|
|
# operator who wants exactly one still sets it directly, which
|
|
# `mkDefault` allows.
|
|
config.services.hyperhive.deploy.victoriametrics.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
config.services.hyperhive.deploy.grafana.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The log store, from the same switch for the same reason as the rest: a
|
|
# hive that is not the service host is a *client* of it, not a second one.
|
|
config.services.hyperhive.deploy.victorialogs.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The plumbing those services are reached over: every one of them is
|
|
# fronted by the gateway, resolved through the hive's dnsmasq, and runs
|
|
# in a container hanging off the bridge. Written as `mkIf … mkDefault`
|
|
# rather than `mkDefault allSwarmServices` because the modules that need
|
|
# these also assert them — a `false` from here would collide with their
|
|
# `true` instead of losing to it.
|
|
config.services.hyperhive.gateway.enable = lib.mkIf deployCfg.allSwarmServices (lib.mkDefault true);
|
|
config.services.hyperhive.gateway.dns.enable = lib.mkIf deployCfg.allSwarmServices (
|
|
lib.mkDefault true
|
|
);
|
|
config.services.hyperhive.network.enable = lib.mkIf deployCfg.allSwarmServices (lib.mkDefault true);
|
|
|
|
# The secret store. Once per swarm and optional, so it belongs to the
|
|
# same switch: a hive that does not run it is a *client*, reading its
|
|
# own secrets from whoever does. `mkDefault` is what keeps the store
|
|
# placeable on a host of its own — it can be set directly here and
|
|
# turned off wherever this switch happens to be on.
|
|
config.services.hyperhive.deploy.bao.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The forge. Every swarm needs it, but one host runs it: a hive that does
|
|
# not is a *client*, reaching it at `swarm.forge.domain`. Without this a
|
|
# `singleHostSwarm` box, which gets here through `allSwarmServices`, would
|
|
# have no forge at all.
|
|
config.services.hyperhive.deploy.forgejo.enable = lib.mkDefault deployCfg.allSwarmServices;
|
|
|
|
# The review classifier. Its module asserts a store identity, which
|
|
# glue-bao-tls.nix mints only on the host that runs the store; with the store
|
|
# placed elsewhere both files stay null and an unconditional `true` here would
|
|
# fail that host's eval. So the default follows the identity, and a host
|
|
# holding a copy of the pair (`deploy.bao.reviewClassifierClient{Cert,Key}File`)
|
|
# gets the classifier too.
|
|
config.services.hyperhive.deploy.review-classifier.enable = lib.mkDefault (
|
|
deployCfg.allSwarmServices
|
|
&& deployCfg.bao.reviewClassifierClientCertFile != null
|
|
&& deployCfg.bao.reviewClassifierClientKeyFile != null
|
|
);
|
|
|
|
# The builder, by the same rule and for the same reason: its module asserts
|
|
# the builder's store identity. It evaluates unreviewed config PRs, which is
|
|
# why ./swarm-builder.nix gives it a container with its own netns.
|
|
config.services.hyperhive.deploy.builder.enable = lib.mkDefault (
|
|
deployCfg.allSwarmServices
|
|
&& deployCfg.bao.builderClientCertFile != null
|
|
&& deployCfg.bao.builderClientKeyFile != null
|
|
);
|
|
|
|
# The claude login minter, defaulted on the same terms as the review
|
|
# classifier.
|
|
config.services.hyperhive.deploy.claude-login.enable = lib.mkDefault (
|
|
deployCfg.allSwarmServices
|
|
&& deployCfg.bao.claudeLoginClientCertFile != null
|
|
&& deployCfg.bao.claudeLoginClientKeyFile != null
|
|
);
|
|
}
|