Repository navigation
Conversation
…trator password After install, any process running as the user could run `sudo -n pmset -a disablesleep 1` with no password and with Insomnia not running. Sleep turned off that way is not journaled, so the recovery agent never undoes it, and the grant lasts until uninstall. install.sh now writes three lines instead of four: disablesleep 0, lowpowermode 1 and lowpowermode 0. None of them can keep the Mac awake; turning sleep back on and the Low Power Mode floor stay passwordless so the app, backstop.sh and uninstall.sh can recover unattended. The file is always rewritten, so a reinstall over the old four-line rule drops the disablesleep 1 line. The installer's `sudo -n -l ... disablesleep 0` check is unchanged. The app turns sleep off through the standard macOS administrator dialog: /usr/bin/osascript running one fixed literal, `do shell script "/usr/bin/pmset -a disablesleep 1" with administrator privileges with prompt "..."` (AdministratorPrompt.swift). A child process rather than NSAppleScript in-process, because AppleScript is main-thread only and a dialog waited on from the main actor would freeze the menu bar and the lifecycle queue with no way to time out. 120 s limit; at the deadline osascript gets SIGTERM and nothing stronger, and the runner waits for it (and the pipe its root pmset inherits) to finish before reporting the timeout, so nothing is rolled back while a root pmset may still run. The runner is injected behind AdministratorPromptRunning; PmsetSleepGuard routes only setSleepDisabled(true) through it. Start order is unchanged (session.json, journal, backstop armed, then the dialog), so a crash mid-prompt leaves recovery a record. Cancel, wrong password, timeout and pmset failure all take the existing startFailed path; the notification now names the prompt. Reconcile no longer re-applies the guard for a valid session (that would prompt at login or after a crash with nobody at the keyboard). It reads pmset -g: SleepDisabled 1 continues the session as before, without a prompt; 0 ends it with a new reason, sleepReenabled, and the notification "Sleep was turned back on while Insomnia was not running, so the session ended." A failed read ends it too. Start is the only caller that can prompt; there is no auto-start, URL scheme or scheduled start. Tests: AdministratorPromptTests (the osascript runner against a fake osascript: exact script literal, cancel, failure, launch failure, SIGTERM-only timeout that waits for the child; PmsetSleepGuard delegation; lifecycle: start, cancel/fail/hang roll back clean with the record present while the dialog is up, reconcile with sleep still off continues without a prompt, with it on ends without a prompt, only Start prompts), RecoveryScriptTests (install writes exactly the three lines; reinstall over a four-line rule leaves three), and the existing reconcile tests updated. README, SECURITY.md, spec sections 1, 2, 8, 9 and the manual plan, and five "Not run" rows in the validation record. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d sudoers line back when the app will not quit Two review findings on PR #32. The osascript runner read both pipes to EOF and only then looked at whether the deadline had fired. An osascript that did not exit on SIGTERM, or a root command holding its output, would keep the start transaction, and with it the lifecycle queue and the recovery lock, waiting forever, with no message and no way out short of killing the app. The runner now drains the pipes on their own threads and, 3 s after the deadline (the backstop's grace), answers a caller still waiting with AdministratorPromptError.stillRunning: osascript's pid, whether osascript itself is still alive, and a handle that resolves when the child has been reaped and its output has closed. Nothing is killed. The start keeps session.json, the journal entry and the recovery lock, because the command may still turn sleep off and a backstop running beside it would clear a journal the late pmset then contradicts. It posts "Password prompt still running" with the pid, offers kill <pid> only while the pid is osascript's own, puts the same on the menu warning line, waits for the handle, and then runs the usual startFailed rollback. That is the rule PR #22 applies to a stuck sudo pmset; the refusal path it adds can take stillRunning once both are in. install.sh wrote the three-line rule and then asked the running app to quit. If the app did not quit within 15 s, the previous bundle stayed installed with a rule that denies the sudo -n disablesleep 1 an older build starts sessions with, so that install could not start a session until a rerun. The rule is still written first: writing it after the bundle would let a cancelled password prompt on a fresh install leave a new app with no undo rule. On that one stop the installer now writes the rule again with the disablesleep 1 line, using sudo's cached credential, and says which rule is in place, including when the second write fails. A successful install writes the file once. The rule is printed by one function so the two writes cannot drift. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rdless-sleep-off # Conflicts: # SECURITY.md # docs/spec.md
…ep 1 Review finding on PR #32: the previous fix put the passwordless `pmset -a disablesleep 1` line back when an upgrade stopped because the running app would not quit. That re-grants every process running as the user a way to keep the Mac awake with no journal entry, until a rerun or uninstall, which is the exposure this PR exists to remove. The installer now never writes that line. It asks a running Insomnia to quit first and stops with nothing changed, the sudoers file included, if the app is still running after 15 s. Then it writes the three-line rule, checks that the app was not opened again during the password prompt, and replaces the bundle. A stop between the rule and the new bundle (rule not effective, app opened again, a failed copy or signature) leaves an older build unable to start a session; an EXIT trap armed for exactly that window prints a plain note with the rerun command. That fails closed. If the password prompt fails after the app was quit, the message says the app was quit and nothing else changed, so the old build can be opened again with its own rule. Four installer tests encoded the old password-before-quit order and are rewritten for the new one. A static test checks that no non-comment line of install.sh mentions `disablesleep 1`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cancelled or failed sudo password used to arrive after the installer had already quit Insomnia, which ends the session, so a failed upgrade cost the user their running session for nothing. install.sh now runs `sudo -v` before anything else. A cancelled or failed password exits with nothing changed and the app still running. Then it quits the app (stopping with nothing changed if it is still running after 15 s), writes the three-line rule on sudo's cached credential (asking once more only if `sudo -n -v` says it expired during the quit), checks the app was not reopened, and replaces the bundle. When session.json holds a future deadline it first prints "A session is running and the upgrade will end it." and, with a terminal on stdin, asks "Continue? [y/N]"; anything but y stops before any sudo call. No path writes `disablesleep 1`. The stuck-prompt notification no longer says `kill <pid>`: it stays in Notification Center after osascript exits, when the pid may belong to another process. The hint stays on the menu warning line, which is replaced once osascript exits. .greptile/config.json and rules.md described the four-line grant this branch removes. The rule is now sudoers-rule-is-three-exact-lines, and the instructions and the journal, backstop and SIGKILL rules name the administrator prompt as the only path to `disablesleep 1`. Tests: fixture scripts get /dev/null as stdin, or a pty holding the answer. New installer cases cover a cancelled password during a session, n and y at the question, an expired session, an expired credential and a failed second password; the existing ones now expect `sudo -v` first. README, SECURITY.md, spec section 2 and two new release-validation rows follow the new order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A password dialog can outlive its start. If the app crashed or was force-quit with the dialog up, osascript kept running; reconcile or the backstop then cleared session.json and the journal, and a later answer still ran `pmset -a disablesleep 1` with nothing left to undo it. Each Start now writes a random nonce to APP_SUPPORT/pending-start just before the dialog. The dialog runs a fixed root command, /bin/sh -c AdministratorPrompt.rootCommand with the marker path and nonce as $1 and $2 (passed through `quoted form of`, compared, never run). It runs pmset only while the marker holds the nonce, reads it again after pmset, and turns sleep back on if it is gone by then (exit 3 and 4). The start deletes the marker on every outcome before it releases the recovery lock, and at once on a stuck prompt. Every other lock holder deletes it before it touches the journal: exclusive() in the app, backstop.sh right after lockf, and uninstall.sh before it runs a possibly older backstop; uninstall refuses to remove anything while the marker is present. A marker that cannot be written rolls the start back with no dialog. SleepGuarding.setSleepDisabled(Bool) is split into disableSleep(PendingStart) and enableSleep(), so no path can turn sleep off without a marker. The sudoers rule keeps its three lines. The stuck-prompt menu line kept offering `kill <pid>` after osascript had exited while a command it started still held its output. The runner now reports osascript's own exit to the handle as soon as it is reaped (UnfinishedPrompt.waitUntilOsascriptExits), and the start replaces the line then, before it waits for the output to close and rolls back. The runner tests started the deadline while the fake could still be installing its TERM trap, and the output-holder test relied on a 5 s sleep. OsascriptAdministratorPrompt takes a beforeDeadline hook; tests block in it until the fake has written a ready file after its trap, and every holder runs until the test releases it (60 s watchdog). Tests run the real root command under /bin/sh with AppleScript's quoting and a fake pmset: nonce match, missing marker, a newer start's nonce, an empty nonce, the marker deleted while pmset runs, and a path and nonce full of quotes and $(...). Lifecycle tests cover the marker around the dialog, fresh nonces, a marker that cannot be written, the relaunch path and a newer start after it; script tests cover the backstop path, lock contention and uninstall. osacompile checks the script compiles. Docs, spec sections 1 and 8, a release-validation row and the Greptile rules describe the marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of a2ffdd4 found four ways the marker could still fail. The root command re-read the marker after pmset and ran `disablesleep 0` if it was gone, but ignored that pmset's status, and an old command whose marker had been replaced by a newer start's turned sleep back on during the newer session. Recovery that could not delete the marker (an immutable flag, an ACL) logged it and cleared the journal anyway, leaving an old dialog able to turn sleep off with nothing to undo it. A cancelled dialog took the undo path and ran `disablesleep 0`, which cleared a SleepDisabled another tool had set. The root command now runs as `/usr/bin/lockf -k -n -t 10 <marker> /bin/sh -c <rootCommand>`. lockf holds the marker's flock from before the nonce check until pmset exits and never creates the file (exit 69 when it is missing). Every deleter takes that lock first and then unlinks: Store.removePendingStart (flock, polled up to 10 s, then .markerBusy), and `lockf -k -n -s ... $RM -f` in backstop.sh and uninstall.sh, both through a new RM=/bin/rm. So the marker goes either before the check, which fails, or after pmset, which the journal entry still covers. The second read and the compensating pmset are gone, so neither can fail or act on a newer start. sleepDisabledByUs is now cleared only by a transaction that removed the marker. A marker that cannot be locked or deleted leaves recovery incomplete: sleep is still restored, the entry stays, and the app reports it (log, "Restore incomplete", menu line), refuses new starts, and retries on every transaction. backstop.sh keeps the entry, adds the marker to its failures and exits 1, also with a clean journal. A cancel (osascript's stderr ending in "(-128)") and a launch failure ran nothing as root, so the start restores session.json and the journal exactly and runs no pmset. A wrong password, a timeout and a pmset failure keep the undo path. A stuck prompt whose command still holds the marker lock gets the marker removed once the prompt exits. The SIGTERM test now sets its stop grace and accepts .stillRunning, waiting on its handle, so a slow TERM handler cannot fail it. Tests run the real root command under lockf with a fake pmset (marker missing, removal blocked while pmset runs, an answer that waits on a removal), Store removal (held lock, uchg, directory, dangling link), the app's keep-the-entry and refusal paths, cancel and launch failure with a foreign SleepDisabled, and the backstop and uninstall with a locked or undeletable marker. Docs, spec sections 1, 8 and 9, two release-validation rows and the Greptile rules describe the lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round three of review on the pending-start marker. - Start reads the installed backstop.sh's "# insomnia-backstop-version:" line and shows no dialog below 2, the first version that deletes pending-start, telling the user to run install.sh again. install.sh now installs backstop.sh under the recovery lock before the bundle, with install -S so a running older copy keeps its inode, and waits up to 30 s for runs of the older script to exit; it stops before the bundle if one stays or pgrep fails. - A stuck prompt whose marker the start deleted under the marker's lock can no longer run pmset, so the start rolls back and releases the recovery lock at once. A task outside the transaction keeps the menu line true: the kill hint goes when osascript exits, the line once the prompt has. A prompt whose command holds the marker's lock is still waited for, as before. - The root command gets the session's end as $3 and refuses (exit 4) unless /bin/date +%s is below it. A $3 that [ cannot compare refuses too. The text stays one fixed literal. - A journal write that fails after sleep was restored is logged and reported like the other failed clears instead of being dropped with try?; the entry stays and the next run retries. - install.sh calls rm, rmdir, mkdir, cp and install through fixed paths, the EXIT traps included. - The marker-removal test waits until lockf is blocked on the marker's lock (proc_pidinfo) instead of sleeping 300 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since this branch, reconcile reads `pmset -g` and ends a session whose sleep is back on. Three App Nap reconcile tests and testDeadlineTimerFiresEnd came from main without that seed, so they saw sleep on and ended the session early. They now start with sleepDisabled set, and testDeadlineTimerFiresEnd expects the pmset read before the undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install.sh still made and wrote the temporary sudoers file with bare mktemp and cat, and handed sudo bare `visudo` and `install`. sudo looks a bare name up in the caller's PATH and runs what it finds as root, so a tool first on PATH could change the rule that lands in /etc/sudoers.d. The LaunchAgent move used a bare mv. The tool block now has MV, MKTEMP, CAT and VISUDO, and sudo is given "$VISUDO" and "$INSTALL". The fake sudo only knows visudo and install by those full paths. testInstallCleansUpWithoutPATH also puts mv, mktemp and a cat that adds `NOPASSWD: ALL` to any rule first on PATH, and checks the installed rule never gets that line. The fixed-path rule in .greptile/config.json names the new tools and the sudo case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uninstall.sh asked sudo to run bare `test` and `rm` on the sudoers rule, so sudo looked them up in the caller's PATH and ran what it found as root. They are now "$TEST" (TEST=/bin/test) and "$RM". The fake sudo only knows /bin/test and /bin/rm. With sudo-root.mode "search" it can see through a directory the user cannot search, as root can. testUninstallFindsAndRemovesARuleOnlyRootCanSee uses that to cover the `sudo test -e` branch, which no test reached before because the fixture's rule was always visible to the user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testEndDuringReconcileMustNotLeaveSleepDisabled came from main (#44). It parks reconcile's `disablesleep 1` on sleepGate and waits for that call to start. Reconcile on this branch never turns sleep off again: it reads `pmset -g` and keeps the session only if sleep is still off. The gate never opened and the async test hung, which stalled both full runs after the merge. The test now seeds sleep off and parks reconcile on readGate, the `pmset -g` read. It still checks that an end requested meanwhile queues behind reconcile and that `disablesleep 0` is the last call. An end that skips the queue fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The osascript runner this branch adds waited with `process.waitUntilExit()` on a GCD worker and no terminationHandler. #46 found that this can wait forever on macOS 26 for a child that has already exited. A Start whose prompt never reports its exit keeps the recovery lock. The runner now creates a ProcessExit before `run()` and waits on it, as Shell, CancellableCommand and ShellTimeout do since #46. The test helpers this branch adds follow: RootCommandProcess and the osacompile check wait on a ProcessExit, and the backstop lock test stops its holder with `stop()`. #46 changed holdLock() to return a LockHolder, so that test did not compile on top of main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts, each resolved by keeping both sides: the notification titles in SessionManager, the StoreError cases, the test helpers in TestSupport, the tool block in uninstall.sh (one RM line), and spec step 1/2 (main's unreadable session.json text, then this branch's step 2). backstop.sh had RM twice after the merge; main's line stays. TestIsolationTests now also checks that pending-start resolves inside the test home. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dialog turns sleep off on its own, but the end, backstop.sh and uninstall.sh can turn it back on only through the passwordless `sudo -n /usr/bin/pmset -a disablesleep 0`. With /etc/sudoers.d/insomnia gone (an uninstall that stopped after removing it, a hand-deleted file) a start would succeed and leave sleep off past its deadline. performStart now calls SleepGuarding.checkPasswordlessRestore after the backstop version check and before it writes anything or shows the dialog. PmsetSleepGuard runs `sudo -n -l` on the exact restore command (PmsetSleepGuard.restoreArguments, which enableSleep also uses): it lists, never runs pmset, and fails instead of prompting when a password would be needed. A failure refuses the start with nothing changed, a "Session not started" notification and a message that says to run scripts/install.sh again. Tests: the lifecycle tests cover the missing rule (no dialog, no writes, the message) and the present one (checked once, before session.json, the journal, the marker, the arm and the dialog). PmsetSleepGuard takes a sudo path so a fake sudo can check the exact argv and the failure text without running the real one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lockf locks the file it opened, while the root command's nonce check and every deleter go by path. A file put in the marker's place could be locked and deleted while the root command still held the original and ran pmset, and the start could then clear sleepDisabledByUs too early. Store.removePendingStart compares fstat of the locked descriptor with stat of the path, and looks again if they differ. savePendingStart returns the identity of the file it wrote, taken from the temp file before the rename. When performStart voids a stuck prompt, it passes that identity, so a replaced marker (a copy has no lock) or a missing one (it went without the lock) does not count as voided and the start waits for the prompt. backstop.sh and uninstall.sh now open the marker on fd 8 (regular files only, since open(2) on a FIFO blocks under the recovery lock), lock that descriptor with "$LOCKF" -s -t N 8, and compare "$STAT" -f %d:%i <&8 with "$STAT" -L -f %d:%i of the path before "$RM" -f. The scripts never wrote the marker, so they cannot see a copy swapped in before their open. The .greptile rule, rules.md, spec and SECURITY.md say so. backstop.sh's inode helper now calls "$STAT" too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run that could not delete pending-start recorded it and then exited 0 when session.json was still valid, or after it moved a malformed session.json aside with nothing journaled. The marker stayed, but the caller saw a clean run. Every exit 0 after the marker step now goes through one check that logs the stuck marker and exits 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The retire wait counted any process whose arguments contained the installed backstop.sh path, so an editor or a tail on that file blocked every upgrade. It now counts only a run: /bin/bash and the installed path, plus --force, which is how launchd, install.sh and uninstall.sh start it. The wait cannot use the recovery lock instead, because the installer holds it and an old run is waiting on it. The sudoers rule was written before the wait, so a timeout there left the new rule beside the old app, which cannot start a session with it. The rule is now written under the recovery lock after the new backstop.sh is installed and the wait is over. Every stop before it leaves the old rule beside the old app and says only backstop.sh changed. The credential check moves to right after the quit, so a failed second password still changes nothing. The installer also looks for a reopened Insomnia after the wait, before the rule, and once more right before it removes the bundle, so a running old build is not left under a rule it cannot start sessions with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a start voiding its own stuck prompt knows which file it wrote. A transaction after a relaunch has the same limit as backstop.sh and uninstall.sh; SECURITY.md named only the scripts. The spec now states the limit and why a same-user swap is not a new exposure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The restore check can be slow (sudo may wait on a directory service), and the deadline was compared only before it, so a session that ended during the check could still get `disablesleep 1` (Greptile 4211874293). The command now compares /bin/date +%s with $3 again right before `disablesleep 1` and exits 4 past it; the start is undone like an end, and the only pmset that ran is the restore that undo runs anyway. New RootCommandTests case holds the fake pmset in the restore check until the clock passes the deadline. Docs: SECURITY, spec section 1, .greptile/rules.md and the AdministratorPrompt comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bf4d6e2 changed rootCommand but not the copy disableSleepScript embeds, which is what osascript actually runs. The embedded copy is now generated from rootCommand with AppleScript's escaping, so the two match (testScriptIsTheExactLiteral, and a new test reading the command back out of the AppleScript). The wall-clock test from bf4d6e2 is replaced by deterministic ones. RootCommandProcess takes the command to run and an optional fake clock: /bin/date is replaced by a fake that reads a file, and the fake pmset moves that file forward when the restore check runs it. The tests run the command read back from the AppleScript, starting 100 s before the deadline: - the check ends 1 s before the deadline: restore, then disablesleep 1 - the check ends at the deadline, 1 s after or a day after: exit 4, only the restore ran - the check fails: exit 5, no pmset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FakeClock already names the app's injectable clock in TestSupport, so 2f6b11f's struct of the same name did not compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lled one install.sh runs the staged copy's backstop.sh before the bundle swap. That copy handed frozen entries recorded with startedAtMicros to the installed binary, so an upgrade over a build whose Info.plist has no InsomniaResumeFrozenVersion kept them and stopped after the three-line rule was written (Greptile issue comment 6046657261). backstop.sh --own-bundle takes the binary and Info.plist beside its own copy. It finds them from BASH_SOURCE, never the environment, and exits 2 before the lock or the journal unless that path is absolute and ends in .app/Contents/Resources/backstop.sh. install.sh passes the flag to the staged copy codesign has checked. The LaunchAgent and uninstall.sh keep using the installed app. A stopped install now says to rerun the installer; a hand-run scripts/backstop.sh would ask the installed build again. Tests: source and --app upgrades over such a build resume with the staged binary under the recovery lock and never run the old one. A failed, unverifiable, malformed or late answer, or a staged build without the interface, keeps the entries and the previous app and agent. --own-bundle refuses copies outside a bundle's Contents/Resources and relative paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in scripts/backstop.sh, in the lock_shared check: keep #32's "$STAT" (absolute /usr/bin/stat) inode helper and #50's comparison, so a "stat" on PATH cannot make a foreign fd 9 look shared, and a shared fd 9 still skips the stale status-file cleanup that would delete a live supervisor's files. Everything else merged cleanly: #50's supervise_command and run_bounded (the supervisor owns its job, ignores TERM and HUP, keeps fd 9 until it reaps the command, TERM only, 125 keeps state and stops) sit beside #32's marker, nonce, deadline, --own-bundle and pending-start handling. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wn change The round 14 independent review (P1 at 2b8028c) showed the restore proof setting 0 over a SleepDisabled 1 another tool set while the dialog was up, also when the start then ran past its deadline. The root command now reads `pmset -g` itself after the nonce, deadline and uid checks, before it changes anything. A 1 the journal does not claim, or a failed read, exits 6 with nothing changed. The start passes whether the journal already owns the 1 as item 5 of argv; with "1" both reads are skipped. Root then runs `disablesleep 1`, and the proof (the user's `sudo -k -n /usr/bin/pmset -a disablesleep 0`) undoes that change instead of writing 0 over whatever it found. A failed proof makes root set 0 itself before it writes any output, then exit 5, so a closed dialog cannot stop the restore with SIGPIPE. After a passing proof come the deadline (4), a second read (6) and the final `disablesleep 1`. Exits 3, 4, 6, 69 and 75 are the new `.refused` error and, like exit 5, roll back with no pmset: each leaves no change of the command's own. Any other status is still undone like an end. Not closed: pmset has no compare-and-set, so a 1 set between the first read and root's own 1, or while that 1 is in effect during the proof, is still cleared, and sleep is off while the proof runs even without the rule. The docs and .greptile/rules.md say so. Tests: RootCommandTests cover the order, every sudo policy (cached, listing only, PASSWD, deny, no rule, no root entry), foreign 1s at each point on a fake clock, unreadable reads, journal ownership, a failing root restore and a closed dialog; StartOwnershipEndToEndTests run the real prompt, guard and manager against one fake machine. The backstop test that holds the marker expects the new calls; its lock and undo assertions are unchanged. testLockSharingIgnoresAStatOnPATH covers the "$STAT" kept in the #50 merge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The round 17 independent review (needs changes at 42e42e5) found three blockers. R1 (P0, Greptile 4213796939): root turned sleep off before the passwordless restore was established, so a root shell that died, or a root fallback that failed, could leave sleep off with no unattended way back. R2 (P1): the temporary 1, the proof and root's fallback set 0 over another tool's SleepDisabled 1, and with the dialog's output closed a refusal died by SIGPIPE, lockf reported 70 and the start undid it like a failure. R3 (P1): a read could use up the deadline, and the write still followed at or past it. The root command no longer runs the restore and has no temporary 1 or fallback. Its only write is the last step, `pmset -a disablesleep 1`. Before it, root drops to the user who pressed Start (`sudo -n -u "#$4"`) and has that user's sudo answer three queries with an empty environment but LC_ALL=C and stdin from /dev/null; none of them runs a command: - `sudo -V` must show sudo 1.9.15 to 1.9.x with only the sudoers policy, I/O and audit plugins. - `sudo -k -n -l` must list without a password and show no Runas or command-specific Defaults, which apply to the restore but not to a listing. - `sudo -k -n -ll /usr/bin/pmset -a disablesleep 0` must print exactly the six lines of /etc/sudoers.d/insomnia's restore rule: that file, RunAsUsers root, Options !authenticate and nothing else, the restore line, and Matched with the restore line. Anything else exits 5 with nothing written: an older or unknown sudo, other plugins, a path-only or truncated answer, another file, run-as or option, a denial, a failed root switch, a listpw policy that wants a password. The command ignores SIGPIPE, so a refusal keeps its status when the dialog's output is gone, and sets LC_ALL=C for every tool it runs by absolute path. The clock is compared with the deadline after the nonce check, after the sudo queries and right before the write; equality refuses. `pmset -g` is read once, after the queries. Exit 5 is still `.restoreNeedsPassword` and 3, 4, 6, 69 and 75 are still `.refused`; both now mean nothing was written, so the rollback runs no pmset. Error texts say "sleep was not turned off". Not closed, and stated in README, SECURITY.md, the spec and .greptile/rules.md: a 1 set between root's read and its write is taken for Insomnia's own; a status lost to a crash or a signal is still undone; a listing is not the restore (a rule removed later, a log sudo cannot write, other groups); sudo outside 1.9.15 to 1.9.x, bound Defaults, listpw=always and a later rule refuse every Start. The .greptile/config.json requirement is kept; only its mechanism sentence names the queries. Tests: RootCommandTests and RootCommandSudoAnswerTests run the real command and its three awk readers against source-derived answers from a fake sudo for every policy, a fake clock for each call before the write, foreign 1s at each point and closed output. StartOwnershipEndToEndTests run the real prompt, guard and manager against one fake machine. Tests that expected the temporary 1 or the fallback now expect no write before the queries pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sudoers rule said the root command "establishes" the passwordless restore. Its three sudo queries are policy answers that run nothing, so the rule now says it checks the restore with them and that they are not a run of the restore. The requirement is unchanged: no pmset when the check fails, and sleep is never turned off while the restore is missing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rsions; record before the read The round 19 review found two configurations the sudo answers accepted while the restore was already blocked, and a failed start that could clear another tool's SleepDisabled 1. - Any /private/etc/sudo.conf refuses (5). sudo -V skips an approval plugin with no show_version, and macOS installs no sudo.conf, so without one only the built-in plugins load. - /etc/pam.d/sudo must have exactly one session line, macOS's own "session required pam_permit.so" (5). sudo opens the session for a run, not for a listing. - sudo -V must show 1.9.17p2, the version whose source the check was read against (5). The refusal says another version needs an Insomnia release checked against it, not that a reinstall helps. - sudo -k -n -l may show only Defaults from an accepted list (environment, lecture, prompt, timestamp and logging on/off entries); anything else, a backslash, a tab or a bound Defaults header refuses (5) and names the entry. - Before it reads pmset -g, the command replaces the marker's nonce with "<nonce> writing", as the user (7 if that fails), and puts the nonce back on refusals 4 and 6. After a failure the app reads the marker under its lock: the file this start wrote, still holding only the nonce, means no command reached the sleep setting, so the start is rolled back without pmset. Any other marker is undone as before. Relaunch, the backstop and uninstall do not read it. Docs, review rules and release rows describe the new checks and what they still cannot establish. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…shed starts from it The round 21 review found that a process running as the user could put the bare nonce back into the marker after pmset's write, so a failed start dropped the restore it owed, and that relaunch, backstop.sh and uninstall.sh never read the record at all. The record moves to /private/var/db/com.kgarg.insomnia/<uid>, which install.sh creates through sudo as root's, mode 0644, 45 bytes, and which only root can write. The root command checks the receipt and every folder up to / by lstat, reads pmset -g, then writes "<nonce> writing" in place with dd conv=notrunc,fsync and reads it back before its only pmset write. It never writes the marker. Start journals sleepOffAttempt (nonce, the restore owed before, the receipt's and the marker's device:inode, the deadline). Every reader that deletes the marker under its lock settles the start from the receipt: the app on any transaction, backstop.sh (version 3) and uninstall.sh. The start's session.json never resumes, a receipt that shows no write keeps the earlier owed restore, and anything else runs the restore. A settlement that cannot be written keeps the record, restores sleep and refuses Start until it can. README and SECURITY.md now describe the 120 s wait, the waits on a running command, the agent's schedule and the receipt as the code does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…art, flush with F_FULLFSYNC The round 23 review found that two Insomnia folders of one user share the receipt, so a second start's line could hide the first start's write; that a root command keeps the marker inode it opened after a reader deleted a replacement; that a timed-out dialog could still be answered after its start was settled; that a never-write settlement lost its evidence when the journal could not be written; that dd's fsync is not a drive flush; and that backstop.sh and uninstall.sh ran cat and head through PATH. The receipt line is now "<nonce> <predecessor> writing|refused" (82 bytes). A start claims it under its lock through <uid>.released, the user's own file, and gives the claim back only once settled, so a start from another folder is refused while one is unsettled. The root command takes an exclusive flock on the open receipt (lockf -s -t 10, exit 75) before any check and holds it through pmset; the app, backstop.sh, install.sh and uninstall.sh take the same lock before they claim, read or remove it, and a busy or failed lock decides nothing. The command checks the descriptor and the path against the claimed device and inode, writes only while the receipt begins with the predecessor (exit 8), and writes the line through /usr/bin/perl with F_FULLFSYNC, refusing before pmset when perl or the flush fails. A start's expires is at most 130 s after it wrote pending-start. Until then a receipt that still holds the predecessor decides nothing unless the dialog ended by itself: the start stays journaled with its claim, Starts are refused and nothing is undone. After a timeout the app waits up to 15 s for expires. A settlement step that fails keeps the attempt with what the receipt showed, so a never-write result is retried without pmset. backstop.sh (version 4) and uninstall.sh call cat and head by fixed path. uninstall.sh's settle_stop no longer aborts on an unset second argument, which bash 3.2's EXIT trap had turned into exit 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four conflicts, each resolved as a union of both sides: - SessionManager.init keeps this branch's markerLockTimeout and receiptLockTimeout and main's keptRecheckDelay, keptRecheckAttempts, keptRecheckSlowDelay and bootSession. - RuntimeState.CodingKeys has main's kept-display keys and this branch's sleepOffAttempt. - The backstop patch map in RecoveryScriptTests has this branch's CAT, HEAD, RECEIPTS and RECEIPT_OWNER and main's MV. - TestSupport.makeManager takes both sides' parameters, in that order, with both sides' notes. Main's PrivateDisplayGuardTests did not build against this branch: its AfterSwitchOffSleepGuard wrapper now forwards SleepGuarding as this branch has it (checkSleepSettingForStart, disableSleep, enableSleep in place of setSleepDisabled), and its direct SessionManager call passes the harness's receipts. Everything else merged without conflict. backstop.sh and uninstall.sh check the journal with main's record_text_problems before a settlement republishes state.json, a settlement runs before main's kept-brightness handling, and uninstall.sh stops while an attempt stays, so a kept state.json never holds one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sions with sleep off README, SECURITY.md, spec sections 1, 2 and 8, release-validation.md and .greptile/rules.md now describe the receipt as the code has it: the 82-byte line with the predecessor, the receipt's lock, the claim in <uid>.released, the perl write with F_FULLFSYNC, exit 8, the 130 s answer window, which receipts show no write, which are undone at once and which only after the window, and that an undecided start stays recorded with Start refused. Release-validation rows are all Not run. EarlierBootLowPowerClaimTests (from #43) seeded a still-valid session from boot A without SleepDisabled 1 in the fake pmset. Under this branch a relaunch never turns sleep off again and ends a session whose sleep was turned back on, so three of its tests saw the session end at reconcile. The seed now shows the 1, as LidActionsTests' resumed sessions already do. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
krishhgg
added a commit
that referenced
this pull request
Oct 8, 2026
…till what was read, under a root-only lock /etc/sudoers.d/insomnia is one file for the whole Mac. install.sh read it through sudo, judged whose it was, and wrote it in a later sudo call; uninstall.sh read it and removed it the same way. Another account's install.sh could write its rule in between, and the later write replaced that account's grant, or the removal deleted it (Codex P1 on 38a9d1d, which the PR body had wrongly called accepted). Now the compare and the change are one `sudo /bin/bash -c` call. As root it takes /var/run/insomnia-sudoers.lock with lockf (umask 077, a symlink refused; /var/run is writable only by root and the daemon group), checks the rule is still absent or the bytes this run read (cmp), and only then writes or removes it. install.sh stages the new rule beside the old one (a name with a dot, which sudo skips), makes it root:wheel 0440, checks that copy with visudo and renames it over the rule; a failure removes the copy. The root shell's script is the function's text after the scripts' fixed tool paths, so nothing it runs comes from PATH. A rule that changed since the read, or a lock held past LOCK_TIMEOUT_SECONDS, stops the run: install.sh changes nothing, uninstall.sh keeps the rule and the app, and both ask for a rerun. The sudo reads of the rule now use fixed paths (/bin/test, /bin/cat, /usr/sbin/visudo). The grants do not change, nothing new runs as root outside the two scripts, and the other-account process stop still comes before the first sudo call. Tests: the fake sudo runs the transaction unprivileged only when the rule and the lock in its script are inside the fixture, and a gate file holds it between a run's read and its write. New tests cover two accounts installing from two homes at once, an uninstall whose rule is replaced while it waits, an uninstall racing another account's install, the lock held by another run (install and uninstall), visudo rejecting the staged copy, a failed rename, the shape of the transaction, #32's three-command rule, and both scripts naming the same lock. The two race tests fail on the previous scripts (rule overwritten, rule removed). Docs: README, spec section 2, and two "Not run" rows for a real two-account race and the root-side file modes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…old the sleep undo while a command may act Round 26 (independent25 at e741a7f): - R25-1: every settlement (app, backstop.sh, uninstall.sh) removes the start's session, then journals the decision with sleepOffAttempt.settled while the claim is still held, then gives the claim back and drops the record. A settled record is finished from its decision and never read against the receipt again; a failure or crash after the decision keeps it, holds nothing back, and is retried. backstop.sh declares version 5 and the app requires it. - R25-2: an undecided start (receipt locked, or a dialog that can still be answered) holds the sleep undo at any time, whatever an earlier session owes; Low Power Mode, processes and audio still undo. - R25-3: install.sh reads the receipt and the release file only under the receipt's lock, keeps a held claim (also right after it made the receipt), stops at a claim in an unsafe file, and checks everything again before it replaces a release file. - R25-4: prepare_low_power_off reads through the fixed $HEAD. - R25-5: the root command reads pmset -g again after the writing line is flushed and read back, before its last clock check and pmset. - R25-6: README, SECURITY, spec, release validation, both Greptile rule sources and source comments describe the new order and its costs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n lock the receipt, settled sessions resume Independent round 27 (needs changes at 93fb3dc), findings 2 to 8: - uninstall.sh takes the receipt's lock and checks the release file before it removes anything, refuses the whole uninstall while another Insomnia folder holds a claim or the lock stays busy, and keeps the lock, checking again, until the shared rule, agent, bundle, receipt and release file are gone. - install.sh makes the receipt 0600 with one ACL entry, "user:<name> allow read", and repairs the receipt of earlier builds in place. The app (acl(3)), the root command, backstop.sh, uninstall.sh and install.sh accept only that mode and entry, so no other account can open the receipt and hold its lock. - Reconcile resumes the session of a settled start that went through while its claim or record cleanup fails, with Starts refused until the cleanup finishes. - Recovery reads of the journal and session in backstop.sh and uninstall.sh use fixed paths for head, cat and tr. - Messages and docs say what deleting the marker, the answer window and a "writing" line prove, and no more. - supervise in install.sh and uninstall.sh ignores TERM and HUP with errexit off for the call's life; only the call gets the default actions back, so fd 9 is held until the call is reaped. - uninstall.sh reads a private copy of state.json, carries every failed read to its decision, stops before removing anything when one fails, and removes state.json only while it equals the checked copy. backstop.sh's raw reader and conversion report failure too. Finding 1 (F7) stays open and unwaived. The app that gets exit 6 but cannot journal its rollback now keeps "never wrote" for that start in every later settlement in the same process. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… calls stay supervised, finite session math Independent round 29 review (GPT-6.1-Sol, xhigh) of d933b68: needs changes, findings 1 to 13. - Root command: a `refused` line that cannot be written is tried again, at most three times within 3 s, under the receipt's lock, before it exits (finding 1). F7 stays open. - uninstall.sh: every read of the receipt, the release file and the journal keeps its bytes and its exit status apart, and a failed read, one file without the other, or a file or folder that fails the checks stops it before the bootout. `sudo -v`, then `sudo -n -v`, then every root command through bounded `sudo -n` under both locks; a failed, stopped or still-running call stops the run there (findings 2 and 4). - install.sh repairs an earlier receipt only under its lock and while the release file shows no claim, the entry before the mode (finding 5). - The app's ACL reader refuses a list it cannot read whole (finding 6). - backstop.sh carries a later read's failure to its decision instead of taking it for absence (findings 7 and 8). - A session.json whose extensions do not add up is moved aside like one that does not parse; a settled start's own session is told by its first end and start, with nothing that can trap; the menu keeps the cleanup line while it goes on (findings 9 and 10). - Fixed paths for the remaining trust reads, no persisted sudo pid (finding 11). README, SECURITY.md, spec and release-validation.md match (finding 12). A fixture run whose output cannot be captured throws (finding 13). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and rule text The shared shape_of reader set the global `problems` to its lines. In uninstall.sh that name is step 4's array, so a clean journal left one empty element in it and every uninstall that reached step 4 stopped as incomplete, before the LaunchAgent bootout. shape_of now sets shape_lines, in both scripts, which stay byte-for-byte shared. - Two backstop tests expect the publish failure's reason in the log line. - The repair test's lock lists include the fake launchctl bootout's entry. - .greptile/config.json: the install rule says a repair happens only while the release file shows no claim, entry before mode, under the receipt's lock when the user can open it; a held claim stops the install. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
testUninstallStopsWhenTheSettlementCannotReadTheJournalOrTheSession built its expected messages from the setUp fixture's paths, then ran each case in a fixture of its own, so every case failed on the path alone. The expected text is now made from the case's fixture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…blish from the live journal Greptile on 7e233e8, a full disk: the backstop exited 1 when mktemp could not make its folder for the copies it reads, so nothing was restored. It now keeps those copies in memory (copy_private with COPY_IN_MEMORY=1, read through plutil's standard input, plutil's messages kept in memory) with the same checks, and a copy whose file cannot be written is made again in memory. A NUL byte, which a shell variable cannot hold, is unknown (2). uninstall.sh keeps COPY_IN_MEMORY=0 and stops on such a copy. Greptile on 7e233e8, an interrupted uninstall: a stop between removing the receipt and removing its release file left the release file alone, which every rerun refused. Under the receipt's lock, just before it removes the receipt, the uninstall now writes .uninstall-receipt-removal with the release file's device, inode, change time to the nanosecond and line. A rerun that finds the release file without the receipt finishes the removal only while the record names that file exactly and it is free; anything else stays refused. The hosted failure on 7e233e8 (testBackstopKeepsAnOwnerACLAndStillUndoesTheJournal, 1 ACL entry expected, 0 found) was a regression from 468db03: it published from the private copy, which carries no ACL. Every publish now copies the live journal with cp, as main does, and requires the copy to hold the bytes the run read (same_as_read), in backstop.sh and in uninstall.sh's edit_state. Tests: the copy on its own (memory, RLIMIT_FSIZE, an unwritable folder), the backstop with no folder or a folder nothing can be created in, publish failures and a same-size rewrite during the read, an extended attribute as a stand-in for the ACL, and the uninstall's record across a failed, stopped, credential-less and still-running removal, with each change the record must refuse. Docs: README, SECURITY, spec and release validation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o ACL without -p r30-broad3 on 29949a6 failed two tests on their own fixtures. testScriptsAndAppReadTheSameSessionDates copies the readers into a harness and lacked plutil_on, which plutil_run now runs plutil through. testUninstallStopsWhenTheSettlementCannotReadTheJournalOrTheSession faked COPY_PERL with a reason on standard error; COPY_PERL prints it on standard output since 29949a6, and the fake now does too. Expected messages are unchanged. 29949a6's comments and spec said a cp of the live journal carries its access control list. cp(1) copies extended attributes unless -X is given and documents copying an ACL only with -p, which neither main nor these scripts pass. The text now says so; the hosted ACL fixture keeps its entry because its cp fails, as on main. No behavior change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment on lines
+2108
to
+2111
| if release_state && [[ "$release_word" == free ]] && progress_line && [[ "$progress_seen" == "$release_now" ]]; then | ||
| shared_seen="finishing $release_now" | ||
| release_seen="$release_nonce $release_word" | ||
| return 0 |
There was a problem hiding this comment.
Rerun deletes new install files
The new finishing path proceeds without holding the receipt’s lock. If this rerun uses another INSOMNIA_HOME, a standard installer has a different recovery lock and can recreate the shared receipt after the rerun’s last shared_unchanged() check. The removal loop then deletes the new receipt and .released by path, even while the installer holds the new receipt’s lock. That installation loses files required for Start and recovery.
Serialize this cleanup with receipt creation using a shared lock that survives receipt removal.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/uninstall.sh
Line: 2108-2111
Comment:
**Rerun deletes new install files**
The new `finishing` path proceeds without holding the receipt’s lock. If this rerun uses another `INSOMNIA_HOME`, a standard installer has a different recovery lock and can recreate the shared receipt after the rerun’s last `shared_unchanged()` check. The removal loop then deletes the new receipt and `.released` by path, even while the installer holds the new receipt’s lock. That installation loses files required for Start and recovery.
Serialize this cleanup with receipt creation using a shared lock that survives receipt removal.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
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.
Why
After a normal install, any program running as the user could run
sudo -n pmset -a disablesleep 1with no password and with Insomnia not running. Sleep turned off that way is never journaled, so the recovery agent does not undo it, and the grant lasted until uninstall. A laptop could stay awake in a bag until someone noticed. The review marked this a launch blocker.A root helper that only accepts the signed app needs a stable Developer ID signature the project does not have yet. This PR takes the maintainer's chosen route instead: drop the one dangerous sudoers line and ask for the administrator password each time a session turns sleep off.
What
scripts/install.shwrites three sudoers lines (disablesleep 0,lowpowermode 1,lowpowermode 0) instead of four. None of them can keep the Mac awake. Turning sleep back on and the Low Power Mode floor stay passwordless so the app,backstop.shanduninstall.shcan recover unattended. The rule is printed by one function and always rewritten, so a reinstall over an older four-line file drops the old line. Its check of the rule now runssudo -k -n /usr/bin/pmset -a disablesleep 0, and only whilepmset -greports SleepDisabled 0 or no SleepDisabled line (fifth round). Review fixes: the installer never writesdisablesleep 1, on any path. It asks for the password first (sudo -v), so a cancelled or failed password changes nothing and a running session keeps going. Before that, when session.json holds a future deadline, it prints "A session is running and the upgrade will end it." and, with a terminal on stdin, asks "Continue? [y/N]"; anything but y stops before any sudo call. Then it asks a running app to quit and stops with nothing changed, the sudoers file included, if the app is still running after 15 s. Then it writes the rule on sudo's cached credential (sudo -n -vchecks it; one more prompt only if it expired during the quit), checks that the app was not opened again meanwhile, and replaces the bundle. Any stop between the rule and the new bundle prints a note that an older build cannot start a session until the rerun, with the rerun command; an EXIT trap armed for exactly that window prints it. A successful install still writes the file once.New
Sources/Insomnia/System/AdministratorPrompt.swift:AdministratorPromptRunningprotocol, a fixed script literal, a 120 s limit, andOsascriptAdministratorPrompt, which runs/usr/bin/osascript -e <literal> <marker> <nonce>as a child and sends SIGTERM only at the deadline. The literal isdo shell script "/usr/bin/lockf -k -n -t 10 " & quoted form of (item 1 of argv) & " /bin/sh -c " & quoted form of "<rootCommand>" & " insomnia " & quoted form of (item 1 of argv) & " " & quoted form of (item 2 of argv) with administrator privileges with prompt "Insomnia needs your password to turn off system sleep for this session.".AdministratorPrompt.rootCommandis fixed text: it runs/usr/bin/pmset -a disablesleep 1only while the pending-start marker ($1) holds the nonce ($2), and exits 3 otherwise. Second review round:lockf(AdministratorPrompt.markerLock) holds the marker's flock from before that check until pmset exits, never creates the file (exit 69 when it is missing) and gives up after 10 s (exit 75). The second read after pmset and its compensatingdisablesleep 0are gone. Review fix: the wait is bounded. Both pipes are drained on their own threads; 3 s after the deadline (AdministratorPrompt.stopGrace, the backstop's grace) a caller still waiting getsAdministratorPromptError.stillRunningwith anUnfinishedPrompthandle carrying osascript's pid,osascriptAlive,waitUntilOsascriptExits(), which resolves as soon as osascript itself is reaped, andwaitUntilExit(), which resolves once its output has closed too. Nothing is ever killed.Pending-start marker (Codex P0).
performStartwrites a fresh UUID toAPP_SUPPORT/pending-start(atomic, no newline) right before the dialog and deletes it on every outcome before it releases the recovery lock; onstillRunningit deletes it first (third round: a start whose deletion succeeded no longer waits; see below).exclusive()deletes a leftover marker right after it takes the lock, so reconcile and every other app transaction void a dialog left from a start that died.backstop.shdeletes it right afterlockf, before any decision or sudo call, and logs it; install.sh reaches that through the backstop it runs under its lock.uninstall.shdeletes it right after its own lock, before it runs a backstop that may be an older copy, andjournal_problemsrefuses to remove anything while it is still there. A marker that cannot be written rolls the start back without showing the dialog. NewPaths.pendingStartFile,Store.savePendingStartandStore.removePendingStart. Second review round: every deleter takes the marker's own lock before it unlinks, so the file never goes between the root command's check and the end of pmset.Store.removePendingStartreplacesdeletePendingStart: flock, polled for up to 10 s, then.markerBusy; a missing file or a link to nothing is not an error. Both scripts run"$LOCKF" -k -n -s -t 10 "$PENDING" "$RM" -f "$PENDING", through a newRM=/bin/rm(Greptile). A marker that cannot be locked or deleted leaves recovery incomplete. The app still restores sleep, but keepssleepDisabledByUs, reports it (log, "Restore incomplete", a menu line naming the file), refuses new starts with "Session not started", and retries on every transaction. backstop.sh restores sleep, keeps the entry, lists the marker among its failures and exits 1, also when the journal is otherwise clean.PmsetSleepGuardtakes the runner by injection.SleepGuarding.setSleepDisabled(Bool)is split intodisableSleep(PendingStart), which goes through the dialog, andenableSleep(), which runssudo -n pmset -a disablesleep 0, so no path can turn sleep off without a marker. The Low Power Mode calls still usesudo -n.SessionManager.performStartis unchanged in order (session.json, journal, backstop armed, then the dialog). A wrong password, a timeout and a pmset failure take the existingstartFailedpath, which undoes from the journal (disablesleep 0); the notification names the password prompt. Second review round (Codex P1): a cancel (osascript's stderr ends in(-128)) and an osascript that never launched ran nothing as root, so the start puts session.json and the journal back exactly as it read them and runs no pmset; aSleepDisabledanother tool set stays, and the notification says nothing was changed. OnstillRunningit deletes the marker (or, while the dialog's root command holds its lock, deletes it once the prompt exits), posts "Password prompt still running" with the pid, and sets the menu warning line (the only place that offerskill <pid>; a notification outlives the pid). Codex P1 fix: it then waits for osascript's own exit and replaces the line with "osascript (pid N) stopped, but a command it started as root is still running" if the output is still held, so the line never offers a kill for a pid that is gone. It keeps session.json, the journal entry and the recovery lock until the output closes, then runs the samestartFailedrollback. Starts, ends and the agent queue behind it.SessionManager.performReconcilestep 2 no longer callssetSleepDisabled(true). It readspmset -g.SleepDisabled 1continues the session (journal, backstop, timers as before, no prompt).SleepDisabled 0ends it with the newEndReason.sleepReenabledand the notification "Sleep was turned back on while Insomnia was not running, so the session ended." A failed read ends it withrecoveryUnavailable. Start is the only caller ofdisableSleep(_:).Docs: README (install paragraph, "Exactly what gets installed", Start step, recovery section), SECURITY.md (what the remaining lines allow, the pending-start check), spec sections 1, 2, 8 (a new invariant for the marker), 9, install and manual plan, and thirteen "Not run" rows in docs/release-validation.md. Second review round: the README Start step says a cancel changes nothing while a wrong password or a timeout runs
disablesleep 0, and describes an undeletable marker; SECURITY.md and spec section 8 describe the lock, and section 8 gains an invariant for clearingsleepDisabledByUs; the cancel row is split from the wrong-password row, and a new row covers an immutable marker..greptile/config.jsonand.greptile/rules.md(from CI: bash 3.2 syntax check, warnings as errors, workflow lint; Greptile project config #31) described the four-line grant this PR removes. The rule is nowsudoers-rule-is-three-exact-lines: the three commands, anddisablesleep 1never passwordless on any install.sh path, only throughAdministratorPrompt.disableSleepScript. The instructions and the journal-before-change, backstop-armed and never-SIGKILL rules name the prompt path; rules.md describes it and lists the three-line rule, the bounded prompt and the new install order as deliberate. The rest of each rule is unchanged. For the Codex fixes,no-shell-interpolationnow namesrootCommandand says the marker path and nonce reach it only as$1and$2throughquoted form of, a newabandoned-password-prompt-cannot-turn-sleep-offrule states the marker invariant, and rules.md lists the marker and the menu line change as deliberate. Second review round: the abandoned-prompt rule describes the lock, the lock-then-unlink deleters, the keep-the-entry gate, the start refusal and the cancel rollback without pmset, andno-shell-interpolationnamesmarkerLock.Third review round (Codex and Greptile at b180529), fixed in aced667 unless a bullet names another commit:
backstop.shdeletespending-start. backstop.sh now carries# insomnia-backstop-version: 2. NewBackstopVersion.swiftreads it throughLaunchdBackstop.checkVoidsPrompts(), the first step ofperformStartafter the marker-problem refusal, before anything is written. A missing line, a lower number or an unreadable script refuses Start: "start refused, nothing changed: the installed backstop.sh at is older than this build and cannot cancel a password dialog left open by a crash; run scripts/install.sh again", plus a "Session not started" notification."$INSTALL" -S -m 0755on the script. That is a rename, so a run of the old copy keeps its own inode. It then waits up toRETIRE_WAIT_SECONDS(30) untilpgrep -lfshows no process running$APP_SUPPORT/backstop.sh. If one stays, or pgrep fails, it stops before the bundle and says that the rule and the new backstop.sh are installed and the app and the LaunchAgent were not. Then come the bundle, recovery (backstop --force) and the LaunchAgent, as before.clearPendingStart()removed its marker under the marker's lock. Its root command can no longer change anything, so the transaction restores sleep, finishes and releases the recovery lock.watchVoidedPromptfollows the leftover process outside the transaction: the menu line drops thekill <pid>hint once osascript exits and goes once the whole prompt has, unless something else has replaced it. A prompt whose command holds the marker's lock (past its checks, maybe in pmset), or whose marker cannot be deleted, is still waited for as before.endsAtas$3:PendingStart.deadlinein whole seconds, rounded down, passed asitem 3 of argvthroughquoted form of. It refuses with exit 4, "the session this password was for has already ended; sleep was not turned off", unless/bin/date +%sis below it. A$3that[cannot compare fails the test, so it refuses too. The command is still one fixed literal.restoreAllno longer drops a failed journal write after a successfulenableSleep()withtry?(Greptile). It callsfail(...)with "sleep restored but the journal entry could not be cleared: ; it will be retried". That logs the error, sets the menu line, and makes the end report itself incomplete. The entry stays and the next run retries.rm,rmdir,mkdir,cpandinstallthrough fixed variables (RM, RMDIR, MKDIR, CP, INSTALL), its two EXIT traps included (Greptile). In 241ea92 the LaunchAgent move,mktempand thecatthat writes the temporary sudoers file follow (MV, MKTEMP, CAT), and sudo is handed"$VISUDO"and"$INSTALL", because with a bare name sudo searches the caller's PATH and runs what it finds as root. In afc4948 uninstall.sh hands sudo"$TEST"(TEST=/bin/test) and"$RM"the same way.ProcessExit(from Process: wait for children with an exit handler, not waitUntilExit #46) instead ofprocess.waitUntilExit()on a GCD worker, which Process: wait for children with an exit handler, not waitUntilExit #46 found can wait forever on macOS 26 after the child has exited. A Start whose prompt never reported its exit would have kept the recovery lock (887bc71)..greptile: the lock rule names the voided prompt as the one command that no longer holds the lock; the fixed-path rule lists the new variables and covers EXIT traps and commands run through sudo; rules.md describes the voided rollback, the deadline and the backstop version contract.Fourth review round (local Codex and Greptile, both at 887bc71):
SleepGuarding.checkPasswordlessRestore().PmsetSleepGuardrunssudo -n -l /usr/bin/pmset -a disablesleep 0, which lists the rule without running pmset and never prompts.performStartcalls it right after the backstop version check, before anything is written or shown. A non-zero exit, or a sudo that cannot run, refuses Start with "start refused, nothing changed: sleep can only be turned off while it can be turned back on without a password, andsudo -n -l ...did not confirm that (); /etc/sudoers.d/insomnia is missing or not in effect, run scripts/install.sh again", plus a "Session not started" notification.PmsetSleepGuardtakes the sudo path by injection, so tests use a fake sudo, and its restore uses the same argument list as the check. The fifth round replaces the listing with a run of the restore (below).Store.removePendingStartcomparesfstatof its locked descriptor withstatof the path, which follows links as lockf does. It unlinks only when they match, otherwise looks again, and throws.markerReplacedat the limit.savePendingStartreturns the device and inode it wrote. A start that voids its own stuck prompt passes them asexpecting:, so a replaced or missing marker does not count as voided and the start waits for the prompt. backstop.sh and uninstall.sh share adelete_pending_markerfunction. It opens the marker on fd 8 (regular files only), locks it with"$LOCKF" -s -t 10 8, compares"$STAT" -f %d:%i <&8with"$STAT" -L -f %d:%iof the path, and only then runs"$RM" -f.STAT=/usr/bin/statis new. A mismatch counts as a stuck marker.exit 0after the marker step now goes throughexit_unless_marker_stuck./bin/bash $APP_SUPPORT/backstop.sh, with or without--force. The sudoers rule is now written after the wait, still under the recovery lock. The order is password, quit, lock, backstop.sh, wait, rule, bundle. A stop before the rule leaves the old rule beside the old app and says that only backstop.sh changed. The prompt for an expired credential moves to right after the quit. The installer also looks for a running Insomnia after the wait, where it stops before the rule, and right before it removes the bundle, where it stops with the rerun note.sudo -lcannot see (f72ba11), the path check and the deleters that cannot compare a written identity (04e188e), and the install order. Spec sections 2 and 8. rules.md and config.json: the sudoers rule names thesudo -n -lcheck and the install order, the marker rule names the identity checks and the backstop's exit 1, and the fixed-path list gains STAT. One new "Not run" row (the sudoers file moved aside) and one changed (an upgrade stopped before the rule).Fifth review round (local Codex P0 and Greptile 4171411041, both at f72ba11):
sudo -llists a command the admin group may run with its password, and it lists without a password whenever any NOPASSWD entry exists, so the listing passed without Insomnia's rule. A cached credential passed too.checkPasswordlessRestore(sleepOffIsOurs:)now runs/usr/bin/sudo -k -n /usr/bin/pmset -a disablesleep 0.-kignores a cached credential and-nfails instead of prompting, so only the sudoers policy can make it exit 0.performStartpasses the journal'ssleepDisabledByUs. When it is set, the next end or backstop.sh run owes the restore anyway, so the check runs it without a read. Otherwise the check readspmset -gfirst. SleepDisabled 0, or no SleepDisabled line, runs the restore, which then changes nothing. SleepDisabled 1 runs nothing and refuses Start with "start refused, nothing changed: sleep is already off (pmset reports SleepDisabled 1) and Insomnia did not turn it off, so Start leaves it alone ... To re-enable sleep: sudo pmset -a disablesleep 0, then start again". Apmset -gthat cannot be read also runs nothing and refuses Start. Nothing parsessudo -loutput.sudo -n -l(926d639). It now readspmset -gthrough a fixedPMSET=/usr/bin/pmsetand runs"$SUDO" -k -n /usr/bin/pmset -a disablesleep 0only while SleepDisabled reads 0 or has no line. A non-zero value or an unreadablepmset -gprints "sudoers rule not checked: ..." and the install goes on, because the app checks before every Start. A failed run still stops before the bundle.testSessionWithOffsetDatesIsResumed, which expects a relaunch to rundisablesleep 1as main does. This branch never prompts on relaunch, so the test now seeds the journal and SleepDisabled 1 and expects onepmset -gand no dialog (a1f9f82).sudo -v, install.sh with SleepDisabled 1).Sixth review round (Greptile 4171743070 and 4171743074, both at 12c8e40), fixed in 23d625f after the two merges below:
sudo -k -nrun went throughCancellableCommand, which sends SIGKILL a second after SIGTERM. It is gone, so there is no privileged preflight left to time out.checkPasswordlessRestoreis replaced bySleepGuarding.checkSleepSettingForStart(sleepOffIsOurs:), which only readspmset -g. A SleepDisabled 1 the journal does not claim, or an unreadable setting, refuses Start with nothing run, and the read is skipped when the journal already owns the bit. The prompt stays SIGTERM-only and keeps the recovery lock while its command may still act./usr/bin/sudo -n -u "#$4" /usr/bin/sudo -k -n /usr/bin/pmset -a disablesleep 0.$4is the uid of the user who pressed Start (getuid(), passed asitem 4 of argvthroughquoted form of). Root drops to that user without a password, and the user's sudo runs the exact restore with-kand-n, so only the sudoers policy can pass it. Only on exit 0 does/usr/bin/pmset -a disablesleep 1run, withoutexec, so pmset's own status cannot read as 5. Otherwise the command exits 5.OsascriptAdministratorPromptmaps that to the newAdministratorPromptError.restoreNeedsPassword, andnothingToUndo(renamed fromnothingRan) is true for it, so the start puts session.json and the journal back exactly and runs no pmset. The message is "sleep was not turned off: turning it back on needs a password (sudo -k -n /usr/bin/pmset -a disablesleep 0failed: ...), so a session could not end without you. /etc/sudoers.d/insomnia is missing or not in effect; run scripts/install.sh again". A uid that is not a positive whole number exits 5 without running sudo.sudo -nand checks asudo -k -n -llisting. Every one of those calls goes through Backstop: run only the copy sealed in the signed bundle #28's supervisor: SIGTERM only, with fd 9 kept until the call exits. Its comment and success line no longer present the listing as proof.BackstopVersionreads the copy sealed in the bundle (Backstop: run only the copy sealed in the signed bundle #28'sLaunchdBackstop.scriptPath). Its doc comment says so, andtestLaunchdBackstopChecksTheScriptItsAgentRuns, which the Backstop: run only the copy sealed in the signed bundle #28 merge broke, now builds the bundle'sContents/Resources(edfb2d7).sudo -n -lrecheck becomes the rule written and listed under the lock), release-validation.md (two rows changed, two new: the order in sudo's log, and a foreigndisablesleep 1during the dialog),.greptile/config.jsonand rules.md.Greptile review 11 (at fa281c1):
disablesleep 1, and exits 4 at or past it (4211874293). The start is then undone like an end, and the only pmset that ran is the restore that undo runs too. bf4d6e2 changedrootCommand; 2f6b11f made the AppleScript copy osascript runs match, generated fromrootCommand.Greptile issue comment 6046657261 (P1, outside the diff), fixed in 2b8028c. An upgrade over a build whose Info.plist has no
InsomniaResumeFrozenVersion, with a frozen process journaled withstartedAtMicros, stopped at the recovery before the swap. The staged backstop.sh handed that process to the installed binary, found no declaration in the installed Info.plist, and kept the entry. On this branch the three-line rule is already written at that point, so the older app could not start a session until the install finished.--own-bundleflag. With it,Insomnia --resume-frozenis theContents/MacOS/Insomniaand the version check reads theContents/Info.plistof the bundle the script itself sits in, not those of ~/Applications/Insomnia.app. The path comes fromBASH_SOURCE[0]; no environment variable can choose it. It must be absolute and end in.app/Contents/Resources/backstop.sh, or the script exits 2 before it opens the lock file or reads the journal.InsomniaResumeFrozenVersioncheck (now of the staged Info.plist), the 30 s limit, the check of every answer line, the microsecond and boot-session identity checks,startedAtMicrosin the journal, and the fixed tool paths. A failed, unverifiable, malformed or late answer keeps those entries, and the install stops before the swap with the previous app and LaunchAgent in place.scripts/backstop.sh --forceby hand. That hands the processes to the installed build, so over a build without the interface it keeps the entries again. Both kinds of install now say only to rerun the installer, which stages and checks a new copy and runs that copy's recovery..greptile/config.jsonand rules.md, and a new release-validation row for a real upgrade, also from a quarantined download.Independent round 14 review (P1 at 2b8028c), changed in 42e42e5. The restore proof set 0 over a
SleepDisabled 1another tool set while the dialog was up, also when the start then ran past its deadline. The change narrows this and moves the proof's write onto Insomnia's own change. It does not close it: Not covered says what remains and the decision it needs. Round 18 removes the proof's temporary 1 and root's fallback (below).pmset -gitself after the nonce, deadline and uid checks and before it changes anything. A SleepDisabled 1, or a read that fails, stops it with a new exit 6 and nothing changed. That read comes after the whole time the dialog was up, which is when the review's reproduction set the foreign 1. The start passes whether the journal already claims the 1 asitem 5 of argv($5, "1" or "0", throughquoted form of). Only an exact "1" skips the reads, as Start's own read is skipped.pmset -a disablesleep 1, then the user'ssudo -k -n /usr/bin/pmset -a disablesleep 0sets it back to 0. Before, the proof wrote 0 over whatever value it found, before Insomnia had changed anything. When the proof fails, root sets 0 itself, and only then writes its message and exits 5, so a dialog whose output is closed cannot stop that restore with SIGPIPE. If root's own restore fails, the command exits 1, which the app undoes like any pmset failure.pmset -gread (exit 6) and the finaldisablesleep 1. A 1 another tool sets after the proof is seen and left alone.AdministratorPromptError.refused(rootStatus:stderr:)is new. Exits 3, 4 and 6, and lockf's 69 and 75, map to it, and exit 5 still maps to.restoreNeedsPassword.nothingToUndois true for both, so the start puts session.json and the journal back without running pmset. None of those exits leaves a change of the command's own: it changed nothing, or the proof or root's restore set its own 1 back to 0. Before, 3, 4, 69 and 75 were undone like an end, which randisablesleep 0over whatever was there.rootStatusreads the status from the number osascript appends to the error. Every other status is still undone like an end.pmset -gread about every 0.1 s shows SleepDisabled 1 at most for the moment of the check) and.greptile/rules.md(the order step by step, which changes to flag, and the disclosed limits).Recovery round 15 merged main for Backstop: the supervisor owns each undo command's limit and signal #50 (70f5fac, Merged main below).
testBackstopKeepsTheSleepEntryWhileTheMarkerIsLockednow expects the root command's five pmset calls; its lock and undo assertions are unchanged.testLockSharingIgnoresAStatOnPATHis new.Independent round 17 review (GPT-6.1-Sol, xhigh, needs changes at 42e42e5), changed in 576c215 and 7dcf51f. Round 16's proof turned sleep off before the passwordless restore was established (R1, P0; Greptile 4213796939). The proof and root's fallback set 0 over another tool's 1, and a refusal whose dialog output was gone died by SIGPIPE, which lockf reports as 70 and the start undid like a failure (R2, P1). A read could use up the deadline and the write still followed at or past it (R3, P1). The root command no longer runs the restore:
pmset -a disablesleep 1. The temporary 1 and root's fallback 0 are gone.sudo -n -u "#$4") and has that user's sudo answer three queries, each through/usr/bin/env -i LC_ALL=Cwith stdin from /dev/null. None of them runs a command.sudo -Vmust show sudo 1.9.15 to 1.9.x, the sudoers policy plugin of the same version, and at most the sudoers I/O and audit plugins.sudo -k -n -lmust list without a password and show no "Runas and Command-specific defaults" section, because Defaults bound to a command apply when the restore runs but not to a listing.sudo -k -n -ll /usr/bin/pmset -a disablesleep 0must print exactly six lines:Sudoers entry:/private/etc/sudoers.d/insomnia (or /etc/sudoers.d/insomnia),RunAsUsers: root,Options: !authenticate,Commands:, a tab and the restore line, andMatched:with the restore line. sudo prints the last rule that matches, the one that decides when the command runs, and prints nothing for a denial. Round 20 narrows these checks and adds two before them (below).listpwsetting that wants a password, any sudo error.LC_ALL=Cfor every tool it runs, all by absolute path.pmset -gis read once, after the queries: a 1 the journal does not own, or a failed read, exits 6, and$5= "1" still skips the read..restoreNeedsPassword; 3, 4, 6, 69 and 75 are.refused; anything else is undone like an end. Every refusal now comes before any write, so the rollback runs no pmset. The texts say "sleep was not turned off" again. The exit 5 text namessudo -n /usr/bin/pmset -a disablesleep 0, says to run scripts/install.sh again if the rule is missing or not in effect, and names the sudo versions, plugins and bound Defaults the check refuses..greptile/config.jsonkeeps the requirement that the root command check the passwordless restore before it turns sleep off and run no pmset when it is missing. Only its sentence on how changed: it names the three queries, says they run nothing and are not a run of the restore (7dcf51f), and says a genericsudo -l,sudo -v, a bare exit status or a NOPASSWD grep is not proof..greptile/rules.mddescribes the new order and the disclosed limits, and no longer presents round 16's order as a deliberate exception.sudo -V,-land-lloutput in Terminal with the macOS and sudo versions, and refusals forDefaults!/usr/bin/pmset log_outputand for a later rule without NOPASSWD), and install.sh's comment and success line.Independent round 19 review (GPT-6.1-Sol, xhigh, needs changes at 7dcf51f), changed in c5456f8. sudo's answers passed two setups in which the restore already failed: an approval plugin with no
show_version, whichsudo -Vdoes not list and a listing never consults (F1), and user or global Defaults such aslog_outputwith!ignore_iolog_errorsand aniolog_dirit cannot create (F2). And a failure before the write could still clear a 1 another tool set while the dialog was up (F3, the round 14 and 17 ownership finding, not waived). The root command now, in this order:/private/etc/sudo.conf, a link or an empty file included (5). Plugins load only from that file, its path is compiled in (Apple's sudo-114.100.11,pathnames.h:70, with no override in the Xcode project), and macOS installs none, so without it sudo has only its built-in sudoers plugins.sudo -Vskips an approval plugin whoseshow_versionis NULL (sudo.c:1902), so no answer could show one.session required pam_permit.so(5). sudo opens the PAM session to run a command, never for a listing.sudo -k -n -lanswer (5 otherwise, naming the entry): the environment lists, the lecture and its file, the prompt and bad-password texts, password time limit and tries, the timestamp settings,tty_tickets,pwfeedback,insults, andlog_allowedandlog_deniedon or off. Anything else refuses,log_output,logfile,preserve_groups,group_sourceandignore_logfile_errorsamong them, and so do a backslash, a tab, a layout it cannot read and the bound-Defaults header. macOS's own Defaults are all on the list. A log sudo cannot write stops no command whileignore_logfile_errorskeeps its default (audit.c:491-508), and the list accepts no entry that changes it.<nonce> writing, in place and as the user through root'ssudo -n -u "#$4"(new exit 7 if that fails, a.refusedstatus). Refusals 6 and 4 after it put the bare nonce back. Replaced in round 22: the record goes in a receipt only root can write, after the read (below).After a wrong password, a signal, a timeout or a stuck prompt, the app reads the marker under its lock before it deletes it (
RemovedMarker). Only the file this start wrote, holding exactly its nonce, counts as untouched: no command for it reachedpmset -g, so the start is rolled back like a refusal, with no pmset, and a 1 another tool set meanwhile stays. A missing, replaced or changed marker is undone like an end, as before. Relaunch, backstop.sh and uninstall.sh do not read the record and stay as they were. Replaced in round 22: the receipt, not the marker, shows what the command did, and relaunch, backstop.sh and uninstall.sh settle from it (below).Texts: each exit 5 message names what it found (sudo.conf, PAM, the version, the Defaults entry, the listing, the rule). The app's message says to run scripts/install.sh again if the rule is missing or not in effect, and that install.sh changes none of the other causes; SECURITY.md adds that macOS keeps its sudo on the sealed system volume.
Docs: README (the dialog paragraph and how recovery works), SECURITY.md (the two files, 1.9.17p2, the Defaults list, the record, exits 3 to 7, and what this does not close), spec section 1, its not-closed paragraph and a section 8 invariant for the record,
.greptile/rules.md,.greptile/config.jsonrules 36 and 78 (the requirement unchanged), and release-validation.md (three rows changed, and two new Not run rows: another tool's 1 through a wrong password and a timeout, and an/etc/sudo.confholding only a comment).Independent round 21 review (GPT-6.1-Sol, xhigh, needs changes at c5456f8), changed in bd7db43. Finding 1 (P1, from round 20): root wrote the record into the marker as the user, in a file any process running as the user can rewrite, so a process that put the bare nonce back into the same inode after root's write made the app drop the restore it owed after an ambiguous failure, and sleep stayed off with no journal entry. Finding 2 (P1): relaunch, backstop.sh and uninstall.sh never read the record, so the session of an abandoned dialog was resumed on another tool's 1 while it was valid, or undone as Insomnia's own once it had expired. Finding 3 (P2, with Greptile 4215430637): README and SECURITY.md promised more than the code does. The record moves to a receipt only root can write, and every reader settles from it:
/private/var/db/com.kgarg.insomnia/<uid>: root's, mode 0644, one link, exactly 45 bytes, holding a nonce, a space,writingorrefused, and a newline. install.sh creates the folder withsudo -n /bin/mkdir -m 0755and the file withsudo -n /usr/bin/install -m 0644 -o root -g wheel, holding00000000-0000-0000-0000-000000000000 refused, under the recovery lock after the sudoers rule. It checks every folder from /private/var/db up to / by lstat first, and the folder and file again after. A receipt already as install.sh makes it is kept. Anything else (another owner, group or other write permission, a link, the wrong size or link count, an access control entry that allows anything) stops the install, and install.sh changes neither its owner nor its mode.statandls -lde(exit 7 for any failure, nothing written). Then it readspmset -g(exit 6, nothing written). Then it writes<nonce> writingover the 45 bytes withdd conv=notrunc,fsync, which keeps the inode and returns after fsync(2), and reads the bytes back. A failed write or readback writes<nonce> refusedand exits 7. The deadline check that comes next writes<nonce> refusedand exits 4. The last step is stillpmset -a disablesleep 1.state.jsongainssleepOffAttempt: the nonce,owedBefore(sleepDisabledByUs before the start), the receipt's device:inode when the start began, the deadline in whole seconds, and the marker's device:inode. Start journals it with sleepDisabledByUs before session.json, and adds the marker's identity before the dialog. A start that finishes or rolls back removes it.refused, shows that the command never turned sleep off. This nonce withwritingshows that it may have.SleepOffReceipts.livetrusts uid 0 alone. Tests build aSleepOffReceiptswith their own uid for a folder in their temporary directory, and patch the root command's-v o=0and the scripts'RECEIPTSandRECEIPT_OWNERlines in private copies; nothing in the shipped build reads a setting that widens the trust.RemovedMarker.isUntouchedis gone.owedBefore, which keeps a restore an earlier session still owes; anything else sets it and the restore runs. If session.json or the journal cannot be written, the attempt stays: the app ends an unexpired session instead of resuming it and refuses Start, backstop.sh undoes the journal as--forcewould and exits 1, and uninstall.sh stops with "Nothing was removed; rerun." The next run settles again, then with no marker of its own to match, so as "may have"./bin/rm -fand the folder with/bin/rmdirthrough sudo, once the folders above it pass the same checks. It keeps the folder while another account's receipt is in it, and leaves a receipt that is not a regular file..greptile/rules.md, and.greptile/config.jsonrules 36 and 78 (the receipt and settlement; the requirement is unchanged).Independent round 23 review (GPT-6.1-Sol, xhigh, needs changes at bd7db43), changed in 7e3ddc8 and e741a7f, with main merged in 15c2fc3. Finding 1 (P1): two Insomnia folders of one user shared the receipt, so a second start's line made the first start's settlement drop a restore it owed. Finding 2 (P1): a reader that deleted a replaced marker could finish recovery while the root command, holding the original marker, could still write. Finding 3 (P1): a settled start's dialog could still be answered after its marker was recreated. Finding 4 (P1): a never-write settlement that could not be written restored sleep and cleared another tool's 1. Finding 5 (P1): dd's fsync is not a drive flush. Finding 6 (P1, the bot's P0 privilege claim not established): backstop.sh and uninstall.sh ran
catandheadthrough PATH. Finding 8 (P2): docs promised more than the code does. Finding 7, ownership, stays open and unwaived (Not covered).<nonce> <predecessor> writing|refusedand a newline, 82 bytes; install.sh makes it holding the all-zero UUID twice andrefused. A 45-byte receipt from bd7db43 stops install.sh with nothing changed and a message to remove it by hand./usr/bin/lockf -s -t 10 8before any other check (exit 75 if it stays locked), and its shell and pmset keep fd 8 until they exit. The app (SleepOffReceipts.lock), backstop.sh, uninstall.sh (lock_receipt) and install.sh take the same lock and check that the path still names the locked file before they claim, read, write the release file or remove it. A busy or failed lock decides nothing.<uid>.releasedbeside the receipt is the user's own file (0600, 42 bytes,<nonce> free|held), made by install.sh. A start claims the receipt under its lock only while that file shows the receipt's noncefree, journalssleepOffAttempt(now with the predecessor andexpires), then writes<nonce> held. Every settlement gives the claim back. Start in another folder is refused while a claim is unsettled.$6is the predecessor nonce,$7the receipt's device:inode at the claim. fd 8 and the path must both be$7(7). After the sudo checks, the two nonces must be different uppercase UUIDs and the start's nonce not all zeros (7), and the receipt must still begin with$6(exit 8, a new.refusedstatus). Then root readspmset -g(6) and writes the line with one fixed perl program run through/usr/bin/env -i /usr/bin/perl: sysopen 257 (O_WRONLY|O_NOFOLLOW, no create or truncate), the same device:inode as fd 8, one full syswrite,fcntlF_FULLFSYNC (51), close, reopen and read back. Any failure, or no perl, writesrefusedand exits 7 before pmset.expiresis the session's end or 130 s after the marker is written (AdministratorPrompt.answerWindow: the 120 s dialog, 3 s grace, 7 s launch), whichever is first. It is$3and is journaled.SleepOffReceipts.verdict). Never wrote: this nonce withrefused, another start's line naming the same predecessor, or the predecessor itself once the dialog ended by itself (AdministratorPromptError.dialogOver) orexpireshas passed. May have written, undone like an end at once: this nonce withwriting, or a later line naming another predecessor. Afterexpires, a missing, replaced, unsafe or malformed receipt is "may have" too; before it, and at any time for a lock that stays busy or fails, the verdict is undecided. Undecided keeps the attempt, the claim andsleepDisabledByUs, runs no pmset beforeexpiresunlessowedBefore, and refuses Start. A timed-out or stuck dialog with at most 15 s left waits forexpiresbefore the read.owedBeforeit runs no pmset (attemptHoldin the app,attempt_holdin backstop.sh) and Starts stay refused; uninstall.sh stops with no pmset./bin/catand/usr/bin/headthroughCATandHEAD. uninstall.sh'ssettle_stopno longer aborts on an unset second argument, which bash 3.2's EXIT trap had turned into exit 0..greptile/rules.md, and.greptile/config.jsonrules 36, 48, 60 and 78.Independent round 25 review (GPT-6.1-Sol, xhigh, needs changes at e741a7f), changed in 93fb3dc. R25-1 (P1): a settlement gave the shared claim back before it published its journal, so a crash between the two, followed by later starts of another folder whose
refusedlines replaced the one the settlement read, could turn an honest never-wrote start into "may have written" and clear another tool's 1. R25-2 (P1): with the receipt locked, the app and backstop.sh restored sleep onceexpireshad passed or an earlier restore was owed, though a command already in pmset may still turn sleep off after that undo. R25-3 (P1, with Greptile 4220347392): install.sh read the release file before it took the receipt's lock, and replaced a held claim withfreewhen it had just made the receipt. R25-4 (P2, the outside-diff Greptile comment 6046657261):prepare_low_power_offin backstop.sh, as Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43 merged it, ranheadthrough PATH. R25-5 (P1, the ownership finding, F7): open and unwaived; this round narrows one window (below, Not covered). R25-6 (P2): docs and comments promised more than the code proves.publishSettlement,finishSettlement), backstop.sh (edit_state,finish_settlement) and uninstall.sh: remove the start's session.json (the one whose end is the start's deadline), then journal the decision while the claim is still held (sleepOffAttempt.settledtrue, withsleepDisabledByUsas it was before the start after "never wrote", or true after "may have written"), then give the claim back, then drop the record. A record already marked settled is only finished: no reader reads the receipt against it again, because once its claim went back a later start's lines may show something else. A failure or crash after the decision keeps the settled record, which holds nothing back: the undo follows the decision, Start is refused until the record is gone, and every later run, relaunch or uninstall tries to finish it. A crash or failure before the decision keeps the claim held, so the receipt still shows what it showed. A rolled-back start whose claim cannot be given back stays settled the same way. install.sh's recovery runs the staged bundle's backstop.sh, so it settles the same way. backstop.sh declares version 5 and the app requires it (BackstopVersion.required).expires, and whether or not an earlier session owes a restore (keepAttemptandattemptHoldin the app,keep_attemptandattempt_holdin backstop.sh). The earlier restore stays owed in the journal and runs once a run gets the lock and the receipt settles the start. "Never wrote" with a failed settlement still holds it unlessowedBefore; "may have written" holds nothing. Frozen processes, Low Power Mode and audio are still undone. uninstall.sh already stopped while a start is undecided; its message now also says a started command holds the receipt until pmset exits.freein a file of that shape is kept. Anything else (no file, another noncefree, other bytes, or the right bytes with another mode) is written new as the receipt's noncefreebysudo -n install -m 0600, which replaces the file by rename. Just before that write, install.sh checks again that fd 7 and the path are the same file, that the receipt and its folders pass the same checks, that the receipt still holds the line read under the lock, and that the release file's owner, mode, links, size, type and bytes are what it read (or that no file has appeared). The 45-byte receipt is still refused, not migrated. The sudoers rule is unchanged.prepare_low_power_offcalls"$HEAD"(/usr/bin/head) for both-c 1checks.pmset -ga second time afterwritingis written, flushed with F_FULLFSYNC and read back, and before its last clock check and pmset, unless$5is1(the journal owns the bit, so neither read runs). A SleepDisabled 1, or a read that fails, writesrefusedover the record the same way and exits 6: "pmset -g, read again once this start's record was written, shows a SleepDisabled 1 this start did not set, or could not be read; it was left alone and sleep was not turned off". The early read stays, beforewriting(exit 6 as before). The clock check after the second read writesrefusedand exits 4, as before. The exit codes and the app's handling of each are unchanged..greptile/rules.md, the.greptile/config.jsonrules on lines 36 and 78, and the source comments in SleepOffReceipts.swift, AdministratorPrompt.swift, SessionManager.swift, backstop.sh, uninstall.sh and install.sh.Independent round 27 review (GPT-6.1-Sol, xhigh, needs changes at 93fb3dc), changed in d933b68. R27-1 (P1, F7, ownership): open and unwaived; this round narrows A3 for the app that received the refusal (below, Not covered). R27-2 (P1): uninstall.sh removed the shared sudoers rule before it took the receipt's lock and found another Insomnia folder's held claim. R27-3 (P1): the 0644 receipt let any local account hold its advisory lock and so hold an owed sleep undo. R27-4 (P2): a settled successful start whose claim or record cleanup failed ended its healthy session on relaunch. R27-5 (P2): recovery reads of the journal and session in backstop.sh and uninstall.sh still ran
head,catandtrthrough PATH. R27-6 (P2): source messages overstated what deleting the marker, the answer window andwritingprove. R27-7 (P1):supervisein install.sh and uninstall.sh died on a process-group SIGTERM or SIGHUP, or on a failed status write under errexit, and let fd 9 go while sudo still ran. R27-8 (P1): uninstall.sh counted a journal read that failed as clean and could drop a kept brightness. R27-9 (P2): the hosted Swift job hit its 1200 s watchdog.lockf -s -t 10, after its own recovery lock, the order every reader uses) and reads the receipt and the release file under it. It stops with nothing removed when the release file showsheld, names another nonce or cannot be read, when the receipt or its folders cannot be locked or read, or when the release file is not a regular file; the LaunchAgent, the sudoers rule, the app, the receipt and the journal stay. A receipt no start can claim (it fails the root command's checks) does not block. The lock stays held through the removals: before the rule goes, and again before the receipt and release file go, uninstall.sh checks that fd 7 is still the path's file, that both files read the same and that the folders still pass. The lock goes only after the bundle is removed.sudo -n install -m 0600 -o root -g wheel) and adds one ACL entry withsudo -n /bin/chmod +a "user:<name> allow read", the name fromid -unand checked withid -u. A receipt an earlier build made (root's, one link, 82 bytes, no group or other write, no entry or only that one) gets mode 0600 and the entry in place, bytes, inode and any held claim kept. Any other entry stops the install. The app (SleepOffReceipts, through acl(3) andmbr_uuid_to_id), the root command, backstop.sh, uninstall.sh and install.sh accept the receipt only at mode 0600 with exactly that entry: allow, the read right alone, the user's uid, not inherited, no flags. The shell copies read the list with/bin/ls -lethrough one awk program, the same text in all five copies. Folders keep the no-allowing-ACL check. The root command's exit code is unchanged (7).TR=/usr/bin/tr; uninstall.sh getsTRandCMP. The journal and session shape checks, the session read, the post-undo journal check and the diagnostic excerpt use"$HEAD","$CAT"and"$TR". The status-file PID reads in install.sh and uninstall.sh use"$CAT"and stay diagnostic only. The journal shape checks no longer pipe plutil intohead.writingmeans the command was about to turn sleep off and may have; that deleting the marker stops a command the dialog starts from then on, unless this user writes the marker again or the clock is set back; and that a voided start is settled once its answer window has ended and the receipt can be locked, staying unsettled with Starts refused and sleep left as it is while a command holds that lock.answerWindow's doc gives the window as a minimum. release-validation.md's lock-timeout rows say a stalled sudo gets SIGTERM only and the run keeps the recovery lock until sudo has exited and been reaped.supervise, still byte-identical in install.sh and uninstall.sh, now turns errexit off and ignores TERM and HUP for the call's whole life, and starts the call as( trap - TERM HUP; exec "$@" ). sudo is still never sent SIGKILL, and fd 9 goes only afterwait.record_text_problems, the same in both scripts, returns 2 when the file cannot be read. uninstall.sh reads throughplutil_read: a missing key and a null value are absent; any other failure is listed inREAD_FAILURESand returns 2.journal_problemscopies state.json into the run's private folder with a boundedcpand reads only the copy. The caller checks every reader's status andREAD_FAILURES, and any failure stops the uninstall before anything is removed. state.json is removed only whilecmpfinds it equal to the checked copy. The settlement stops on a failed read before it takes the receipt's lock. backstop.sh's raw reader and conversion return 2 too, and a journal it cannot read whole is left as unknown.refusedNonce, and every later settlement of that nonce in the same process keeps "never wrote" whatever the receipt shows.AppEncodedJournalScriptTestsruns its script rows two at a time, and the fake sudo'shang_on_termis ready only after its trap and runs no command substitution after it (below, Tests). No workflow, watchdog, partition or rerun change; the two-job split stays the parent's choice..greptile/rules.mdand the.greptile/config.jsonrules on lines 36, 48 and 78.Independent round 29 review (GPT-6.1-Sol, xhigh, needs changes at d933b68), changed in 468db03, 1468950, 7e233e8, 29949a6 and f643d49. Finding 1 (F7) stays open and unwaived; finding 3 is reported, not fixed.
refusedline that cannot be written is tried again under the receipt's lock, at most three tries within 3 s.sudo -v,sudo -n -v, and every root command through boundedsudo -n(30 s, SIGTERM only); a call that fails or times out stops it, and one still running keeps both locks.sedandgrep, no sudo pid printed. Finding 12: README, SECURITY.md, spec, release-validation.md, the.greptile/config.jsoninstall rule. Finding 13: a fixture run whose output cannot be captured throws..uninstall-receipt-removal; a rerun removes a lone release file only on an exact match withfree.testBackstopKeepsAnOwnerACLAndStillUndoesTheJournal, 0 entries for 1): 468db03 published from the private copy, without the entry. 29949a6 publishes from acpof the live journal, as main does, only if it holds the bytes the run read.Tests
swift build: ok.swift test(full suite, alone, under the shared lock): 504 tests, 0 failures, 0 skipped, 107 s, before the review fixes. After the first review fixes: 461 tests, 0 failures, 0 skipped, 97 s. After merging main (5330c28): 482 tests, 0 failures, 0 skipped, 102 s. After the install-order fix (0087d0a): 482 tests, 0 failures, 0 skipped, 100 s. After the password-first fix (71253d7): 489 tests, 0 failures, 0 skipped, 99 s. After the Codex fixes and merging main (b5f6de9): 515 tests, 0 failures, 0 skipped, 122 s. After the second review fixes (b180529): 535 tests, 0 failures, 0 skipped, 142 s (the run before theRMchange: 534 tests, 0 failures, 155 s). After the third review fixes and merging main (887bc71): 630 tests, 0 failures, 0 skipped, 145 s. After the fourth review fixes and merging main (e1e56b0; the two commits after it change docs only): 706 tests, 0 failures, 0 skipped, 261 s. After the fifth review fixes and merging main (12c8e40): 776 tests, 0 failures, 0 skipped, 230 s. The same suite passed at ca56ec0, before Browser: ask before quitting, and never relaunch a browser that has not quit #30 landed: 749 tests, 0 failures, 0 skipped, 217 s. Two full runs after the first merge hung in Tests: wait for an end to finish, not start, before checking what it restored #44'stestEndDuringReconcileMustNotLeaveSleepDisabled, which waited for a reconciledisablesleep 1this branch never runs (fixed in b01a97b). One more hung inRecoveryLockTestsinwaitUntilExit, the Foundation bug Process: wait for children with an exit handler, not waitUntilExit #46 fixes, and its rerun passed 625 of 625 at b01a97b before the second merge. The run before it failed one test this PR does not change,testBackstopSharesLockHandedDownOnFd9, whose fake sudo did not start within the fixture's 1 s command timeout; it passed 10 of 10 runs alone and in the rerun. Every run since the first review fixes used--skip UIStatusTests --skip UIStartupTests(those two suites put real status items in the maintainer's menu bar; hosted CI runs them on every push). One earlier full run under heavy load from parallel workers (load average above 30) failed 5 tests: this PR's SIGTERM timeout test, whose fake child was signalled before its TERM trap was installed (the test now uses a 3 s deadline), and four untouchedRecoveryScriptTestswith 1 s fixture timeouts (testLegacyAndIdentitylessPidsAreNeverSignaledAndStayDirty,testLiveSupervisorDoesNotHoldACallersCapturePipe,testUninstallAbortsWhenBootoutAndPrintBothFailAmbiguously,testUninstallRunsRecoveryUnderItsOwnLockAndKeepsLockInode), which pass alone and in every other full run.swift build -c release -Xswiftc -warnings-as-errors(what CI now runs): ok, also at b5f6de9, b180529, 887bc71, f72ba11 and 12c8e40./bin/bash -non every script under /bin/bash 3.2.57 and the CI bash 4 grep: clean, also at b180529, 887bc71, f72ba11 and 12c8e40.shellcheck scripts/*.sh: clean, also at b180529, 887bc71, f72ba11 and 12c8e40.Full suite, sixth round, under the shared lock with
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests. From this round on KeychainStoreTests is skipped locally, because it calls the real Security APIs on temporary keychain files; hosted CI still runs it and both UI suites. The local full runs listed above from before 23d625f did not skip it, so they were not entirely on fakes.testLaunchdBackstopChecksTheScriptItsAgentRuns, which the Backstop: run only the copy sealed in the signed bundle #28 merge broke. After the fix, edfb2d7 passed 1040 tests with 0 failures in 591 s.testADeviceChangeBeforeTheLaunchReconcileWaitsForTheLidOfTheSessionOnDisk, fixed in fa281c1. Only that test was rerun at fa281c1, and it passed; fa281c1 changes nothing else.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: ok at edfb2d7 and fa281c1./bin/bash -non every script under /bin/bash 3.2.57, the CI bash 4 grep andshellcheck scripts/*.sh(0.10.0): clean at 23d625f and 6c02da5 (fa281c1 changes no script). actionlint and zizmor are not installed on this Mac;.github/workflowsis unchanged from main, and hosted CI runs both.Upgrade fix for 6046657261 (2b8028c):
--own-bundletaken out of install.sh's call, both upgrade tests that expect success failed (6 assertions, install exit 1). install.sh was then put back from a saved copy and compared withcmp./usr/bin/lockf /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1125 tests, 0 failures, 0 skipped, 747 s, at 2b8028c.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: ok on the tree committed as 2b8028c (Sources are unchanged since 8d3fe57)./bin/bash -non every script under /bin/bash 3.2.57, the CI bash 4 grep andshellcheck scripts/*.sh(0.10.0): clean..github/workflowsis unchanged; actionlint and zizmor are not installed on this Mac.Round 16 (the Backstop: the supervisor owns each undo command's limit and signal #50 merge and the round 14 P1), all on fakes. Each focused command used an anchored filter, listed the selected names before the run, passed
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTestsand was checked afterwards against the names that ran./usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1161 tests, 0 failures, 0 skipped, 760.4 s, exit 0. All 1161 names that ran passed, none from the three skipped classes.pmset -g logshows no sleep during the run.testBackstopKeepsTheSleepEntryWhileTheMarkerIsLockedexpected the old pmset order (fixed above).testInstallLeavesAVisudoThatIgnoresSigtermHoldingTheLockAndTheRuleUntouchedtook 197.4 s, and its fake sudo ended by its own 60 s watchdog ("watchdog", not "released").pmset -g logshows a 192 s Clamshell Sleep from 17:11:41 to a DarkWake at 17:14:53, and the case ran from about 17:11:37 to 17:14:54. The fake's watchdog usesdateand install.sh's 5 s limit and TERM grace use bash'sSECONDS, both wall clocks, so all three ran out during the sleep. The assertions that check the run itself passed in that run: fd 9 open, SIGTERM logged, the "still running as pid" message with itssudo killline, "Nothing was changed", and noinstall,-lorlaunchctlcall. Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43's review found a fixture race where a group signal kills the fake'sdateand the fake exits at once. That race would have made install.sh report a stop on SIGTERM, which this run did not. The test is unchanged. An anchored rerun of it passed in 12.8 s.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: ok./bin/bash -non every script under /bin/bash 3.2.57, the CI bash 4 grep,shellcheck scripts/*.sh(0.10.0) andgit diff --check: clean. No script changed in round 16.Round 18 (the round 17 findings), all on fakes. No real sudo, pmset or osascript ran, no dialog was shown, and the installed sudo was never run; its version, 1.9.17p2 on macOS 26.2, comes from the installed man pages. Each focused command used an anchored filter, listed the selected names before the run, passed
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTestsand was checked afterwards against the names that ran.testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked. 118 selected, 118 run, 0 failures, 119.0 s. An earlier run of the same set failed 2 end-to-end tests: the new fake sudo looked the app's restore up under the fake pmset's path, while the app always runs /usr/bin/pmset. The fake now maps that path; no assertion changed..greptile/config.json; source fingerprint c44f310027475abc before and after),/usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1162 tests, 0 failures, 0 skipped, 803.0 s, exit 0 (2026-10-08T03:28:23Z to 03:42:12Z).swift test listgave 1229 names; without the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests, 1162 were selected, and the 1162 executed are the same names. None from the three classes ran. The shared lock file kept inode 201732085.rootCommand) against the new one, run as the user under the real lockf with a fake sudo, pmset and clock: 27 rows, all as expected. R1: with no rule, the old command runs root'sdisablesleep 1before the user's sudo, then its fallback 0; the new one runs no pmset. R3: with the clock reaching the deadline, or 1 s or a day past it, during root's secondpmset -g, the old command exits 0 and leaves 1; the new one exits 4 with no write when the clock gets there duringsudo -V,-l,-llorpmset -g. R2: with no rule and another tool's 1 set during the check, the old command exits 5 and leaves 0, or 70 and 0 with stderr closed; the new one exits 5 and leaves the 1 either way. With the rule and a 1 set during-ll, the new one exits 6 and leaves it, also with stderr closed.testARefusalKeepsItsStatusWhenTheDialogsOutputIsGonegets 70 instead of 5 and pmset-g,-a disablesleep 1,-a disablesleep 0instead of nothing;testWritesNothingWhenTheDeadlineComesDuringAnyCallBeforeTheWrite,testAnEndDuringRootsReadWritesNothingandtestASettingMadeWhileSudoIsAskedSurvivesARefusalsee the old writes. Most of the 810 failed assertions there are structural: the fakes countenvanddatecalls the old command never makes, hooks keyed to the new queries never fire, and RootCommandSudoAnswerTests fails its count of the command's awk programs (one in the old command, four in the new) and stops there. The harness rows above are the precise controls.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: ok./bin/bash -non each script under /bin/bash 3.2.57, the CI bash 4 grep,shellcheck scripts/*.sh(0.10.0) andgit diff --check: clean on 576c215. actionlint and zizmor are not installed on this Mac;.github/workflowsis unchanged.Round 20 (the round 19 findings), all on fakes. No real sudo, pmset or osascript ran, no dialog was shown, no plugin was built or loaded, and no real sudoers, sudo.conf or PAM file was read. The tests put private fixture files in place of /private/etc/sudo.conf and /private/etc/pam.d/sudo and check that the command names each path exactly once. Apple's sudo-114.100.11 source was downloaded and read, not built; its hashes and the cited lines are in the evidence manifest. Focused commands used anchored filters, listed the selected names first, passed
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTestsand were checked against the names that ran.testBackstopVoidsTheDialogOfAStartThatDiedBeforeItUndoesAnything. 199 selected, 199 run, 0 failures, 143.6 s./usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1179 tests, 0 failures, 0 skipped, 857.8 s, exit 0. 1179 selected (1246 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests) and the same 1179 run; none of those three classes ran. Against round 18's 1162, 3 names were removed and 20 added.rootCommandin the same end-to-end fixtures (scratch tests, not committed). With the silent approval plugin's sudo.conf, and with the review's user Defaults, it turned sleep off (-g,-g,-a disablesleep 1), and the end's restore then failed with status 1, leaving SleepDisabled 1 and the journal entry. The new command refuses both before any pmset (testASudoConfStopsTheStartBeforeSudoIsAsked,testUserDefaultsTheCheckDoesNotAcceptStopTheStart)..untouchednever returned: 5 of the 8 F3 tests fail. The file not compared, content only: only the unit test failed, so the lifecycle tests did not cover a replaced marker.testAFailureWithAReplacedMarkerIsUndoneEvenWhenItHoldsTheNonceandtestAFailureWithNoMarkerLeftIsUndonewere added, and the first fails under that mutation.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: exit 0 on c5456f8 (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1)./bin/bash -non each script under /bin/bash 3.2.57, the CI bash 4 grep,shellcheck scripts/*.sh(0.10.0),git diff --checkandosacompileof the dialog script (compile only): clean. No script changed this round.Round 22 (the round 21 findings), all on fakes. No real sudo, pmset, osascript, install.sh or uninstall.sh ran, nothing was written under /private/var/db, no dialog was shown, and no system sudoers, sudo.conf, PAM or account file was read. Every receipt is a file in a test's temporary folder. Focused commands used anchored filters, listed the selected names first, passed
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTestsand were checked against the names that started and ended.ProcessExit, as Process: wait for children with an exit handler, not waitUntilExit #46 asks, notwaitUntilExit()), and the three changed install tests: 239 selected, 239 run, 0 failures, 53 s.testInstallWritesExactlyThreePasswordlessLinesAndNoneTurnsSleepOffandtestReinstallOverFourLineRuleLeavesThreeLines: they counted everysudo -n /usr/bin/installcall as the sudoers file's, and install.sh now makes a second one, for the receipt. They now require exactly one install of the sudoers file, as before, and the receipt's as the only other.testInstallWithNoAppRunningStopsBeforeTheBundleWhenSudoersRuleIsNotEffective, which looked for any install call, now names the sudoers file and checks that nothing touched the receipt before the rule's check. All three ran again in the next focused run./usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1223 tests, 0 failures, 0 skipped, 888.0 s, exit 0 (09:30:52Z to 09:45:41Z on 2026-10-08). 1223 selected (1290 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), the same 1223 started and ended, none from those three classes. Against round 20's 1179: 5 removed, 49 added. The lock file kept inode 201732085 before, while lockf held it (lsof of lockf's descriptor) and after.testAFailureAfterTheWriteIsUndone. The same-inode forgery istestAForgedMarkerDoesNotDropTheRestore: the marker holds the bare nonce again in the same inode, and the start is still undone, with pmset-g,-g,-a disablesleep 1,-a disablesleep 0, a clean journal and no "never turned sleep off" notice. The two relaunch probes aretestARelaunchDoesNotResumeAnUnexpiredStartOnAnotherToolsSettingandtestARelaunchDoesNotRestoreAnExpiredStartThatNeverTurnedSleepOff. Each asserts what should now happen, where the probes asserted the unsafe outcome.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: exit 0 on bd7db43, a clean tree (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1)./bin/bash -non each script under /bin/bash 3.2.57, the CI bash 4 grep,shellcheck scripts/*.sh(0.10.0),git diff --checkandosacompileof the dialog script (compile only): clean on bd7db43, which changes backstop.sh, install.sh and uninstall.sh.git diff --checkwas clean from c5456f8 and from main.osacompileran insidetestScriptCompilesin the full suite, which passed and did not skip..github/workflowsis unchanged from main; actionlint and zizmor are not installed on this Mac.Round 24 (the round 23 findings and the Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43 merge), all on fakes. No real sudo, pmset, osascript, perl as root, install.sh or uninstall.sh runs, and no test writes under /private/var/db. The fake perl records each line and can fail each step of the write (open, short write, F_FULLFSYNC, close, read-back) or be missing. Each focused command used an anchored filter, listed the selected names before the run, passed
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTestsand was checked afterwards against the names that started and ended.SleepOffReceiptsTests(the claim, the receipt lock, the verdict over line, predecessor and time,dialogOver, two starts claiming one line, a never-write that cannot be published, a missing receipt kept recorded), 12 inAdministratorPromptTests(the receipt lock and exit 75, the device:inode check, the predecessor check and exit 8, every perl fault, the 130 s window and the 15 s wait, a signal the runner did not send) and 20 inRecoveryScriptTests(backstop.sh and uninstall.sh: the lock, the claim, a dialog that can still be answered, a later line, a replaced receipt, a never-write that cannot be published, the owed restore, andCAT/HEADwith a hostilecatandheadfirst on PATH). 16 tests whose names stated the round 23 rules were replaced.sudo /bin/rmalso matched the expectedsudo /bin/rmdir; the test now looks forsudo /bin/rm -f. Two were uninstall.sh reading an unset$2insettle_stop; bash 3.2's EXIT trap turned that abort into exit 0. Fixed in the script. d3: those 3, 0 failed.pmset -g). d5: those 4, 0 failed.PrivateDisplayGuardTests); my wrapper printed the failure but returned 0, and I first read it as a pass. Fixed in 15c2fc3 and rebuilt: exit 0.EarlierBootLowPowerClaimTests, which seeded a still-valid session withoutSleepDisabled 1; changed in e741a7f (Merged main).EarlierBootLowPowerClaimTests,LidCloseCopyTestsandReleaseWorkflowTests, 24 tests, 0 failures, exit 0./usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1402 tests, 0 failures, 0 skipped, 1221.3 s, exit 0 (14:09:20Z to 14:29:42Z on 2026-10-08). 1402 selected (1469 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), 1402 started and ended, no duplicates and none of the three classes run. Clean tree, source fingerprint ffd53d415a63c777 before and after; swift test was lockf's direct child, and the lock file kept its inode. Other sessions' focused runs used the machine during this run outside the shared lock; they were not stopped. The 1223 passes on bd7db43 are proof for that tree only.swift build -c release -Xswiftc -warnings-as-errors,scripts/check-lid-simulation-gate.sh,/bin/bash -non every script under /bin/bash 3.2.57,shellcheck scripts/*.sh(0.10.0),git diff --checkand a JSON parse of.greptile/config.json: exit 0 on e741a7f, a clean tree, and no findings from the checks.Round 26 (the round 25 findings), all on fakes. No real sudo, pmset, osascript, perl as root, install.sh or uninstall.sh runs, and no test writes under /private/var/db. Each focused command used an anchored filter, listed the selected names before the run, passed
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTestsand was checked afterwards against the names that started and ended; none of those three classes ran.SleepOffSettlementTests(7): a crash after the decision was journaled, with the claim still held or already given back, followed by zero, one or two laterrefusedstarts of another folder, for never-wrote with nothing owed, never-wrote behind an owed restore, and may-have-written; a claim that cannot be given back, for never-wrote and may-have-written; a settled record that cannot be removed; a rolled-back start whose claim cannot be given back; a claim that cannot be written before the dialog; and a busy receipt across all four combinations of an expired or open window and an owed or clear restore, with Low Power Mode, frozen processes and the volume still undone.RootCommandTests(4): another tool's 1 set while the record is written and flushed, and the second read failing, through both copies of the command; arefusedover the record that cannot be written after the second read (the limit, below); a journal-owned 1 that skips both reads; and the clock read again after the second read, at the deadline's second and one before.RecoveryScriptTests(9): backstop.sh finishing a settled record (claim held or given back, behind one or two later lines) and uninstall.sh finishing one behind two later lines; backstop.sh keeping a settled record it cannot finish (release file not as install.sh made it, immutable state.json, receipt locked) with nothing held back; backstop.sh holding the sleep undo while the receipt is locked across the four window and owed combinations, and while the dialog can still be answered with a restore owed;prepare_low_power_offwith a hostileheadfirst on PATH beside the ordinary PATH; and install.sh keeping a claim taken while it waited for the lock (receipt made just now and made before), keeping a claim right after it made the receipt, stopping at a claim in an unsafe file, rewriting a release file with no claim, and stopping when the release file changes between the read and the write.testBackstopRunsTheRestoreAnEarlierSessionOwesWhileTheDialogCanStillBeAnsweredis nowtestBackstopHoldsEvenAnOwedRestoreWhileTheDialogCanStillBeAnswered.testBackstopSettlesAStartOnlyOnceTheCommandBehindItsDialogIsDone,testALockedReceiptKeepsTheStartRecordedUntilItIsLetGoandtestARelaunchKeepsTheRestoreAnEarlierSessionOwesexpected the early undo R25-2 removes; they now expect no restore while the command may act and the restore after the lock is let go. Every root command call list gains the second-g, run as root. The tests that show another tool's 1 being cleared (testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWrite,testASettingMadeRightAfterTheCommandsReadIsClearedOnlyWhenTheWriteFollows,testWhereAnotherToolsOneLandsDecidesWhetherItSurvives) now set that 1 after the second read; they describe F7 and are not guards.swift test --skip-build --list-tests, which gave it no list, and the runner went on with an empty selection instead of stopping; the filter, anchored only at its start, ran 37 tests ofBackstopVersionTests,SleepOffReceiptsTestsandSleepOffSettlementTests(none of the three skipped classes), 4 failed (7 assertions), all expectations of the early undo or the single-g. Discovery then moved toswift test list --skip-buildwith the three skips; it still lists those classes, and the selection drops them.-gand its thirdrootin the call lists; the F7 description tests, whose 1 set after the first read the second read now caught (exit 6, the 1 stayed), so they now set it after the second read; the timed-out dialog's message for the R25-2 hold; and one assertion of a new test. d3, the same classes: 193 tests, 0 failed, 179.4 s.RecoveryScriptTests, 309 tests, 306 passed and 3 failed (14 assertions), 1083.5 s.testBackstopKeepsTheSleepEntryWhileTheMarkerIsLockedmissed the second-g;testBackstopRestoresSleepAndKeepsTheStartWhenTheReceiptOrItsFolderCannotBeTrustedexpected an unsettled record where a lower-case nonce case now leaves a settled one; both test fixes.testTheSupervisorOutlivesItsRunAndGroupSignalsAndHoldsTheLockUntilItReapsTheCommandfailed once at 26 s (one SIGTERM, then its watchdog); this round changes no supervisor code, and it passed in d5 (6.5 s). d5: those 3 and the fixture probe, 4 tests, 0 failed.LoginItemTests,LidCloseCopyTests,PrivateDisplayGuardTests,ReleaseWorkflowTests,BackstopVersionTestsandSleepOffReceiptsTestsafter the docs, 63 tests, 0 failed.ScriptFixtureinit about 0.023 s, the app journal about 0.005 s, teardown about 0.003 s, against 2.5 to 3.5 s per uninstall.sh run and 2.2 to 3.0 s per backstop.sh run. One traced backstop.sh run executed 1226 trace lines, 73 of themplutil, 14 fakedateand 5 0.1 s polls: the cost is the scripts' own work, not the fixtures. Preparing fixtures once would save about 0.03 s per case (about 0.7 s for the 24-case matrix), so no fixture change was made. One change: the new locked-receipt test gives the backstop'slockfno wait (-t 0) while the test holds the lock, 18.5 s to 8.9 s, with the same assertions. The round adds 20 tests, about 50 s locally. The hosted job already exceeded its 1200 s watchdog on e741a7f, so it is at risk of doing so again; no timeout or matrix was changed./usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1421 tests, 1420 passed, 1 failed (6 assertions), 0 skipped, 1546.9 s, exit 1 (17:24:26Z to 17:50:13Z on 2026-10-08). 1421 selected (1488 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), 1421 started and ended, no duplicates and none of the three classes run. Clean tree, source fingerprint 15abc0f993dca810 before and after; swift test was lockf's direct child, and the lock file kept its inode. The failure istestValidKeptDisplayRecordsAreKeptForTheApp, a display-record test from main whose test code and fixture this round did not change: in two of its cases the fixture's 1 s command limit (COMMAND_TIMEOUT_SECONDS=1) ended the fakesudo pmset -a disablesleep 0with SIGTERM, so backstop.sh kept the journal dirty and exited 1. It took 31.6 s against 19.2 s on e741a7f; the load average was about 5.6 with XprotectService near 50% CPU, and the Mac did not sleep. Rerun alone on 93fb3dc (same fingerprint, anchored filter, the three skips) once the shared lock was free: 1 test, passed, 20.6 s, exit 0; one failing run in the full and one passing run alone, not labelled a flake. This full is not a clean pass, and no second full was run.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: exit 0 on 93fb3dc, a clean tree (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1)./bin/bash -non every script under /bin/bash 3.2.57, the CI bash 4 grep,shellcheck scripts/*.sh(0.10.0),git diff --checkfrom e741a7f and from main, and a JSON parse of.greptile/config.json: no findings;.githubis unchanged from main. actionlint and zizmor are not installed and did not run.Round 28 (the round 27 findings), all on fakes. No real sudo, pmset, osascript, chmod +a, ACL change, perl as root, install.sh or uninstall.sh runs, and no test writes under /private/var/db. Each focused command passed
--skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests, listed the catalog with those skips (Swift 6.3.1 still lists the three classes, so the runner drops them by name), selected exact anchored names, stopped on an empty or unmatched selection, and was checked afterwards against the names that started and ended; none of those three classes ran.RecoveryScriptTests): another folder'swritingandrefusedstarts with a held claim, which that folder's backstop then settles through the kept rule before the uninstall finishes; a locked receipt; a damaged, other-nonce, unreadable or folder release file, a damaged receipt and a group-writable folder; a claim that appears while uninstall waits for the lock; a receipt replaced under the lock; a release file that changes after the check; a lock probe that shows the lock held at the bootout and at eachsudo rm; and a receipt no start can claim, removed only while its release file shows no claim. They replace three tests that expected the shared rule gone after a refusal. R27-3, the receipt's mode and entry (8): the root command (both copies) refusing modes 644, 640, 400 and 700 and 23 other lists with exit 7 and no calls; backstop.sh trusting only mode 600 with that entry, and uninstall.sh treating any other mode or list as a receipt no start can claim; one awk program in all five copies, run against realidfor this user anddaemon; install.sh repairing a receipt it made before and stopping at one it cannot repair; the app's acl(3) reader accepting only that entry; and the Swift reader againstls -ledof real folders that already carry ACLs (read-only, skipped when none is found). R27-4 (3,SleepOffSettlementTests): the settled start's own session resuming while its cleanup fails (claim held or already free, a locked receipt, a read-only or damaged release file, an immutable journal), then cleanup and a new Start; the same beside another folder's later claim; and only the start's own session resuming (extensions, the 899.6 s clamp, another session, 1 s and 2 s offsets, no sleep entry, sleep turned back on, expired). R27-5 (2): backstop.sh reading the session and publishing the journal through its own tools, and uninstall.sh reading a leftover session the same way; the three hostile-PATH tests now require no hostile call at all. R27-7 (3): the exactboundedandsupervisetext of each script with a fake sudo that closes fd 9 and sets its traps before it reports ready, in an owned process group: the main shell killed, then group TERM and HUP (two TERMs and one HUP reach the call, the lock stays held until "sudo exiting" with status 124); a failed status write under errexit; and the call's default signal actions and its stop at the limit. R27-8 (9): a copy made unreadable after its first conversions (the review's schedule, purge and not); five later read failures, including partial output then an error; one of two kept levels unreadable; null and missing keys read as absent; a journal that becomes a FIFO before it is copied; a journal that changes or appears after the check; a failed read for the settlement; the record reader returning 2 in both scripts; and backstop.sh leaving a journal it cannot read whole as unknown. F7 (1):testARefusalWhoseRollbackCannotBeJournaledStaysARefusalInThisProcess.superviseand against a mutant without the call'strap - TERM HUP: fail as expected. The F7 test with the retention removed: fails at the same-process undo; the source was put back by hash and rebuilt.$(...), a fixture helper that wrote the receipt 0600 with no entry, and the stand-in ACL fixture. f5-f8-new-3 (15 selected, 4 failed) and f5-f8-new-4 (1 failed): the new hostile-PATH tests caught a barecatin the fakes and then in an uninstall.sh heredoc, now"$CAT". f8-new-2 (9 selected, 1 failed): a quoting error in the test's failure trigger. f5-f8-new-5 (15 selected, 1 failed, 687.8 s) and f8-fifo-rerun-1 (123.1 s): the FIFO test ran inside maintenance sleeps of 590 s and 122 s (read-onlypmset -g log); it now measures awake time, and f8-fifo-rerun-2 passed. f9-concurrency-1 and -2: CI cost, below. f7f9-broad-1 (580 selected, 577 passed, 3 failed, 2316.1 s):testScriptsAndAppReadTheSameSessionDates, whose harness lacked uninstall.sh's newplutil_read(test fixed);testUninstallAbortsOnMalformedJournal, because this round had put a type read before the whole-journal conversion check, so a malformed journal got the "could not be read whole" message (uninstall.sh order fixed); andtestUninstallTreatsAHungPgrepAsRunningat 170.2 s against 30 s, which ran 18:04:57 to 18:07:48, the span of a maintenance sleep. fix-uninstall-2 then passed 104 of 104 (329.4 s) and the adjacent classes 317 of 317. f7-supervisor-1, f7-supervisor-2 and fix-uninstall-1 stopped before running anything because a selector matched no test.AppEncodedJournalScriptTests' three script matrices (agent, uninstall and the record reader) now build each row's home and environment one at a time, run the rows' scripts at most two at a time, and check each row one at a time afterwards; App Harness work, journal encoding and the global home stay serial. On the same binary in the same hour, the three methods took 108.4 s two at a time (f9-concurrency-3) against 132.4 s one at a time (f9-serial-control-1, a temporary patch): 24 s, or 18%, saved, about half the review's 52 s estimate. Two earlier two-at-a-time runs failed: f9-concurrency-1 during a clamshell sleep and a 743 s maintenance sleep, and f9-concurrency-2 in the uninstall matrix, where one row stopped after twopgrep -x Insomniacalls; an 11 s idle sleep and another session's swift test overlapped it, and the fixture's unchanged 1 s command limit under load is the likelier cause, not proven. Two uninstall rows at once add load against that limit. Fake tool startup is not where the time goes: the logging fakeheadcosts 2.2 ms per call more than the real one and the fake sudo's$(cat)mode read 0.6 ms more thanread, at about 10 to 12 fake calls per 3 to 5 s row. The fake sudo'shang_on_termnow sets its deadline fromSECONDS, sets its trap, and only then writes "ready" and its pid, with no command substitution after the trap. In a Bash 3.2 probe of the old loop with a slowdate, a group SIGTERM sent during$(date +%s)ended the wait at once (5 of 5); with its sleep removed, one SIGTERM was logged twice in 73 of 200 trials. The new loop was released 5 of 5 and logged once in 200 of 200. Those match the two recorded supervisor-test failures (d4's early end and PR Install, uninstall: identify a running Insomnia by path or bundle id, not by name #18's extra TERM) in kind; the probe does not prove they were the cause. PR Backstop: end a valid session when the app is gone, the battery is below the end floor, or heat is critical #34 has a related fixture change; nothing from it was copied./usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1452 tests, 1452 passed, 0 failed, 0 skipped, 1579.4 s, exit 0 (suite 18:43:03 to 19:09:23 PDT on 2026-10-08, after about 15 minutes waiting for the lock behind another session's run). 1452 selected (1519 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), 1452 started and ended, no duplicates and none of the three classes run. Clean tree, source fingerprint b60c89ccae70897f before and after, and the lock file kept its inode. The 34 new tests took 122.7 s; RecoveryScriptTests took 1214.5 s (1166.1 s at 93fb3dc); the three two-at-a-time AppEncodedJournalScriptTests methods took 80.6 s (104.3 s serial at 93fb3dc, a different run under different load). One full, no rerun.swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh: exit 0 on d933b68, a clean tree, no warnings (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1)./bin/bash -non all seven scripts under /bin/bash 3.2.57, the CI bash 4 grep,shellcheck scripts/*.sh(0.10.0),git diff --checkfrom 93fb3dc and from main, and a JSON parse of.greptile/config.json: no findings;.githubis unchanged from main. actionlint and zizmor are not installed and did not run.Round 30, all on fakes (no real sudo, pmset, osascript, install.sh or uninstall.sh). Runs dropped the three classes by name, selected exact anchored IDs and were checked against what started and ended.
RootCommandTests/testAMissingOrUnsafeReceiptStopsBeforeAnyQuestionwhile it still ran/bin/chmod +aand-Non its own temporary files, against this round's instructions; it now uses a fakels. Main'sReconcileTests/testJournalReadableOnlyThroughAnOwnerACLIsStillRestored,RecoveryScriptTests/testBackstopKeepsAnOwnerACLAndStillUndoesTheJournalandRecoveryScriptTests/testInstallFromAPrebuiltAppDropsOtherAccountsWriteAccessButKeepsTheSignatureAndQuarantinestill do that, so no run included them and the full command did not run.shape_ofreader set the globalproblems, uninstall.sh's step 4 array, so every uninstall stopped there as incomplete. 1468950 fixes it and 4 moved expectations; 7e233e8 fixes a new test's expected paths. The 55 then passed.testUninstallStopsAHungDefaultsReadAndFinishes7.8 s in (about 9.3 s normally). Build release and the lid gate were skipped.plutil_on, a fake perl with its reason on stderr); f643d49 fixes them and its 15-test focused run passed. A 980 s Mac sleep inside one passing RootCommandTests case explains the time. Nothing skipped, duplicated, missing or unfinished.bash -n, the bash 4 grep),shellcheck scripts/*.sh,swift build -c release -Xswiftc -warnings-as-errorsandscripts/check-lid-simulation-gate.sh(cached: no Swift source changed since 468db03),git diff --check, a parse of.greptile/config.json: exit 0..githubunchanged.New coverage:
OsascriptAdministratorPromptTests(fake osascript in a temp dir, never the real one): the script literal is exactly the expected text withrootCommandembedded, compiles underosacompile(compile only, nothing runs), and reaches osascript as-e <literal> <marker> <nonce>, with a marker path full of quotes and$(...)arriving intact; the root command's refusal comes back as.failedwith its message; exit 0 succeeds;User canceled. (-128)is.cancelled; a wrong password is.failedwith stderr kept; a missing executable is.launchFailed; at the deadline the child gets SIGTERM only, the runner waits 1.5 s for its TERM handler to finish (longer than the 1 s SIGKILL graceCancellableCommandwould allow) and still reports.timedOut. Review fixes: a child that ignores SIGTERM is reported.stillRunning3 s after the deadline with a live pid, is not killed (kill(pid, 0)succeeds), and its handle resolves only when it exits on its own; a child that dies on SIGTERM while a TERM-ignoring holder keeps its output open leaves a handle whose osascript has exited and which resolves only when the holder lets go. Codex fixes: the deadline starts only after the fake has written a ready file after installing its TERM trap (beforeDeadlinehook), and every holder runs until the test releases it (60 s watchdog), so no test depends on a scheduling window; a new test releases osascript first and checks thatosascriptAliveturns false while the handle is still running, then releases the holder.RootCommandTests(new): the realrootCommand, run as the user under/bin/shwith AppleScript'squoted form ofquoting and/usr/bin/pmsetreplaced by a recording fake. A matching nonce runs-a disablesleep 1and exits 0; a newer start's nonce and an empty nonce run nothing and exit 3, a missing marker exits 69 from lockf; a path and nonce full of quotes, backticks and$(...)match and run nothing else. Second review round: the command runs under the real/usr/bin/lockfwith the literalmarkerLock.testTheMarkerCannotBeRemovedWhilePmsetRunsholds the fake pmset and checks thatremovePendingStarttimes out with the marker in place, then succeeds once pmset is done;testAnAnswerThatWaitsOnARemovalRunsNothingstarts the command while a remover holds the lock and checks it runs no pmset after the removal.PmsetSleepGuardPromptTests:disableSleep(_:)delegates to the prompt with itsPendingStartunchanged; a cancel surfaces as.cancelled; a.stillRunningsurfaces with its handle. Thesudo -npaths are not exercised (they would run the real sudo).SleepPromptLifecycleTests: start shows the dialog once, after the journal and the backstop; cancel and failure roll back with session.json gone, journal clean and the "Session not started" notification naming the prompt; a hung dialog leaves session.json,sleepDisabledByUsand an armed backstop in place while it is up, then rolls back on timeout; reconcile withSleepDisabled 1continues without a prompt; withSleepDisabled 0ends with the "turned back on" notification and no prompt; an unreadable setting ends without a prompt; extend, Low Power changes, lid undo, countdown pause/resume, end and relaunch never show the dialog, and one Start shows it exactly once. Review fix: a stuck prompt (fake pid 4242) is reported with its pid while session.json,sleepDisabledByUsand the recovery lock (checked withRecoveryLock.tryAcquire) stay, an end requested meanwhile runs nothing until the handle resolves, and the rollback then leaves everything clean with onedisablesleep 0.kill 4242is on the menu line while the prompt runs, never in the notification, and gone from the menu line after the exit. Codex fixes: the marker holds the nonce while the dialog is up and is gone after every outcome; it is gone as soon asstillRunningarrives; when osascript exits while its output is held, the menu line drops the kill at once, with no second notification and no rollback yet. Each start writes a fresh nonce. A directory atpending-startrolls the start back with no dialog. Relaunch path: session.json, the journal entry and an orphan's marker on disk with sleep on, reconcile ends the session and deletes the marker, and the real root command with the orphan's nonce then runs nothing; after a newer start has written its own marker, the orphan's nonce still runs nothing. Any transaction (here a Low Power change) clears a leftover marker.RecoveryScriptTests:install.shwrites exactly the three lines and nodisablesleep 1, in onesudo install; a reinstall over a four-line rule leaves three, also in one write. Review fixes:sudo -vis the first sudo call, before the quit. A failed password with the app running stops with no quit and everything untouched; a cancelled password during a session also leaves session.json byte for byte, and the session line is printed before the password step. On a pty (the fixture'sterminalInput), "n" at "Continue?" stops with no sudo call and "y" goes ahead; an expired session asks nothing; without a terminal the line is printed and the install goes ahead. A credential that expires during the quit is asked for once more and the install finishes; a second password that fails after the quit leaves everything else untouched and says the app was quit. An app that keeps running stops aftersudo -vwith no other sudo call and the sudoers file untouched; a rule that is not effective prints the rerun note; an app opened again during the password prompt stops after the rule with three lines on disk, the old bundle in place, and the rerun note; no non-comment line of install.sh mentionsdisablesleep 1. The lock test now expects one more pgrep check. The two key tests fail against 8d57dad's installer, and the five order tests fail with the up-frontsudo -vremoved. Fixture scripts now get /dev/null as stdin instead of the test runner's, so a terminal-runswift testcannot block on the new question. Existing uninstall tests still show the file removed. Codex fixes, backstop path: with the app dead under its dialog and the session expired, the backstop deletes the marker before its first sudo call (the fake sudo records whether the marker existed), restores, logs the deletion, and the real root command with the old nonce then runs nothing; with the session still valid it deletes the marker and changes nothing else; with the lock held it leaves the marker byte for byte. Uninstall deletes the marker before the backstop it runs (a stub that records it), and refuses to remove anything, the sudoers rule included, when the marker cannot be deleted.Second review round, new or changed:
PendingStartRemovalTests(new; a marker and a missing one, waiting for a lock to be let go, a held lock giving.markerBusywith the file kept, an immutable file and a directory reported, a link to nothing removed).testCancelTextInACommandsOutputIsNotACancel(a pmset failure whose output mentions(-128)mid-text is.failed) andtestOnlyCancelAndLaunchFailureRanNothing. Lifecycle:testCancelLeavesASleepSettingSomeoneElseOwns(cancel with a foreignSleepDisabled 1runs nodisablesleep 0),testLaunchFailureRollsBackWithoutPmset,testCancelKeepsAnEntryAnEarlierRestoreLeft(the rollback puts back the journal exactly, including an entry a failed earlier restore left),testRelaunchWhileTheAbandonedDialogsCommandRunsKeepsTheSleepEntry(relaunch while the old dialog's command holds the lock: sleep restored, entry kept, "Restore incomplete", Start refused; after the command is done a reconcile clears it and the menu line),testUndeletableMarkerKeepsTheSleepEntryAndRefusesStarts(chflags uchg),testStuckPromptWhoseCommandHoldsTheMarkerClearsItAfterExit; the cancel test now expects onlydisablesleep 1and the new body. Scripts:testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked(exit 1, entry kept, then cleared on the next run),testBackstopKeepsTheSleepEntryWhenTheMarkerCannotBeDeleted(uchg),testBackstopFailsOnAStuckMarkerEvenWithACleanJournal,testUninstallAbortsWhileTheMarkerIsLocked,testScriptsDeleteTheMarkerWithoutPATH(anrmfirst on PATH that leaves the marker does not stop either script).testTimeoutSendsSigtermOnlyAndWaitsForTheChildToExitsets a 20 s stop grace and accepts.stillRunning(asserting the grace it reports, then waiting on its handle) as well as.timedOut, so a slow TERM handler cannot fail it; with a 1 s grace the new body passes and the old one fails.Second-round mutation checks, one at a time, each failing the named tests: the root command without
lockf(script literal, waits-on-removal, cannot-remove-while-pmset, relaunch tests);lockfwithout-kor without-n;removePendingStartunlinking without the lock (held lock, cannot-remove, relaunch-in-flight, stuck-prompt tests);restoreAllclearing the entry despite a stuck marker and Start not refused (relaunch-in-flight, undeletable); cancel through the undo path (the four cancel and launch tests); no second removal after a stuck prompt exits;(-128)matched anywhere in stderr; backstop.sh and uninstall.sh deleting withoutlockf; backstop.sh clearing the entry despite a stuck marker, or exiting 0 with a clean journal; a barermin either script's lockf command or in the backstop's fallback.Each Codex fix was checked against its tests: with the menu line replacement removed, the stuck-prompt test fails; with the marker deletion removed from
exclusive(), the relaunch and any-transaction tests fail; with it removed from backstop.sh, both backstop marker tests fail.Updated:
ReconcileTestsandReconcileLidGatingTestsnow seedSleepDisabled 1and expectpmset -ginstead ofdisablesleep 1;FakeSleepGuardroutesdisablesleep 1through aFakeAdministratorPrompt(succeed, cancel, fail, hang) so every manager test can see whether a path would have prompted.Third review round, new or changed:
BackstopVersionTests(new, 5 tests): the repository's backstop.sh declares the required version; only the first version line counts; 2 and above pass; an older line, no line and a missing file all refuse, naming the path and "run scripts/install.sh again";LaunchdBackstop.checkVoidsPrompts()checks the script its agent runs.testStartWithAnOlderBackstopShowsNoPrompt: no dialog, no pmset, no backstop arm and nothing written, and the next start with a current script shows the dialog.testStuckPromptWhoseMarkerIsGoneIsRolledBackWithoutWaitinguses an osascript fake that never exits. The start returns with the rollback done and the recovery lock free, andendreturns.restored. The menu keepskill 4242until osascript exits, then names the root command, and the line goes once the prompt exits. A later start shows a new dialog.testVoidedPromptWatcherLeavesALaterLineAlone: a line set later survives the prompt's exit.testStuckPromptWhoseCommandHoldsTheMarkerIsWaitedForreplaces the two earlier stuck-prompt tests and keeps the wait for a command that holds the marker's lock. If a start waits when it should not, the voided tests release the prompt and fail instead of hanging.RootCommandTests:testDoesNothingOnceTheSessionHasEnded(deadlines of one second ago, now and a day ago: exit 4, no pmset) andtestAnUnreadableDeadlineNeverPasses(empty, words,1e12, hex, digits with a suffix, a 20-digit overflow,$(...): exit 4, no pmset).testPasswordTypedAfterTheSessionsEndTurnsNothingOff: a 60 s session answered 90 s later rolls back, with the fake prompt applying the same rule. The start-shows-prompt test checks that the deadline is the session'sendsAt. The osascript argument test checks that 1800000900.9 arrives as1800000900.testSleepRestoreWhoseJournalClearFailsIsReported: an immutable state.json after a successful undo gives.incomplete, the menu line, the "Restore incomplete" notification and the[error]log line, and keeps the entry; the next reconcile clears it.testAnAnswerThatWaitsOnARemovalRunsNothingno longer sleeps 300 ms.waitUntilLockfWaits(under:)waits until lockf is blocked in the kernel on the marker's lock: every thread inTH_STATE_WAITINGand no Unix system call across five looks 10 ms apart. It passed 15 of 15 runs.RecoveryScriptTests:testInstallReplacesTheBackstopBeforeTheBundleUnderTheLock(a fake codesign records whether the installed backstop is the new one and whether the recovery lock is held when the bundle is signed),testInstallWaitsForAnOlderBackstopRunBeforeTheBundle,testInstallStopsBeforeTheBundleWhileAnOlderBackstopRunStays,testInstallStopsBeforeTheBundleWhenBackstopRunsCannotBeListed, andtestInstallCleansUpWithoutPATH. That last one puts shadows for rm, rmdir, mkdir, cp, install, mv, mktemp and cat first on PATH; the cat shadow addsNOPASSWD: ALLto any rule it is given. It checks that the temporary sudoers file goes after a step 2 stop and after a full install, that no shadow is called for the bundle, backstop.sh or LaunchAgents, that mktemp never comes from PATH, and that the installed rule never gets the added line. The fake sudo knows visudo, install, test and rm only by full path.testUninstallFindsAndRemovesARuleOnlyRootCanSee(new, afc4948) covers uninstall.sh'ssudo test -ebranch for a rule in a directory the user cannot search; no test reached that branch before. The lock-refusal and started-again tests now also check that the old backstop.sh is untouched.AppNapTests(App Nap: opt in, journal the previous value and put it back #27) and the strictertestDeadlineTimerFiresEnd(Tests: wait for an end to finish, not start, before checking what it restored #44) assumed reconcile turns sleep off again. Here reconcile readspmset -g, so those tests seedSleepDisabled 1and expectpmset -g, as the other reconcile tests in this PR do (f88e93b). Tests: wait for an end to finish, not start, before checking what it restored #44'stestEndDuringReconcileMustNotLeaveSleepDisabledparked reconcile'sdisablesleep 1on a gate and hung here, because that call no longer exists. It now parks reconcile at itspmset -gread and still checks that the end queues behind it and thatdisablesleep 0comes last (b01a97b). An end that skips the queue fails it.Third-round mutation checks, one at a time; each failed the tests named:
try?back inrestoreAll: the journal-clear test.rmin either trap, or a barecp,mkdir,install,mv,mktemporcat: the PATH test.visudoorinstallhanded to sudo: the install tests, because the fake sudo refuses it and the rule step fails. A baretesthanded to sudo in uninstall.sh, or nosudo testfallback: the new uninstall test. A barermhanded to sudo: that test and 12 other uninstall tests.backstop.sh, the backstop after the bundle, the bundle before the lock: the install-order tests.>= 1,checkVoidsPromptsas a no-op, the repository script at version 1: BackstopVersionTests and the older-backstop test.-lefor-lt, with the test inverted, or in theif [ now -ge $3 ]form that lets an unreadable deadline pass: RootCommandTests.endsAt + 1 h, osascript without the deadline argument, rounding up: the lifecycle and argument tests.ProcessExitswitch. The hang it removes needs a newProcessat the address of an earlier one, which only Process: wait for children with an exit handler, not waitUntilExit #46's ProcessExitTests sets up.Fourth review round, new or changed:
testStartWithoutThePasswordlessRestoreShowsNoPrompt(no dialog, no pmset, no arm, nothing written, the refusal text, then a start with the rule shows the dialog) andtestThePasswordlessRestoreIsCheckedBeforeAnythingIsWritten.PmsetSleepGuardPromptTestsruns a fake sudo from a temp dir, never the real one:testRestoreCheckListsTheExactRestoreCommand(argv is-n -l /usr/bin/pmset -a disablesleep 0),testRestoreCheckFailsWithoutTheRuleandtestRestoreCheckFailsWhenSudoCannotRun.PendingStartRemovalTestsgainstestTheMarkerAStartWroteIsRemoved,testAReplacedMarkerIsNotTakenForTheOneWritten,testAMarkerDeletedWithoutItsLockDoesNotCountAsRemoved,testAFileSwappedInAfterTheOpenIsLockedBeforeItGoesandtestAMarkerThatKeepsBeingReplacedIsReported.testAReplacedMarkerIsNotDeletedWhilePmsetRunsuses the real root command and lockf.testStuckPromptWhoseMarkerWasReplacedIsWaitedForcovers the start. For the scripts,testBackstopLeavesAMarkerReplacedAfterItWasLockedandtestUninstallLeavesAMarkerReplacedAfterItWasLockedpatch in a lockf wrapper that swaps the file right after the lock, andtestBackstopDoesNotOpenAFIFOAtTheMarkercovers a FIFO at the path.testBackstopFailsOnAStuckMarkerWhileTheSessionIsValidandtestBackstopFailsOnAStuckMarkerAfterMovingASessionAside./usr/bin/tail -f <installed path>and can list a--forcerun, and the fake visudo records whether the lock is held and backstop.sh is new. New:testInstallDoesNotWaitForAProcessThatOnlyNamesTheBackstop,testInstallWaitsForAnOlderForcedBackstopRun,testInstallStopsBeforeTheRuleWhenTheAppIsOpenedDuringTheWaitandtestInstallStopsAfterTheRuleWhenTheAppIsOpenedBeforeTheBundle, which replaces the password-prompt test. The lock-held, started-again, retire-timeout and pgrep-failure tests now expect the sudoers file unchanged and no rerun note. The two rule-not-effective tests are renamed to "StopsBeforeTheBundle" and expect the new backstop.sh. The order tests check that the rule comes after the last look and before codesign.pending-startresolves inside the test home.Fourth-round mutation checks, one at a time. Each failed the tests named:
-l, or the wrong command listed: the restore-check tests (and the no-prompt test for the wrong command). The exit status ignored: the missing-rule test.exit 0restored, or the helper exiting 0: the stuck-marker exit tests.--forceform: the forced-run test. No look after the wait, or before the bundle: the reopened-app tests. The sudoers step moved back before the lock: eight install tests.Fifth review round, new or changed:
FakeRestoreToolsputs a fake sudo and a fake pmset in a temp dir; the real ones never run. The fake sudo records its arguments and answers by policy: the rule, listing only (-lpasses and a run needs a password, as with another NOPASSWD entry), a cached credential (passes unless-k), or no rule. The fake pmset prints SleepDisabled 0 or 1, or fails.PmsetSleepGuardPromptTests:testRestoreCheckRunsTheExactRestoreCommandWhileSleepIsOn(onepmset -g, then sudo argv-k -n /usr/bin/pmset -a disablesleep 0; it replacestestRestoreCheckListsTheExactRestoreCommand),testRestoreCheckFailsWhenListingPassesButRunningNeedsAPassword(the fake first lists the command without a password),testRestoreCheckIgnoresACachedCredential,testRestoreCheckRunsNothingWhileSleepIsAlreadyOff,testRestoreCheckRunsTheRestoreTheJournalOwes(no read),testRestoreCheckRunsNothingWhenTheSleepSettingCannotBeRead,testRestoreCheckFailsWithoutTheRuleandtestRestoreCheckFailsWhenSudoCannotRun.SleepPromptLifecycleTestsruns a realPmsetSleepGuardon the same fakes:testStartWithTheRuleRunsTheRestoreBeforeThePrompt,testStartIsRefusedWhenListingPassesButTheRestoreNeedsAPasswordandtestStartIsRefusedWhenOnlyACachedCredentialWouldRunTheRestore(refused, nothing written, no dialog, no arm),testStartWhileSleepIsAlreadyOffRunsNothing(onlypmset -g) andtestStartRunsTheRestoreTheJournalOwesBeforeThePrompt.testCancelLeavesASleepSettingSomeoneElseOwnsbecametestStartLeavesASleepSettingSomeoneElseOwns, because a foreign 1 now stops Start before any dialog: the start is refused, then works once the bit reads 0. The before-anything and earlier-restore tests check thesleepOffIsOursvalue Start passes.ReconcileTests.testNoSessionButSleepDisabledIsLeftAloneAndReportedandRecoverySafetyTests.testLateReconcileMustNotClearNewSessionSleepGuard: a start while the foreign 1 stays is refused and changes nothing, and a start after it reads 0 turns sleep off and clears the menu line.RecoveryScriptTests: the fake sudo handles-kand has a cached-credential mode, and the fake pmset answers-gwith 0, 1, no line or a failure.testInstallWritesExactlyThreePasswordlessLinesAndNoneTurnsSleepOffchecks thatpmset -gcomes before the-k -nrun and that no-lcall happens. New:testInstallStopsWhenOnlyTheCachedCredentialWouldRunTheRestoreandtestInstallRunsTheCheckOnlyWhileSleepReadsOn(1 and a failed read print "not checked" and run nothing; no line and 0 run the check).Fifth-round mutation checks, one at a time. Each failed the tests named:
-k: the cached-credential, listing-only, missing-rule, journal-owed and exact-command tests, at both levels.-lback in place of the run: the same tests.pmset -gread ignored: the two already-off tests. No read at all: those two, the unreadable test, the exact-command test and the with-the-rule lifecycle test. An unreadable read taken as 0: the unreadable test. The exit status ignored: the listing-only, cached, missing-rule and journal-owed tests.sleepOffIsOursalways true: the someone-else-owns, already-off, with-the-rule and before-anything tests. Always false: the earlier-restore and journal-owed lifecycle tests.-k, or-lback: the cached-credential and three-lines tests. The read ignored, an unreadable read taken as 0, or a missing line taken as unreadable: the reads-on test.Sixth review round, new or changed. Everything runs on fakes; no real sudo, pmset or osascript runs:
RootCommandTestsruns the realrootCommandunder the real lockf, as the user, with a fake sudo and a fake pmset. Invoked by root (no uid recorded, or 0), the fake sudo runs the command, as the-uuser if one is given. Invoked by a user, it applies a policy:ruleruns only the exact restore and only for the matching uid,listOnlylists but needs a password to run,cachedruns unless-k,noRulerefuses, andnoRootEntryrefuses root itself. New:testTheCheckRunsTheRestoreTheEndRuns,testRefusesWhenOnlyACachedCredentialWouldRunTheRestore,testRefusesWhenTheRestoreIsListedButNeedsAPasswordToRun,testRefusesWithoutTheRule,testRefusesWhenRootCannotRunTheCheckAsTheUser,testTheCheckIsForTheUserItIsGiven,testAnUnusableUidRefusesWithoutRunningSudoandtestTheMarkerCannotBeRemovedWhileTheRestoreCheckRuns. Each refusal expects exit 5 and nodisablesleep 1. The marker-gone, nonce, deadline and waits-on-removal tests now also expect no sudo call.OsascriptAdministratorPromptTests: the literal and argument tests includeitem 4 of argvand the uid. New:testARefusedRestoreCheckIsReportedWithTheFix,testOtherStatusesAreNotARefusedRestoreCheckandtestOnlyCancelLaunchFailureAndARefusedRestoreLeaveNothingToUndo.PmsetSleepGuardPromptTests:testSleepSettingCheckOnlyReads,testSleepSettingCheckRefusesWhileSleepIsAlreadyOff,testSleepSettingCheckTrustsTheJournalandtestSleepSettingCheckRefusesWhenTheSettingCannotBeReadreplace the fifth round's fake-sudo restore-check tests.SleepPromptLifecycleTests:testStartRunsNoSudoBeforeTheDialog,testStartWhoseRestoreCheckFailsRollsBackWithNothingToUndo,testARefusedRestoreCheckKeepsAnEntryAnEarlierRestoreLeft,testTheSleepSettingIsReadBeforeAnythingIsWritten,testStartWhileSleepIsAlreadyOffRunsNothing,testStartWithAnUnreadableSleepSettingRunsNothingandtestStartWithARestoreTheJournalOwesGoesOn. The fifth round's lifecycle tests on a realPmsetSleepGuardwith a fake sudo went with the app's sudo preflight.sudo -k -n -llisting or avisudothat ignores SIGTERM keeps the lock until it exits and never gets SIGKILL (testInstallLeavesASudoersCheckThatIgnoresSigtermHoldingTheLock,testInstallLeavesAVisudoThatIgnoresSigtermHoldingTheLockAndTheRuleUntouched).Sixth-round mutation spot checks, two by hand and no broader run: without
-kon the user's sudo, 10 RootCommandTests fail, the cached-credential test among them. Without-u "#$4", 15 fail, the no-rule test among them.Review 11:
RootCommandTests.testDoesNothingWhenTheRestoreCheckEndsAtOrAfterTheDeadlineputs a fake clock first on the root command's PATH. Two mutation spot checks: with the recheck taken out of both copies, that test fails in all three clock cases; taken out of the AppleScript copy only, it fails along withtestTheAppleScriptEmbedsTheRootCommandUnchangedandtestScriptIsTheExactLiteral. Afterwardsgit diff HEAD -- Sourceswas empty and the build was redone from the restored source.Upgrade fix for 6046657261, all on fakes. No real install, sudo, launchctl or signal to a real process:
OLD-APPand whose Info.plist has noInsomniaResumeFrozenVersion, with its agent loaded, an expired session and two frozen entries withstartedAtMicros. The new build's Info.plist declares the interface, and its binary records whether the recovery lock is held and then runs the fake responder.testUpgradeOverABuildWithoutResumeFrozenResumesWithTheStagedBinary(source) andtestUpgradeFromAPrebuiltBundleOverABuildWithoutResumeFrozenResumesWithTheStagedBinary(--app): the install exits 0 and the old binary never runs. The staged copy's binary runs once, from the staging folder, with the lock held and the lock file on its fd 9, before the old agent is booted out. Both entries are cleared and logged as resumed by the app binary, the new binary is installed with one bootstrap, and no staging folder is left.testUpgradeKeepsWhatTheStagedBinaryCannotResumeAndThePreviousPair, five cases on fresh fixtures: a failed SIGCONT, an unverifiable process, a malformed answer, no answer in time, and a staged build that does not declare the interface (no binary runs). Each exits 1 with the stop message and the rerun line. The entries the binary did not resume stay with theirstartedAtMicros. The old binary, its Info.plist and the agent plist stay byte for byte, nothing is booted out or bootstrapped, the staging folder is gone and the lock is free.testOwnBundleUsesTheBinaryAndInfoPlistBesideItsCopy: a declaring bundle's own binary resumes the entry, though the installed app here would answer too. A bundle beside it whose Info.plist does not declare the interface runs no binary and keeps the entry with its microseconds.testOwnBundleRefusesACopyOutsideABundlesResources: the checkout's copy, a loose copy, a copy inContents/instead ofContents/Resources/, and a bundle's copy run by a relative path all exit 2, with no binary run, the journal unchanged, no lock file and an empty log. The same bundle copy run by its full path then resumes the entry.testInstallKeepsTheTrustedAgentWhenRecoveryIsUnresolvedandtestInstallQuotesTheCheckoutPathsInTheCommandsItPrintsexpect the installer's rerun command and nobackstop.sh --forceline.Round 16, new or changed. No real sudo, pmset or osascript runs:
RootCommandTestsruns the command read back from the AppleScript literal androotCommanditself. The fake pmset records who ran each call, root or the user's uid, and can set SleepDisabled 1 after any chosen call, as another tool would. A run that passes now makes five calls:-g,-a disablesleep 1as root,-a disablesleep 0as the user,-gand-a disablesleep 1. A refused proof makes three and leaves 0.testEveryPolicyButTheRuleLeavesSleepOn: a cached credential, a listing-only policy, no rule, a rule that needs a password (PASSWD), an explicit deny and a sudoers with no root entry each exit 5 with SleepDisabled 0 and root's restore as the last call, in both literals. Only the three-line rule exits 0.testWhereAnotherToolsOneLandsDecidesWhetherItSurvives: eleven rows put one foreign 1 at one point, through a passing check, a failing check and a deadline that passes during the check. Set while the dialog was up, or after the proof, the 1 survives every path. Set between the first read and root's change, or while that change is in effect, it does not. These rows record the limit; they do not remove it.$5other than exactly "1" read as not owned, the read parsed as Start parses it, root's restore failing (exit 1, not a refusal) and the dialog's output closed before the command runs (testAClosedDialogCannotStopRootsRestore: root's restore still runs).OsascriptAdministratorPromptTests: the fifth argument,testRootRefusalsLeaveNothingToUndo(3, 4, 6, 69 and 75) andtestOtherRootStatusesAreFailuresToUndo.StartOwnershipEndToEndTests(new) runs a realSessionManager,PmsetSleepGuardandOsascriptAdministratorPrompt. Its fake osascript runs the real root command under the real lockf against one fake machine. A 1 set while the password is typed survives the start and a late end. A late password runs nothing. The dialog is told whether the journal owns the 1. Start then end turns sleep off and on. A 1 set after the check survives a late end. An end during the check rolls back with no undo, and so does a failed check. Two cases record the limits: an ambiguous failure still undoes a 1 set while the dialog was up, and a 1 set right after the command's read is still cleared.SleepPromptLifecycleTestschecks the new texts.Round 16 mutation checks. Each mutant was applied to both literals, or to the Swift mapping for the last one, and run against OsascriptAdministratorPromptTests, RootCommandTests, SleepPromptLifecycleTests and StartOwnershipEndToEndTests (98 tests). Every mutant failed tests. The source was put back after each, its SHA-256 matched the original, and the tests were rebuilt.
disablesleep 1before the proof: 44 tests, seven of them end-to-end.-kon the proof: 18..restoreNeedsPasswordundone like an end: 5 tests.Round 18, new or changed. No real sudo, pmset or osascript runs:
listOnly), no NOPASSWD entry, the rule without NOPASSWD, an explicit deny, a later rule without NOPASSWD,Defaults!/usr/bin/pmset log_output,(ALL), an extra tag, aNOTAFTERlimit, a rule from LDAP, a path-only answer, a truncated answer, sudo 1.9.14p3, an approval plugin and no root entry. It records every call and refuses anything the root command never asks, a query without-k -nincluded. A fake env checksenv -i LC_ALL=C, and a fake clock can move after any chosen call.RootCommandTests:testTurnsSleepOffOnceSudoConfirmsThePasswordlessRestore(three queries, one read, one write),testTheQueriesNameTheRestoreTheEndRunsAndRunNothing(exact argv),testEveryOtherPolicyRefusesBeforeAnyPmset(every policy above exits 5 at the first answer that does not fit, with no pmset call, in both literals),testTakesTheRuleUnderEitherNameOfItsFile,testTheQueriesAreForTheUserTheyAreGiven,testWritesNothingWhenTheDeadlineComesDuringAnyCallBeforeTheWrite(R3: at, 1 s and a day past the deadline, during each query and the read),testAJournalOwnedSettingWritesNothingWhenTheDeadlineComesDuringTheQuestions,testARefusalKeepsItsStatusWhenTheDialogsOutputIsGone(R2: exits 3, 4, 5 and 6 with stderr closed, none writing),testAFailedWriteIsNotARefusal,testLeavesASleepSettingMadeWhileSudoIsAskedandtestASudoRefusalLeavesASettingMadeWhileSudoIsAsked(R1 and R2),testTheMarkerCannotBeRemovedWhileSudoIsAsked, and the eleven-rowtestWhereAnotherToolsOneLandsDecidesWhetherItSurvives, where a 1 set before root's read now survives every path and only one set between the read and the write is taken for Insomnia's.RootCommandSudoAnswerTests(new) runs each awk reader alone. The version reader takes 1.9.15 to 1.9.x with the sudoers plugins in order, and refuses 1.9.14p3, 1.10 and 2.0, other plugins, root's long answer, a missing grammar line, a repeated or misordered plugin line, a trailing blank, CRLF and nothing. The listing reader refuses the bound-Defaults section. The rule reader takes only the six lines, under either file name, and refuses PASSWD, another option, another run-as user or group, pmset with any arguments, two commands, another matched command, another file, a Timeout line, spaces for the tab, CRLF, a second entry, an extra or missing Matched line and nothing.StartOwnershipEndToEndTestsrewritten for the new order: start and end, a 1 set while the dialog is up or while sudo is asked is left alone, a late password and an end during the queries or the read write nothing, a refused query leaves sleep on with no undo (no rule, a later rule, an old sudo), and a 1 set right after the read is cleared only when the write follows.testAClosedDialogCannotStopRootsRestore) now expect no write before the queries pass. Those expectations described the unsafe order R1 removes. Lock, settlement, deadline and upgrade assertions are unchanged;testBackstopKeepsTheSleepEntryWhileTheMarkerIsLockedand the relaunch test hold the command at its write and expect its two pmset calls (-g,-a disablesleep 1); their sleep-entry and marker-lock assertions are as before.Round 20, new or changed. No real sudo, pmset or osascript runs:
Plugin ..._approvalline, an approval plugin with noshow_version, its-V,-land-llanswers stay those of the rule and only running the restore fails, assudo.cdoes (testTheSilentApprovalFixtureChangesNothingButTheRestore). A newuserDefaultspolicy lists the review'slog_output,!ignore_iolog_errorsandiolog_dirand fails the restore.RootCommandTests:testAnySudoConfStopsTheCommandBeforeSudoRuns(the plugin, an empty file, comments only and a setting with no plugin, in both literals: exit 5 with no sudo and no pmset call),testOnlyMacOSsOwnPamSessionLinePasses,testTheMarkerTakesTheRecordOnlyAfterEveryCheck,testTheRecordIsWrittenInPlaceAsTheUserandtestAMarkerThatCannotTakeTheRecordStopsBeforeTheRead(7).RootCommandSudoAnswerTests: the version reader takes only 1.9.17p2 with grammar 50 and the sudoers plugins. The listing reader takes macOS's own Defaults and each listed entry, and refuses 29 other answers: the review's Defaults,log_output,!ignore_iolog_errors,iolog_dir,logfile,use_pty,requiretty,rootpw,!authenticate,group_source,preserve_groups,runas_default, bound Defaults, a list operator on a flag, a backslash, an escaped comma, a tab, an unknown or upper-case name and layouts it cannot read.PendingStartRemovalTests.testOnlyTheFileTheStartWroteStillHoldingItsNonceIsUntouched.SleepPromptLifecycleTests: a failure before the record leaves a 1 set meanwhile, and one after it is undone; a replaced marker holding the nonce, and no marker, are undone; a stuck prompt whose command exits before its record is rolled back with no undo, and one whose marker held the record is undone.StartOwnershipEndToEndTests: a command stopped by SIGTERM while sudo is asked (lockf 70) leaves another tool's 1; one stopped at root's read, after its record, is undone and clears it (the limit); a sudo.conf and the user Defaults stop the start before any pmset.testAnAmbiguousFailureStillUndoesASettingMadeMeanwhileis nowtestAnAmbiguousFailureAfterTheRecordStillUndoesASettingMadeMeanwhile, and the 1.9.15-and-later and bound-Defaults-only reader tests are now the 1.9.17p2 and Defaults-list tests. The test interrupt hook ispkill -TERM -a -P, because macOS pkill leaves out its own ancestors without-a.Round 22, new or changed. No real sudo, pmset, osascript, install.sh or uninstall.sh runs, and no test writes under /private/var/db:
SleepOffReceiptsbuilt with the test user's uid trusts it; the root command and the scripts trust it only in private copies whose-v o=0,RECEIPTSandRECEIPT_OWNERlines are patched. The fake sudo maps install.sh'sinstall,mkdir,rmandrmdirof the receipt's fixed paths to that folder. Unsafe states are made with chmod, a second link, a symlink, a replacing rename and an allow ACL entry; publication failures with an immutable flag (chflags uchg) and read-only folders.SleepOffReceiptsTests(new): the verdict follows the nonce and the word (testTheVerdictFollowsTheNonceAndTheWord); a receipt that is another file now, with the same bytes, shows nothing (testAReceiptThatIsAnotherFileNowShowsNothing); so does every receipt or folder the checks do not trust: missing, not 45 bytes, a malformed line, a second link, a symlink, group or other write, an allow ACL entry, a folder that is a link or writable by others (testAReceiptOrFolderTheChecksDoNotTrustShowsNothing);SleepOffReceipts.livetrusts uid 0 alone, and a receipt the user owns is refused (testOnlyRootIsTrustedAndAReceiptTheUserOwnsIsNot); only a plain absolute folder is accepted; identities are whatstatprints.SleepOffSettlementTests(new), each through the real SessionManager with fake commands: a relaunch with an unfinished start does not resume its unexpired session on another tool's 1, and does not restore an expired one that never turned sleep off; it keeps the restore an earlier session owes; it undoes a start whose receipt showswriting, and one whose evidence does not match (the receipt replaced, the marker replaced or already gone, the receipt missing); it settles a start that showed no dialog as never turning sleep off; it does not resume a session beside a marker no journaled start accounts for, with the plain session as its control. A settlement that cannot be written ends the session and refuses Start until it can (a read-only home, then an immutable state.json, each released and settled afterwards). An earlier start'swritingdoes not count for a new start; a replayed dialog writes nothing; Start refuses a receipt the user owns under the shipped trust; a normal start and end leave no record. The matched pair:testAFailureAfterTheWriteIsUndoneandtestAForgedMarkerDoesNotDropTheRestore.RootCommandTests:testTheReceiptTakesTheRecordOnlyAfterEveryCheckAndTheRead(install.sh's content while sudo is asked and while root reads, this nonce withwritingfrom the write on, the marker unchanged),testTheReceiptIsWrittenInPlaceAndTheMarkerNeverIs(same inode, 45 bytes, no write as the user),testAReceiptThatCannotBeWrittenStopsBeforeTheWrite(exit 7 after the read, no pmset write),testAMissingOrUnsafeReceiptStopsBeforeTheRead(exit 7, nothing read or written),testTheShippedCommandTrustsOnlyRootsReceiptandtestOnlyAnUppercaseUUIDNonceReachesTheReceipt. Every other test reads the command's record from the receipt where it read the marker.PendingStartRemovalTests.testTheFileThatGoesIsReportedByIdentityAlonereplacestestOnlyTheFileTheStartWroteStillHoldingItsNonceIsUntouched: the removal reports which file went, and no longer what it held.RecoveryScriptTests(backstop.sh version 3): it settles a start whose receipt shows no write, or this start'srefused, keeps an earlier owed restore, leaves a session that is not the start's, undoes a start whose receipt showswriting, is another file, or cannot be trusted, undoes a start whose marker is not the one it wrote, settles a start that showed no dialog, waits while the command behind the dialog holds the marker, and restores sleep, keeps the start and exits 1 when the settlement or the session's removal cannot be published. uninstall.sh settles before its backstop, removes the receipt and the folder through sudo, stops with "Nothing was removed; rerun." when the settlement cannot be published, leaves a folder someone else could change, and keeps the folder while another account's receipt is in it. install.sh creates the receipt through sudo by its fixed paths, keeps one it made before, and stops at a receipt or folder it did not make without changing its owner or mode.testBackstopVoidsAnAbandonedDialogEvenWhileTheSessionIsValidasserted that the valid session stays beside a marker no journaled start accounts for. It is nowtestBackstopEndsAValidSessionBesideAMarkerNoJournaledStartAccountsFor(the session goes and the entry is undone), withtestBackstopLeavesAValidSessionWithNoMarkeras the control that keeps the old outcome.testACommandStoppedBeforeItsRecordLeavesASettingMadeWhileTheDialogWasUpgains the read as a stop point, andtestACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWritenow stops the command at its write, since the record follows the read; it still asserts the undo and the cleared 1. Notices that said the command "never reached the sleep setting" now say the receipt shows it never turned sleep off. RootCommandSudoAnswerTests counts 6 awk programs (the receipt check added one). BackstopVersionTests requires version 3 and refuses 2. Two install tests that counted everysudo -n /usr/bin/installcall now require exactly one for the sudoers file and the receipt's as the only other.Decisions
osascript child, not NSAppleScript in-process. AppleScript is main-thread only. A dialog waited on from the main actor would freeze the menu bar, the lifecycle queue (which holds the recovery lock) and every timer for as long as the dialog is up, with no way to time out, and a late answer after a rollback would turn sleep off with a clean journal. A child can be waited on from a background queue and signalled at the deadline. The cost is that the dialog's app name may read as osascript; the
with prompttext states what Insomnia is doing, and the hardware row asks the maintainer to check how it reads.SIGTERM only, a bounded wait, and the lock stays with the command. The command osascript runs after authentication is a root pmset that a SIGKILL could not reach, so nothing is ever killed. The runner answers the caller 3 s after the deadline instead of blocking on pipe EOF, carrying the pid and a handle. The manager does not release the lock on that answer: the command may still turn sleep off, and a backstop or reconcile that ran beside it would clear a journal the late pmset then contradicts. So the transaction keeps the lock and session.json (third round: unless it deleted the marker under the marker's lock first, below), tells the user what is running (a pid they can
killonly while it is osascript's own; once osascript has exited the pid may be reused and the holder is a root command), and rolls back after the exit. Other transactions queue behind it rather than being refused with a reason; Sleep guard: never SIGKILL sudo pmset; keep the lock while it runs #22 adds that refusal path and the lock hand-off forsudo pmset, and the maintainer can routestillRunningontoCommandStillRunningErroronce both are in. This PR does not touchCancellableCommand.120 s limit. The user is typing a password; shorter limits would cut off a slow reader, and the journal and armed backstop already protect the interval.
Relaunch never prompts. With
SleepDisabled 0the session ends and says why, rather than prompting with nobody at the keyboard. A failedpmset -gread also ends it (the existingrecoveryUnavailablereason, whose notification now covers "or the sleep setting").No other start path exists. The only caller of
start(duration:)is the Enter key in the menu bar. There is no auto-start, URL scheme, scheduled start or command-line start, and launch at login only reconciles. If one is added later it must not reachsetSleepDisabled(true); the protocol comment andtestOnlyStartShowsThePromptsay so.Failed installs leave the rule in place, as before. With this change the leftover lines can only turn sleep back on or toggle Low Power Mode on battery, and an older LaunchAgent still loaded after a failed install needs them to recover. Removing the file would also need a second
sudoprompt. SECURITY.md says the rule stays after a failed install.Password, then quit, then the rule, then the bundle. The password-first step is the maintainer's design after the third review: with the quit first, a cancelled password ended a running session and upgraded nothing.
sudo -vasks before the app is touched; the rule is then written on the cached credential, with one more prompt only if the up-to-15 s quit outlasted it. The session line and the "Continue?" question come before the password, follow Install: build the sudoers rule from the account, show it, and confirm #20's[[ -t 0 ]]andread -r -pstyle, and are skipped without a terminal so scripted installs still run. Earlier history: quit first, then the rule, then the bundle, as the maintainer suggested after the second review. The first review fix kept the rule first and put the olddisablesleep 1line back when the app refused to quit; Greptile flagged that as re-granting the exposure this PR removes, and it did. Now the installer never writes that line. A refused quit changes nothing. A failed second password after a successful quit leaves the old bundle and its rule as they were, so the old build can be opened again; the message says the app was quit. A stop between the rule and the new bundle fails closed: an older build cannot start a session until the rerun, and the note says so with the command. The app is checked again after the password prompt, because the prompt is now between the quit and the bundle and the app may have been reopened. Fourth round: the rule now comes after backstop.sh and the retire wait, still under the lock (below).FakeSleepGuardkeeps its call strings (disablesleep 1,disablesleep 0), so the other workers' tests that readcallskeep applying; its methods follow the protocol split.A pending-start marker with a nonce, checked by the root command, voids a dialog that outlived its start (Codex P0). The app cannot close the dialog when it dies: osascript is reparented and keeps running, and a SIGKILL would not reach the root command. The root command cannot read the journal without a JSON parser. A file that only lock holders delete ties the dialog to the recovery lock instead: the start holds the lock for the whole dialog, so whoever else holds it knows no start is waiting and may void any dialog still on screen. A nonce rather than mere presence means an old dialog cannot act on a newer start's marker. The marker path and nonce are positional parameters of fixed command text, so nothing reaches the root shell as code. The sudoers rule keeps its three lines.
A lock on the marker instead of a second read and compensation (second review round, a deviation from the suggested re-check). Both reviewers showed the second read had its own failure modes: its
disablesleep 0could fail with nobody told, a faileddisablesleep 1skipped it, and an old command that found a newer start's nonce turned sleep back on in that session. Checking that compensation worked would still leave a window between pmset and the check. Withlockf -k -naround the check and pmset, and every deleter taking the same lock before it unlinks, there is no window: the marker goes before the check (nothing runs) or after pmset (the journal still covers it, and the same recovery restores sleep). An old command cannot reach a newer start's marker either, because a start writes its marker only after its own transaction removed the old one under that lock.lockfis a fixed path with fixed flags, the marker path reaches it throughquoted form of, nothing read from a file is evaluated, and the sudoers rule keeps its three lines.An unremovable marker is reported, not worked around. The app and the backstop restore sleep anyway, because a Mac that cannot sleep is the worse outcome, but keep
sleepDisabledByUswhile the file is there so a late answer to the old dialog still has a journal entry behind it. New starts are refused while it is there, because a start may write its marker only after it removed the previous one under that lock, which is what keeps an old command away from a newer nonce; every transaction and every backstop run retries, so the state clears on its own once the command exits or the flag is removed.Definitive and ambiguous prompt failures. Only a cancel and a launch failure prove nothing ran as root, so only they get the exact rollback without pmset. A cancel is recognised only when osascript's trimmed stderr ends in
(-128), the formdo shell scriptgives a cancelled dialog; a command whose output merely contains that text is a failure. A wrong password, a timeout and a pmset failure keep the undo path, because pmset may have run.10 s for the marker lock, matching the recovery lock. pmset returns in well under a second; a longer hold means a stuck root command, which is reported rather than waited on.
Splitting
setSleepDisabled(Bool)intodisableSleep(PendingStart)andenableSleep()makes a marker part of the call's type, so a later caller cannot reach the dialog without one.A voided prompt does not hold the recovery lock (third round, maintainer direction). The lock rule is that the lock stays while a command that may still change something runs. Once
clearPendingStart()has removed the marker under the marker's own lock, the root command cannot reach pmset. If it has not started,lockf -nexits 69. If it was waiting on that lock, it gets the lock on the unlinked file, reads nothing at the path and exits 3. Keeping the lock and session.json for a process that can change nothing would only block ends and the agent, so only the menu line keeps following it. If the removal fails because the command holds the lock, it may be in pmset, and the old wait applies. The rule text in.greptile/config.jsonsays this.The deadline is
$3, not marker content. Both come from the app running as the user, so neither is more trusted.$3keeps the marker a bare nonce (its format, its two script readers and thehead -c 64read stay as they were) and adds oneifto the root command, so the change stays small and the maintainer's fallback (the app voiding the marker at the deadline) was not needed.[ "$(/bin/date +%s)" -lt "$3" ]fails closed: a$3it cannot compare is an error, and the!turns that into a refusal. Rounding down makes a refusal come at most a second early, never late. The app still ends a session at its deadline as before; this check only covers a password typed after it.A version line read before every Start, plus install order (third round). The app cannot tell an old backstop from its behaviour, so the script states the contract it implements in a comment line bash ignores. Reading it costs one file read before a dialog the user waits on anyway. install.sh replaces backstop.sh under the recovery lock before the bundle, so the only way to end up with the new app and an old script is an install that stopped before the script, and that app refuses Start. The wait for old runs closes the window in which a run that started before the swap still executes the old code from its own inode. launchd starts one run at a time, and an old run holds the recovery lock for at most its 10 s timeout plus its undo, so 30 s is generous; a run still there after that is reported, not killed. With Backstop: run only the copy sealed in the signed bundle #28, which seals backstop.sh inside the bundle:
checkVoidsPrompts()readsLaunchdBackstop.scriptPath, which Backstop: run only the copy sealed in the signed bundle #28 keeps (computed from the bundle), so the check follows the agent's script without changes. Backstop: run only the copy sealed in the signed bundle #28's install.sh already replaces the bundle under the recovery lock, so this PR's backstop step becomes part of that bundle copy when the two are merged, and the version line travels with the script. The retire wait still applies to runs of the old copy under Application Support.The handshake reads kernel state instead of adding a hook to the root command. lockf blocks in the open(2) with O_EXLOCK after it has resolved the path, and its descriptor is not visible while it waits, so the test cannot look for an open file. It finds the lockf child by
proc_pidpathand waits until every thread is waiting and its Unix system call count stops moving. The command text stays exactly what runs as root.The restore check runs the restore (fifth round, as the maintainer directed). Only running a command shows whether sudo would ask for a password.
sudo -lsays whether a command is allowed, and it lists without a password whenever any NOPASSWD entry exists; a cached credential passes both.-ktakes the cache out, so exit 0 comes from the sudoers policy alone. The run changes something only while SleepDisabled is 1. A 1 the journal owns is what the next end or backstop.sh run restores anyway, so the check runs it. A 1 nobody journaled belongs to someone else, and the design keeps it: a cancel runs no pmset, and reconcile step 3 only reports it. So Start readspmset -gfirst and refuses with the command that clears it, instead of guessing. No code parsessudo -loutput. Its format cannot be checked here without the real sudo, and a wrong guess would refuse every Start or pass a rule that needs a password. Round 18 reverses this inside the root command, with the format taken from sudo's source and a refusal on anything else (below).Path identity rather than a new lock (fourth round). lockf locks a file and rm goes by path, so the deleters now prove the path still names the locked file. The scripts lock through fd 8, because
lockf <path>closes its descriptor when it exits and leaves nothing to compare. They read the locked file's identity withstat <&8, becausestat /dev/fd/8reports the fdesc device instead of the file's. The maintainer asked for an equivalent in the scripts that keeps the fixed-path rule: this uses only$LOCKF,$STATand$RM. The one thing the scripts cannot check is in Not covered.Rule after the retire wait, not a put-back (fourth round). A timeout used to leave the new rule beside the old app. Putting the old rule back on a timeout was ruled out, because an older four-line rule includes the passwordless
disablesleep 1line. So the rule moved after the wait: every stop before it leaves the old rule and the old app together, and the only new file is backstop.sh, which the old app does not depend on. The wait matches exact arguments instead of waiting on the lock, because the installer itself holds the recovery lock and the runs it waits for are blocked on it.The restore proof runs inside the root command (sixth round). Greptile's race needs a run of the restore at a point nothing has journaled. Behind the password, the start has already journaled
sleepDisabledByUsand armed the backstop, so the run can only make the change the session's end or the rollback would make anyway. It runs as the user, through root'ssudo -n -u "#uid", because sudo never asks root for a password, so a run as root would prove nothing.do shell script ... with administrator privilegesruns with real and effective uid 0 (Apple TN2065), and macOS's defaultroot ALL = (ALL) ALLlets root's sudo switch users; a sudoers without that entry fails closed. The cost is that the user types the password before a missing rule is reported. The other routes were a privileged helper (ruled out by the maintainer), parsingsudo -l(not proof) and a read-then-run in the app (the race).The installer's recovery uses the staged binary (6046657261). The pre-swap recovery has to settle microsecond entries before the old bundle goes, and the old bundle may not have
--resume-frozen. Running it would open the menu bar app, so its entries could only stay and stop the install. The staged copy's binary is the build its backstop.sh was sealed with, codesign has checked both, and it speaks the interface the script expects. A flag keeps the LaunchAgent and uninstall.sh on the installed app. The path comes fromBASH_SOURCE[0]rather than an argument or the environment, so no caller can point the script at another binary. The check is of the path's shape only, and does not verify the bundle; the script's header says the caller must run a copy it has verified, and install.sh does. The other route was to swap first and let the new build's agent resume the processes, but install.sh keeps the previous app and agent whenever the recovery leaves entries it could not settle.The proof undoes the command's own change (round 16, within Option 2). pmset has one
SleepDisabledbit with no owner and no compare-and-set, so the only change a root command can tell apart is one it made itself. The fresh read as root is what stops the review's reproduction, because it comes after the dialog. Moving the proof after root's owndisablesleep 1makes the proof's write the undo of a change the command just made, which the start had journaled before the dialog, instead of a write over whatever it found before Insomnia changed anything. It also makes every exit the app treats as "nothing to undo" true by construction. It does not make the remaining window shorter than a read followed by the old proof would. The cost is that sleep is off while the proof runs, even when the rule is missing. Not taken: a listing (sudo -l) instead of a run, which the fifth round showed is not proof, and a process-owned sleep assertion or compare-and-set write, which needs the privileged helper Option 2 rules out. Replaced in round 18 (next).The root command asks sudo instead of running the restore (round 18, within Option 2). The round 17 review showed that any write before the permission is known can strand sleep off, and that the proof's write is what overwrote another tool's 1. Given a command,
sudo -llprints the rule that decides it (the 1.9.17p2 manual;display_cmndis the same in the 1.9.15, 1.9.16 and Apple sudo-114.100.11 sources), so the permission can be read without changing anything.-kkeeps a cached credential out and-nrefuses instead of prompting, as before. The readers accept one exact shape, the one install.sh's rule gives in those sources, and refuse everything else, so an unknown sudo costs a refused Start, never a write. 1.9.15 is the oldest source read, so older versions refuse.-Vis checked first because a listing does not consult approval plugins, and-lbecause Defaults bound to pmset change how the restore runs without showing in-ll. The costs: a Mac whose sudo or sudoers differs in those ways refuses every Start, a listing is not a run (Not covered), and the format comes from source, not from the installed sudo. Not taken: a genericsudo -l,sudo -vor a NOPASSWD grep (not proof), and running the restore before or after a write (R1 and R2). Narrowed in round 20 (next).Refuse what the answers cannot show (round 20, within Option 2). F1 and F2 are setups in which sudo answers a listing one way and runs the restore another. sudo has no query that shows a silent approval plugin, and a listing runs neither PAM's session stack nor the logging the restore would. So the command takes only the setup whose run it can predict from source: no sudo.conf (macOS installs none), macOS's own PAM session line, the one sudo version read, and Defaults that change no outcome of a run that the listing passed. The cost lands on users who changed those files, and on the next macOS sudo, which refuses until a release is checked against it. Not taken: parsing sudo.conf or naming known plugins (a plugin's name says nothing about what it checks), and a denylist of Defaults (a missed setting would pass).
A record in the marker, not a new file or mechanism (round 20). Ambiguous failures were undone because a write may have happened. The marker is already the one file the root command and the app share under one lock, so the command marks it right before it reads the setting, and the app reads it under that lock before it deletes it. The record is written as the user, in place, so the file the app compares is the one it wrote, and root writes nothing into the user's folder. Only the live app reads it: relaunch, backstop.sh and uninstall.sh have no written identity to compare (fourth round), and a bare nonce in a swapped-in file must never cancel an owed restore there. Replaced in round 22, after the round 21 review forged the record from a process running as the user (below).
A root-owned receipt plus a journaled attempt (round 22, the review's B and C together, within Option 2). Anything in the marker or the journal can be written by a process running as the user, which is Finding 1. A file only root can write, in folders only root can change, is the one record such a process cannot forge, and the root command already runs as root behind the password, so the receipt needs no new sudoers line, no helper and no new privileged command. The attempt in the journal tells every reader which start, which receipt file and which marker file to compare, and what the journal owed before the start. The costs: one more install step, Start refused until install.sh has made the receipt, and a root write and fsync per Start. Not taken: a receipt in the journal only (the same forgery as Finding 1), dropping the no-undo rollback (that brings back the before-record loss of another tool's 1 the round 19 review found), and a helper making the same writes (no ownership of the bit either).
One fixed path per user, rewritten in place. The path is fixed text in the command, the uid must be plain digits, and every folder up to / must be root's alone, so root's write never follows a path a user can change, and root never creates or changes files in the user's folders.
dd conv=notrunc,fsynckeeps the inode, owner, mode and size install.sh set, so the identity a start journals still matches after the write, and nothing accumulates: one 45-byte file per account. Each start compares its own nonce, so a record an earlier start left can never stand for a new one. No reader running as the user deletes or resets the receipt; only uninstall.sh removes it, through sudo. A rename-based replace was not taken: root would create files in the folder, and the identity would change on every Start.The read before the record. Round 20 wrote the record before root's
pmset -gread. Now the read comes first, so a 1 found at the read exits 6 with the receipt unchanged, andwritingmeans only that the read found no foreign 1 (or the journal owned it) and that only a clock comparison and pmset's write remain. A command stopped at its read now leaves another tool's 1, as every refusal does.Settlement removes the unfinished start's session.json whatever the receipt shows. That session never began, because its start never finished, so resuming it would read another tool's 1 as Insomnia's (Finding 2). Only the sleep entry depends on the verdict. A session.json that does not end at the attempt's deadline belongs to something else and is left to the usual reconcile.
A failure to record the settlement errs toward the restore. The attempt stays, sleep is turned back on, Start is refused and every run tries again, then with no marker of its own to match, so the second try reads "may have". This can clear another tool's 1 even when the receipt showed no write (Not covered), and it never leaves an owed restore unrecorded.
A marker with no journaled attempt drops session.json. Such a marker comes from a build before this round, or from a start that finished or rolled back but could not delete it. Nothing shows whether its command turned sleep off, so its session is never resumed on a 1 that may not be Insomnia's. The app does this only with no session in memory. The cost: a session whose own marker could not be deleted ends at the next relaunch or backstop run.
Merged main
Merged origin/main (5330c28) with a merge commit. Two documentation conflicts, both resolved by keeping both sides: SECURITY.md keeps the three-line sudoers text beside #16's unified-log and Location Services sentences, and spec section 9 keeps the password-prompt notifications beside #29's "battery unreadable twice in a row". Swift files merged without conflicts; #29's new
batteryUnreadableend reason and this PR'ssleepReenabledcoexist, and Start is still the only caller ofsetSleepDisabled(true).Merged origin/main again (b5f6de9) after #26 landed. Two conflicts. In SessionManager.swift, reconcile step 3 takes #26's version: it no longer clears a bit Insomnia did not journal, so this PR's
enableSleep()call there is gone, andpromptStuckTitlesits beside #26's foreign-sleep constants. In spec.md, section 8 keeps this PR's step 2 and #26's step 3, and section 9 lists both PRs' notifications. #26's new tests merged without changes.origin/main had not moved (afe8c3a), so b180529 needed no merge.
Merged origin/main (178dde8) after GitHub reported a conflict: #24, #27, #35, #42, #44 and #45 had landed. Eight files conflicted, all resolved by keeping both sides. backstop.sh and uninstall.sh keep this PR's
RMbeside #27'sDEFAULTS, and uninstall.sh keepsPENDINGbeside #27'sCONFIGand agent list.FakeSleepGuardkeeps this PR'sdisableSleep(_:)/enableSleep()split with #44'srestoreGateandrestoreCalledAtfolded intoenableSleep(). The fixture'srunkeeps the pty input beside #27's private TMPDIR..greptile/config.jsontakes #27's journaled App Nap wording (the old exception is gone) and its builtin-killsentence, and addsDEFAULTSto the variable list. SECURITY.md and docs/release-validation.md keep both sides' text and rows. SessionManager.swift merged without conflicts: #27's App Nap apply and restore sit beside this PR's start and restore changes, and its failed journal clear is reported the same way as the sleep one here.Merged origin/main again (4ea445b) for #46. GitHub showed no conflict, but #46 changed the fixture's
holdLock()to return aLockHolder, so a test this branch adds no longer compiled on top of main, and CI builds the PR's merge ref. #46 also replaced everywaitUntilExit()withProcessExit; the osascript runner and the test helpers this branch adds now use it too (887bc71). No file conflicted.Merged origin/main (0c2e791) in 4c7bdfa after #36, #23, #17 and #21 landed. Five conflicts, each resolved by keeping both sides: the notification titles in SessionManager.swift (
promptStuckTitlebeside main'ssessionFileTitle), theStoreErrorcases (this PR's marker cases beside main's.unreadableand.notRegularFile), the test helpers in TestSupport.swift (#23'sFIFOWatchafterLockHolderBox), uninstall.sh's tool block (oneRMline), and spec section 8 steps 1 and 2 (main's unreadable session.json text, then this PR's step 2). backstop.sh hadRMtwice after the merge, so main's line stays. TestIsolationTests now also checkspending-start, and no test this PR adds builds a path under the real home.Merged origin/main (24a26ff) in 2ccab01 after #47 and #38 landed. One conflict, in ReconcileLidGatingTests.swift: both sides added a line to the same fixture setup, so both stay (the fake reports SleepDisabled 1, and the fake process control reports both pids as stopped). #47's new
testSessionWithOffsetDatesIsResumedexpected a relaunch to rundisablesleep 1as main does, so a1f9f82 changes it to this branch's resume.Merged origin/main (5e833d9) in ca56ec0 for #40. README.md merged without a conflict.
Merged origin/main (af9c4f6) in 12c8e40 for #30. README.md, docs/spec.md and docs/release-validation.md merged without a conflict, and #30 does not touch the sleep guard.
Merged origin/main (64886e9) in 3b1d726 for #22 (still-running privileged commands). Both sides kept: #22's SIGTERM-only runner with the recovery lock on the command's stdin,
stopTransaction, the unfinished-command record, crash settlement and the launch retry, beside this branch's three-line rule, the dialog, the marker and reconcile'spmset -gread.PmsetSleepGuardtakes main's init. Two StillRunningCommandTests about asudo disablesleep 1left running are dropped, because Start runs it through the dialog (the stuck-dialog tests cover that) and reconcile no longer runs it; the launch-retry tests expect reconcile'spmset -g.Merged origin/main (c45e8fb) in 497cf15 for #28 (sealed backstop, installer supervisor). install.sh follows main's sealed layout, its independent supervisor with clock deadlines and its fixed tool paths. This branch's order stays on top: the password, the quit, the three-line rule written under the recovery lock with
sudo -nand full paths, asudo -k -n -llisting, then main's recovery and bundle swap. The older-backstop retire wait and the installer's pmset run are gone, because the sealed bundle has no separately installed backstop.sh to wait for. uninstall.sh keeps the pending-start deletion with main's sealed selection. A stop between the rule and the new bundle prints the rerun command.Merged origin/main (aed25a5) in 6c02da5 for #49 (lid close leaves meeting apps running). Two conflicts, both resolved by keeping both sides. In SessionManager's end notification bodies, #49's backstop text, which depends on outputs still waiting for their audio restore, sits beside this branch's
recoveryUnavailable,startFailedandsleepReenabledtexts. Spec section 9 lists this branch's prompt notifications and #49's one-time lid-close settings notification. #49's per-device audio entries, save IDs, locked reconnect and lid gate, warning recovery and retained-session retry are unchanged. #49'stestADeviceChangeBeforeTheLaunchReconcileWaitsForTheLidOfTheSessionOnDiskresumed a session from disk without setting the fake's SleepDisabled to 1; on this branch reconcile reads that setting instead of turning sleep off again, so the test now seeds it, as the other reconcile tests here do (fa281c1).Merged origin/main (781b596) in 8f8693e for #33 (release pipeline). Both sides kept. From #33: the prebuilt
--appinstall (signature, identifier, version and sealed backstop checked on a private copy before the password prompt, the arm64 check and--allow-unverified-origin),build-app.shfor source builds, ditto staging that drops group and other write bits and ACLs, andSCRIPT_DIRin place ofROOT. From this branch: the order (the password withsudo -v, the quit, then the three-line rule under the lock). Conflicts were in install.sh, README and release-validation.md:--allow-unverified-origin --app <path>for a prebuilt bundle).sudo visudo. On this branch the prompt issudo -v, and visudo runs later under the lock, so the tests marksudo -v. The fake sudo now swaps the--appbundle duringsudo -v, which falls between the checks on the private copy and the staging copy, so the swap test still shows that the checked copy is what gets installed.Merged origin/main (bfc9a57) in 70f5fac for #50 (the supervisor owns each undo command's limit and signal). One conflict, in backstop.sh's
lock_sharedcheck. It keeps this branch's inode helper through"$STAT"(/usr/bin/stat) with #50's comparison, so astaton PATH cannot make a foreign fd 9 look shared, and a shared fd 9 still skips the stale status-file cleanup that would delete a live supervisor's files. Everything else merged cleanly. #50'ssupervise_commandandrun_boundedsit beside this branch's marker, nonce, deadline,--own-bundleand pending-start handling: the supervisor owns its job, ignores TERM and HUP, keeps fd 9 until it reaps the command, sends TERM only, and on 125 keeps state and stops. Both sides' RecoveryScriptTests additions stay, andtestLockSharingIgnoresAStatOnPATH(round 16) covers the kept"$STAT".origin/main had not moved (bfc9a57), so 576c215 and 7dcf51f needed no merge.
origin/main had not moved (bfc9a57), so c5456f8 needed no merge.
origin/main had not moved (bfc9a57), so bd7db43 (bd7db43) needed no merge.
Merged origin/main (b5f7cf0, #43) with a merge commit, 15c2fc3, after the round 24 fixes in 7e3ddc8; no rebase and no force push. Four conflicts, each resolved by keeping both sides:
SessionManager.initkeeps this branch'smarkerLockTimeoutandreceiptLockTimeoutand #43'skeptRecheckDelay,keptRecheckAttempts,keptRecheckSlowDelayandbootSession;RuntimeState.CodingKeyshas #43's kept-display keys and this branch'ssleepOffAttempt; the backstop patch map in RecoveryScriptTests has this branch'sCAT,HEAD,RECEIPTSandRECEIPT_OWNERand #43'sMV;TestSupport.makeManagertakes both sides' parameters. Two of #43's test files needed this branch's contracts, which no textual conflict showed.PrivateDisplayGuardTests' sleep-guard wrapper now forwardscheckSleepSettingForStart,disableSleep(_:)andenableSleep()in place ofsetSleepDisabled, and its directSessionManagercall passes the harness's receipts (in 15c2fc3; the first merged build failed on both). In e741a7f,EarlierBootLowPowerClaimTestsseeds a still-valid session withSleepDisabled 1in the fake pmset, because this PR's relaunch never turns sleep off again and ends a session whose sleep was turned back on. In backstop.sh and uninstall.sh, #43'srecord_text_problemschecks the journal before a settlement republishes state.json, a settlement runs before #43's kept-brightness handling, and uninstall.sh stops while an attempt stays, so a kept state.json never holds one. Main's own bareheadandcatcalls, among them #43'shead -c 1checks inprepare_low_power_offand thecat "$SESSION"session read, are left as main has them; finding 6 covers the calls this PR added.origin/main had not moved (b5f7cf0), so 93fb3dc (93fb3dc) needed no merge.
origin/main had not moved (b5f7cf0), so d933b68 (d933b68) needed no merge.
origin/main had not moved (b5f7cf0), so f643d49 (f643d49) needed no merge.
Not covered
Everything in the eleven new "Not run" rows in docs/release-validation.md: force-quitting Insomnia with the dialog up, then relaunching it or waiting for the agent, then answering the old dialog (no
SleepDisabled 1), a stuck osascript on real hardware (the fakes are the only coverage), the installer's session line and "Continue?" question in a real terminal, a cancelled installer password during a session leaving the app and session running, an upgrade whose app refuses to quit leaving the sudoers file as it was, an upgrade stopped after the rule printing the rerun command, the dialog on real hardware (its wording and app name), cancel and wrong password and the 120 s timeout rolling back, whether SIGTERM to osascript closes the dialog, relaunch with sleep still off keeping the session, relaunch afterdisablesleep 0by hand ending it, and a reinstall over the maintainer's existing four-line file.The sudoers block is also being changed by the installer-identity PR (Install: build the sudoers rule from the account, show it, and confirm #20). It builds the file from a list of four commands and compares the installed file against that list, so whichever PR merges second has to drop
/usr/bin/pmset -a disablesleep 1from that list; the rest of this PR's install.sh change (the heredoc) goes away in favour of Install: build the sudoers rule from the account, show it, and confirm #20's generator. Sleep guard: never SIGKILL sudo pmset; keep the lock while it runs #22 (sudo SIGKILL) editssudoPmsetandCancellableCommand, which this PR does not touch, but it still callssetSleepDisabled(true)inperformStartand in reconcile, and its tests use the old name. Whichever of Sleep guard: never SIGKILL sudo pmset; keep the lock while it runs #22 and this PR merges second must usedisableSleep(_:)after writing the marker for Start,enableSleep()for turning sleep back on, and drop the reconcile re-apply, which this PR replaces with thepmset -gread. This PR now also putssudo -vand the session question ahead of the quit and the quit ahead of the sudoers step, and Install: build the sudoers rule from the account, show it, and confirm #20 (sudoers block) and Install, uninstall: identify a running Insomnia by path or bundle id, not by name #18 (how a running Insomnia is detected) edit both steps, so install.sh and the installer tests will need a hand merge against whichever lands first; the rule to keep is that no path writesdisablesleep 1, the password comes before the quit, the app is quit before the rule is written, and the rule is written under the recovery lock after the retire wait. Install: build the sudoers rule from the account, show it, and confirm #20's--yesflag should also answer the session question.The earlier narrow case (an old command's second read seeing a newer nonce and turning sleep back on) no longer exists: there is no second read. What stays: a wrong password, a timeout or a pmset failure still runs
disablesleep 0, because pmset may have run; only a cancel and a launch failure are known to have run nothing. Since the fifth round, Start is refused while another tool'sSleepDisabled 1is set, so that undo can clear only a 1 set while the dialog was up. A root pmset that holds the marker lock longer than 10 s keeps the sleep entry journaled and refuses starts until it exits; nothing kills it.The lock tests run
lockfand the root command as the user, not as root, and the immutable-marker path is covered withchflags uchgin a temp dir; the hardware rows cover both on a real install.No test runs the real osascript or shows a real dialog; the root command runs as the user with a fake pmset, and the dialog path on hardware is in the new "Not run" row.
Third round: everything in the new "Not run" rows (a stuck osascript rolled back at once on real hardware, a one-minute session answered after 70 s, an older backstop.sh refusing Start, an upgrade replacing backstop.sh before the bundle). The retire wait recognises old runs by their exact arguments in
pgrep -lfoutput (fourth round); a run started through a different path to the same file, or with a different interpreter path, would not be seen. Barerm,rmdir,mkdir,cpandmvcalls that run as the user and are already on main in backstop.sh and uninstall.sh (including App Nap: opt in, journal the previous value and put it back #27's new EXIT trap in uninstall.sh) are unchanged; this PR adds none, and converting them would widen the conflict with Backstop: run only the copy sealed in the signed bundle #28. The two that ran as root through sudo are converted (afc4948). A voided prompt that the user never closes stays on screen until they do; it can no longer change anything, and the menu names it.Fourth round:
sleepDisabledByUsinstate.json. A start voiding its own stuck prompt does compare against what it wrote.rm -rfof the bundle. An app opened in between is not seen, and the installer never kills it.Fifth round:
pmset -gand then runs the restore. A tool that sets SleepDisabled 1 between the two has it turned back on by the check. pmset has no compare-and-set that would close the window.pmset -gfails, finishes without checking the rule. It says so, and the app checks before every Start.-k -nunder a listing-only policy or with a cached credential come from the fake sudo, not from sudo itself.Sixth round (stated for the maintainer, not accepted on their behalf):
pmset -gread is not seen. The session's end sets it to 0, and if the tool sets it while the dialog is up, the root command's check sets it to 0 for a moment before setting 1. Nothing is left unjournaled, and the check never makes a change the end or a rollback would not make, but that tool's setting is not kept. This is what remains of Greptile 4171743074. README, SECURITY.md and the spec say so.pmset -a disablesleep 1there is still the time pmset takes to start. A session that ends in it ends at once, because the start arms its deadline timer next and a timer for a past date fires immediately.-uand the user's-kand-n. Two new release-validation rows cover the real ones.Upgrade fix for 6046657261:
--own-bundlechecks the shape of the script's path, not the bundle's signature. It relies on install.sh running only a copy codesign has verified. Someone who runs another bundle's copy with the flag gets that bundle's binary, run as the same user who could run that binary directly.Round 16, the round 14 P1. This is narrowed, not closed, and needs the maintainer's decision; it is stated here, not accepted on their behalf:
SleepDisabledbit with no owner and no compare-and-set. A 1 another tool sets after the root command's first read and before its owndisablesleep 1, or while that 1 is in effect during the proof, cannot be told from Insomnia's own. The proof, root's restore after a failed proof, or the session's end clears it. That window runs from the read to the proof's write, including however long the user's sudo takes; it was not measured on hardware..failed,.timedOut,.stillRunning) and crash recovery still undo the entry, because pmset may have run, so they can clear a 1 set while the dialog was up. While a session owns the bit, its end clears a 1 another tool set. Both are as before.Round 18, the round 17 findings. Stated for the maintainer, not accepted on their behalf:
log_outputset some way the listing does not show), or other groups for the user when the app or backstop.sh runs sudo than when root switched to that user can still make a later restore fail, and backstop.sh then keeps the entry and retries. Round 20 refuseslog_outputand every other Defaults entry outside its list (below).SleepDisabledbit with no owner and no compare-and-set. A 1 another tool sets between root's read and its write is taken for Insomnia's, and the session's end clears it. Between that read and the write there is now only the clock comparison, not a sudo run; the moment was not measured on hardware. A 1 set while a session owns the bit is cleared at its end, as on main.$5= "1") leaves that 1 and the owed restore in place, which the next end or backstop.sh run settles.listpw=always, a sudoers with no root entry, or a later rule for the restore. The message names these. Round 20 takes only 1.9.17p2 and adds refusals (below).sudo -k -n -llisting of the three commands; the root command's check runs at every Start.Round 20, the round 19 findings. Stated for the maintainer, not accepted on their behalf:
sudo -ugives, can still make a later restore fail, and backstop.sh keeps the entry and retries, as on main.testAnAmbiguousFailureAfterTheRecordStillUndoesASettingMadeMeanwhile,testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWrite). The record cannot be written in the same step as pmset's write, so between root's read and the end of that write a failure cannot say which side it fell on. The window is the read, its awk and one clock comparison, and was not measured on hardware. Round 22 moves the record after the read (below).sleepDisabledByUsin state.json. Round 22: the record is in the receipt, which such a process cannot write (below).Round 22, the round 21 findings. Stated for the maintainer, not accepted on their behalf. F3, the ownership finding from rounds 14, 17 and 19, is narrowed again and stays open and unwaived.
<nonce> writing, a failure before pmset's write ends (a signal, a crash, pmset failing, the app's timeout after the password) is undone like an end. Trigger: another tool sets SleepDisabled 1 after root'spmset -gread, then the command fails or is stopped before its write is known. The restore clears that 1. The window holds the read's awk, theddwrite with fsync, the readback and one clock comparison; it was not measured on hardware.sleepOffAttempt, which on main was already enough to drop an owed restore. The marker's lock is advisory, so it can delete or replace the marker while a root command holds it. A live start then waits for that command, because its removal expects the file it wrote. A relaunch, backstop.sh or uninstall.sh reads that as "may have" and restores, and a command still running can then write after that restore, leaving sleep off with no journal entry. If it recreates a settled start's marker with the old nonce and the user then types the password into that start's old dialog, sleep goes off with no journal entry, and reconcile reports it as set by something else.Round 24, the round 23 findings. Stated for the maintainer, not accepted on their behalf. Finding 7, the ownership finding from rounds 14, 17, 19, 21 and 23, stays open and unwaived. pmset's
disablesleepis one Boolean with no owner and no compare-and-set, so in each pair below two different causes leave the same bit, receipt and journal. The tests that show another tool's 1 being cleared describe this finding; they are not guards.pmset -gread. Then, before Insomnia knows pmset wrote, the command fails, is signalled or crashes, osascript's status is lost, the app's 120 s wait ends, or the app quits. Once the command lets go of the receipt, every reader reads this nonce'swritingas "may have written", and the undo clears the other tool's 1. A1 (pmset wrote 1 and the status was lost) and A2 (the command stopped before pmset and another tool set 1) both leavewritingand a 1. The window runs from the read through the perl write, its F_FULLFSYNC and the read-back to the end of pmset. That is wider than round 22'sdd ... fsyncby the drive flush, which has not been measured on any Mac.testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWriteshows the loss.writingand runsdisablesleep 1. The session claims the bit, and End clears the other tool's 1. B1 (Insomnia's 1 alone) and B2 (another tool's 1 in the gap, then Insomnia's write) both end withwriting, success and a 1. The F_FULLFSYNC write is inside this gap, so round 24 likely made it longer; not measured.testASettingMadeRightAfterTheCommandsReadIsClearedOnlyWhenTheWriteFollowsshows the loss.$5is1and root skips the read), is cleared by the next End, reconcile or backstop run. C1 (Insomnia's 1) and C2 (Insomnia's 1 plus another tool's) look the same. Main does the same, and that is no reason to accept the two cases above.expires, a missing, replaced, unsafe or malformed receipt reads as "may have written"; only root or an administrator can change the receipt or its folders. A marker with no journaled attempt ends its session (the legacy rule). A process running as the user can rewrite state.json or session.json (inherited).pmset -gagain right afterwritingand refuse on a 1. Both cases above shrink to the read, theexpirescheck and pmset's start. Cost: one more F_FULLFSYNC write on that refusal, and if it fails,writingstays and the 1 just seen is cleared. It narrows the gap and does not give ownership.sudo pmset -a disablesleep 0. Cost: a possible Insomnia 1 stays past the session's end, so the Mac does not sleep until someone acts, against the deadline promise and the backstop's job./usr/bin/perl; Apple's Catalina release notes say future macOS versions will not include scripting runtimes by default, and without perl no session can start. F_FULLFSYNC's effect on real drives, the real /private/var/db folder modes and a power cut are not measured. Any local account can hold the receipt's lock, which keeps an attempt undecided and Starts refused but cannot make a write look absent. An unsettled claim from a folder whose agent never runs refuses Start in every other folder until that folder's app or backstop settles it, or someone removes both files withsudo rm -fand runs install.sh again (SECURITY.md). A timed-out dialog can keep Start refused for the rest of its 130 s window.headandcatcalls in backstop.sh and uninstall.sh, state.json and session.json written without fsync, and the legacy marker rule. A bd7db43 development build's 45-byte receipt and attempt record are not migrated; install.sh stops at the receipt.Round 26, the round 25 findings. Stated for the maintainer, not accepted on their behalf. F7, the ownership finding (R25-5), stays open and unwaived. The second read narrows two windows; it is not an owner token and does not close them. In each pair below, two different causes leave the same bit, receipt and journal. The tests that show another tool's 1 being cleared describe this finding; they are not guards.
writingand SleepDisabled is 1. A1: pmset wrote Insomnia's 1. A2: the command stopped afterwritingand before pmset (a signal, a crash, a power cut), and another tool set 1. A3: the second read saw another tool's 1, therefusedover the record could not be written, and status 6 never reached the app. Every reader readswritingas "may have written" and the undo clears the other tool's 1 in A2 and A3. Before round 26 a 1 set at any time from the first read on counted for A2; now the command refuses on a 1 it sees at the second read, so A2 needs the 1 to come after that read or the command to stop before it.testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWriteandtestASecondReadRefusalWhoseRefusedLineCannotBeWrittenLeavesTheRecordshow the loss.disablesleep 1. Both end withwriting, success and a 1; the session claims the bit and End clears the other tool's 1. The gap is now the clock check (/bin/date) and pmset's start; it no longer contains the perl write and its F_FULLFSYNC. Not measured on any Mac.testASettingMadeRightAfterTheCommandsReadIsClearedOnlyWhenTheWriteFollowsandtestWhereAnotherToolsOneLandsDecidesWhetherItSurvivesshow the loss.$5is1and root skips both reads), is cleared by the next End, reconcile or backstop run. C1 (Insomnia's 1) and C2 (Insomnia's 1 plus another tool's) look the same.written(seven letters, so the line stays 82 bytes), flushed the same way, andwritingwithout it read as "never wrote". A1 with a lost status then readswrittenand is undone; A2 and A3 readwritingand keep the other tool's 1. Cost: a crash, signal or power cut after pmset wrote and beforewrittenis flushed leaves Insomnia's 1 with a journal that says it never wrote, so the Mac does not sleep until someone runssudo pmset -a disablesleep 0; another backstop version and receipt-reader change; one more F_FULLFSYNC per Start. B2 is unchanged.sudo pmset -a disablesleep 0. Cost: A1's Insomnia 1 stays past the session's end, against the deadline promise and the backstop's job.expires. A settled record that cannot be finished (a release file not as install.sh made it, an immutable state.json, a busy lock) keeps Start refused in its folder, and its held claim refuses Start in every folder of the user, until a run finishes it; a relaunch then ends an unexpired session rather than resume it. install.sh now stops at a held claim in a release file that is not the user's 0600 file with one link; removing both files by hand (SECURITY.md) is the way out.expiresrests on the wall clock not going back: root's clock check uses/bin/date, so after a clock is set back, a later answer to a dialog settled that way passes the clock check; the marker check (exit 3) and the predecessor check (exit 8) still apply. Afterexpires, a missing, replaced, unsafe or malformed receipt still reads as "may have written". The receipt's F_FULLFSYNC flush is not atomic with state.json, session.json or pmset's own settings; a power cut between them is not tested. The receipt write needs/usr/bin/perl; the 45-byte receipt of bd7db43 is refused, not migrated; sudo, PAM or policy refusals stop Start. The window sizes above, the real folder modes and real drives were not measured.Round 28, the round 27 findings. Stated for the maintainer, not accepted on their behalf. F7, the ownership finding (R27-1), stays open and unwaived. pmset's
disablesleepis one Boolean with no owner and no compare-and-set.refusedover its record, rolled back with no undo, but when it could not journal that rollback it kept the attempt, and its next transaction readwritingagain and cleared the other tool's 1. Now that app keeps the nonce inrefusedNonce, and its later settlements of the start keep "never wrote" whatever the receipt shows.testARefusalWhoseRollbackCannotBeJournaledStaysARefusalInThisProcesscovers it; with the retention removed it fails at the same-process undo.writingand before pmset, and another tool set 1 after the second read or the command stopped before it) read the same, and the undo clears the other tool's 1 in A2. A3 (the second read refused another tool's 1 andrefusedcould not be written) is now cleared only when the status is lost (a signal, the 120 s limit, a crash before the app acts on it), when the app quits or crashes before it can journal the rollback, or when backstop.sh or uninstall.sh settles the start first. B2 (another tool's 1 after root's second read and before its clock check and pmset) reads as a successful Start, and End clears it. C1 and C2 (an owned session's 1, or that 1 plus another tool's) are inherited from main. The tests that show another tool's 1 being cleared describe this finding; they are not guards.refusedwrite inside the root command (helps only a passing write failure and changes the root text in both copies), and publishing the negative verdict outside state.json (the only other files the app may write are the user's own, every reader would need a new format, and it fails under the same storage faults).writtenreceipt state after pmset returns, withwritingalone read as "never wrote"; it fixes A2 and A3 for every reader, but a crash after pmset wrote and beforewrittenis flushed leaves Insomnia's 1 with a journal that says it never wrote, so the Mac does not sleep past the deadline. 3. No automatic restore on "may have written" from a lost status; A1's 1 then stays past the deadline. 4. An assertion-only awake mode; lid-closed, battery and owner-death behavior change. 5. A helper or a compare-and-set at Start; new privileged code that still cannot tell C1 from C2. Options 3 to 5 are not authorized.ls -leprints it: ls never prints the synchronize right, prints folder-only rights and inheritance flags only for folders, and skips an entry it cannot translate, so a receipt whose only extra is one of those passes the root command, backstop.sh, uninstall.sh and install.sh; the app's acl(3) check is exact. install.sh repairs an earlier receipt before it takes the receipt's lock (the user cannot open a 0600 receipt with no entry), so for that moment every reader refuses it; and an older build still running in another folder reads the repaired receipt as unsafe, so a start it left open across the upgrade settles as "may have written" onceexpireshas passed. Not tested across builds. backstop.sh'stype_ofandextractstill read a failed plutil call as absence; only its raw reader and its conversion report failure. Bash may not report an error partway through$(<file). The healthy-session match allows 1 s, which grows with relaunches between clamped extensions; past it the session ends as before. Left as bare calls, none reading the journal, the session or a power state: heredoccatthat prints a message, install.sh'ssed | head -n 1over codesign output, uninstall.sh'sawkover App Nap IDs and backstop.sh'sgrep -qover a probe file.ls, and the Apple sources read are chmod.1, chmod_acl.c, ls.1 and ls's print.c. "Never wrote" fromexpiresstill assumes the wall clock does not go back. The receipt's flush is still not atomic with state.json, session.json or pmset and is not measured on real drives.Round 30. Stated for the maintainer, not accepted on their behalf. F7 stays open and unwaived.
sudo -vhas no time limit while it holds both locks; a credential that runs out after the bootout stops it there (rerun it); install.sh repairs nothing under a held claim and counts a missing release file as no claim; an older build refuses a repaired receipt, also after a rollback; a session a failed start put back beside a settled record ends at relaunch; the shell ACL checks pass rightsls -ledoes not print.sudo pmset -a disablesleep 0.capacity/).sudo killline (round 15).Codex review
Local Codex review (gpt-6.1-sol, xhigh) of 71253d7. All three findings are fixed in a2ffdd4 (merged with main in b5f6de9).
AdministratorPrompt.swift:267: an abandoned password prompt could disable sleep after recovery had cleared the journal. Fixed: the pending-start marker and the nonce-checking root command (What, Decisions). The relaunch path is covered bytestRelaunchVoidsTheDialogOfAStartThatDiedandtestDialogOfAStartThatDiedCannotActForANewerStart, the backstop path bytestBackstopVoidsTheDialogOfAStartThatDiedBeforeItUndoesAnything,testBackstopVoidsAnAbandonedDialogEvenWhileTheSessionIsValidandtestBackstopLeavesTheMarkerWhenTheLockIsHeld, the race with recovery bytestMarkerDeletedWhilePmsetRunsTurnsSleepBackOn, plus uninstall, rollback and the stuck-prompt path. The sudoers rule keeps its three exact lines.SessionManager.swift:1109: the menu kept recommendingkill <pid>after osascript had exited while a descendant held its pipes. Fixed: the runner reports osascript's own exit to the handle as soon as it is reaped, separately from pipe closure; the start waits for that, replaces the line, and still waits for the pipes before the rollback. Covered bytestOsascriptExitReachesTheHandleBeforeItsOutputClosesand the extendedtestStuckPromptIsReportedWithItsPidAndRolledBackAfterItExits.AdministratorPromptTests.swift:156: the output-holder test depended on a scheduling window. Fixed:OsascriptAdministratorPrompttakes abeforeDeadlinehook, the tests block in it until the fake has written a ready file after its TERM trap, and each holder runs until the test releases it (60 s watchdog). The SIGTERM and ignore-SIGTERM tests use the same handshake.Second local Codex review (gpt-6.1-sol, xhigh) of b5f6de9. All four findings are fixed in b180529.
scripts/backstop.sh:129: recovery cleared the journal while the marker could not be deleted, and the app'sclearPendingStartswallowed the same failure. Fixed:sleepDisabledByUsis cleared only by a run that removed the marker. Otherwise sleep is still restored, the entry stays, the app reports it ("Restore incomplete", menu line) and refuses starts, the backstop exits 1, and every run retries. Covered bytestUndeletableMarkerKeepsTheSleepEntryAndRefusesStarts,testRelaunchWhileTheAbandonedDialogsCommandRunsKeepsTheSleepEntry,testBackstopKeepsTheSleepEntryWhenTheMarkerCannotBeDeleted,testBackstopKeepsTheSleepEntryWhileTheMarkerIsLockedandtestBackstopFailsOnAStuckMarkerEvenWithACleanJournal.AdministratorPrompt.swift:167: a failed compensatingdisablesleep 0went unnoticed, and an ambiguousdisablesleep 1failure skipped compensation. Fixed by removing compensation rather than checking it: the root command holds alockflock on the marker from its check until pmset exits, and every deleter takes that lock first, so the marker can no longer go while pmset runs and whatever pmset did is still covered by the journal (Decisions). Covered bytestTheMarkerCannotBeRemovedWhilePmsetRuns,testAnAnswerThatWaitsOnARemovalRunsNothingandPendingStartRemovalTests.SessionManager.swift:424: cancelling Start randisablesleep 0and cleared aSleepDisabledanother tool owned. Fixed: a cancel and a launch failure put session.json and the journal back exactly and run no pmset; a wrong password, a timeout and a pmset failure keep the undo path, because pmset may have run (Not covered). Covered bytestCancelLeavesASleepSettingSomeoneElseOwns,testLaunchFailureRollsBackWithoutPmset,testCancelKeepsAnEntryAnEarlierRestoreLeft,testCancelTextInACommandsOutputIsNotACancelandtestOnlyCancelAndLaunchFailureRanNothing.AdministratorPromptTests.swift:158: the SIGTERM test depended on the 3 s default grace. Fixed: it passes a 20 s grace and accepts.stillRunningtoo, checking the reported grace and waiting on the handle before it reads the trace. With a 1 s grace the new test passes and the old one fails.Third local Codex review (gpt-6.1-sol, xhigh) of b180529. All four findings are fixed in aced667 (merged with main in 178dde8).
scripts/install.sh:190: an upgrade that stopped before the backstop step left the new app beside an old backstop.sh, which does not deletepending-start, so a dialog left open by a crash could still turn sleep off. Fixed at both ends. backstop.sh declares# insomnia-backstop-version: 2, and Start refuses, with nothing written and no dialog, while the installed script is missing that line or is older, telling the user to run install.sh again. install.sh installs backstop.sh under the recovery lock before the bundle and waits for runs of the old copy to exit (Decisions). Covered by BackstopVersionTests,testStartWithAnOlderBackstopShowsNoPrompt,testInstallReplacesTheBackstopBeforeTheBundleUnderTheLockand the three retire-wait tests.SessionManager.swift:448: a stuck osascript kept the recovery lock, and so blocked sleep restoration, even after its marker was gone. Fixed as directed: onceclearPendingStart()has removed the marker under its lock, the start restores sleep, finishes the transaction and releases the lock, and a separate task keeps the menu line true until the prompt exits. A command that holds the marker's lock is still waited for. Covered bytestStuckPromptWhoseMarkerIsGoneIsRolledBackWithoutWaiting(an osascript fake that never exits after the marker is voided),testVoidedPromptWatcherLeavesALaterLineAloneandtestStuckPromptWhoseCommandHoldsTheMarkerIsWaitedFor.AdministratorPrompt.swift:187: a password accepted after the session's deadline still turned sleep off. Fixed in the root command: the deadline arrives as$3and the command refuses with exit 4 unless/bin/date +%sis below it, still as fixed text (Decisions). Covered bytestDoesNothingOnceTheSessionHasEnded,testAnUnreadableDeadlineNeverPasses,testPasswordTypedAfterTheSessionsEndTurnsNothingOffand the argument test.AdministratorPromptTests.swift:393: the removal test assumed lockf opened the marker within 300 ms. Fixed:waitUntilLockfWaits(under:)confirms that lockf is blocked in the kernel waiting for the marker's lock before the test deletes it (Decisions). 15 of 15 runs passed, and removing the wait fails the test.Fourth local Codex review (gpt-6.1-sol, xhigh) of 887bc71. The one finding is fixed in 1b36a53.
SleepGuard.swift:60: Start could turn sleep off when the passwordless restore was missing, so the end, the backstop and uninstall would all fail to turn it back on. Fixed at the root: Start now refuses, before anything is written or any dialog is shown, unlesssudo -n -l /usr/bin/pmset -a disablesleep 0exits 0. The message says to run scripts/install.sh again. The check never prompts and never runs pmset, and the restore itself uses the same argument list. Covered bytestStartWithoutThePasswordlessRestoreShowsNoPrompt,testThePasswordlessRestoreIsCheckedBeforeAnythingIsWrittenand three fake-sudo tests. The fifth review replaced the listing with a run of the restore (below).Fifth local Codex review (gpt-6.1-sol, xhigh) of f72ba11. The one finding is fixed in 926d639 (merged with main in ca56ec0 and 12c8e40).
SleepGuard.swift:95: a successfulsudo -n -ldid not prove that sleep could be turned back on without a password. An administrator account with another NOPASSWD entry, or with a cached credential, passed without Insomnia's rule, so Start could turn sleep off with no unattended way back. Greptile reported the same line (4171411041). Fixed at the root in the app and in install.sh, which had the same check: both runsudo -k -n /usr/bin/pmset -a disablesleep 0. The app runs it without a read when the journal owes the restore. Otherwise it readspmset -gfirst, runs it only while SleepDisabled reads 0, and refuses Start with nothing run while another tool's 1 is set or the read fails. install.sh skips the check in those two cases and says so. Covered bytestRestoreCheckFailsWhenListingPassesButRunningNeedsAPassword,testStartIsRefusedWhenListingPassesButTheRestoreNeedsAPassword,testRestoreCheckIgnoresACachedCredential,testStartIsRefusedWhenOnlyACachedCredentialWouldRunTheRestore,testStartWhileSleepIsAlreadyOffRunsNothing,testStartRunsTheRestoreTheJournalOwesBeforeThePrompt,testInstallStopsWhenOnlyTheCachedCredentialWouldRunTheRestoreandtestInstallRunsTheCheckOnlyWhileSleepReadsOn.Greptile review of 12c8e40 (review 10, confidence 0/5). Both P0 findings are addressed in 23d625f. The earlier replies on those two threads, which deferred them, are superseded; no new thread reply was posted.
SleepGuard.swift:144: the restore preflight could SIGKILL sudo throughCancellableCommand. Fixed by removing it: Start runs no sudo before the dialog. The privileged commands at Start are osascript (SIGTERM only;.stillRunningkeeps the recovery lock) and, inside it, the root command's sudo and pmset, which the app never signals. The installer's sudo calls go through Backstop: run only the copy sealed in the signed bundle #28's supervisor, SIGTERM only, with fd 9 kept. Covered bytestStartRunsNoSudoBeforeTheDialog, the OsascriptAdministratorPromptTests SIGTERM and still-running tests,testTheMarkerCannotBeRemovedWhileTheRestoreCheckRunsand the installer's ignore-SIGTERM tests.SleepGuard.swift:139: the read-then-restore could change a setting no journal entry covered, and install.sh had the same window. Addressed in both. install.sh runs no pmset. The app's run of the restore moved into the root command, after the journal entry and the armed backstop. Not closed: pmset has no compare-and-set, so a 1 another tool sets after Start's read is still set to 0, at the end or for a moment by the check. The docs and Not covered state that residual for the maintainer to accept or reject. Covered by the RootCommandTests above andtestStartWhoseRestoreCheckFailsRollsBackWithNothingToUndo.Greptile review 11 of fa281c1 (confidence 2/5):
AdministratorPrompt.swift:227: a restore check that ends at or after the deadline still randisablesleep 1. Fixed in bf4d6e2 (rootCommand) and 2f6b11f (the AppleScript copy, which bf4d6e2 missed). Covered bytestDoesNothingWhenTheRestoreCheckEndsAtOrAfterTheDeadline(fake clock at the deadline, 1 s and a day past: exit 4, only the restore ran), the controltestTurnsSleepOffWhenTheRestoreCheckEndsBeforeTheDeadline,testAFailedRestoreCheckStillExitsFiveOnTheFakeClockandtestTheAppleScriptEmbedsTheRootCommandUnchanged. All of them run the command read back from the AppleScript. The mutation spot checks under New coverage fail them with the recheck taken out.install.sh:614at fa281c1: an upgrade from a bundle that does not declareInsomniaResumeFrozenVersion, with a frozen process in the journal, stopped at the recovery, because the staged backstop resumed through the installed binary. Main has the same stop (install.sh:635-636at 781b596); on this branch it also left the three-line rule ahead of the older app. Fixed in 2b8028c: install.sh runs the staged copy with--own-bundle, so the staged binary, which declares the interface, resumes the processes under the recovery lock before the swap, and the old app never runs. Entries that binary cannot settle stay with theirstartedAtMicros, and the install stops with the previous app and agent in place. Covered by the two upgrade tests, the five-case keep test and the two--own-bundletests under New coverage; the control run without the flag fails both upgrade tests. No reply was posted on the comment.Independent round 14 review (GPT-6.1-Sol, xhigh) of 2b8028c. Verdict: needs changes, one P1.
AdministratorPrompt.swift:234and the literal at line 244: the restore proof cleared another tool'sSleepDisabled 1set while the dialog was up, also when the start then expired. Changed in 42e42e5 (What, Decisions): the root command reads the setting as root after the dialog and stops on a 1, and the proof undoes the command's own change. The reproduction now exits 6 after onepmset -g, with no sudo call and the foreign 1 kept, whether the deadline passes during the check or not (testASettingMadeWhileTheDialogWasUpSurvivesADeadlineDuringTheCheck). Through Start, the rollback then runs no pmset (testASettingMadeWhileTheDialogIsUpIsLeftAloneandtestASettingMadeWhileTheDialogIsUpSurvivesALateEndend to end, andtestASleepSettingMadeWhileThePasswordIsTypedIsLeftAlone). Not closed: Not covered states the remaining window and the decision it needs.Independent round 17 review (GPT-6.1-Sol, xhigh) of 42e42e5. Verdict: needs changes, three blockers. All three are changed in 576c215 (What, Decisions); the Greptile thread 4213796939 on R1 is left open for review.
AdministratorPrompt.swift:292and the literal at line 303 (Greptile 4213796939): root randisablesleep 1before the user's proof, so with the rule missing an interrupted command, or a failed root fallback, left sleep off with no unattended restore. Changed: the command writes nothing until sudo's-V,-k -n -land-k -n -llanswers fit the rule, and its only write is the last step. Covered bytestEveryOtherPolicyRefusesBeforeAnyPmset,testARefusedRestoreQueryLeavesSleepOnWithNoUndoand the R1 harness rows. Not covered states what a listing cannot show.SessionManager.swift:1066: the proof and root's fallback set 0 over another tool's 1, and with the dialog's output gone a refusal died by SIGPIPE (lockf 70), which the start undid. Changed: no write before the answers, and SIGPIPE is ignored, so every refusal keeps its status and leaves the 1. Covered bytestARefusalKeepsItsStatusWhenTheDialogsOutputIsGone,testLeavesASleepSettingMadeWhileSudoIsAsked,testASudoRefusalLeavesASettingMadeWhileSudoIsAsked,testASettingMadeWhileSudoIsAskedSurvivesARefusaland the eleven-row table. Narrowed, not closed: Not covered states the read-to-write moment and the ambiguous exits.testWritesNothingWhenTheDeadlineComesDuringAnyCallBeforeTheWrite,testAJournalOwnedSettingWritesNothingWhenTheDeadlineComesDuringTheQuestionsandtestAnEndDuringRootsReadWritesNothing.Independent round 19 review (GPT-6.1-Sol, xhigh) of 7dcf51f. Verdict: needs changes, three P1s. Changed in c5456f8 (What, Decisions); none is waived.
AdministratorPrompt.swift:314and its literal:sudo -Vdoes not list an approval plugin with noshow_version, which still rejects the restore when it runs. Changed: any /etc/sudo.conf, the only file that loads plugins, refuses before any sudo call, and so does a PAM session stack other than macOS's own. Covered bytestAnySudoConfStopsTheCommandBeforeSudoRuns,testTheSilentApprovalFixtureChangesNothingButTheRestore,testOnlyMacOSsOwnPamSessionLinePassesandtestASudoConfStopsTheStartBeforeSudoIsAsked.testTheListingReaderTakesOnlyTheDefaultsItAccepts,testTheVersionReaderTakesOnlySudo1_9_17p2WithTheSudoersPluginsandtestUserDefaultsTheCheckDoesNotAcceptStopTheStart.SessionManager.swift:1041and:1087: the ownership requirement stays unmet. Narrowed: a failure whose marker still holds the bare nonce runs no undo. Not closed; Not covered lists what remains and the alternatives. Covered by the marker tests listed under New coverage. Round 22 replaces the marker rule with the receipt (below).Independent round 21 review (GPT-6.1-Sol, xhigh) of c5456f8. Verdict: needs changes, two P1s and one P2. Changed in bd7db43 (What, Decisions); none is waived.
Store.swift:344(RemovedMarker.isUntouched), read atSessionManager.swift:1097and:2601, with the record written atAdministratorPrompt.swift:369: root wrote the record into the marker as the user, so a process running as the user could put the bare nonce back into the same inode after pmset's write. A start that then failed was rolled back with no undo, and sleep stayed off with no journal entry. Changed: the record is in a receipt only root can write, under folders only root can change, and the marker is no longer evidence.isUntouchedis gone. Covered bytestAForgedMarkerDoesNotDropTheRestoreand its matched controltestAFailureAfterTheWriteIsUndone, the receipt trust tests inSleepOffReceiptsTestsandtestTheShippedCommandTrustsOnlyRootsReceipt.SessionManager.swift:528,:1097,:1495and:2149: relaunch, backstop.sh and uninstall.sh deleted the marker without reading the record, so an abandoned dialog's session was resumed on another tool's 1 while it was valid, or undone as Insomnia's own once it had expired. Changed: the start journalssleepOffAttempt, and every reader that deletes the marker settles the start from the receipt: its session.json never resumes, and the sleep entry goes back to what was owed before unless the receipt shows this start'swritingor cannot be trusted. Narrowed, not closed: the after-record phases and the read-to-write moment stay (Not covered). Covered by the relaunch tests inSleepOffSettlementTests(unexpired, expired, an earlier owed restore,writing, evidence that does not match, no dialog, a marker with no journaled start, a settlement that cannot be written) and the backstop.sh, uninstall.sh and install.sh tests listed under New coverage.Independent round 23 review (GPT-6.1-Sol, xhigh) of bd7db43. Verdict: needs changes, seven P1s and one P2. Findings 1 to 6 and 8 are changed in 7e3ddc8 and e741a7f (What); finding 7 stays open and unwaived. Green checks and resolved threads were not taken as clearance.
SleepOffReceipts.swift:104, with Greptile 4217474520: a valid receipt holding another nonce read as "never wrote", and every folder of the user shares the receipt, so a start in a second folder overwrote the first start'swriting. Changed: the claim in<uid>.released, the predecessor in the line, and exit 8 when the receipt no longer begins with it. Another start's line naming the same predecessor shows the first start never wrote; one naming another predecessor shows it may have.expiresis at most 130 s after the marker. Until then, a receipt that still holds the predecessor decides nothing unless the dialog ended by itself. A command that comes later stops atexpires(4), and one for a settled start whose line moved on stops at the predecessor (8).dd conv=notrunc,fsyncis fsync, not F_FULLFSYNC. Changed: perl'sfcntlF_FULLFSYNC (51) with a refusal before pmset when perl or the flush fails. Sources: Apple's fcntl(2) and fsync(2) pages, xnu'sfcntl.h, perlfunc. Untested: what a drive does with the request, which Macs ship/usr/bin/perl, and a real power cut.catandheadthrough PATH. Changed:CAT=/bin/catandHEAD=/usr/bin/headin the fixed-tool blocks, covered by the fake substitution map and by shadowed-PATH tests for backstop.sh and uninstall.sh. The bot's P0 root-privilege claim was not established; the conditional P1 recovery defect was.SleepOffReceipts.swift:34,SessionManager.swift:2660and the marker rule in config.json: another nonce does not prove no write and a deleted marker does not stop an old dialog. Changed with the code: every description now gives the three never-wrote receipts, when "may have" applies at once, when only afterexpires, and that undecided keeps the start recorded.Independent round 25 review (GPT-6.1-Sol, xhigh) of e741a7f. Verdict: needs changes, four P1s (one of them F7) and two P2s, plus the hosted Swift job's watchdog failure. Findings 1 to 4 and 6 are changed in 93fb3dc (What); finding 5, F7, stays open and unwaived. Green checks and resolved threads were not taken as clearance.
sleepOffAttempt.settledwhile the claim is held, then gives the claim back and drops the record; a settled record is only finished, never read against the receipt again. Backstop version 5.expireshad passed or an earlier restore was owed, while the command may still be in pmset. Changed: undecided holds the sleep undo whatever the time and whatever is owed; the owed restore stays journaled and runs after the lock is let go. The cost is in Not covered: a command that never exits, or any account holding the lock, delays recovery indefinitely.sudo installreplaces it.prepare_low_power_off, as Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43 merged it, ranheadthrough PATH. Changed: both calls use"$HEAD"; a hostileheadfirst on PATH no longer stops the boot record or Low Power Mode off. Other inherited bare calls were left as main has them. The bot's root-execution impact was not established; the conditional recovery refusal was. On 93fb3dc Greptile changed 6046657261 to "No findings outside the diff remain." (2026-10-08T17:58:42Z), and Greptile's thread 4220347392 shows as resolved; neither is an independent review.pmset -gagain after the flushedwritingand before its last clock check and pmset, and refuses withrefusedand exit 6 on a 1 or a failed read. That narrows the A and B windows and is not an owner token. Not covered lists the remaining histories, the alternatives and their costs.expires, the retry after a failed publication or claim release, and the availability costs.Independent round 27 review (GPT-6.1-Sol, xhigh) of 93fb3dc. Verdict: needs changes, five P1s (one of them F7) and four P2s, the hosted Swift job's watchdog failure among them. Findings 2 to 8 are changed in d933b68 (What); finding 1, F7, stays open and unwaived; finding 9 has fixture changes and no CI change. Green checks and resolved threads were not taken as clearance.
user:<name> allow readentry, made and repaired by install.sh and required by every reader. The installing user can still hold the lock, and a command that never exits still holds it; both costs stay (Not covered).AppEncodedJournalScriptTestsruns its script rows two at a time, which saved 24 s of 132 s against a same-hour serial control and does not close the gap, and the fake sudo's wait loop runs no command substitution after its trap. No workflow, watchdog, partition or rerun change; the two-job split is the parent's choice. Its hosted result at d933b68 is in Not covered (CI).Independent round 29 review (GPT-6.1-Sol, xhigh) of d933b68: needs changes, seven P1s and six P2s. Changed in 468db03, 1468950, 7e233e8, 29949a6 and f643d49 (What): findings 2 and 4 to 12, and 13's capture errors. Open: 1 (F7, narrowed, unwaived), 3 (reported only) and 13's runtime (Not covered). Green checks and resolved threads were not taken as clearance.
🤖 Generated with Claude Code
Fix the uninstall rerun’s race with receipt creation before merging.
Fix with agent prompt
Summary
This PR removes passwordless sleep disabling and asks for an administrator password at Start. The latest changes improve recovery when files cannot be created and let an interrupted uninstall finish removing its receipt pair.
Diagram
Reviews (21) · Last reviewed commit: "Round 30: two fixtures follow the reader..." · Reviewed by Greptile