docs: extract best-effort oneshot service contract + matrix-avatar (#718 batch 6)

Two new sections housing rationale that was inline in
`harness-base.nix`:

- `docs/conventions.md::Best-effort oneshot services` — shape
  contract shared by `tea-login`, `forge-avatar-sync`, and
  `matrix-avatar-sync`: always exit 0, no `set -e`, skip silently
  on missing prerequisites, wired to multi-user.target,
  re-runnable. Plus the artefact-under-agent-home + service-stays-
  root posture and the operator-visible-via-journalctl trade-off.

- `docs/persistence.md::matrix-avatar-sync` — the two-step `media
  upload` → `set avatar_url` matrix-spec dance + why
  `RemainAfterExit = false` (so the `.path` watcher's re-fire on
  token appearance actually re-executes the unit).

In-code comments trim to one-liner purpose + pointers; the script
bodies stay (they're the actual implementation) but their inline
`#` rationale comments collapse where the docs cover them.

`description = ''…''` blocks (operator-facing options docs)
preserved per iris #718.

`nix flake check` clean; `nix fmt` clean.
This commit is contained in:
atlas 2026-05-31 15:50:25 +02:00 committed by mara
commit a043c61828
3 changed files with 94 additions and 56 deletions

View file

@ -984,11 +984,9 @@ in
(pkgs.callPackage ../packages/hive-forge-tools.nix { })
];
# One-shot: write tea's config.yml from the seeded forge token so
# the agent can use `tea` without interactive prompts. Runs on
# every boot so a rotated token (hive-c0re remints on each agent
# rebuild) is always reflected. *Always* exits 0 — never fail a
# NixOS switch-to-configuration over a missing/temperamental forge.
# One-shot: tea config.yml from the seeded forge token. Shape
# contract (always exit 0, no set -e, skip-silently, re-runnable):
# docs/conventions.md::Best-effort oneshot services.
systemd.services.tea-login = {
description = "configure tea CLI from hive-forge token (best-effort)";
wantedBy = [ "multi-user.target" ];
@ -1005,12 +1003,9 @@ in
environment.HOME_DIR = homeDir;
environment.AGENT_USER = userName;
script = ''
# No `set -e`: any subshell failure must not propagate.
# A failed unit aborts `nixos-container update` which blocks rebuilds.
# No `set -e`: best-effort posture (see docs pointer above).
FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url}
# $HYPERHIVE_STATE_DIR is set system-wide by the meta flake
# (systemd.globalEnvironment, /agents/<name>/state per agent
# including manager post-#604).
# $HYPERHIVE_STATE_DIR is system-wide via the meta flake.
TOKEN_FILE="$HYPERHIVE_STATE_DIR/forge-token"
if [ ! -f "$TOKEN_FILE" ]; then
echo "tea-login: no forge-token at $TOKEN_FILE; skipping"
@ -1027,15 +1022,8 @@ in
echo "tea-login: could not resolve username from forge API; skipping"
exit 0
fi
# tea reads config from ~/.config/tea/config.yml. The
# agent user's home is $HOME_DIR (set by NixOS via
# hyperhive.user.name). Write the config under that
# home + chown to the agent user so tea reads it when
# invoked as that user. Still runs as root (this
# service stays root-owned to avoid bootstrap
# ordering issues — see comment near serviceConfig
# below), but the artefact it produces is for the
# agent user.
# Config under the agent user's home, chown'd to them;
# service stays root-owned (see docs pointer above).
CONFIG="$HOME_DIR/.config/tea/config.yml"
mkdir -p "$(dirname "$CONFIG")" || true
cat > "$CONFIG" << EOF
@ -1059,11 +1047,8 @@ in
'';
};
# One-shot: upload the agent's configured icon to its Forgejo user avatar
# so the icon shows up on commits / PRs / issue comments in the forge.
# Only runs when `/etc/hyperhive/icon.svg` is present (set via
# `hyperhive.icon`). No-op when the forge is unreachable or the icon
# is not set. *Always* exits 0.
# One-shot: hyperhive.icon → Forgejo profile avatar. Shape contract:
# docs/conventions.md::Best-effort oneshot services.
systemd.services.forge-avatar-sync = {
description = "sync agent icon to Forgejo user avatar (best-effort)";
wantedBy = [ "multi-user.target" ];
@ -1158,36 +1143,20 @@ in
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token";
};
# Path-trigger sibling so matrix-avatar-sync fires the moment
# `<state>/matrix-token` appears (#571 closes argus's first-boot
# ordering nag on #567). On a clean boot hive-c0re's matrix
# provisioning runs concurrently with the agent container coming
# up; without this the oneshot would skip silently because the
# token didn't exist yet, and avatar would only land on the next
# restart. With the path watcher the appearance of the token
# triggers a re-fire of the service so the avatar is set in the
# same boot cycle as provisioning completes. The glob matches
# every agent (manager sees its own state at `/agents/hm1nd/state/`
# via the `/agents` bind).
# Path-trigger sibling: re-fires matrix-avatar-sync the moment
# `<state>/matrix-token` appears. Same first-boot-ordering pattern
# as hive-matrix-daemon above.
systemd.paths.matrix-avatar-sync = {
description = "trigger matrix-avatar-sync when matrix-token appears";
wantedBy = [ "multi-user.target" ];
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token";
};
# One-shot: upload the agent's configured icon to its matrix profile
# avatar so the icon shows up next to messages in matrix rooms (#548
# phase 2.5). Mirrors the forge-avatar-sync flow above, only differs
# in protocol: matrix avatars are a two-step `media upload` → `set
# avatar_url` dance, both authenticated by the access_token written
# by hive-c0re's `matrix::ensure_user_for`. No-op when the icon
# isn't configured, the matrix token isn't present, or the
# homeserver isn't reachable. *Always* exits 0.
#
# Triggered by EITHER boot (`wantedBy = multi-user.target`) OR
# the path-watcher above (`matrix-avatar-sync.path`) firing on
# token appearance (#571). Both paths re-run the oneshot
# idempotently — running the avatar set twice is harmless.
# One-shot: hyperhive.icon → matrix profile avatar (two-step media
# upload + set avatar_url). Shape contract:
# docs/conventions.md::Best-effort oneshot services. Protocol +
# why RemainAfterExit = false:
# docs/persistence.md::matrix-avatar-sync.
systemd.services.matrix-avatar-sync = {
description = "sync agent icon to matrix profile avatar (best-effort)";
wantedBy = [ "multi-user.target" ];
@ -1195,11 +1164,8 @@ in
# equivalent prerequisite; we just need the homeserver up.
serviceConfig = {
Type = "oneshot";
# NB: NOT `RemainAfterExit = true` — we want re-runs from
# the path trigger to actually re-execute. With
# RemainAfterExit, systemd treats the service as "still
# running" after the first exit and the second trigger
# becomes a no-op.
# RemainAfterExit = false so the .path trigger can re-fire
# the unit (see docs/persistence.md::matrix-avatar-sync).
RemainAfterExit = false;
};
path = [
@ -1276,9 +1242,9 @@ in
'';
};
# Write declared dashboardLinks to the state dir so hive-c0re can read
# them without accessing the container's /etc/ from the host.
# Runs every boot; idempotent (overwrite). Always exits 0.
# Write declared dashboardLinks to the state dir so hive-c0re can
# read them without accessing the container's /etc/ from the host.
# Best-effort oneshot (always exit 0): docs/conventions.md.
systemd.services.hive-dashboard-links = lib.mkIf (config.hyperhive.dashboardLinks != [ ]) {
description = "write declarative dashboardLinks to agent state dir";
wantedBy = [ "multi-user.target" ];