Repository navigation
Add an in-app upgrade path from KeyPath 1.0.1 - #1362
Conversation
The session runtime refuses to start beside 1.0.1's root kanata daemon, and until now the app offered no way to remove it. Settings → Status now offers Remove Old Service when that refusal stands. It calls InstallerEngine.removeLegacyPrivilegedRuntime (also reachable as AutoFixAction.removeLegacyPrivilegedRuntime), which runs inside the installer transaction: best-effort SMAppService unregistration, then one administrator batch that boots out every com.keypath.* label 1.0.1 used, stops root KeyPath processes the app cannot signal (ADR-040), and deletes KeyPath's own system files. Success requires a fresh read-only observation with no loaded legacy service, no root KeyPath process, and no legacy file. An unreadable service state never counts as gone. A cancelled or failed batch is never success. The Karabiner VirtualHID driver is reported, not removed, since Karabiner-Elements shares it; the user is told what remains and pointed to a new guide. After removal the daemon evidence caches are invalidated so the refusal clears on fresh evidence, then KeyPath starts. This is not a recipe: privileged recipes stay unavailable, and a forged plan naming the removal is refused. The clean-VM run from 1.0.1 is written up in docs/testing/upgrade-from-1-0-1.md and has not been run (the mini is offline). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Probe and boot out gui/<uid>/ as well as system/ for every legacy label. The refusal's own detector reads both, so the cleanup must agree with it. - Match /Library/PrivilegedHelperTools/com.keypath.helper in the process pattern; it was missed. A test now checks every 1.0.1 process path. - Count a process as legacy only when its owner UID is 0, read with sysctl, and re-check the UID as root before signalling. EPERM alone also matches another signed-in user's KeyPath. The real-process test grounds both PID 1 and this process. - Parse the real root script with bash -n in a test. - Move the Settings flow into LegacyRuntimeUpgradeFlow and test each branch. Drop Open Wizard from that state, since the wizard cannot detect 1.0.1. - Correct ADR-045: Settings is the only entry point today. - Fix misleading log and comments; document the turned-off and two-user cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings → Status never renders StatusDetail.actions, so the button added earlier did not exist on screen. While the 1.0.1 refusal stands, the hero's primary button is now Remove Old Service (in place of Fix it, which opens a wizard that cannot detect 1.0.1), and the hero message is the refusal. Also from review: - Drop the SMAppService unregistration step. Release builds do not ship 1.0.1's plists, so it could never run; the ADR now says the record is left. - Bound each launchctl bootout with kp_timeout (#927). - Remove 1.0.1's other KeyPath-owned logs. - Delete an unused constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Review (diff was truncated at 60KB, so the tail of 1.
2. Unreadable launchctl state makes removal fail
3. Minor
No other correctness issues found in the reviewed portion. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21c84611f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /// Matches the 1.0.1 kanata launcher and kanata (in the bundle or | ||
| /// `/Library/KeyPath/bin`), the bundled helper, and the SMJobBless helper copy. | ||
| /// `pgrep -f` and `grep -E` both take this extended regular expression. | ||
| public static let processPattern = #"Library/(KeyPath|HelperTools)/|PrivilegedHelperTools/com\.keypath\.helper"# |
There was a problem hiding this comment.
Restrict root process matching to KeyPath-owned executables
On a Mac running another app's root helper from Contents/Library/HelperTools/, this pattern also classifies that unrelated helper as part of KeyPath 1.0.1. inspectLive() includes it in rootProcesses, and batch(stopping:userID:) sends it SIGTERM after rechecking the same overly broad pattern. For example, /Applications/Other.app/Contents/Library/HelperTools/OtherHelper matches without containing any KeyPath identifier. Consequently, Remove Old Service can terminate unrelated privileged services, and a helper that respawns can also prevent cleanup verification from succeeding. Restrict the matches to the known KeyPath executable paths or identities.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Anchored the pattern on KeyPath executables only: KeyPath.app/Contents/Library/(KeyPath|HelperTools)/, /Library/KeyPath/bin/, and /Library/PrivilegedHelperTools/com.keypath.helper. Added tests that another vendor's HelperTools helper and a non-KeyPath privileged helper do not match.
🤖 Addressed by Claude Code
| return "The Karabiner virtual keyboard driver is still installed. Karabiner-Elements uses it, so KeyPath left it in place." | ||
| } | ||
| return "The Karabiner virtual keyboard driver from KeyPath 1.0.1 is still installed. KeyPath no longer needs it. " | ||
| + "To remove it, see “Upgrading from KeyPath 1.0.1” in KeyPath Help." |
There was a problem hiding this comment.
Make the upgrade guide discoverable in KeyPath Help
After cleanup on a Mac without Karabiner-Elements, this notice directs the user to an article that cannot be found in KeyPath Help. Repository-wide search finds no navigation link to the new guide, and HelpBrowserView.filteredTopics searches only the hard-coded HelpTopic.allTopics, which does not include it. Although build-offline-help.py packages the Markdown, packaging alone does not expose it in the browser. Add the guide to the help topics or provide a direct action that opens it so users can reach the promised driver-removal instructions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Added "Upgrading from KeyPath 1.0.1" to HelpTopic.allTopics (Getting Started), so the guide the notice names appears and is searchable in the Help browser.
🤖 Addressed by Claude Code
The process pattern matched any Contents/Library/HelperTools/ path, so a root helper from another app could be counted as legacy and sent SIGTERM. Anchor it on KeyPath.app, /Library/KeyPath/bin, and com.keypath.helper, with tests for another vendor's helper. The driver notice points to "Upgrading from KeyPath 1.0.1" in KeyPath Help, but Help only lists HelpTopic.allTopics. Add the guide there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the claude[bot] review:
🤖 Addressed by Claude Code |
|
Review of PR #1362 (the supplied diff was truncated at 60KB, so I did not see the rest of the InstallerEngine/wizard wiring or the tests and docs.) No blocking correctness problems in the part I saw. Cleanup is only reported as success after a fresh read-only check. It re-checks the PID and uid as root before sending a signal. A failed batch is never reported as success. Points to check:
|
A root 1.0.1 process that ignores SIGTERM now gets SIGKILL 2s later, in the same administrator batch, after its owner and command line are re-checked as root. A test also asserts the script defines kp_timeout, which every bootout uses. The guide says the old Login Items entry can be ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the second claude[bot] review:
🤖 Addressed by Claude Code |
|
Review note: the diff was truncated at 60KB, so I only reviewed the part that was supplied. That covers the session-runtime changes, the upgrade flow, the settings UI, the InstallerEngine changes, and most of No blocking issues found. The design is sound: root-owned PIDs are re-checked inside the root script before each signal, success is only reported after a fresh observation, and a failed batch is never reported as success. Two non-blocking points:
|
|
Re the third claude[bot] review (both points answered in the previous reply):
🤖 Addressed by Claude Code |
Earlier commits were formatted with Homebrew's swiftformat 0.64.5 instead of
the pinned 0.61.1. That expanded one-line bodies in files this PR touches,
which broke SessionTerminalDiagnosticPlacementTests (it matches a one-line
`if sessionStartIsCurrent(generation) { onError?`) and added unrelated churn.
Rebuilt each touched file as master plus only this PR's changes, then
formatted with the pinned 0.61.1 (`mise exec swiftformat`). `--lint` passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Correction to my earlier replies about formatting churn. The churn did not come from the pinned SwiftFormat. I had run Homebrew's 0.64.5 by mistake. Commit 048fd66 rebuilds each touched file as master plus this PR's changes only, formatted with the pinned 0.61.1. The unrelated hunks are gone, and that churn was also what broke 🤖 Addressed by Claude Code |
|
Review of PR #1362 (1.0.1 legacy runtime removal) The diff I received was truncated at 60 KB, so I did not see the rest of the test changes or the docs and guides. I found no blocking defects in the part I did see. The design is sound: one admin prompt, a re-check of owner and command line as root before each signal, and success only after a fresh read-only observation. The Karabiner driver is reported but never removed. Non-blocking points to check:
|
`launchctl print gui/<uid>/<label>` exits 112 ("could not find domain") when
there is no GUI session, not 113. A domain that does not exist holds no job,
so 112 now counts as absent instead of unreadable. Measured on this Mac.
A new test pulls the `grep -E` argument out of the generated root script and
runs it through bash, so a quoting or escaping regression fails a test. The
verification failure now tells the user to try again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the fourth claude[bot] review:
🤖 Addressed by Claude Code |
|
Automated review of the supplied diff. The diff was truncated at 60000 bytes, so the tail of I found no blocking issues in the reviewed part. The design is sound: one administrator batch, a re-check of PID owner and command line as root before each signal, success only on fresh read-only evidence, and unreadable launchd state never counted as absent. Minor notes:
🤖 Generated with Claude Code |
As root, PID 1 is signalable and the test's premise does not hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the fifth claude[bot] review:
🤖 Addressed by Claude Code |
|
Review of PR 1362 (1.0.1 upgrade cleanup) I found no blocking correctness issues in the supplied diff. The diff was truncated at 60 KB, so I did not see the rest of the tests or the final files. These are minor points to confirm:
|
1.0.1's ServiceManagement record is left behind on purpose. With an .enabled record, no process and no TCP listener, the daemon check now has a test that it stays not-running across five samples after invalidateEvidenceCaches(). The removal failure message also suggests a restart if retrying keeps failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the sixth claude[bot] review:
🤖 Addressed by Claude Code |
|
Review (diff truncated at 60 KB, so the tail of the tests and the docs were not reviewed) I found no blocking issues in the visible portion. The cleanup is well-structured:
Minor, non-blocking notes:
Overall, this looks good to me. |
The read-only probe used the real UID, while the root script re-checks with ps -o uid=, which reports the effective UID. Use the effective UID in both, so a process cannot pass one check and fail the other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the seventh claude[bot] review:
🤖 Addressed by Claude Code |
|
Review (diff truncated at 60 KB, so the tail of I found no blocking correctness or security issues in the supplied portion. The cleanup is well-guarded:
Two non-blocking things to confirm:
Test coverage for the cleanup logic, the shell quoting and the live probes looks solid. |
Two review rounds asked the same questions from a truncated diff. Answer them where the code is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the eighth claude[bot] review. Both points repeat earlier rounds, so the answers are now comments in the code (e7db5ec):
🤖 Addressed by Claude Code |
|
Review of PR #1362 (the supplied diff was truncated after about 60KB, so the tail of the tests and the docs were not reviewed). I found no blocking issues in the part I could see. What I checked:
Minor notes, none blocking:
|
|
Re the ninth claude[bot] review: no change needed. Both notes are the accepted cases, and in both the post-batch verification reports a failure instead of a false success. That failure message tells the user to try again or restart first. I'm keeping the log files in 🤖 Addressed by Claude Code |
|
Review (diff truncated at 60KB; tests/docs tail not reviewed) Findings:
No blocking correctness or security issues found in the reviewed portion. Test coverage of the cleanup flow (cancellation, partial failure, PID recycling guard, shell-quoting) is strong. |
removeLegacyPrivilegedRuntime took a PrivilegeBroker it never used; the admin prompt goes through WizardDependencies.executePrivilegedBatch. Dropping it avoids implying broker-mediated privilege. Also note that invalidateEvidenceCaches() must follow a verified removal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the tenth claude[bot] review (d43c872):
🤖 Addressed by Claude Code |
|
Automated review of the supplied diff. The diff was truncated at 60KB, so the rest of the test files and the docs and guide changes were not reviewed. No blocking findings in the reviewed portion.
🤖 Generated with Claude Code |
|
Re the eleventh claude[bot] review: no change needed.
🤖 Addressed by Claude Code |
|
Review (diff truncated at 60KB; test files and docs after the cut were not reviewed) No blocking issues found. Notes:
|
A bare -128 could appear in unrelated output, such as a PID or path, and be misreported as a cancelled password prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the twelfth claude[bot] review (36968c5): 🤖 Addressed by Claude Code |
|
Review (diff truncated at 60KB; tests and docs tail not reviewed). Findings:
No blocking correctness or security issues found in the reviewed portion. Item 2 is the one I'd address before merge. |
The pattern matched anywhere in the command line, so a root process with a KeyPath path as an argument, such as vim /Library/KeyPath/bin/kanata, would be counted as legacy and killed. Anchor every alternative at the start of the command line. pgrep -f and grep -E both honour the anchor (checked live). Also stop showing a stale refusal as the reason a start failed after removal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re the thirteenth claude[bot] review (0da1376): 🤖 Addressed by Claude Code |
|
Review of the visible diff. It was truncated at 60KB, so I did not see the rest of the tests or the docs. No blocking issues found. The root script is guarded: it re-checks the owner and command line of each PID as root, it bounds Non-blocking:
|
|
Re the fourteenth claude[bot] review: no change needed.
That concludes review rounds on this PR. Further changes will only be for a CI failure or a real defect. 🤖 Addressed by Claude Code |
Conflict in ServiceLifecycleCoordinator+SessionRuntime.swift: both sides added independent declarations at the same spot (legacyDaemonRefusalMessage here; sessionLaunchArguments and awaitCapabilityReport from #1361). Kept both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review (diff truncated at 60KB; test file tail not reviewed) Overall the cleanup design looks solid: single admin prompt, fresh read-only verification, PID/owner/command re-check as root before signalling, and the driver is reported rather than removed. Findings:
No blocking issues found in the supplied portion. |
Part of #1353 (release-blocker). It does not close the issue: the clean-VM run from 1.0.1 has not happened, and nothing in the release process enforces it yet.
What a user upgrading from 1.0.1 now sees
The session runtime still refuses to start beside 1.0.1's root kanata daemon. While that refusal stands, Settings → Status shows it as the main message and replaces Fix it with Remove Old Service. One administrator prompt removes the old stack, then KeyPath starts. The toast says whether the Karabiner driver is still installed.
How it works
InstallerEngine.removeLegacyPrivilegedRuntime(using:)runs inside the installer transaction. The published refusal issue carriesAutoFixAction.removeLegacyPrivilegedRuntime, andrunSingleActionroutes it. The wizard cannot detect 1.0.1 yet, so Settings is the only entry point (ADR-045 updated).LegacyPrivilegedRuntimeCleanup(new,KeyPathInstallationWizard/Core):launchctl printonsystem/andgui/<uid>/for eachcom.keypath.*label 1.0.1 used, root-owned KeyPath processes (owner UID read withsysctl), and KeyPath's own system files. If nothing is found, no prompt is shown.kp_timeout-boundedlaunchctl bootoutfor every target, SIGTERM to the observed root PIDs only after re-checking UID 0 and the command line as root, thenrm -fof KeyPath's own files.SMAppServicecannot resolve them. A stale Login Items entry may remain. Whether a reboot brings anything back is step 12 of the VM plan.Testing
LegacyPrivilegedRuntimeCleanupTests(18 tests) cover: no prompt when nothing is left, one batch for the whole cleanup, polling for teardown, cancelled authorization, a failed batch never counting as success, verification failure, unreadable state, the driver never being touched, engine routing and completion states, smuggled-recipe refusal, every 1.0.1 process path matching the pattern,bash -non the real root script, and a real-process ADR-040 grounding test (PID 1 is root and returns EPERM, this process is not root).LegacyRuntimeUpgradeFlowTestscover every Settings outcome. Existing refusal, MainAppStateController, InstallerEngine, and all Lint suites updated and passing.swift test --filter 'LegacyPrivilegedRuntimeCleanupTests|LegacyRuntimeUpgradeFlowTests|SessionLegacyDaemonRefusalTests|SessionFailureRecoveryMessageTests|MainAppStateControllerTests|InstallerEngine|Lint': 215 XCTest and 42 Swift Testing tests pass, no warnings. The full suite was not run locally.python3 Scripts/check-accessibility.pypasses.docs/testing/upgrade-from-1-0-1.md. It needs vm-lab on the mini, which is offline. Nothing has exercised a real 1.0.1 upgrade end to end, and nothing was deployed to/Applications.Review gate
./Scripts/review-gate.shran locally three times. The first two passes found real defects, all fixed here: the GUI launchd domain was missed, the SMJobBless helper path didn't match, another user's processes could be killed, and the button was never rendered (Settings does not drawStatusDetail.actions). The third pass: REVIEW-VERDICT: PASS. Remaining notes, all PLAUSIBLE or lower, none a false success:invalidateEvidenceCaches()has no direct test of its effect onevaluateStatus.Still open on #1353
docs/testing/upgrade-from-1-0-1.mdon clean macOS 26 and 27 VMs starting from 1.0.1, and record the results there.release-doctor.shhas no such check).guides/upgrading-from-1-0-1.mdto gh-pages and link it fromdocs.md. Check the driverdeactivatecommand it gives on the VM.SystemValidatorsession snapshot) so it can offer the fix too.🤖 Generated with Claude Code