feat(pi): schedule the Pi credential renewal so it runs unattended - #252
Closed
ruby-dlee wants to merge 1 commit into
Closed
feat(pi): schedule the Pi credential renewal so it runs unattended#252ruby-dlee wants to merge 1 commit into
ruby-dlee wants to merge 1 commit into
Conversation
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.
ruby-dlee
force-pushed
the
fm/pi-auth-refresh-schedule
branch
from
August 19, 2026 04:43
879f9e5 to
4ea2df1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #251. Review that one first; this PR's diff is only the schedule.
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.jsonserves 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: unavailableat 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-schedulermints an activation nonce, bakes it into the plist environment, andrecord_heartbeatwrites 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 --scheduledwrites 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.shholds itself to the same rule the same way.Proved against the real launchd: launchd's own run stamped a matching nonce and
scheduler-statusreportedinstalled; a hand run afterwards left the heartbeat byte-identical.States
absent,orphaned,foreign,unloaded,unproven,stale,failing,attention,installed.orphanedrather than reading asabsent.unprovenages against its own plist mtime: loaded six months ago and never fired isstale, not waiting for a first fire.attentionis a working schedule whose profiles need a browser.ensurepasses 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-schedulerre-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 seambin/fm-checkout-refresh.shuses, 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 retargetedmain.Mutations
Seven that an adversarial review found surviving, or that this change introduced, checked by making the edit and running the suite:
uninstall-schedulerdrops itsbootoutinstall-schedulerignores a refusedbootstrapinstall-schedulerdrops the post-install verificationinstall-schedulerdrops the--interval-secondsboundsbin/fm-bootstrap.shdrops theinstall pi-auth-refreshtargetrecord_heartbeatstamps regardless of nonceThe third needed a new case to kill: the fake
launchctlalways 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.mdgets 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.pyrather than at a worktree that disappears.