Repository navigation
Fix the Computer Use Gatekeeper prompt that returned after every update on macOS 26.4 - #13819
Conversation
… attribute The #13602 test checked Foundation's quarantineProperties read-back, which is derived from the record and differs between macOS releases. Gatekeeper reads the com.apple.quarantine extended attribute itself, so these tests apply it with setxattr(2) and assert with getxattr(2), through both the release call and the real staging path (installHelper, widened from private to internal for the test). They cover a quarantined source and a clean source; on macOS 26.4.1 the clean source ends up with an empty record, which is what brings the Gatekeeper dialog back after every update (#13803). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…vexattr `URLResourceValues.quarantineProperties = nil` is not a removal primitive. It writes a quarantine record, and what it writes for nil depends on the macOS release: 26.4.1 stores an empty record (`0200;<time>;;` on files, `0200;00000000;;` on directories) that still triggers the Gatekeeper first-open dialog, 15.7.4 throws an I/O error on an entry that carries no record (so staging a clean bundle fails outright), and 27.0 removes it. Every update re-stages the helper, so the dialog came back after every update on every install type, and the Homebrew record from #13430 was only replaced by the empty one. Gatekeeper reads the raw `com.apple.quarantine` attribute, so the new `ComputerUseHelperQuarantineRelease` probes each entry with getxattr(2) and removes the attribute with removexattr(2) and XATTR_NOFOLLOW, the way Sparkle's SUFileManager does. Symbolic links are neither followed nor modified, entries without the attribute are left alone, and a failed removal is logged and reported rather than aborting the stage. The reuse branch releases an already-staged copy in place so helpers staged by the affected nightly are healed without restaging. Supersedes #13602 and re-fixes #13430. Closes #13803. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
All contributors have signed the CLA ✍️ ✅ |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughThe change adds raw quarantine-attribute removal for helper bundle trees, integrates it into helper staging, logs removal failures, and adds tests for traversal, symlinks, cancellation, failures, and clean or quarantined sources. ChangesQuarantine release
Priority: ⬆️ High Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix · Severity of issue fixed: High Sequence Diagram(s)sequenceDiagram
participant RuntimeService
participant ReleaseHelper
participant FileSystem
participant UnifiedLog
RuntimeService->>ReleaseHelper: release(treeAt: destination)
ReleaseHelper->>FileSystem: inspect entries with getxattr
ReleaseHelper->>FileSystem: remove attributes with removexattr
FileSystem-->>ReleaseHelper: Report
ReleaseHelper-->>RuntimeService: return released URLs and failures
RuntimeService->>UnifiedLog: log failed removals
Merge Risk: 🔵 Low · up to A narrow same-user race can make cleanup affect files outside the helper tree. The PR is otherwise mergeable with this risk explicitly accepted or addressed. 🚥 Pre-merge checks | ✅ 24 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (24 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Ran the immutable tests-only commit 8154c89 and current fix head 86073ec through the whole CmuxComputerUse package on two standard GitHub-hosted macOS runners in my personal fork: run 35805814496.
The macOS 15 failures were the clean-helper nil quarantine setter (Cocoa error 512) and clean-helper staging returning nil. The newer macOS 26.6.2 image did not reproduce the reported 26.4.1 behavior, so this is additional cross-runtime evidence, not a replacement for the author's 26.4.1 reproduction. Runtime and SDK/toolchain metadata, original command exits, counts, and raw logs are retained in the run artifacts. This was package-only: no app build, signing, or Gatekeeper launch claim. |
|
Thanks, that closes the matrix nicely: 15.7.9 fails the same two clean-helper cases (Cocoa 512, staging nil) as my 15.7.4 builder run, and 26.6.2 behaving like 27.0 (no-op on a clean copy) narrows the empty-record write to the 26.4.x line. I folded both rows into the OS table in the description with a link to your run. One more data point from the tagged-build verification on the 26.4.1 Mac: while the fixed DEV build was running, another agent's DEV build of unfixed |
|
Review audit against HEAD 86073ec (re-checked after the final CI run):
Inline review threads: none. CHANGES_REQUESTED: none. Required checks ( |
Closes #13803. Supersedes #13602 and re-fixes #13430.
Problem
On macOS 26.4.1, seconds after a Sparkle update relaunches cmux, Gatekeeper shows the first-open dialog for "cmux Computer Use" ("downloaded on an unknown date"). The app bundle carries no quarantine at all. The staged helper copy under
~/Library/Application Support/cmux/cmux-cua/helper/<scope>/carries0200;00000000;;on every directory and0200;<copy time>;;on every file: a record with no agent and no download UUID.#13602 releases the copied helper by assigning
URLResourceValues.quarantineProperties = nilon every entry. That call is not a removal primitive. It sets quarantine properties, and what it does withnildepends on the macOS release. Replayed with a standalone script against the same Foundation, on aFileManager.copyItemcopy of a clean tree and of a tree with a real web-download record:NSCocoaErrorDomain 512(underlyingioErr -36) on the first entry;installHelperreturns nil and the helper is not staged at all0200;00000000;;on directories and0200;<now>;;on files(15.7.4, 26.4.1 and 27.0 from my replays; 15.7.9 and 26.6.2 from @teamleaderleo's GitHub-hosted runs of this PR's commits, linked below.)
On 26.4.1 the setter builds a record out of the nil dictionary: flags
0x0200(LSQuarantineIsOwnedByCurrentUser; the process resolves the file owner through the Membership API while doing it), a timestamp of now for files and 0 for directories, no agent, no UUID.NSURL.setResourceValue(NSNull())andsetResourceValue(nil)do the same; an empty dictionary[:]writes a full default0281;<now>;;<UUID>record instead, sonilis its own code path, not "empty properties". LaunchServices treats any record as quarantine: syspolicyd evaluates the helper (GK evaluateScanResult: 0 … (id: com.cmuxterm.cua)), CoreServicesUIAgent raisesGKQuarantineResolver, and the helper stays held in exec until the Open click sets the user-approved bit (0240). Every update re-stages the helper because its contents change, so the dialog returns after every update on every install type. For Homebrew installs the real download record is replaced by the empty one, which still prompts, so #13430 was not fixed either.Why it shipped: the
swift-package-testsjob was skipped on #13602 (it only runs with thefull-cilabel), and the test asserted through Foundation'squarantinePropertiesread-back, which is derived from the record. On 26.4.1 that read-back returns[LSQuarantineIsOwnedByCurrentUser, LSQuarantineTimeStamp]after the call, so the #13602 test fails on this OS (reproduced: 4 issues). On 15.x and 27.0 it passes because the setter does remove an existing record, and the test only ever exercised that case. Foundation's view cannot stand in for the attribute Gatekeeper reads.Fix
ComputerUseHelperQuarantineRelease(new file) walks the copied tree withlstat, probes each entry withgetxattr(2), and removescom.apple.quarantinewithremovexattr(path, name, XATTR_NOFOLLOW), the primitive Sparkle'sSUFileManager releaseItemFromQuarantineAtRootURLuses. Symbolic links are neither followed nor modified, entries without the attribute are not touched, and the Foundation setter is never called. The pass continues past an entry it cannot change and reports it.ComputerUseRuntimeService.releaseCopiedHelperFromQuarantinelogs each failure (file name and errno, viaos.Logger) and keeps the copy.Two call sites share that path: staging (
installHelper, on the temporary copy before it moves into place) and the reuse branch ofensureStandaloneHelperInstalledWithinLifecycle, which releases an already-staged copy in place so a helper staged by the affected nightly is healed at the next launch without restaging or restarting the daemon.Resulting behavior: a fresh stage from a clean bundle and from a quarantined bundle both end with zero
com.apple.quarantineentries and no Gatekeeper dialog, and on macOS 15 staging a clean bundle no longer fails.Trade-offs
getxattrper entry at startup (15 syscalls) inside the existing detached check task; it only writes when an entry actually carries the attribute.installHelperwent fromprivatetointernalso the tests cover the real staging path (copy, release, move), not only the primitive.os_logerror line), so no localization entries were added; no shortcuts changed.Tests
setxattrand assert withgetxattr, through both the release call andinstallHelper, for a quarantined source and a clean source. Against the unfixed code: macOS 26.4.1: 4 of 4 fail (29 issues); macOS 15.7.4: 2 of 4 fail (clean source: Cocoa error 512 from the setter, and staging returns nil).main(including ci: bound the suite coverage gate #13820, which fixed amain-side CI guard); the package suite passes on the merged tree with no warnings in the package.Ran:
swift test --package-path Packages/macOS/CmuxComputerUseon this Mac (26.4.1, Swift 6.3.3) at both commits and on an AWS macOS 15.7.4 builder at both commits;python3 scripts/swift_file_length_budget.py(pass; the runtime service file shrank from 2146 to 2145 lines);python3 scripts/check-package-resolved-policy.py(OK). CI runs with thefull-cilabel somacos / swift-package-testsis a real gate here: this head first ran with thefull-cilabel:macos / swift-package-testspassed (so did compile admission, release-build and release-admission). Theapp-host unit testsshards failed on tests this PR does not touch (GhosttyTerminalViewVisibilityPolicyTests,WorkspacePanelGitBranchTests,WorkspaceRenameShortcutDefaultsTests,ZshShellIntegrationHandoffTests); all six shards also fail onmain's last two full-suite runs andci-statusis red there (#13707). Since that lane cannot go green on any PR right now, the label was removed afterwards and the PR is judged under the repository's standard compile-only PR policy like every other PR; the package-test evidence above stays on record in that first run. Final:ci-statusgreen on run 35809550189 (compile admission reused for the unchanged input fingerprint)..Real app verification (tagged Debug build of 86073ec,
cmux DEV issue-13803-helper-quarantine-regression, macOS 26.4.1)Staged scope for this tag:
helper/issu-cc22b4f14211e7ee. Nothing else underhelper/was touched.Fresh stage from a clean bundle. With no staged directory for the tag, launching the app staged the helper (15 entries).
xattr -lron the staged copy: 0com.apple.quarantineentries (onlycom.apple.provenance, which every file written under provenance tracking gets). Both helper daemons started from that copy. Unified log for the launch: the app openedcom.apple.coreservices.quarantine-resolveras LaunchServices always does, syspolicyd loggedGK evaluateScanResult: 2 … (id: com.cmuxterm.cua), and noGKQuarantineResolverline was logged (the nightly failure loggedevaluateScanResult: 0followed byGKQuarantineResolver initWithProperties). System Events reported no CoreServicesUIAgent window.Homebrew case. Wrote a web-download record (
0081;<time>;Chrome;<UUID>) on all 15 entries of the helper bundled inside the tagged app, deleted the staged directory again, relaunched. The new staged copy: 0 of 15 entries quarantined; the bundled source still carries its record on all 15 entries (only the copy is released). Same log shape:evaluateScanResult: 2, noGKQuarantineResolver, no CoreServicesUIAgent window, both daemons running. The bundled helper was restored to clean afterwards.In-place heal of an already-staged copy (the state the affected nightly leaves behind). Wrote the nightly's exact records onto this tag's staged copy (
0200;00000000;;on directories,0200;6ab30fce;;on files, 15 entries), quit the app, relaunched throughreload.sh --launch. The copy was judged current and released in place: same inode before and after (1143313763, so no restage), 0 of 15 entries quarantined, both daemons running, noGKQuarantineResolver, no CoreServicesUIAgent window.Control on the same Mac. At 18:35, while this build was running, another agent's DEV build of unfixed
mainre-staged its own helper intohelper/issu-928b8f80211aa4f1. That copy carries0200;00000000;;on the bundle directory and0200;<copy time>;;on the executable, syspolicyd loggedGK evaluateScanResult: 0for it, and CoreServicesUIAgent raisedGKQuarantineResolverthree times until its helper was approved. Same Mac, same minute, same helper binary; only the release code differs. (Looked at read-only; that directory belongs to another build.)Evidence files (xattr listings before/after, unified-log excerpts, daemon lists) are kept on the reporter's Mac under
~/.local/share/cmux-hq/issue-evidence/helper-quarantine-13803-fix/. A screenshot could not be taken from the agent shell (no Screen Recording grant for its responsible app), so the "no dialog" evidence is the unified log plus the System Events window enumeration; the tagged app is left running for a manual look.Not done: the build-fleet controller was unreachable from this Mac for the whole session (
cmux-ci statustimed out; feedback spooled locally), so there is no exact-SHA fleet build or HQ opener link. The verification above used a local./scripts/reload.sh --tag issue-13803-helper-quarantine-regression --launchbuild of the pushed merge commit at the user's request.🤖 Generated with Claude Code