Watch
0
0
Fork
You've already forked hyperhive
0

add swarm-metrics, an agent CLI for the metrics store

swarm-metrics reads the swarm's VictoriaMetrics from inside an agent
container: query, query-range, series, labels and label-values, each one
GET against the store's Prometheus read API through the gateway, printing
the store's JSON. It authenticates the way swarm-logs does, with the
agent's own queue secret read from bao under the agent's leaf, sent as
a swarm-agent bearer.

On the metrics store's host the read API's auth map gains an agent arm
ahead of the Grafana one. It routes to swarm-metrics-agent-auth, a
swarm-logs-auth instance logged in under the existing swarm-metrics-auth
leaf, whose bao grant now includes swarm/agents/+/queue read. The check is
rate-limited per address like the log store's
(deploy.victoriametrics.agentAuth{Rate,Burst,VerdictTtl}).

nix/agent-modules/metrics.nix installs the wrapped binary when the hive
forwards HIVE_AGENT_METRICS_URL (from swarm.victoriametrics.domain) and a
store address, the same gate as swarm-logs. Adds the base:swarm-metrics
skill and the CLI reference with its freshness check.

Fixes #5395
This commit is contained in:
atlas 2026-10-17 00:55:46 +02:00 • committed by mara
commit e92f118bda
30 changed files with 1417 additions and 88 deletions

View file

@ -48,6 +48,7 @@ in
./matrix.nix
./mcp.nix
./memory-cap.nix
./metrics.nix
./network.nix
./otel.nix
./otel-removed.nix

View file

@ -0,0 +1,66 @@
# `swarm-metrics` on this agent's PATH, configured for this swarm's metrics
# store. The sibling of ./logs.nix, in the same shape for the same reasons:
# the binary defaults nothing (see `swarm-metrics/README.md`), so it is
# installed wrapped, and it is gated on the secret store's address as well as
# on the store URL, because the identity it presents is the agent's own queue
# secret read from that store.
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.hyperhive.agent.metrics;
baoCfg = config.services.hyperhive.agent.bao;
configured = cfg.url != null && baoCfg.addr != null;
# The coordinates ./logs.nix gives `swarm-logs`. Paths and a name only: a
# value in `--set` lands in the world-readable nix store.
wrapperEnv = {
HIVE_AGENT_METRICS_URL = cfg.url;
HIVE_AGENT_NAME = config.services.hyperhive.agent.user.name;
BAO_ADDR = baoCfg.addr;
BAO_CLIENT_CERT = baoCfg.clientCertFile;
BAO_CLIENT_KEY = baoCfg.clientKeyFile;
BAO_CACERT = "/run/credentials/hive-agent.service/hive-agent-bao-server-ca";
};
swarmMetricsConfigured = pkgs.symlinkJoin {
name = "swarm-metrics-configured";
paths = [ config.services.hyperhive.agent.packages.swarm-metrics ];
nativeBuildInputs = [ pkgs.makeWrapper ];
postBuild = ''
wrapProgram $out/bin/swarm-metrics ${
lib.concatStringsSep " " (
lib.mapAttrsToList (name: value: "--set ${name} ${lib.escapeShellArg value}") wrapperEnv
)
}
'';
};
in
{
options.services.hyperhive.agent.metrics = {
url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "https://metrics.example.com";
description = ''
The swarm metrics store's gateway origin, as this container reaches
it. `swarm-metrics` appends the read API's paths (`/api/v1/query`,
...) to it.
Set by the generated meta flake from the host's own
{option}`services.hyperhive.swarm.victoriametrics.domain`.
`null` means this hive has not been given the metrics store's address
for its agents, and `swarm-metrics` is then not installed at all.
Not defaulted: a guessed address resolves cleanly to the wrong place.
'';
};
};
config = lib.mkIf configured {
environment.systemPackages = [ swarmMetricsConfigured ];
};
}

View file

