Skip to content

feat(pi): schedule the Pi credential renewal so it runs unattended - #254

Merged
ruby-dlee merged 2 commits into
mainfrom
fm/pi-auth-refresh-schedule
Aug 19, 2026
Merged

feat(pi): schedule the Pi credential renewal so it runs unattended#254
ruby-dlee merged 2 commits into
mainfrom
fm/pi-auth-refresh-schedule

Conversation

@ruby-dlee

Copy link
Copy Markdown
Owner

Follows #251, now merged.

Why

A renewal an operator has to remember is not a renewal. #251 makes renewal possible; this makes it happen on its own, which is the actual requirement.

Machine-global, and identified by what it runs

One credential pool at ~/.pi/agent/auth.json serves every Firstmate home, and this machine has nine. So there is one label, and every fact the report depends on is read from the one plist at that label, never from the checkout that is asking.

The first version of this compared the installed job against the reporting checkout's own path. That would have let exactly one home report healthy while the other eight printed PI_AUTH_REFRESH: unavailable at every session start forever, and the remedy that line prints would have booted out the working job and repointed it at whichever home ran it. An adversarial review reproduced the thrash across two homes.

The nonce is what makes the liveness claim true

install-scheduler mints an activation nonce, bakes it into the plist environment, and record_heartbeat writes nothing unless the process carries the matching one. A nonce reaches a process only through launchd's copy of the job environment.

So a hand-typed run-once --scheduled writes nothing at all: it can neither forge proof of life nor destroy the real one. The earlier version claimed this property in four places and did not have it. bin/fm-report-retention.sh holds itself to the same rule the same way.

Proved against the real launchd: launchd's own run stamped a matching nonce and scheduler-status reported installed; a hand run afterwards left the heartbeat byte-identical.

States

absent, orphaned, foreign, unloaded, unproven, stale, failing, attention, installed.

  • launchd is asked before the disk, so a job loaded with nothing describing it is orphaned rather than reading as absent.
  • unproven ages against its own plist mtime: loaded six months ago and never fired is stale, not waiting for a first fire.
  • attention is a working schedule whose profiles need a browser. ensure passes on it, because calling a working schedule unavailable is the alarm that gets ignored.

Install and uninstall are transactional now

The plist used to be published before the bootstrap, so a refused bootstrap left the new definition on disk with the old job running, and every report read the file. It now restores the previous definition and rebootstraps it. It also binds the loaded job rather than the file it just wrote, because launchctl can report success while launchd holds nothing.

uninstall-scheduler re-probes after its bootout and refuses to remove the plist while launchd still holds the label, rather than reporting a removal that did not happen and leaving a credential refresher running with nothing describing it.

Platform

The macOS-only refusal goes through FM_PI_REFRESH_PLATFORM, the seam bin/fm-checkout-refresh.sh uses, because CI runs this suite on Linux and the test has to exercise a macOS-only install there. Without it this file went red the moment the stack retargeted main.

Mutations

Seven that an adversarial review found surviving, or that this change introduced, checked by making the edit and running the suite:

mutation result
uninstall-scheduler drops its bootout red
install-scheduler ignores a refused bootstrap red
install-scheduler drops the post-install verification red
install-scheduler drops the --interval-seconds bounds red
bin/fm-bootstrap.sh drops the install pi-auth-refresh target red
record_heartbeat stamps regardless of nonce red
the installed job is compared against this checkout's own path red

The third needed a new case to kill: the fake launchctl always registered on a successful bootstrap, so checking the return code and checking the loaded state were the same assertion. It now models launchctl accepting a bootstrap that launchd does not end up holding.

Also

The suite exports its own state root at file scope. Only one unit set it, so the other units' isolation rested on the very property one of them asserts, and a mutation wrote a fixture heartbeat into the operator's real state root.

docs/configuration.md gets its own heading rather than sitting inside the checkout-refresh section, where the paragraphs that follow it described a different job's guarantees.

Follow-up, not in this PR

Installing it on this machine for real waits for the stack to land, so the plist points at ~/firstmate/bin/fm-pi-refresh.py rather than at a worktree that disappears.


