vps-health-api/agent/run-push.sh
le king fu e24f5b6f00 feat(agent): push workstation metrics to /hosts/<id> from cron
Zero-dependency local agent: collect one snapshot through the shared
metrics.js, POST it once to /hosts/<id>, exit. Cron runs it every five
minutes. Nothing is queued and nothing is replayed — a heartbeat from five
minutes ago describes a machine that no longer exists, so a failed push is
logged and dropped rather than spooled.

run-push.sh sources ~/.config/maximus-host-agent.env (mode 600), refuses to
start when HOSTS_API_URL, HOSTS_INGEST_TOKEN or HOST_ID is missing, and never
enables shell tracing: the script is meant to be piped into `logger`, where
`set -x` would echo the ingest token into /var/log/syslog and journald for
good. Same reason the failure path reports only err.code and the HTTP status —
never an error object, request options, headers, or a response body. The cost
is accepted: a 400 says the payload was rejected, not why.

50 tests, including two that spawn the real process against a failing server
and scan its actual stdout and stderr for any five-character fragment of the
token. The payload is pinned against the server's own sanitizeSnapshot(), and
one case pushes a real collectMetrics() snapshot through the real ingestion
handler, so a drift between agent and server turns a test red instead of
producing a 400 at 3 a.m. on the ThinkPad.

agent/ stays out of the Docker image: the COPY line is untouched, and a test
pins that it copies files one by one and never names the directory.

Installing on a workstation — frozen copy, env file, crontab line, dry run,
and why two machines must never share a HOST_ID — is documented in
agent/README.md. The install itself belongs to the commissioning issue.

Resolves #15
2026-08-16 12:21:59 -04:00

67 lines
2.2 KiB
Bash
Executable file

#!/bin/sh
#
# Cron wrapper for push-metrics.js: load the secrets, check the install, hand
# over to node. Every five minutes, forever, unattended.
#
# NEVER add `set -x`. This script is meant to be piped into `logger`, and the
# shell would echo the `. "$ENV_FILE"` expansion — the ingest token — straight
# into /var/log/syslog and journald, where it would stay. Same reason nothing
# below ever echoes a variable that holds a secret: the checks print the NAME of
# what is missing, never its value.
#
# Suggested crontab line (see README.md):
# */5 * * * * $HOME/.local/share/maximus-host-agent/agent/run-push.sh 2>&1 | logger -t host-agent
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
AGENT="$SCRIPT_DIR/push-metrics.js"
# Overridable for testing only; cron uses the default.
ENV_FILE="${HOST_AGENT_ENV_FILE:-$HOME/.config/maximus-host-agent.env}"
if [ ! -f "$ENV_FILE" ]; then
echo "host-agent: env file not found: $ENV_FILE" >&2
exit 1
fi
# A warning, not a refusal: a wrong mode bit is worth shouting about, but it is
# not worth silencing the heartbeat of the whole workstation over.
FILE_MODE=$(stat -c "%a" "$ENV_FILE" 2>/dev/null || echo "")
if [ -n "$FILE_MODE" ] && [ "$FILE_MODE" != "600" ]; then
echo "host-agent: warning: $ENV_FILE is mode $FILE_MODE, expected 600" >&2
fi
# shellcheck source=/dev/null
. "$ENV_FILE"
if [ -z "${HOSTS_API_URL:-}" ]; then
echo "host-agent: HOSTS_API_URL is not set in $ENV_FILE" >&2
exit 1
fi
if [ -z "${HOSTS_INGEST_TOKEN:-}" ]; then
echo "host-agent: HOSTS_INGEST_TOKEN is not set in $ENV_FILE" >&2
exit 1
fi
if [ -z "${HOST_ID:-}" ]; then
echo "host-agent: HOST_ID is not set in $ENV_FILE" >&2
exit 1
fi
if [ ! -f "$AGENT" ]; then
echo "host-agent: agent not found: $AGENT" >&2
exit 1
fi
# cron's PATH is famously short (/usr/bin:/bin), and a node installed through
# nvm or /usr/local lives outside it. Set NODE_BIN in the env file when
# `command -v node` comes up empty under cron but works in a login shell.
NODE_BIN="${NODE_BIN:-node}"
if ! command -v "$NODE_BIN" >/dev/null 2>&1; then
echo "host-agent: node not found (NODE_BIN=$NODE_BIN); set NODE_BIN in $ENV_FILE" >&2
exit 1
fi
export HOSTS_API_URL HOSTS_INGEST_TOKEN HOST_ID
exec "$NODE_BIN" "$AGENT" "$@"