@ -478,6 +478,21 @@ in
touch "$out"
'';
# `swarm-metrics` CLI reference freshness check, the same shape as
# `swarm-logs-docs` above.
swarm-metrics-docs =
pkgs.runCommand "swarm-metrics-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; }
''
${self.packages.${system}.swarm-metrics}/bin/swarm-metrics markdown-docs > generated.md
if ! diff -u ${../docs/tools/swarm-metrics-cli.md} generated.md; then
echo "" >&2
echo "ERROR: docs/tools/swarm-metrics-cli.md is out of date — regenerate it:" >&2
echo " nix build .#swarm-metrics && ./result/bin/swarm-metrics markdown-docs > docs/tools/swarm-metrics-cli.md" >&2
exit 1
fi
touch "$out"
'';
# `hive-forge` CLI reference freshness check — same shape as
# `hivectl-docs` above, `hive-forge markdown-docs` (clap-markdown over
# its own command tree) instead. Reuses `packages.<system>.hive-forge`

View file

@ -285,8 +285,8 @@ in
[ -s ${pkiDir}/logs-auth.pem ] || ${signLeaf} ${pkiDir} logs-auth \
${lib.escapeShellArg deployCfg.bao.logsAuthCommonName} "" clientAuth
# The metrics store's Grafana auth responder, which reads Grafana's
# metrics bearer. Minted here for the matrix-ctl leaf's reason: the
# The metrics store's auth responders, which read Grafana's metrics
# bearer and agents' queue secrets. Minted here for the matrix-ctl leaf's reason: the
# metrics store is a swarm singleton, so elsewhere this is the file an
# operator copies.
[ -s ${pkiDir}/metrics-auth.pem ] || ${signLeaf} ${pkiDir} metrics-auth \

View file

@ -1,5 +1,5 @@
# Glue: point the metrics store's Grafana auth responder at the bao leaf minted
# for it.
# Glue: point the metrics store's auth responders (Grafana's and the agents')
# at the bao leaf minted for them.
#
# ONE PAIRING PER FILE — the swarm-metrics-auth principal ← bao, and nothing
# else. Deleting this leaves a metrics store with no responder identity unless

View file

@ -287,6 +287,9 @@ in
# `swarm-victorialogs.nix`'s location for the whole reasoning.
lib.optionalAttrs haveBaoClientIdentity {
HIVE_AGENT_LOGS_QUERY_URL = "https://${config.services.hyperhive.swarm.victorialogs.domain}/select/logsql/query";
# The metrics store's origin, for `swarm-metrics`; same credential and
# gate. Its read locations answer a bare 401, never a login redirect.
HIVE_AGENT_METRICS_URL = "https://${config.services.hyperhive.swarm.victoriametrics.domain}";
}
//
# Where the swarm's secret store is, and the identity this hive presents to

View file

@ -1060,15 +1060,18 @@ let
}
];
# The metrics store's responder, which checks the bearer Grafana presents
# against the one stored for it; and the host unit that fetches the store
# container's journal forwarder secret under the same leaf. Nothing else.
# The metrics store's responders under one leaf: the agent one checks the
# secret an agent presents against the one stored for it (the same `+` leaf
# as the log store's), the other checks the bearer Grafana presents; and the
# host unit that fetches the store container's journal forwarder secret.
# Nothing else.
metricsAuthReaders = [
{
name = "swarm-metrics-auth";
cn = baoDeploy.metricsAuthCommonName;
policyText =
readStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}"
readStanza "${credentialMountPath}/data/swarm/agents/+/queue"
+ readStanza "${credentialMountPath}/data/${grafanaMetricsBearerLeaf}"
# The metrics store container's journal forwarder, fetched on its host.
+ readStanza "${credentialMountPath}/data/swarm/services/${hyperhiveCfg.swarm.victoriametrics.otel.clientId}/oidc/client";
}
@ -1977,8 +1980,9 @@ in
default = "swarm-metrics-auth";
description = ''
Subject the store's `swarm-metrics-auth` cert-auth role accepts: the
identity the metrics store's Grafana auth responder presents to read
the bearer Grafana's datasource sends,
identity the metrics store's auth responders present to read agent
queue credentials, `swarm/agents/<agent>/queue`, and the bearer
Grafana's datasource sends,
`swarm/services/<grafana client id>/metrics/bearer`, and the one
`swarm-victoriametrics-forwarder-oidc` presents to read
`swarm/services/<metrics store forwarder client id>/oidc/client`.

View file