Reopened against main after #251 merged: GitHub closes a pull request when its base branch is deleted, and a stacked base gets no CI. Same branch, replayed onto the squashed base.

Dongkeun Lee added 2 commits August 19, 2026 01:00
A renewal an operator has to remember is not a renewal. This installs the
machine-global macOS LaunchAgent that runs `run-once --all --scheduled` on an
interval, so a credential is renewed whether or not anyone is at the keyboard.

The label is machine-global because the thing it protects is: one credential
pool at ~/.pi/agent/auth.json serves every Firstmate home, and this machine has
nine of them. Everything the report depends on is therefore read from the one
plist at the fixed label, never from the checkout that is asking. An earlier
shape compared the installed job against the reporting checkout's own path,
which would have let exactly one home report healthy while the other eight
printed a warning at every session start forever, and the remedy that warning
printed would have torn the working job down and pointed it at whichever home
ran it.

A heartbeat counts only when it carries the activation nonce baked into the
installed plist, which reaches a process only through launchd's copy of the job
environment. That is what makes "a manual run cannot fake scheduler liveness" a
property rather than a convention: a hand-run `--scheduled` writes nothing at
all, so it can neither forge proof of life nor destroy the real one. Proved
against the real launchd: launchd's own run stamped a matching nonce and
reported installed, and a hand run left the heartbeat byte-identical.
bin/fm-report-retention.sh holds itself to the same rule the same way.

The heartbeat also carries what the run was. A profile that needs a browser is
not a broken schedule, so it reports attention and ensure passes, because
calling a working schedule unavailable is the alarm that gets ignored. A
schedule whose runs actually fail reports failing.

scheduler-status distinguishes absent, orphaned, foreign, unloaded, unproven,
stale, failing and attention. launchd is asked before the disk, so a job that
is loaded with nothing describing it is expressible rather than reading as
absent. An unproven job ages against its own plist: loaded six months ago and
never fired is stale, not waiting for its first fire.

install rolls back. The plist is published before the bootstrap, so a refused
bootstrap used to leave the new definition on disk with the old job running and
every report reading the file; it now restores the previous definition and
rebootstraps it. It also binds the loaded job rather than the file it just
wrote, because launchctl can report success while launchd holds nothing.
uninstall re-probes after its bootout and refuses to remove the plist while
launchd still holds the label, rather than reporting a removal that did not
happen and leaving a credential refresher running with nothing to describe it.

Bootstrap reports an unhealthy schedule and never installs one. It is silent
under the test bypass unless a test opts in, the same seam checkout-refresh and
report-retention use.

The platform refusal now goes through FM_PI_REFRESH_PLATFORM, the seam
fm-checkout-refresh.sh uses, because CI runs this suite on Linux and the test
must exercise a macOS-only install there.

Seven mutations that an adversarial review found surviving, or that this change
introduced, were checked by making the edit and running the suite: dropping the
bootout from uninstall, ignoring a refused bootstrap, dropping the post-install
verification, dropping the interval bounds, deleting bootstrap's install
target, stamping the heartbeat regardless of nonce, and comparing the installed
job against this checkout's own path. All seven go red.

The suite also now exports its own state root at file scope. Only one unit set
it, so the other units' isolation depended on the very property one of them
asserts, and a mutation wrote a fixture heartbeat into the operator's real
state root.
…report

CI runs this suite on Linux, and bootstrap's ensure checked uname directly
rather than through the seam its Python half already honored. The assertion
that bootstrap surfaces an unhealthy schedule therefore returned early on the
runner and went red there while passing on a Mac.

Both halves now read FM_PI_REFRESH_PLATFORM, the same shape
bin/fm-checkout-refresh.sh uses for the same reason. Verified by running the
whole file behind a uname that reports Linux: the assertion sees the line with
the seam applied and does not without it.
@ruby-dlee
ruby-dlee merged commit 4feb8c1 into main Aug 19, 2026
13 checks passed
@ruby-dlee
ruby-dlee deleted the fm/pi-auth-refresh-schedule branch August 19, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant