Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
8 changed files with 31 additions and 120 deletions

View file

@ -535,12 +535,9 @@ container lifetime:
2. **Migrate any leftover `/root/.claude` content into
`${homeDir}/.claude`** — legacy `claude` wrote to root's
empty home; the bind mount didn't exist yet. Marker
(`/var/lib/hive-agent-user-migrated`) guards single-shot; it's
only written once there's nothing left to migrate, so a `cp`
failure leaves it absent and the next boot retries.
(`/var/lib/hive-agent-user-migrated`) guards single-shot.
`cp -an` (no-clobber) so any pre-existing files at the new
location win — never blow over data already there, and so a
retry after a partial copy is as safe as the first attempt.
location win — never blow over data already there.
3. **Chown the bind-mounted state dir** (`/agents/*/state`)
recursively so the agent user can read/write it. Wildcard
matches the single agent that container sees; `-h` skips

View file

@ -172,29 +172,16 @@ in
userName=${lib.escapeShellArg userName}
mkdir -p "$homeDir"
chown "$userName:$userName" "$homeDir"
# The marker is only written once nothing is left to migrate — either
# there was nothing under /root/.claude, or `cp` copied it all. A
# failed `cp` (disk full, permission error) leaves the marker absent,
# so the next boot's activation retries; `-an` never clobbers a file
# this attempt already placed, so a retry after a partial copy is
# exactly as safe as the first attempt.
marker=/var/lib/hive-agent-user-migrated
if [ ! -e "$marker" ]; then
if [ -d /root/.claude ] && [ "$(ls -A /root/.claude 2>/dev/null)" ]; then
mkdir -p "$homeDir/.claude"
if cp -an /root/.claude/. "$homeDir/.claude/" 2>/dev/null; then
rm -rf /root/.claude
echo "hive-agent-user-migrate: moved /root/.claude → $homeDir/.claude"
mkdir -p "$(dirname "$marker")"
: > "$marker"
else
echo "hive-agent-user-migrate: copying /root/.claude to $homeDir/.claude failed; will retry next boot" >&2
fi
else
mkdir -p "$(dirname "$marker")"
: > "$marker"
if [ ! -e "$marker" ] && [ -d /root/.claude ] && [ "$(ls -A /root/.claude 2>/dev/null)" ]; then
mkdir -p "$homeDir/.claude"
if cp -an /root/.claude/. "$homeDir/.claude/" 2>/dev/null; then
rm -rf /root/.claude
echo "hive-agent-user-migrate: moved /root/.claude → $homeDir/.claude"
fi
fi
mkdir -p "$(dirname "$marker")"
: > "$marker"
# Scope state + harness chowns to THIS container's own dirs only.
# The glob `/agents/*/state` also matches other agents' state dirs
# bind-mounted into a `ManageRootAgent` holder's container, which

View file

@ -80,8 +80,6 @@ let
# resolution, so this is the kind of mistake only reading the target module
# catches.
matrixMachine = "hive-matrix";
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
in
{
config = lib.mkMerge [
@ -189,8 +187,6 @@ in
script = ''
set -euo pipefail
${atomicWriteSecret}
# A sealed or uninitialised store answers on the port and never
# answers the read, so "the store is up" is not the same as "the
# store can answer". `TimeoutStartSec` above is the bound; the
@ -241,7 +237,9 @@ in
exit 0
fi
atomic_write_secret 0600 "" ${lib.escapeShellArg (toString deployCfg.matrix.appserviceTokenFile)} "$token"
umask 077
printf '%s\n' "$token" > ${lib.escapeShellArg (toString deployCfg.matrix.appserviceTokenFile)}
chmod 0600 ${lib.escapeShellArg (toString deployCfg.matrix.appserviceTokenFile)}
# Re-stamp the registration file from the token just written. The
# token is half an agreement — the registration the homeserver loads

View file

@ -77,8 +77,6 @@ let
# its own use of it: it is asserted set wherever
# `deploy.hive-controller.enable` is.
credentialPath = "secret/swarm/hives/${hyperhiveCfg.hiveName}/queue/agent";
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
in
{
options.services.hyperhive.deploy.hive-controller.queue = {
@ -219,8 +217,6 @@ in
script = ''
set -euo pipefail
${atomicWriteSecret}
# `bao`'s own message is the only thing separating a missing value from
# a refused identity from an unreachable host. This unit's degraded
# mode is correct for all three, so it reports which one rather than
@ -283,11 +279,14 @@ in
install -d -m 0755 ${lib.escapeShellArg credentialDir}
atomic_write_secret 0600 "" ${lib.escapeShellArg secretFile} "$secret"
umask 077
printf '%s\n' "$secret" > ${lib.escapeShellArg secretFile}
chmod 0600 ${lib.escapeShellArg secretFile}
# `0644` on purpose: an OIDC client id is presented to the token
# endpoint on every connection and is public by construction.
atomic_write_secret 0644 "" ${lib.escapeShellArg clientIdFile} "$client_id"
printf '%s\n' "$client_id" > ${lib.escapeShellArg clientIdFile}
chmod 0644 ${lib.escapeShellArg clientIdFile}
'';
};
})

View file

@ -1,60 +0,0 @@
# Shared shell step for a systemd oneshot that lands a freshly-fetched
# secret on disk: never `> path` the live file directly, because a reader
# racing the write can open it between the truncate and the write, or
# between the write and a `chmod` that follows it, and see an empty file
# or one at the wrong mode. `mktemp` always creates its file at 0600
# regardless of umask, so the temp file is private for its whole life; only
# the `chmod`/`chown`/`mv -f` sequence below ever makes the target mode and
# owner visible, and only once the content is already final.
#
# The value is a function ARGUMENT, not piped in on stdin: a producer that
# exits non-zero partway through a pipe still leaves `cat` a clean EOF, so
# a truncated read is indistinguishable from an intentionally short one,
# and the partial content would still land at the live path via `mv`. A
# caller that computes the value with a command captures it into a
# variable first (`value=$(cmd)`, which fails under `set -e` before this
# function is ever called) and passes the finished value in — never handed
# to an external program, so it never appears in a process's own
# `/proc/<pid>/cmdline`, and written with `printf`, a shell builtin.
#
# Pure function — NOT a NixOS module. Call it from a module's `let`:
#
# atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
# script = ''
# ${atomicWriteSecret}
# atomic_write_secret 0600 "" "$path" "$secret"
# '';
#
# Args: mode, an owner for `chown` ("" to keep mktemp's root:root), target
# path, value — written with a trailing newline like every call site's
# prior `printf '%s\n'`. Requires `coreutils` on the caller's `path`.
{ }:
''
atomic_write_secret() {
local mode="$1" owner="$2" target="$3" value="$4" tmp
tmp="$(mktemp "$(dirname -- "$target")/.$(basename -- "$target").XXXXXX")"
# The write happens in a subshell so its own EXIT trap cleans up `$tmp`
# on failure (a `RETURN` trap does not fire when `set -e` aborts the
# function mid-body) without touching an EXIT trap the calling script's
# own `script` may already have — subshell traps are local to the
# subshell. A named handler, not an inline trap string, so `local rc=$?`
# is a normal function-local assignment shellcheck can see: it only
# removes `$tmp` when `$?` is nonzero — on the ordinary path the
# subshell also exits successfully, and `$tmp` has to survive that to
# reach the `mv` below.
(
_atomic_write_secret_cleanup() {
local rc=$?
[ "$rc" -eq 0 ] || rm -f "$tmp"
exit "$rc"
}
trap _atomic_write_secret_cleanup EXIT
printf '%s\n' "$value" > "$tmp"
chmod "$mode" "$tmp"
if [ -n "$owner" ]; then
chown "$owner" "$tmp"
fi
)
mv -f "$tmp" "$target"
}
''

View file

@ -764,8 +764,6 @@ let
forwarderSecretInContainer = "/var/lib/swarm-bao-otel-oidc/${cfg.otel.clientId}.secret";
forwarderHostSecretPath = "/var/lib/nixos-containers/${cfg.machine}${forwarderSecretInContainer}";
forwarderHostSecretDir = builtins.dirOf forwarderHostSecretPath;
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
# The collector runs under `DynamicUser` and opens `client_secret_file`
# itself, so there is no uid to hand a 0400 file to. `LoadCredential` reads
# it as root before the sandbox exists and re-exposes it under a path that
@ -1799,8 +1797,6 @@ in
script = ''
set -euo pipefail
${atomicWriteSecret}
# `bao`'s own message is the only thing separating a missing value
# from a refused identity from an unreachable store, and this unit
# retries on all three — so it reports which one rather than
@ -1841,7 +1837,10 @@ in
# an argument in /proc the way `install <<<"$secret"` or an `echo`
# from `path` would.
install -d -m 0755 ${lib.escapeShellArg forwarderHostSecretDir}
atomic_write_secret 0400 root:root ${lib.escapeShellArg forwarderHostSecretPath} "$secret"
umask 077
printf '%s\n' "$secret" > ${lib.escapeShellArg forwarderHostSecretPath}
chown root:root ${lib.escapeShellArg forwarderHostSecretPath}
chmod 0400 ${lib.escapeShellArg forwarderHostSecretPath}
'';
};
})
@ -2595,9 +2594,6 @@ in
};
script = ''
set -euo pipefail
${atomicWriteSecret}
install -d -m 0770 -g ${tokenGroup} ${tokenStoreDir}
# Required whenever the store is not at its default location, or the
# library cannot find the token the seal asks for.
@ -2647,12 +2643,8 @@ in
chgrp ${tokenGroup} ${tokenStoreDir}/tpm2_pkcs11.sqlite3
chmod 0660 ${tokenStoreDir}/tpm2_pkcs11.sqlite3
# Captured first, not piped: a `$(cmd)` that fails aborts here
# under `set -e`, before the helper — which cannot see a
# producer's exit code once its output is inside the pipe —
# is ever called.
pin_env_value="BAO_HSM_PIN=$(cat ${tokenStoreDir}/user-pin)"
atomic_write_secret 0400 "" ${pinEnvFile} "$pin_env_value"
( umask 077; printf 'BAO_HSM_PIN=%s\n' "$(cat ${tokenStoreDir}/user-pin)" > ${pinEnvFile} )
chmod 0400 ${pinEnvFile}
'';
};