@ -38,9 +38,9 @@ let
nginxGid = config.ids.gids.nginx;
# The store's read API, which the vhost forwards to a caller holding
# Grafana's bearer or an operator's session. Each location's `proxy_pass`
# carries the path, so the store receives the path nginx matched, not the
# caller's unnormalised spelling.
# Grafana's bearer, an agent's own bearer, or an operator's session. Each
# location's `proxy_pass` carries the path, so the store receives the path
# nginx matched, not the caller's unnormalised spelling.
#
# Grafana's Prometheus datasource reads the query, series, label, metadata,
# buildinfo, exemplar, rules and alerts routes; the store answers the last
@ -128,6 +128,14 @@ let
# same leaf as `grafanaMetricsBearerLeaf` in ./swarm-bao.nix.
grafanaBearerStorePath = "swarm/services/${hyperhiveCfg.swarm.grafana.oidc.clientId}/metrics/bearer";
# The agents' check on the read API: a `swarm-logs-auth` binary, logging in
# under this host's `swarm-metrics-auth` leaf, whose grant covers every
# agent's queue secret. The log store's host runs `swarm-metrics-auth` under
# its `swarm-logs-auth` leaf the same way.
agentAuthUnit = "swarm-metrics-agent-auth";
agentAuthSocket = "/run/${agentAuthUnit}/auth.sock";
agentAuthCredential = id: "/run/credentials/${agentAuthUnit}.service/${id}";
# ── the container's journal forwarder ───────────────────────────────
#
# With no interface but loopback, the forwarder reaches nothing by name. A
@ -242,7 +250,8 @@ in
example = "/var/lib/swarm-bao-pki/metrics-auth.pem";
description = ''
Client certificate `swarm-metrics-auth` presents to the swarm's secret
store to read Grafana's metrics bearer. Its subject must be
store to read Grafana's metrics bearer, and `swarm-metrics-agent-auth`
to read agents' queue secrets. Its subject must be
{option}`services.hyperhive.deploy.bao.metricsAuthCommonName`.
Required when `deploy.victoriametrics.enable` is set; unset fails at
@ -264,6 +273,47 @@ in
'';
};
options.services.hyperhive.deploy.victoriametrics.agentAuthVerdictTtl = lib.mkOption {
type = lib.types.ints.unsigned;
default = 60;
description = ''
Seconds `swarm-metrics-agent-auth` grants an agent bearer it has
verified again without reading the secret store. A denial is never
remembered.
This is also how long a re-minted or deleted queue secret keeps working
at the metrics store's read API after its last successful check. `0`
checks the store on every query.
'';
};
options.services.hyperhive.deploy.victoriametrics.agentAuthRate = lib.mkOption {
type = lib.types.str;
default = "50r/s";
example = "30r/m";
description = ''
nginx `limit_req` rate, per client address, of read API requests
carrying a `swarm-agent.` bearer, which nginx checks with
`swarm-metrics-agent-auth`. Each such request that the responder has
not granted within
{option}`services.hyperhive.deploy.victoriametrics.agentAuthVerdictTtl`
costs a secret store login and read, whether the bearer is right or not.
Keyed by client address, so every agent on one host shares one budget.
Requests over the rate and its burst are answered 403.
'';
};
options.services.hyperhive.deploy.victoriametrics.agentAuthBurst = lib.mkOption {
type = lib.types.ints.positive;
default = 500;
description = ''
Requests a client address may make above
{option}`services.hyperhive.deploy.victoriametrics.agentAuthRate` before
nginx answers 403, served without delay.
'';
};
config = lib.mkIf deployCfg.victoriametrics.enable {
assertions = [
{
@ -287,7 +337,8 @@ in
services.hyperhive.deploy.victoriametrics.authBaoClientCertFile and
authBaoClientKeyFile must both be set.
swarm-metrics-auth reads Grafana's metrics bearer from the swarm's
swarm-metrics-auth reads Grafana's metrics bearer, and
swarm-metrics-agent-auth agents' queue secrets, from the swarm's
secret store with this certificate. They default to the leaf the
store mints where services.hyperhive.deploy.bao.enable is set on
this host; elsewhere, issue a leaf whose CN is
@ -320,14 +371,18 @@ in
# Which check a read location's `auth_request` gets. `auth_request` takes a
# fixed URI, so it names `/__metrics_read_auth`, which rewrites to the
# target this map picks: a bearer goes to Grafana's check, anything else
# (an operator's session cookie) to authelia. After the forwarder's egress
# target this map picks: an agent's `swarm-agent.` bearer goes to the
# agents' check, any other bearer to Grafana's, anything else (an
# operator's session cookie) to authelia. A map tries its regexes in
# order, so the agent arm comes first. After the forwarder's egress
# listener, `egressServer`.
services.nginx.appendHttpConfig = egressServer + ''
map $http_authorization $swarm_metrics_read_auth_target {
"~^Bearer swarm-agent\." /__metrics_agent_auth;
"~^Bearer " /__metrics_grafana_auth;
default /__metrics_authelia;
}
limit_req_zone $binary_remote_addr zone=swarm_metrics_agent_auth:1m rate=${deployCfg.victoriametrics.agentAuthRate};
'';
# `forceSSL`: authelia answers a plain-http auth subrequest
@ -400,6 +455,22 @@ in
'';
};
# An agent's bearer, checked by `swarm-metrics-agent-auth`. Any
# well-formed agent bearer the responder has not recently granted
# costs a store login and read, so the rate per client address is
# capped here. 403 when over: `auth_request` passes only 401 and 403
# through, and `swarm-metrics` retries a 401.
"= /__metrics_agent_auth" = {
proxyPass = "http://unix:${agentAuthSocket}:/";
extraConfig = ''
internal;
limit_req zone=swarm_metrics_agent_auth burst=${toString deployCfg.victoriametrics.agentAuthBurst} nodelay;
limit_req_status 403;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
'';
};
# The authelia subrequest. For the collector's two locations above it
# admits only the collector's client; for the read and vmui locations
# it admits an operator's session (`metricsStoreRules` in
@ -506,6 +577,56 @@ in
};
};
# The agents' check on the read API, see `agentAuthUnit`.
users.users.${agentAuthUnit} = {
isSystemUser = true;
group = agentAuthUnit;
description = "swarm metrics store auth_request responder for agent credentials";
};
users.groups.${agentAuthUnit} = { };
systemd.services.${agentAuthUnit} = {
description = "swarm metrics store auth_request responder for agent credentials";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"swarm-bao-pki.service"
];
serviceConfig = {
ExecStart = lib.concatStringsSep " " [
"${deployCfg.victorialogs.authPackage}/bin/swarm-logs-auth"
"--socket ${agentAuthSocket}"
"--store-cert-role swarm-metrics-auth"
"--verdict-ttl ${toString deployCfg.victoriametrics.agentAuthVerdictTtl}"
];
LoadCredential = lib.mapAttrsToList (id: path: "${id}:${path}") metricsAuthStoreFiles;
# ⚠️ A static user, never `DynamicUser`: see the `swarm-metrics-auth` unit above.
User = agentAuthUnit;
Group = agentAuthUnit;
RuntimeDirectory = agentAuthUnit;
RuntimeDirectoryMode = "0751";
RuntimeDirectoryPreserve = "yes";
Restart = "on-failure";
RestartSec = "5s";
SyslogIdentifier = agentAuthUnit;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = "disconnected";
PrivateDevices = true;
NoNewPrivileges = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
};
environment = {
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
BAO_CLIENT_CERT = agentAuthCredential "bao-client.pem";
BAO_CLIENT_KEY = agentAuthCredential "bao-client-key.pem";
}
// lib.optionalAttrs (metricsAuthStoreFiles ? "bao-ca.pem") {
BAO_CACERT = agentAuthCredential "bao-ca.pem";
};
};
# nginx runs as its own user under `ProtectSystem=strict`, so it can only
# create the socket where it may write. It removes the socket on a clean
# exit only; a stale one after a crash fails every later `bind`, and with

