Running any state-writing `hermes cron` CLI command as root (the
default for `docker exec`) rewrote jobs.json via mkstemp +
atomic_replace, leaving it root:root mode 600. The gateway's ticker
(uid 1000 via PUID/PGID) was then locked out of every tick with
PermissionError — silently: the liveness heartbeat stayed fresh,
`hermes cron status` opened with 'Gateway is running — cron jobs will
fire automatically', and in the field ~14h of scheduled jobs were
skipped before a human noticed the absence of messages.
Fixes, per the issue's suggested items 1 and 3:
1. Ownership preservation on save (cron/jobs.py): snapshot the owner
before the atomic replace; when the writer is privileged (euid 0)
and the previous owner differs, chown the rewritten file back.
First-time creation inherits the cron dir's owner. Unprivileged
writers never call chown. POSIX-only (guarded via os.name/getattr),
best-effort — a chown failure logs a warning but never breaks the
save. 0600 hardening is unchanged.
2. Zombie-ticker surfacing: the ticker loop (both single-profile and
multiplex paths) now persists the failure reason to a
ticker_last_error marker next to the heartbeat files on every
failed tick, and clears it on the next clean tick. `hermes cron
status` shows the recorded reason in its 'ticks may be failing'
branch, plus an actionable ownership hint when the error is a
PermissionError (recommend `docker exec -u <uid>:<gid>`).
Fixes#68483