From 8154c890027396faaa7bdf4466381218c3e209b0 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 22 Sep 2026 17:26:48 -0700 Subject: [PATCH 1/2] test: assert the staged Computer Use helper carries no raw quarantine 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 --- .../ComputerUseRuntimeService.swift | 2 +- .../ComputerUseRuntimeServiceTests.swift | 144 +++++++++++------- .../HelperBundleFixture.swift | 90 +++++++++++ .../TestQuarantineAttribute.swift | 54 +++++++ 4 files changed, 230 insertions(+), 60 deletions(-) create mode 100644 Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/HelperBundleFixture.swift create mode 100644 Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/TestQuarantineAttribute.swift diff --git a/Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseRuntimeService.swift b/Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseRuntimeService.swift index 35a8d89a2a0e..9c7c1402d25e 100644 --- a/Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseRuntimeService.swift +++ b/Packages/macOS/CmuxComputerUse/Sources/CmuxComputerUse/ComputerUseRuntimeService.swift @@ -1838,7 +1838,7 @@ public final class ComputerUseRuntimeService { return paths } - nonisolated private static func installHelper( + nonisolated static func installHelper( nested: URL, destination: URL, directory: URL diff --git a/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/ComputerUseRuntimeServiceTests.swift b/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/ComputerUseRuntimeServiceTests.swift index 2396090df39f..b662fade2faa 100644 --- a/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/ComputerUseRuntimeServiceTests.swift +++ b/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/ComputerUseRuntimeServiceTests.swift @@ -1,80 +1,106 @@ -import CoreServices import Foundation import Testing @testable import CmuxComputerUse +/// Staged helper copies must never carry `com.apple.quarantine`, whatever the +/// bundled source carried: LaunchServices shows the first-open dialog for any +/// record on the copy, including the empty record Foundation's +/// `quarantineProperties = nil` writes on macOS 26.4.1 (#13803). struct ComputerUseRuntimeServiceTests { - @Test func copiedHelperReleasesQuarantineWithoutFollowingSymlinks() throws { - let fileManager = FileManager.default - let root = fileManager.temporaryDirectory.appendingPathComponent( - "cmux-computer-use-quarantine-\(UUID().uuidString)", - isDirectory: true - ) - defer { try? fileManager.removeItem(at: root) } + @Test func releasingAQuarantinedHelperCopyRemovesTheAttributeWithoutFollowingSymlinks() throws { + let fixture = try HelperBundleFixture() + defer { fixture.remove() } + let outside = fixture.root.appendingPathComponent("outside-helper", isDirectory: false) + try Data("outside".utf8).write(to: outside) + let link = fixture.executable + .deletingLastPathComponent() + .appendingPathComponent("outside-link", isDirectory: false) + try FileManager.default.createSymbolicLink(at: link, withDestinationURL: outside) + let record = TestQuarantineAttribute.webDownloadRecord() + for entry in try fixture.bundleEntries() where entry != link { + try TestQuarantineAttribute.apply(record, to: entry) + } + try TestQuarantineAttribute.apply(record, to: outside) - let helper = root.appendingPathComponent( - "cmux Computer Use.app", - isDirectory: true - ) - let macOSDirectory = helper.appendingPathComponent( - "Contents/MacOS", - isDirectory: true - ) - try fileManager.createDirectory( - at: macOSDirectory, - withIntermediateDirectories: true + try ComputerUseRuntimeService.releaseCopiedHelperFromQuarantine( + at: fixture.bundle, + fileManager: .default ) - let executable = macOSDirectory.appendingPathComponent("cmux-cua") - try Data("helper".utf8).write(to: executable) - let outside = root.appendingPathComponent("outside-helper") - try Data("outside".utf8).write(to: outside) - let symlink = macOSDirectory.appendingPathComponent("outside-link") - try fileManager.createSymbolicLink( - at: symlink, - withDestinationURL: outside - ) + for entry in try fixture.bundleEntries() { + #expect(try TestQuarantineAttribute.record(at: entry) == nil, "\(entry.path)") + } + #expect(try TestQuarantineAttribute.record(at: outside) == record) + } - for url in [ - helper, - helper.appendingPathComponent("Contents", isDirectory: true), - macOSDirectory, - executable, - outside, - ] { - try applyTestQuarantine(to: url) + @Test func releasingACleanHelperCopyLeavesNoAttributeBehind() throws { + let fixture = try HelperBundleFixture() + defer { fixture.remove() } + for entry in try fixture.bundleEntries() { + #expect(try TestQuarantineAttribute.record(at: entry) == nil, "\(entry.path)") } try ComputerUseRuntimeService.releaseCopiedHelperFromQuarantine( - at: helper, - fileManager: fileManager + at: fixture.bundle, + fileManager: .default ) - for url in [ - helper, - helper.appendingPathComponent("Contents", isDirectory: true), - macOSDirectory, - executable, - ] { - #expect(try quarantineProperties(at: url) == nil) + for entry in try fixture.bundleEntries() { + #expect(try TestQuarantineAttribute.record(at: entry) == nil, "\(entry.path)") } - #expect(try quarantineProperties(at: outside) != nil) } - private func applyTestQuarantine(to url: URL) throws { - var values = URLResourceValues() - values.quarantineProperties = [ - kLSQuarantineTypeKey as String: kLSQuarantineTypeWebDownload as String, - kLSQuarantineTimeStampKey as String: Date(), - kLSQuarantineAgentNameKey as String: "CmuxComputerUseTests", - ] - var mutableURL = url - try mutableURL.setResourceValues(values) + @Test func stagingACleanBundledHelperProducesAQuarantineFreeCopy() throws { + let fixture = try HelperBundleFixture() + defer { fixture.remove() } + let directory = fixture.root.appendingPathComponent("staged", isDirectory: true) + let destination = directory.appendingPathComponent( + "cmux Computer Use.app", + isDirectory: true + ) + + let installed = try #require( + ComputerUseRuntimeService.installHelper( + nested: fixture.bundle, + destination: destination, + directory: directory + ) + ) + + #expect(installed == destination) + let stagedEntries = try fixture.entries(of: destination) + #expect(stagedEntries.count == (try fixture.bundleEntries().count)) + for entry in stagedEntries { + #expect(try TestQuarantineAttribute.record(at: entry) == nil, "\(entry.path)") + } } - private func quarantineProperties(at url: URL) throws -> [String: Any]? { - try url.resourceValues( - forKeys: [.quarantinePropertiesKey] - ).quarantineProperties + @Test func stagingAQuarantinedBundledHelperProducesAQuarantineFreeCopy() throws { + let fixture = try HelperBundleFixture() + defer { fixture.remove() } + let record = TestQuarantineAttribute.webDownloadRecord(agent: "Homebrew") + for entry in try fixture.bundleEntries() { + try TestQuarantineAttribute.apply(record, to: entry) + } + let directory = fixture.root.appendingPathComponent("staged", isDirectory: true) + let destination = directory.appendingPathComponent( + "cmux Computer Use.app", + isDirectory: true + ) + + let installed = try #require( + ComputerUseRuntimeService.installHelper( + nested: fixture.bundle, + destination: destination, + directory: directory + ) + ) + + #expect(installed == destination) + for entry in try fixture.entries(of: destination) { + #expect(try TestQuarantineAttribute.record(at: entry) == nil, "\(entry.path)") + } + // Only the copy is released; the bundled source keeps its record. + #expect(try TestQuarantineAttribute.record(at: fixture.executable) == record) } } diff --git a/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/HelperBundleFixture.swift b/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/HelperBundleFixture.swift new file mode 100644 index 000000000000..1b1e17e728eb --- /dev/null +++ b/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/HelperBundleFixture.swift @@ -0,0 +1,90 @@ +import Darwin +import Foundation + +/// A minimal `cmux Computer Use.app` tree inside a private temporary directory. +/// +/// The tree mirrors the shipped helper's shape: directories, regular files, +/// and a hidden marker file, so a quarantine pass has to handle each kind. +struct HelperBundleFixture { + /// The temporary directory that owns everything the fixture creates. + let root: URL + /// The helper bundle, `root/cmux Computer Use.app`. + let bundle: URL + /// The helper executable inside `bundle`. + let executable: URL + /// The `Info.plist` inside `bundle`. + let infoPlist: URL + /// The hidden managed-helper marker inside `bundle`. + let hiddenMarker: URL + + private let fileManager: FileManager + + /// Creates the tree on disk. + init(fileManager: FileManager = .default) throws { + self.fileManager = fileManager + root = Self.canonicalTemporaryDirectory(fileManager).appendingPathComponent( + "cmux-computer-use-helper-\(UUID().uuidString)", + isDirectory: true + ) + bundle = root.appendingPathComponent("cmux Computer Use.app", isDirectory: true) + let contents = bundle.appendingPathComponent("Contents", isDirectory: true) + let macOSDirectory = contents.appendingPathComponent("MacOS", isDirectory: true) + let resources = contents.appendingPathComponent("Resources", isDirectory: true) + executable = macOSDirectory.appendingPathComponent("cmux-cua", isDirectory: false) + infoPlist = contents.appendingPathComponent("Info.plist", isDirectory: false) + hiddenMarker = resources.appendingPathComponent( + ".cmux-cua-managed-helper", + isDirectory: false + ) + try fileManager.createDirectory(at: macOSDirectory, withIntermediateDirectories: true) + try fileManager.createDirectory(at: resources, withIntermediateDirectories: true) + try Data("helper".utf8).write(to: executable) + try fileManager.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path) + try Data("".utf8).write(to: infoPlist) + try Data("managed".utf8).write(to: hiddenMarker) + } + + /// Every entry of a bundle tree, the bundle itself first. + /// + /// Symbolic links are listed as themselves and never followed. + func entries(of bundleURL: URL) throws -> [URL] { + guard + let enumerator = fileManager.enumerator( + at: bundleURL, + includingPropertiesForKeys: [], + options: [] + ) + else { + throw CocoaError(.fileReadUnknown) + } + var entries = [bundleURL] + for case let entry as URL in enumerator { + entries.append(entry) + } + return entries + } + + /// Every entry of the fixture's own bundle, the bundle itself first. + func bundleEntries() throws -> [URL] { + try entries(of: bundle) + } + + /// Deletes the temporary directory. + func remove() { + try? fileManager.removeItem(at: root) + } + + /// The temporary directory with `/var` resolved to `/private/var`. + /// + /// `realpath(3)` keeps the `/private` prefix that Foundation's own URL + /// resolution strips, so fixture URLs compare equal to the URLs + /// `FileManager` enumeration produces. + private static func canonicalTemporaryDirectory(_ fileManager: FileManager) -> URL { + let path = fileManager.temporaryDirectory.path + guard let resolved = realpath(path, nil) else { + return URL(fileURLWithPath: path, isDirectory: true) + } + defer { free(resolved) } + return URL(fileURLWithPath: String(cString: resolved), isDirectory: true) + } +} diff --git a/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/TestQuarantineAttribute.swift b/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/TestQuarantineAttribute.swift new file mode 100644 index 000000000000..de31fa982893 --- /dev/null +++ b/Packages/macOS/CmuxComputerUse/Tests/CmuxComputerUseTests/TestQuarantineAttribute.swift @@ -0,0 +1,54 @@ +import Darwin +import Foundation + +/// Raw access to `com.apple.quarantine` for tests. +/// +/// Gatekeeper reads the extended attribute itself, so assertions go through +/// `getxattr(2)` rather than Foundation's `quarantineProperties` view, which +/// is derived from the record and differs between macOS releases. +struct TestQuarantineAttribute { + /// The extended attribute LaunchServices stores quarantine state in. + static let name = "com.apple.quarantine" + + /// A failed `setxattr(2)` or `getxattr(2)` call. + struct CallError: Error, CustomStringConvertible { + let call: String + let path: String + let code: Int32 + + var description: String { + "\(call) failed for \(path): errno \(code)" + } + } + + /// A web-download record in LaunchServices' `flags;time;agent;uuid` format. + /// + /// The timestamp is fixed so a test never depends on the wall clock. + static func webDownloadRecord(agent: String = "CmuxComputerUseTests") -> String { + "0081;6ab30fce;\(agent);\(UUID().uuidString)" + } + + /// Writes `record` on `url` itself with `setxattr(2)`, never following a symbolic link. + static func apply(_ record: String, to url: URL) throws { + let status = record.withCString { value in + setxattr(url.path, name, value, strlen(value), 0, XATTR_NOFOLLOW) + } + guard status == 0 else { + throw CallError(call: "setxattr", path: url.path, code: errno) + } + } + + /// Reads the raw record on `url` itself, or nil when the attribute is absent. + static func record(at url: URL) throws -> String? { + var buffer = [CChar](repeating: 0, count: 1_024) + let length = getxattr(url.path, name, &buffer, buffer.count, 0, XATTR_NOFOLLOW) + if length < 0 { + let code = errno + guard code == ENOATTR else { + throw CallError(call: "getxattr", path: url.path, code: code) + } + return nil + } + return String(decoding: buffer[.. Date: Tue, 22 Sep 2026 17:33:01 -0700 Subject: [PATCH 2/2] fix: release the staged Computer Use helper from quarantine with removexattr `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;