View file

@ -41,19 +41,23 @@ let
agentNoBao = shared.agentBare;
# `swarm-logs` authenticates with the agent's queue secret from the store, so
# the store address gates it beside the query URL.
# `swarm-logs` and `swarm-metrics` authenticate with the agent's queue secret
# from the store, so the store address gates each beside its store URL.
logsUrl = "https://logs.t.local/select/logsql/query";
metricsUrl = "https://metrics.t.local";
agentLogs = agentWith {
services.hyperhive.agent.bao.addr = "https://bao.t.local:8200";
services.hyperhive.agent.logs.queryUrl = logsUrl;
services.hyperhive.agent.metrics.url = metricsUrl;
};
agentLogsNoBao = agentWith { services.hyperhive.agent.logs.queryUrl = logsUrl; };
swarmLogsIn =
machine:
lib.findFirst (
p: (p.name or "") == "swarm-logs-configured"
) null machine.environment.systemPackages;
agentLogsNoBao = agentWith {
services.hyperhive.agent.logs.queryUrl = logsUrl;
services.hyperhive.agent.metrics.url = metricsUrl;
};
configuredIn =
name: machine: lib.findFirst (p: (p.name or "") == name) null machine.environment.systemPackages;
swarmLogsIn = configuredIn "swarm-logs-configured";
swarmMetricsIn = configuredIn "swarm-metrics-configured";
agentBaoIdentity = machine: machine.systemd.services.hive-agent-bao-identity;
cases = [
@ -80,6 +84,27 @@ let
name = "an agent with a log store but no secret store gets no swarm-logs";
ok = swarmLogsIn agentLogsNoBao == null && swarmLogsIn agentLogs != null;
}
{
# The same store identity as `swarm-logs`, and the store's origin.
name = "swarm-metrics is wrapped with the agent's own store identity";
ok =
let
w = (swarmMetricsIn agentLogs).buildCommand;
a = agentLogs.services.hyperhive.agent;
set = k: v: lib.hasInfix "--set ${k} ${lib.escapeShellArg v}" w;
in
set "HIVE_AGENT_METRICS_URL" metricsUrl
&& set "HIVE_AGENT_NAME" a.user.name
&& set "BAO_ADDR" "https://bao.t.local:8200"
&& set "BAO_CLIENT_CERT" a.bao.clientCertFile
&& set "BAO_CLIENT_KEY" a.bao.clientKeyFile
&& set "BAO_CACERT" "/run/credentials/hive-agent.service/hive-agent-bao-server-ca";
}
{
# A metrics URL with nothing to authenticate as installs nothing.
name = "an agent with a metrics store but no secret store gets no swarm-metrics";
ok = swarmMetricsIn agentLogsNoBao == null && swarmMetricsIn agentLogs != null;
}
{
# Both ids or neither: the secret authenticates nobody without the id it
# belongs to, and `swarm-logs` reads both.

View file

@ -788,16 +788,20 @@ let
lib.hasInfix "/logs-auth.pem ]" s && lib.hasInfix "swarm-logs-auth \"\" clientAuth" s;
}
{
# Read-only on the one leaf Grafana's host creates and on the store
# container forwarder's client secret, and nothing else: this principal
# checks a presented bearer, and its host fetches one secret for the
# container.
name = "the metrics store responder's grant is Grafana's metrics bearer and its container forwarder's client secret, read-only, and nothing else";
# Read-only on every agent's queue secret, the one leaf Grafana's host
# creates and the store container forwarder's client secret, and nothing
# else: this principal checks presented bearers, and its host fetches
# one secret for the container.
name = "the metrics store responder's grant is agents' queue secrets, Grafana's metrics bearer and its container forwarder's client secret, read-only, and nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-metrics-auth-policy.script;
in
grantsIn s == [
{
path = "secret/data/swarm/agents/+/queue";
caps = [ "read" ];
}
{
path = "secret/data/swarm/services/swarm-grafana/metrics/bearer";
caps = [ "read" ];

View file

@ -672,15 +672,21 @@ let
{
# The read API's check, link by link: every read location names the
# switch with no login redirect (a machine reader would get login HTML
# as a 200), the switch rewrites to the map's pick, a bearer reaches
# the responder's socket, which the unit binds, and anything else goes
# to authelia. vmui is a browser's, so it redirects to the login.
name = "the metrics read API sends a bearer to swarm-metrics-auth and a session to authelia";
# as a 200), the switch rewrites to the map's pick, an agent bearer
# reaches the agents' responder (listed before the catch-all bearer arm,
# since a map takes the first matching regex), any other bearer reaches
# Grafana's, each socket bound by its unit, and anything else goes to
# authelia. vmui is a browser's, so it redirects to the login.
name = "the metrics read API sends an agent bearer to swarm-metrics-agent-auth, another bearer to swarm-metrics-auth and a session to authelia";
ok =
let
l = allLocal.services.nginx.virtualHosts."metrics.t.local".locations;
http = allLocal.services.nginx.appendHttpConfig;
unit = allLocal.systemd.services.swarm-metrics-auth;
agentUnit = allLocal.systemd.services.swarm-metrics-agent-auth;
agentArm = "\"~^Bearer swarm-agent\\.\" /__metrics_agent_auth;";
bearerArm = "\"~^Bearer \" /__metrics_grafana_auth;";
v = allLocal.services.hyperhive.deploy.victoriametrics;
reads = lib.filterAttrs (n: _: lib.hasPrefix "= /api/v1/" n || lib.hasPrefix "^~ /api/v1/" n) l;
in
reads ? "= /api/v1/query_range"
@ -690,7 +696,9 @@ let
&& !(lib.hasInfix "error_page" r.extraConfig)
) (lib.attrValues reads)
&& lib.hasInfix "map \$http_authorization \$swarm_metrics_read_auth_target" http
&& lib.hasInfix "\"~^Bearer \" /__metrics_grafana_auth;" http
&& lib.hasInfix agentArm http
&& lib.hasInfix bearerArm http
&& !(lib.hasInfix bearerArm (lib.head (lib.splitString agentArm http)))
&& lib.hasInfix "default /__metrics_authelia;" http
&&
lib.hasInfix "rewrite ^ \$swarm_metrics_read_auth_target last;"
@ -698,6 +706,18 @@ let
&& lib.hasInfix "internal;" l."= /__metrics_grafana_auth".extraConfig
&& l."= /__metrics_grafana_auth".proxyPass == "http://unix:/run/swarm-metrics-auth/auth.sock:/"
&& lib.hasInfix "--socket /run/swarm-metrics-auth/auth.sock" unit.serviceConfig.ExecStart
&& lib.hasInfix "internal;" l."= /__metrics_agent_auth".extraConfig
&& l."= /__metrics_agent_auth".proxyPass == "http://unix:/run/swarm-metrics-agent-auth/auth.sock:/"
&& lib.hasInfix "--socket /run/swarm-metrics-agent-auth/auth.sock" agentUnit.serviceConfig.ExecStart
&& lib.hasInfix "/bin/swarm-logs-auth " agentUnit.serviceConfig.ExecStart
&& lib.hasInfix "--store-cert-role swarm-metrics-auth" agentUnit.serviceConfig.ExecStart
&& lib.hasInfix "--verdict-ttl ${toString v.agentAuthVerdictTtl}" agentUnit.serviceConfig.ExecStart
&& agentUnit.serviceConfig.LoadCredential == unit.serviceConfig.LoadCredential
&& lib.hasInfix "limit_req_zone $binary_remote_addr zone=swarm_metrics_agent_auth:1m rate=${v.agentAuthRate};" http
&&
lib.hasInfix "limit_req zone=swarm_metrics_agent_auth burst=${toString v.agentAuthBurst} nodelay;"
l."= /__metrics_agent_auth".extraConfig
&& !(lib.hasInfix "limit_req" l."= /__metrics_grafana_auth".extraConfig)
&& lib.hasInfix "auth_request /__metrics_authelia;" l."^~ /vmui/".extraConfig
&& lib.hasInfix "error_page 401 =302" l."^~ /vmui/".extraConfig
&& l."= /__metrics_authelia".proxyPass == "https://auth.t.local/api/authz/auth-request";
@ -884,6 +904,7 @@ let
owners ? swarm-logs-auth
&& owners ? swarm-logs-grafana-auth
&& owners ? swarm-metrics-auth
&& owners ? swarm-metrics-agent-auth
&& !(lib.any (u: hidden u.serviceConfig) (lib.attrValues owners));
}
{

View file

@ -43,6 +43,8 @@ let
# which is exactly what this list's per-bin packages are for — see
# `nix/agent-modules/logs.nix` for what puts it on PATH there.
swarm-logs = "hyperhive agent-facing swarm log store query CLI";
# The same for the metrics store; `nix/agent-modules/metrics.nix` puts it on PATH.
swarm-metrics = "hyperhive agent-facing swarm metrics store query CLI";
hive-screen-mcp = "hyperhive screen MCP bridge (screenshot + input for GUI agents)";
hive-forge = "hyperhive Forgejo CLI";
hive-forge-notify = "hyperhive per-agent Forgejo notification poller daemon";