View file

@ -168,8 +168,6 @@ let
nginxGid = config.ids.gids.nginx;
grafanaUid = config.ids.uids.grafana;
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
in
{
# `enable` moved to `services.hyperhive.deploy.grafana.enable` — see
@ -635,8 +633,6 @@ in
script = ''
set -euo pipefail
${atomicWriteSecret}
# `bao`'s own message is the only thing separating a missing value from
# a refused identity from an unreachable host. This unit's degraded
# mode is correct for all three, so it reports which one rather than
@ -693,7 +689,10 @@ in
# grants the group nothing; if this mode ever widens, the gid has to be
# discovered at runtime rather than assumed.
install -d -m 0755 ${lib.escapeShellArg hostSecretDir}
atomic_write_secret 0400 ${lib.escapeShellArg "${toString config.ids.uids.grafana}:0"} ${lib.escapeShellArg hostSecretPath} "$secret"
umask 077
printf '%s\n' "$secret" > ${lib.escapeShellArg hostSecretPath}
chown ${toString config.ids.uids.grafana}:0 ${lib.escapeShellArg hostSecretPath}
chmod 0400 ${lib.escapeShellArg hostSecretPath}
'';
};

View file

@ -166,8 +166,6 @@ let
# collector cannot open, discovered at runtime and nowhere else.
collectorSecretPath = "/run/credentials/opentelemetry-collector.service/${collectorCredentialId}";
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
# `attrNames` is sorted, so this is a function of the hive SET and not of
# the order anyone wrote it in.
#
@ -819,8 +817,6 @@ in
script = ''
set -euo pipefail
${atomicWriteSecret}
# `bao`'s own message is the only thing separating a missing value
# from a refused identity from an unreachable host. This unit's
# degraded mode is correct for all three, so it reports which one
@ -866,7 +862,10 @@ in
# no uid to give this to — `LoadCredential` reads it as root before
# the sandbox exists and re-exposes it to whichever uid the unit got.
install -d -m 0755 ${lib.escapeShellArg collectorHostSecretDir}
atomic_write_secret 0400 root:root ${lib.escapeShellArg collectorHostSecretPath} "$secret"
umask 077
printf '%s\n' "$secret" > ${lib.escapeShellArg collectorHostSecretPath}
chown root:root ${lib.escapeShellArg collectorHostSecretPath}
chmod 0400 ${lib.escapeShellArg collectorHostSecretPath}
'';
};