diff --git a/apps/macos/Sources/OpenClaw/AppLaunchPresentationPolicy.swift b/apps/macos/Sources/OpenClaw/AppLaunchPresentationPolicy.swift
index 5048d2a6b011..d7d610f7e30f 100644
--- a/apps/macos/Sources/OpenClaw/AppLaunchPresentationPolicy.swift
+++ b/apps/macos/Sources/OpenClaw/AppLaunchPresentationPolicy.swift
@@ -190,6 +190,10 @@ struct AppLaunchRuntimePlan: Equatable {
!self.isElevationHost
}
+ var allowsCuaComputerControl: Bool {
+ !self.isElevationHost
+ }
+
func shouldAutoOpenChat(arguments: [String]) -> Bool {
self.allowsAutomaticPresentation &&
(arguments.contains("--chat") || arguments.contains("--webchat"))
diff --git a/apps/macos/Sources/OpenClaw/AppState.swift b/apps/macos/Sources/OpenClaw/AppState.swift
index 03788178178b..4bf2a944bc1e 100644
--- a/apps/macos/Sources/OpenClaw/AppState.swift
+++ b/apps/macos/Sources/OpenClaw/AppState.swift
@@ -376,6 +376,7 @@ final class AppState {
self.ifNotPreview {
let computerControlEnabled = isComputerControlEnabled()
let provider = ComputerControlProvider.current()
+ let launchPlan = AppLaunchRuntimePlan.current
let peekabooBridgeEnabled = self.peekabooBridgeEnabled
self.computerControlHostGeneration &+= 1
let generation = self.computerControlHostGeneration
@@ -389,7 +390,9 @@ final class AppState {
guard generation == self.computerControlHostGeneration else { return }
await CuaDriverHostCoordinator.shared.setEnabled(true)
case .peekaboo:
- await CuaDriverHostCoordinator.shared.setEnabled(false)
+ if launchPlan.allowsCuaComputerControl {
+ await CuaDriverHostCoordinator.shared.setEnabled(false)
+ }
guard generation == self.computerControlHostGeneration else { return }
await PeekabooBridgeHostCoordinator.shared.setEnabled(
peekabooBridgeEnabled && computerControlEnabled)
diff --git a/apps/macos/Sources/OpenClaw/ComputerControlProvider.swift b/apps/macos/Sources/OpenClaw/ComputerControlProvider.swift
index def46ba0819c..6f68828a397a 100644
--- a/apps/macos/Sources/OpenClaw/ComputerControlProvider.swift
+++ b/apps/macos/Sources/OpenClaw/ComputerControlProvider.swift
@@ -52,8 +52,10 @@ enum ComputerControlProvider: String, CaseIterable, Sendable {
static func current(
defaults: UserDefaults = AppDefaults.standard,
- cuaAvailable: Bool = CuaDriverArtifact.bundledExecutableURL != nil) -> Self
+ cuaAvailable: Bool = CuaDriverArtifact.bundledExecutableURL != nil,
+ launchPlan: AppLaunchRuntimePlan = .current) -> Self
{
+ guard launchPlan.allowsCuaComputerControl else { return .peekaboo }
guard let rawValue = defaults.string(forKey: computerControlProviderKey),
let provider = Self(rawValue: rawValue)
else { return .peekaboo }
diff --git a/apps/macos/Sources/OpenClaw/CuaDriverHostCoordinator.swift b/apps/macos/Sources/OpenClaw/CuaDriverHostCoordinator.swift
index 11fe56f5eb7d..603f30ed96db 100644
--- a/apps/macos/Sources/OpenClaw/CuaDriverHostCoordinator.swift
+++ b/apps/macos/Sources/OpenClaw/CuaDriverHostCoordinator.swift
@@ -200,6 +200,7 @@ final class CuaDriverHostCoordinator {
static let shared = CuaDriverHostCoordinator(
observeNotifications: true,
+ enablementAllowed: { AppLaunchRuntimePlan.current.allowsCuaComputerControl },
beforeDaemonStop: {
await MacNodeModeCoordinator.shared.prepareForCuaDaemonStop()
})
@@ -229,6 +230,7 @@ final class CuaDriverHostCoordinator {
private let readinessProbe: ReadinessProbe
private let restartSleep: @Sendable (Duration) async -> Void
private let permissionSnapshot: @MainActor () async -> [Capability: CapabilityAuthorizationStatus]
+ private let enablementAllowed: @MainActor () -> Bool
private let beforeDaemonStop: @MainActor () async -> Void
private var desiredEnabled = false
@@ -261,6 +263,7 @@ final class CuaDriverHostCoordinator {
permissionSnapshot: @escaping @MainActor () async -> [Capability: CapabilityAuthorizationStatus] = {
await PermissionManager.authorizationStatus([.accessibility, .screenRecording])
},
+ enablementAllowed: @escaping @MainActor () -> Bool = { true },
beforeDaemonStop: @escaping @MainActor () async -> Void = {})
{
self.notificationCenter = notificationCenter
@@ -271,6 +274,7 @@ final class CuaDriverHostCoordinator {
self.readinessProbe = readinessProbe
self.restartSleep = restartSleep
self.permissionSnapshot = permissionSnapshot
+ self.enablementAllowed = enablementAllowed
self.beforeDaemonStop = beforeDaemonStop
guard observeNotifications else { return }
@@ -297,12 +301,13 @@ final class CuaDriverHostCoordinator {
}
func setEnabled(_ enabled: Bool) async {
+ let effectiveEnabled = enabled && self.enablementAllowed()
let wasEnabled = self.desiredEnabled
- self.desiredEnabled = enabled
- if enabled, !wasEnabled {
+ self.desiredEnabled = effectiveEnabled
+ if effectiveEnabled, !wasEnabled {
self.restartAttempt = 0
}
- if !enabled {
+ if !effectiveEnabled {
self.restartTask?.cancel()
self.restartTask = nil
self.restartAttempt = 0
diff --git a/apps/macos/Sources/OpenClaw/MenuBar.swift b/apps/macos/Sources/OpenClaw/MenuBar.swift
index 004cbdf682ea..0803cc9ac415 100644
--- a/apps/macos/Sources/OpenClaw/MenuBar.swift
+++ b/apps/macos/Sources/OpenClaw/MenuBar.swift
@@ -432,7 +432,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
var nodeTerminationCleanup: @MainActor () async -> Void = {
// CUA shutdown drains the worker before closing the daemon socket; run it
// first so other cleanup cannot consume the app termination deadline.
- await CuaDriverHostCoordinator.shared.shutdown()
+ if AppLaunchRuntimePlan.current.allowsCuaComputerControl {
+ await CuaDriverHostCoordinator.shared.shutdown()
+ }
await TalkMLXSpeechSynthesizer.shared.shutdown()
await MacNodeModeCoordinator.shared.stopAndWait()
}
diff --git a/apps/macos/Tests/OpenClawIPCTests/AppLaunchPresentationPolicyTests.swift b/apps/macos/Tests/OpenClawIPCTests/AppLaunchPresentationPolicyTests.swift
index 87a3570940d6..37279bb36348 100644
--- a/apps/macos/Tests/OpenClawIPCTests/AppLaunchPresentationPolicyTests.swift
+++ b/apps/macos/Tests/OpenClawIPCTests/AppLaunchPresentationPolicyTests.swift
@@ -84,6 +84,7 @@ struct AppLaunchRuntimePlanTests {
#expect(policy.allowsUpdater)
#expect(policy.allowsDockIcon)
#expect(policy.allowsInteractiveServices)
+ #expect(policy.allowsCuaComputerControl)
#expect(policy.shouldAutoOpenChat(arguments: ["OpenClaw", "--chat"]))
#expect(policy.shouldAutoOpenDashboard(arguments: ["OpenClaw", "--dashboard"]))
}
@@ -99,6 +100,7 @@ struct AppLaunchRuntimePlanTests {
#expect(policy.allowsUpdater)
#expect(policy.allowsDockIcon)
#expect(policy.allowsInteractiveServices)
+ #expect(policy.allowsCuaComputerControl)
#expect(!policy.shouldAutoOpenChat(arguments: arguments))
#expect(!policy.shouldAutoOpenDashboard(arguments: arguments))
}
@@ -115,6 +117,7 @@ struct AppLaunchRuntimePlanTests {
#expect(!policy.allowsUpdater)
#expect(!policy.allowsDockIcon)
#expect(!policy.allowsInteractiveServices)
+ #expect(!policy.allowsCuaComputerControl)
#expect(!policy.shouldAutoOpenChat(arguments: arguments))
#expect(!policy.shouldAutoOpenDashboard(arguments: arguments))
#expect(DockIconManager.activationPolicy(
diff --git a/apps/macos/Tests/OpenClawIPCTests/ComputerControlSettingsTests.swift b/apps/macos/Tests/OpenClawIPCTests/ComputerControlSettingsTests.swift
index b6d176babf00..8553d9d0c922 100644
--- a/apps/macos/Tests/OpenClawIPCTests/ComputerControlSettingsTests.swift
+++ b/apps/macos/Tests/OpenClawIPCTests/ComputerControlSettingsTests.swift
@@ -31,6 +31,24 @@ struct ComputerControlSettingsTests {
#expect(ComputerControlProvider.current(defaults: defaults, cuaAvailable: true) == .peekaboo)
}
+ @Test func `elevation host ignores enabled CUA defaults while normal launches preserve them`() throws {
+ let suiteName = "ComputerControlElevationHostTests.\(UUID().uuidString)"
+ let defaults = try #require(UserDefaults(suiteName: suiteName))
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+ defaults.set(true, forKey: computerControlEnabledKey)
+ defaults.set(ComputerControlProvider.cua.rawValue, forKey: computerControlProviderKey)
+
+ #expect(isComputerControlEnabled(defaults: defaults))
+ #expect(ComputerControlProvider.current(
+ defaults: defaults,
+ cuaAvailable: true,
+ launchPlan: AppLaunchRuntimePlan(arguments: ["OpenClaw"])) == .cua)
+ #expect(ComputerControlProvider.current(
+ defaults: defaults,
+ cuaAvailable: true,
+ launchPlan: AppLaunchRuntimePlan(arguments: ["OpenClaw", "--elevation-host"])) == .peekaboo)
+ }
+
@Test func `bundled CUA locator accepts only a regular executable and never follows a symlink`() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("openclaw-cua-artifact-\(UUID().uuidString)", isDirectory: true)
diff --git a/apps/macos/Tests/OpenClawIPCTests/CuaDriverHostCoordinatorTests.swift b/apps/macos/Tests/OpenClawIPCTests/CuaDriverHostCoordinatorTests.swift
index db2fa1eb714a..fe23638c0862 100644
--- a/apps/macos/Tests/OpenClawIPCTests/CuaDriverHostCoordinatorTests.swift
+++ b/apps/macos/Tests/OpenClawIPCTests/CuaDriverHostCoordinatorTests.swift
@@ -58,6 +58,43 @@ struct CuaDriverHostCoordinatorTests {
#expect(launcher.processes.allSatisfy { !$0.isRunning })
}
+ @Test func `elevation host refuses CUA enablement before spawning a child`() async throws {
+ let root = self.shortTemporaryDirectory("elevation-host")
+ defer { try? FileManager.default.removeItem(at: root) }
+ let launcher = CuaProcessLauncherProbe()
+ let coordinator = CuaDriverHostCoordinator(
+ artifactURL: { root.appendingPathComponent("cua-driver") },
+ applicationSupportURL: { root },
+ bundleIdentifier: { "ai.openclaw.test" },
+ processLauncher: { launch, onTermination in
+ launcher.launch(launch, onTermination: onTermination)
+ },
+ readinessProbe: { _ in true },
+ enablementAllowed: {
+ AppLaunchRuntimePlan(arguments: ["OpenClaw", "--elevation-host"]).allowsCuaComputerControl
+ })
+
+ await coordinator.setEnabled(true)
+
+ #expect(launcher.launches.isEmpty)
+ #expect(coordinator.workerEndpoint == nil)
+
+ let normalCoordinator = CuaDriverHostCoordinator(
+ artifactURL: { root.appendingPathComponent("cua-driver") },
+ applicationSupportURL: { root },
+ bundleIdentifier: { "ai.openclaw.test" },
+ processLauncher: { launch, onTermination in
+ launcher.launch(launch, onTermination: onTermination)
+ },
+ readinessProbe: { _ in true },
+ enablementAllowed: {
+ AppLaunchRuntimePlan(arguments: ["OpenClaw"]).allowsCuaComputerControl
+ })
+ await normalCoordinator.setEnabled(true)
+ #expect(await self.waitForReadyLaunch(1, launcher: launcher, coordinator: normalCoordinator))
+ await normalCoordinator.setEnabled(false)
+ }
+
@Test func `socket directory is random owner-only and cleanup removes only its owned leaf`() throws {
let root = self.shortTemporaryDirectory("socket")
defer { try? FileManager.default.removeItem(at: root) }
diff --git a/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift b/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift
index 5bcea508a76b..9e3a9c4a9628 100644
--- a/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift
+++ b/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift
@@ -311,6 +311,36 @@ struct MacNodeHostWorkerTests {
workerManifest: cua) == descriptor)
}
+ @Test func `elevation host never advertises a persisted CUA provider`() throws {
+ let suiteName = "MacNodeElevationHostProviderTests.\(UUID().uuidString)"
+ let defaults = try #require(UserDefaults(suiteName: suiteName))
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+ defaults.set(true, forKey: computerControlEnabledKey)
+ defaults.set(ComputerControlProvider.cua.rawValue, forKey: computerControlProviderKey)
+ let provider = ComputerControlProvider.current(
+ defaults: defaults,
+ cuaAvailable: true,
+ launchPlan: AppLaunchRuntimePlan(arguments: ["OpenClaw", "--elevation-host"]))
+ #expect(provider == .peekaboo)
+
+ let cuaDescriptor = OpenClawProtocol.AnyCodable(["provider": "cua"])
+ let manifest = MacNodeHostManifest(
+ version: "test",
+ caps: ["screen", "computer"],
+ commands: [MacNodeScreenCommand.snapshot.rawValue, OpenClawComputerCommand.act.rawValue],
+ computerUse: cuaDescriptor,
+ pathEnv: "/usr/bin:/bin")
+ let workerManifest = try #require(MacNodeModeCoordinator.workerManifest(manifest, for: provider))
+ #expect(workerManifest.computerUse == nil)
+ let advertised = try #require(MacNodeModeCoordinator.computerUseDescriptor(
+ provider: provider,
+ commands: manifest.commands,
+ workerManifest: workerManifest))
+ let data = try JSONEncoder().encode(advertised)
+ let object = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
+ #expect((object["provider"] as? [String: Any])?["id"] as? String == "peekaboo")
+ }
+
@Test func `stale route updates cannot replace newer worker authority`() {
#expect(MacNodeHostWorker.routeUpdateIsCurrent(candidateGeneration: 4, currentGeneration: 4))
#expect(MacNodeHostWorker.routeUpdateIsCurrent(candidateGeneration: 5, currentGeneration: 4))
diff --git a/scripts/codesign-mac-app.sh b/scripts/codesign-mac-app.sh
index b182e1cdd489..c01b709542bd 100755
--- a/scripts/codesign-mac-app.sh
+++ b/scripts/codesign-mac-app.sh
@@ -329,11 +329,21 @@ verify_team_ids() {
fi
}
+assert_no_elevation_cua_driver() {
+ [[ "$SIGNING_VARIANT" == "elevation-host" ]] || return 0
+ local cua_driver="$APP_BUNDLE/Contents/Resources/cua-driver"
+ if [[ -e "$cua_driver" || -L "$cua_driver" ]]; then
+ echo "ERROR: Elevation host must not contain bundled CUA driver: $cua_driver" >&2
+ exit 1
+ fi
+}
+
# Sign-time twin of verify_elevation_app in mac-elevation-host.sh, which asserts the same identity
# invariants but requires an already notarized and stapled bundle. Dropping this check defers every
# elevation identity failure until after an Apple notarization submission has been spent.
verify_elevation_signature() {
[[ "$SIGNING_VARIANT" == "elevation-host" ]] || return 0
+ assert_no_elevation_cua_driver
local actual_team
actual_team="$(team_id_for "$APP_BUNDLE" || true)"
@@ -368,6 +378,7 @@ verify_elevation_signature() {
}
# Sign bundled helper binaries before signing the app bundle.
+assert_no_elevation_cua_driver
MLX_TTS_HELPER="$APP_BUNDLE/Contents/MacOS/openclaw-mlx-tts"
if [ -f "$MLX_TTS_HELPER" ]; then
echo "Signing MLX TTS helper"; sign_plain_item "$MLX_TTS_HELPER"
diff --git a/scripts/mac-elevation-host.sh b/scripts/mac-elevation-host.sh
index 702b714da753..5e0ddc31f91d 100755
--- a/scripts/mac-elevation-host.sh
+++ b/scripts/mac-elevation-host.sh
@@ -105,6 +105,9 @@ RECOVERY_CURRENT_RECEIPT_SHA=""
RECOVERY_RESTORED_MIGRATION_IDENTITY=""
RECOVERY_RELAUNCHED_ADOPTED_PID=""
RECOVERY_RESUMED=0
+UNSAFE_ELEVATION_APP_QUARANTINE=""
+UNSAFE_ELEVATION_APP_WAS_QUARANTINED=0
+ELEVATION_APP_OWNER_WAS_EVIDENCED=0
OPENCLAW_CLI=()
fail() {
@@ -360,12 +363,189 @@ verify_universal_machos() {
done < <(find "$app" -type f -perm -111 -print0)
}
+elevation_app_is_cua_free() {
+ local app="$1"
+ local cua_driver="$app/Contents/Resources/cua-driver"
+ [[ ! -e "$cua_driver" && ! -L "$cua_driver" ]]
+}
+
+elevation_plist_binds_app() {
+ local plist="$1" args executable label program
+ [[ -n "$plist" && -f "$plist" && ! -L "$plist" ]] || return 1
+ label="$(plist_file_value "$plist" Label)"
+ [[ "$label" == "$ELEVATION_LABEL" ]] || return 1
+ executable="$APP_PATH/Contents/MacOS/OpenClaw"
+ if program="$(plutil -extract Program raw -o - "$plist" 2>/dev/null)"; then
+ [[ "$program" == "$executable" ]] || return 1
+ elif plutil -extract Program xml1 -o /dev/null "$plist" 2>/dev/null; then
+ return 1
+ fi
+ args="$(plutil -extract ProgramArguments json -o - "$plist" 2>/dev/null)" || return 1
+ [[ "$(jq -c . <<<"$args")" == \
+ "$(jq -cn --arg executable "$executable" '[$executable,"--elevation-host"]')" ]]
+}
+
+elevation_receipt_binds_app() {
+ local receipt="$1"
+ [[ -n "$receipt" && -f "$receipt" && ! -L "$receipt" ]] || return 1
+ jq -e \
+ --arg appPath "$APP_PATH" \
+ --arg plistPath "$PLIST_PATH" '
+ type == "object" and
+ .appPath == $appPath and
+ .plistPath == $plistPath and
+ (
+ (.schemaVersion == 3 and .kind == "openclaw-elevation-install") or
+ (
+ (has("schemaVersion") | not) and (has("kind") | not) and
+ keys == ["appPath","archiveSha256","backupPath","peekabooCommit","plistPath","previousPlist","sourceCommit"]
+ )
+ )
+ ' "$receipt" >/dev/null 2>&1
+}
+
+loaded_elevation_job_binds_app() {
+ local snapshot program
+ snapshot="$(job_snapshot "$job_domain")"
+ [[ -n "$snapshot" ]] || return 1
+ program="$(awk -F' = ' '/^[[:space:]]*program = / {print $2; exit}' <<<"$snapshot")"
+ [[ "$program" == "$APP_PATH/Contents/MacOS/OpenClaw" ]] || return 1
+ grep -Eq '^[[:space:]]*--elevation-host[[:space:]]*$' <<<"$snapshot"
+}
+
+elevation_ownership_is_evidenced() {
+ local candidate
+ loaded_elevation_job_binds_app && return 0
+ for candidate in "$PLIST_PATH" "$ROLLBACK_ELEVATION_PLIST" "$RECOVERY_CURRENT_PLIST"; do
+ elevation_plist_binds_app "$candidate" && return 0
+ done
+ for candidate in \
+ "$RECEIPT_PATH" \
+ "$FINAL_RECEIPT_PATH" \
+ "$PENDING_RECEIPT_PATH" \
+ "$ROLLBACK_INSTALL_RECEIPT" \
+ "$RECOVERY_CURRENT_RECEIPT"
+ do
+ elevation_receipt_binds_app "$candidate" && return 0
+ done
+ return 1
+}
+
+quarantine_elevation_plist() {
+ local description="$1" quarantine_path source_identity source_sha="" source_kind
+ [[ -e "$PLIST_PATH" || -L "$PLIST_PATH" ]] || return 0
+ source_identity="$(path_identity "$PLIST_PATH")" || return 1
+ if [[ -f "$PLIST_PATH" && ! -L "$PLIST_PATH" ]]; then
+ source_kind="file"
+ source_sha="$(shasum -a 256 "$PLIST_PATH" | awk '{print $1}')" || return 1
+ [[ "$source_sha" =~ ^[0-9a-f]{64}$ ]] || return 1
+ elif [[ -L "$PLIST_PATH" ]]; then
+ source_kind="symlink"
+ else
+ return 1
+ fi
+ quarantine_path="$(mktemp -u "$STATE_DIR/elevation-host.quarantined-launch-agent.XXXXXX")" || return 1
+ [[ ! -e "$quarantine_path" && ! -L "$quarantine_path" ]] || return 1
+ rename_app_exclusively "$PLIST_PATH" "$quarantine_path" || return 1
+ [[ ! -e "$PLIST_PATH" && ! -L "$PLIST_PATH" ]] || return 1
+ [[ "$(path_identity "$quarantine_path")" == "$source_identity" ]] || return 1
+ if [[ "$source_kind" == "file" ]]; then
+ backup_file_matches "$quarantine_path" "$source_sha" || return 1
+ else
+ [[ -L "$quarantine_path" ]] || return 1
+ fi
+ fsync_parent "$PLIST_PATH" || return 1
+ printf 'Quarantined %s elevation LaunchAgent outside launchd discovery at %s\n' \
+ "$description" "$quarantine_path" >&2
+}
+
+ensure_elevation_job_absent() {
+ local state
+ state="$(job_loaded_state "$job_domain")"
+ if [[ "$state" != "absent" ]]; then
+ launchctl bootout "$job_domain" >/dev/null 2>&1 || true
+ state="$(job_loaded_state "$job_domain")"
+ fi
+ [[ "$state" == "absent" ]]
+}
+
+neutralize_unsafe_elevation_launch_agent() {
+ local description="$1" evidence_path="$2" evidence_sha="$3" neutralization_failed=0
+ if ! quarantine_elevation_plist "$description"; then
+ if [[ -e "$PLIST_PATH" || -L "$PLIST_PATH" ]]; then
+ rm -f -- "$PLIST_PATH" || neutralization_failed=1
+ fi
+ fsync_parent "$PLIST_PATH" || neutralization_failed=1
+ printf 'Removed unquarantinable %s elevation LaunchAgent; exact evidence remains at %s\n' \
+ "$description" "$evidence_path" >&2
+ fi
+ [[ ! -e "$PLIST_PATH" && ! -L "$PLIST_PATH" ]] || neutralization_failed=1
+ ensure_elevation_job_absent || neutralization_failed=1
+ if [[ -n "$evidence_path" ]]; then
+ backup_file_matches "$evidence_path" "$evidence_sha" || neutralization_failed=1
+ fi
+ [[ "$neutralization_failed" == "0" ]]
+}
+
+quarantine_entry_unsafe_elevation_app() {
+ local app_identity app_kind quarantine_container quarantine_app quarantine_failed=0
+ ELEVATION_APP_OWNER_WAS_EVIDENCED=0
+ elevation_ownership_is_evidenced || return 0
+ # Keep this fact across neutralization: launchd and its plist may be gone before
+ # rollback decides whether the displaced CUA-bearing app can return to APP_PATH.
+ ELEVATION_APP_OWNER_WAS_EVIDENCED=1
+ [[ -e "$APP_PATH" || -L "$APP_PATH" ]] || return 0
+ elevation_app_is_cua_free "$APP_PATH" && return 0
+
+ neutralize_unsafe_elevation_launch_agent 'entry for unsafe elevation app' '' '' ||
+ quarantine_failed=1
+ if [[ -L "$APP_PATH" ]]; then
+ app_kind="symlink"
+ app_identity="$(path_identity "$APP_PATH")" || quarantine_failed=1
+ elif [[ -d "$APP_PATH" ]]; then
+ app_kind="bundle"
+ app_identity="$(durable_path_identity "$APP_PATH")" || quarantine_failed=1
+ else
+ quarantine_failed=1
+ fi
+ if [[ "$quarantine_failed" == "0" ]]; then
+ if quarantine_container="$(mktemp -d "$STATE_DIR/elevation-host.quarantined-app.XXXXXX")"; then
+ quarantine_app="$quarantine_container/OpenClaw.app"
+ rename_app_exclusively "$APP_PATH" "$quarantine_app" || quarantine_failed=1
+ if path_matches_identity "$quarantine_app" "$app_identity" &&
+ { [[ "$app_kind" == "symlink" && -L "$quarantine_app" ]] ||
+ { [[ "$app_kind" == "bundle" && -d "$quarantine_app" && ! -L "$quarantine_app" ]] &&
+ ! elevation_app_is_cua_free "$quarantine_app"; }; } &&
+ [[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]]
+ then
+ UNSAFE_ELEVATION_APP_QUARANTINE="$quarantine_app"
+ UNSAFE_ELEVATION_APP_WAS_QUARANTINED=1
+ fsync_parent "$APP_PATH" || quarantine_failed=1
+ else
+ quarantine_failed=1
+ fi
+ else
+ quarantine_failed=1
+ fi
+ fi
+ [[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] || quarantine_failed=1
+ [[ ! -e "$PLIST_PATH" && ! -L "$PLIST_PATH" ]] || quarantine_failed=1
+ if [[ "$UNSAFE_ELEVATION_APP_WAS_QUARANTINED" == "1" ]]; then
+ printf 'Quarantined CUA-bearing elevation app outside its launchd path at %s\n' \
+ "$UNSAFE_ELEVATION_APP_QUARANTINE" >&2
+ fi
+ [[ "$quarantine_failed" == "0" ]]
+}
+
# Canonical elevation identity check: a strict superset of verify_elevation_signature in
# codesign-mac-app.sh, and the only one that runs post-notarization and on the target Mac at install
# time. Dropping it lets the portable installer accept an archive nobody re-verified after signing.
verify_elevation_app() {
local app="$1"
[[ -d "$app" && ! -L "$app" ]] || fail "elevation app not found or symlinked: $app"
+ local cua_driver="$app/Contents/Resources/cua-driver"
+ elevation_app_is_cua_free "$app" ||
+ fail "elevation app must not contain bundled CUA driver: $cua_driver"
[[ "$(plist_value "$app" CFBundleIdentifier)" == "$EXPECTED_BUNDLE_ID" ]] ||
fail "elevation app bundle id must be $EXPECTED_BUNDLE_ID"
local source_commit peekaboo_commit
@@ -531,6 +711,7 @@ restore_install_receipt_after_rollback() {
"$ROLLBACK_INSTALL_RECEIPT_SHA" \
600
elif [[ -e "$RECEIPT_PATH" || -L "$RECEIPT_PATH" ]]; then
+ [[ "$ELEVATION_APP_OWNER_WAS_EVIDENCED" == "1" ]] || return 1
[[ -f "$RECEIPT_PATH" && ! -L "$RECEIPT_PATH" ]] || return 1
rm "$RECEIPT_PATH"
fi
@@ -2163,12 +2344,28 @@ install_host() {
recover_install() {
local recovery_failed=0 elevation_state restored_state prior_owner_state
+ local unsafe_previous_elevation=0 rollback_app_candidate=""
+ quarantine_entry_unsafe_elevation_app || return 1
+ [[ "$UNSAFE_ELEVATION_APP_WAS_QUARANTINED" == "0" ]] || recovery_failed=1
if [[ "$CUTOVER_APP_MUTATED" == "1" && -n "$ROLLBACK_APP_PATH" ]]; then
verify_recorded_rollback_app "$APP_PATH" ||
verify_recorded_rollback_app "$ROLLBACK_APP_PATH" || return 1
fi
- [[ -z "$ROLLBACK_ELEVATION_PLIST" ]] ||
- backup_file_matches "$ROLLBACK_ELEVATION_PLIST" "$ROLLBACK_ELEVATION_PLIST_SHA" || return 1
+ if [[ "$ELEVATION_APP_OWNER_WAS_EVIDENCED" == "1" && -n "$ROLLBACK_APP_PATH" ]]
+ then
+ if [[ -d "$ROLLBACK_APP_PATH" && ! -L "$ROLLBACK_APP_PATH" ]] &&
+ ! elevation_app_is_cua_free "$ROLLBACK_APP_PATH"
+ then
+ rollback_app_candidate="$ROLLBACK_APP_PATH"
+ elif [[ -d "$APP_PATH" && ! -L "$APP_PATH" ]] && ! elevation_app_is_cua_free "$APP_PATH"; then
+ rollback_app_candidate="$APP_PATH"
+ fi
+ [[ -z "$rollback_app_candidate" ]] || unsafe_previous_elevation=1
+ fi
+ if [[ "$unsafe_previous_elevation" == "0" ]]; then
+ [[ -z "$ROLLBACK_ELEVATION_PLIST" ]] ||
+ backup_file_matches "$ROLLBACK_ELEVATION_PLIST" "$ROLLBACK_ELEVATION_PLIST_SHA" || return 1
+ fi
[[ -z "$ROLLBACK_MIGRATION_PLIST" ]] ||
backup_file_matches "$ROLLBACK_MIGRATION_PLIST" "$ROLLBACK_MIGRATION_PLIST_SHA" || return 1
[[ -z "$ROLLBACK_INSTALL_RECEIPT" ]] ||
@@ -2221,7 +2418,21 @@ recover_install() {
[[ -n "$ROLLBACK_APP_PATH" ]] &&
verify_recorded_rollback_app "$APP_PATH" || return 1
fi
- if [[ -n "$ROLLBACK_APP_PATH" && -d "$ROLLBACK_APP_PATH" ]]; then
+ if [[ "$unsafe_previous_elevation" == "1" ]]; then
+ if [[ -d "$ROLLBACK_APP_PATH" && ! -L "$ROLLBACK_APP_PATH" ]]; then
+ verify_recorded_rollback_app "$ROLLBACK_APP_PATH" || recovery_failed=1
+ [[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] || recovery_failed=1
+ elif verify_recorded_rollback_app "$APP_PATH"; then
+ [[ ! -e "$ROLLBACK_APP_PATH" && ! -L "$ROLLBACK_APP_PATH" ]] || recovery_failed=1
+ if [[ "$recovery_failed" == "0" ]]; then
+ rename_app_exclusively "$APP_PATH" "$ROLLBACK_APP_PATH" || true
+ verify_recorded_rollback_app "$ROLLBACK_APP_PATH" || recovery_failed=1
+ [[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] || recovery_failed=1
+ fi
+ else
+ recovery_failed=1
+ fi
+ elif [[ -n "$ROLLBACK_APP_PATH" && -d "$ROLLBACK_APP_PATH" ]]; then
[[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] || return 1
rename_app_exclusively "$ROLLBACK_APP_PATH" "$APP_PATH" || true
verify_recorded_rollback_app "$APP_PATH" || recovery_failed=1
@@ -2229,23 +2440,42 @@ recover_install() {
elif [[ -n "$ROLLBACK_APP_PATH" ]]; then
verify_recorded_rollback_app "$APP_PATH" || recovery_failed=1
fi
- if [[ -n "$ROLLBACK_ELEVATION_PLIST" && -f "$ROLLBACK_ELEVATION_PLIST" ]]; then
- restore_file_atomically \
- "$ROLLBACK_ELEVATION_PLIST" \
- "$PLIST_PATH" \
- "$ROLLBACK_ELEVATION_PLIST_SHA" \
- 644 || recovery_failed=1
- restored_state="$(job_loaded_state "$job_domain")"
- [[ "$restored_state" != 'unknown' ]] || recovery_failed=1
- if [[ "$recovery_failed" == "0" &&
- "$ROLLBACK_ELEVATION_WAS_LOADED" == "1" && "$restored_state" == 'absent' ]]
- then
- launchctl bootstrap "$launch_domain" "$PLIST_PATH" >/dev/null 2>&1 || recovery_failed=1
- elif [[ "$ROLLBACK_ELEVATION_WAS_LOADED" == "0" && "$restored_state" != 'absent' ]]; then
- recovery_failed=1
+ if [[ "$unsafe_previous_elevation" == "1" ]]; then
+ if [[ -n "$ROLLBACK_ELEVATION_PLIST" ]]; then
+ neutralize_unsafe_elevation_launch_agent \
+ 'replacement for unsafe previous' \
+ "$ROLLBACK_ELEVATION_PLIST" \
+ "$ROLLBACK_ELEVATION_PLIST_SHA" || recovery_failed=1
+ printf 'Preserved previous elevation app with bundled CUA driver at %s and LaunchAgent evidence at %s; refusing to restore it as elevation host\n' \
+ "$ROLLBACK_APP_PATH" "$ROLLBACK_ELEVATION_PLIST" >&2
+ else
+ neutralize_unsafe_elevation_launch_agent \
+ 'replacement for unsafe previous' \
+ '' \
+ '' || recovery_failed=1
+ printf 'Preserved previous elevation app with bundled CUA driver at %s; no prior elevation LaunchAgent was recorded, and it will not be restored as elevation host\n' \
+ "$ROLLBACK_APP_PATH" >&2
fi
- else
+ recovery_failed=1
+ elif [[ -n "$ROLLBACK_ELEVATION_PLIST" && -f "$ROLLBACK_ELEVATION_PLIST" ]]; then
+ restore_file_atomically \
+ "$ROLLBACK_ELEVATION_PLIST" \
+ "$PLIST_PATH" \
+ "$ROLLBACK_ELEVATION_PLIST_SHA" \
+ 644 || recovery_failed=1
+ restored_state="$(job_loaded_state "$job_domain")"
+ [[ "$restored_state" != 'unknown' ]] || recovery_failed=1
+ if [[ "$recovery_failed" == "0" &&
+ "$ROLLBACK_ELEVATION_WAS_LOADED" == "1" && "$restored_state" == 'absent' ]]
+ then
+ launchctl bootstrap "$launch_domain" "$PLIST_PATH" >/dev/null 2>&1 || recovery_failed=1
+ elif [[ "$ROLLBACK_ELEVATION_WAS_LOADED" == "0" && "$restored_state" != 'absent' ]]; then
+ recovery_failed=1
+ fi
+ elif [[ "$ELEVATION_APP_OWNER_WAS_EVIDENCED" == "1" ]]; then
[[ ! -f "$PLIST_PATH" ]] || rm -f "$PLIST_PATH" || recovery_failed=1
+ elif [[ -e "$PLIST_PATH" || -L "$PLIST_PATH" ]]; then
+ recovery_failed=1
fi
if [[ -n "$ROLLBACK_MIGRATION_SOURCE" && -f "$ROLLBACK_MIGRATION_PLIST" ]]; then
if [[ "$CUTOVER_MIGRATION_REMOVED" != "1" &&
@@ -2365,6 +2595,13 @@ recover_install() {
restore_current_generation_after_recovery_failure() {
local restore_failed=0 app_restore_failed=0 state
+ local unsafe_current_elevation=0 current_app_evidence_path=""
+ quarantine_entry_unsafe_elevation_app || return 1
+ if [[ "$UNSAFE_ELEVATION_APP_WAS_QUARANTINED" == "1" ]]; then
+ unsafe_current_elevation=1
+ current_app_evidence_path="$UNSAFE_ELEVATION_APP_QUARANTINE"
+ app_restore_failed=1
+ fi
if [[ "$RECOVERY_RELAUNCHED_ADOPTED_PID" =~ ^[0-9]+$ ]]; then
ADOPTION_PID="$RECOVERY_RELAUNCHED_ADOPTED_PID"
@@ -2414,7 +2651,35 @@ restore_current_generation_after_recovery_failure() {
[[ "$(job_loaded_state "$launch_domain/$ROLLBACK_MIGRATION_LABEL")" == "absent" ]] || return 1
fi
- if [[ "$RECOVERY_CURRENT_APP_STATE" == "absent" ]]; then
+ if [[ "$unsafe_current_elevation" == "0" &&
+ ( "$RECOVERY_CURRENT_APP_STATE" == "valid" ||
+ "$RECOVERY_CURRENT_APP_STATE" == "damaged" ) ]]
+ then
+ if [[ -n "$RECOVERED_FAILED_APP_PATH" && -d "$RECOVERED_FAILED_APP_PATH" &&
+ ! -L "$RECOVERED_FAILED_APP_PATH" ]] &&
+ ! elevation_app_is_cua_free "$RECOVERED_FAILED_APP_PATH"
+ then
+ unsafe_current_elevation=1
+ current_app_evidence_path="$RECOVERED_FAILED_APP_PATH"
+ elif [[ -d "$APP_PATH" && ! -L "$APP_PATH" ]] && ! elevation_app_is_cua_free "$APP_PATH"; then
+ unsafe_current_elevation=1
+ if preserve_current_app_for_recovery 'unsafe current elevation app'; then
+ current_app_evidence_path="$RECOVERED_FAILED_APP_PATH"
+ elif [[ -n "$RECOVERED_FAILED_APP_PATH" ]] &&
+ verify_recorded_current_app "$RECOVERED_FAILED_APP_PATH"
+ then
+ current_app_evidence_path="$RECOVERED_FAILED_APP_PATH"
+ app_restore_failed=1
+ else
+ current_app_evidence_path="$APP_PATH"
+ app_restore_failed=1
+ fi
+ fi
+ fi
+
+ if [[ "$unsafe_current_elevation" == "1" && "$app_restore_failed" != "0" ]]; then
+ : # Preserve the unsafe classification and skip every normal restoration path.
+ elif [[ "$RECOVERY_CURRENT_APP_STATE" == "absent" ]]; then
if [[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]]; then
[[ -z "$ROLLBACK_APP_PATH" ]] ||
verify_recorded_rollback_app "$ROLLBACK_APP_PATH" || app_restore_failed=1
@@ -2439,14 +2704,19 @@ restore_current_generation_after_recovery_failure() {
! -L "$RECOVERED_FAILED_APP_PATH" ]] &&
path_matches_identity "$RECOVERED_FAILED_APP_PATH" "$RECOVERY_CURRENT_APP_IDENTITY"
then
- rename_app_exclusively "$RECOVERED_FAILED_APP_PATH" "$APP_PATH" || true
- path_matches_identity "$APP_PATH" "$RECOVERY_CURRENT_APP_IDENTITY" || app_restore_failed=1
- if [[ "$app_restore_failed" == "0" && "$RECOVERY_CURRENT_APP_STATE" == "valid" ]]; then
- verify_recorded_current_app "$APP_PATH" || app_restore_failed=1
+ if [[ "$unsafe_current_elevation" == "1" ]]; then
+ verify_recorded_current_app "$RECOVERED_FAILED_APP_PATH" || app_restore_failed=1
+ [[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] || app_restore_failed=1
+ else
+ rename_app_exclusively "$RECOVERED_FAILED_APP_PATH" "$APP_PATH" || true
+ path_matches_identity "$APP_PATH" "$RECOVERY_CURRENT_APP_IDENTITY" || app_restore_failed=1
+ if [[ "$app_restore_failed" == "0" && "$RECOVERY_CURRENT_APP_STATE" == "valid" ]]; then
+ verify_recorded_current_app "$APP_PATH" || app_restore_failed=1
+ fi
+ [[ ! -e "$RECOVERED_FAILED_APP_PATH" && ! -L "$RECOVERED_FAILED_APP_PATH" ]] ||
+ app_restore_failed=1
+ rmdir "$(dirname "$RECOVERED_FAILED_APP_PATH")" 2>/dev/null || true
fi
- [[ ! -e "$RECOVERED_FAILED_APP_PATH" && ! -L "$RECOVERED_FAILED_APP_PATH" ]] ||
- app_restore_failed=1
- rmdir "$(dirname "$RECOVERED_FAILED_APP_PATH")" 2>/dev/null || true
else
app_restore_failed=1
fi
@@ -2454,18 +2724,49 @@ restore_current_generation_after_recovery_failure() {
case "$RECOVERY_CURRENT_APP_STATE" in
absent) [[ ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] || app_restore_failed=1 ;;
damaged)
- [[ -d "$APP_PATH" && ! -L "$APP_PATH" ]] &&
- path_matches_identity "$APP_PATH" "$RECOVERY_CURRENT_APP_IDENTITY" || app_restore_failed=1
+ if [[ "$unsafe_current_elevation" == "1" ]]; then
+ [[ -n "$current_app_evidence_path" && ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] &&
+ path_matches_identity "$current_app_evidence_path" "$RECOVERY_CURRENT_APP_IDENTITY" &&
+ verify_recorded_current_app "$current_app_evidence_path" || app_restore_failed=1
+ else
+ [[ -d "$APP_PATH" && ! -L "$APP_PATH" ]] &&
+ path_matches_identity "$APP_PATH" "$RECOVERY_CURRENT_APP_IDENTITY" || app_restore_failed=1
+ fi
;;
valid)
- path_matches_identity "$APP_PATH" "$RECOVERY_CURRENT_APP_IDENTITY" &&
- verify_recorded_current_app "$APP_PATH" || app_restore_failed=1
+ if [[ "$unsafe_current_elevation" == "1" ]]; then
+ [[ -n "$current_app_evidence_path" && ! -e "$APP_PATH" && ! -L "$APP_PATH" ]] &&
+ path_matches_identity "$current_app_evidence_path" "$RECOVERY_CURRENT_APP_IDENTITY" &&
+ verify_recorded_current_app "$current_app_evidence_path" || app_restore_failed=1
+ else
+ path_matches_identity "$APP_PATH" "$RECOVERY_CURRENT_APP_IDENTITY" &&
+ verify_recorded_current_app "$APP_PATH" || app_restore_failed=1
+ fi
;;
*) app_restore_failed=1 ;;
esac
- [[ "$app_restore_failed" == "0" ]] || return 1
+ if [[ "$app_restore_failed" != "0" && "$unsafe_current_elevation" == "0" ]]; then
+ return 1
+ fi
- if [[ -n "$RECOVERY_CURRENT_PLIST" ]]; then
+ if [[ "$unsafe_current_elevation" == "1" ]]; then
+ if [[ -n "$RECOVERY_CURRENT_PLIST" ]]; then
+ neutralize_unsafe_elevation_launch_agent \
+ 'replacement for unsafe current' \
+ "$RECOVERY_CURRENT_PLIST" \
+ "$RECOVERY_CURRENT_PLIST_SHA" || restore_failed=1
+ printf 'Preserved current elevation app with bundled CUA driver at %s and LaunchAgent evidence at %s; refusing to restore it as elevation host\n' \
+ "$current_app_evidence_path" "$RECOVERY_CURRENT_PLIST" >&2
+ else
+ neutralize_unsafe_elevation_launch_agent \
+ 'replacement for unsafe current' \
+ '' \
+ '' || restore_failed=1
+ printf 'Preserved current elevation app with bundled CUA driver at %s; no current elevation LaunchAgent was recorded, and it will not be restored as elevation host\n' \
+ "$current_app_evidence_path" >&2
+ fi
+ restore_failed=1
+ elif [[ -n "$RECOVERY_CURRENT_PLIST" ]]; then
restore_file_atomically \
"$RECOVERY_CURRENT_PLIST" \
"$PLIST_PATH" \
@@ -2686,6 +2987,10 @@ recover_host() {
fail 'recovery migration transaction binding is invalid'
RECOVERY_RESTORED_MIGRATION_IDENTITY="$migration_identity"
fi
+ if [[ "$INSTALL_RECEIPT_SCHEMA" != "legacy" ]]; then
+ RECOVERY_CURRENT_APP_CDHASH_ARM64="$(jq -r '.cdhashes.arm64' "$RECEIPT_PATH")"
+ RECOVERY_CURRENT_APP_CDHASH_X86_64="$(jq -r '.cdhashes.x86_64' "$RECEIPT_PATH")"
+ fi
if [[ "$INSTALL_RECEIPT_SCHEMA" == 'legacy' || "$RECOVERY_CURRENT_APP_STATE" != "valid" ||
("$RECOVERY_PENDING_INSTALL" == "1" && "$current_app_matches_receipt" != "1") ]]
then
@@ -2713,8 +3018,6 @@ recover_host() {
[[ -z "$ARCHIVE" && -z "$ARTIFACT_RECEIPT" && -z "$EXPECTED_ARTIFACT_RECEIPT_SHA256" ]] ||
fail 'artifact helper inputs are valid only for legacy recovery'
CONFIG_PATH="$(jq -r '.configPath' "$RECEIPT_PATH")"
- RECOVERY_CURRENT_APP_CDHASH_ARM64="$(jq -r '.cdhashes.arm64' "$RECEIPT_PATH")"
- RECOVERY_CURRENT_APP_CDHASH_X86_64="$(jq -r '.cdhashes.x86_64' "$RECEIPT_PATH")"
prepare_current_app_rename_helper
fi
if [[ "$pending_migration_identity_needs_record" == "1" ]]; then
diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh
index 92bcb0b3ddec..8915a63a90c2 100755
--- a/scripts/package-mac-app.sh
+++ b/scripts/package-mac-app.sh
@@ -25,6 +25,14 @@ MLX_TTS_HELPER_BUILD_ROOT="$MLX_TTS_HELPER_ROOT/.build"
BUNDLE_ID="${BUNDLE_ID:-ai.openclaw.mac.debug}"
PKG_VERSION="$(cd "$ROOT_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0")"
BUILD_CONFIG="${BUILD_CONFIG:-debug}"
+SIGNING_VARIANT="${OPENCLAW_MAC_SIGNING_VARIANT:-standard}"
+case "$SIGNING_VARIANT" in
+ standard | elevation-host) ;;
+ *)
+ echo "ERROR: Unknown OPENCLAW_MAC_SIGNING_VARIANT value: $SIGNING_VARIANT (use standard|elevation-host)" >&2
+ exit 1
+ ;;
+esac
# OPENCLAW_SKIP_MLX_TTS=1 packages the app without the local MLX voice helper.
# The helper pulls in the full mlx-swift Metal shader stack, which some beta
# Xcode toolchains cannot compile (flaky `metal` diagnostics), needlessly
@@ -886,8 +894,12 @@ fi
rm -rf "$APP_ROOT/Contents/Resources/ProviderIcons"
cp -R "$PROVIDER_ICONS_SRC" "$APP_ROOT/Contents/Resources/ProviderIcons"
-echo "🖥 Staging embedded CUA driver"
-"$ROOT_DIR/scripts/stage-cua-driver-macos.sh" "$APP_ROOT/Contents/Resources/cua-driver"
+if [[ "$SIGNING_VARIANT" == "elevation-host" ]]; then
+ echo "🖥 Omitting embedded CUA driver from elevation-host package"
+else
+ echo "🖥 Staging embedded CUA driver"
+ "$ROOT_DIR/scripts/stage-cua-driver-macos.sh" "$APP_ROOT/Contents/Resources/cua-driver"
+fi
echo "📦 Copying CLI installer"
INSTALL_CLI_SRC="$ROOT_DIR/scripts/install-cli.sh"
diff --git a/test/scripts/codesign-mac-app.test.ts b/test/scripts/codesign-mac-app.test.ts
index 5acd48e8cf64..8a1b3fcd1454 100644
--- a/test/scripts/codesign-mac-app.test.ts
+++ b/test/scripts/codesign-mac-app.test.ts
@@ -6,6 +6,7 @@ import {
mkdirSync,
readFileSync,
readdirSync,
+ symlinkSync,
writeFileSync,
} from "node:fs";
import path from "node:path";
@@ -294,6 +295,39 @@ describe("codesign-mac-app temp file hygiene", () => {
expect(script).toContain('assert_no_apple_events_entitlement "$APP_BUNDLE"');
});
+ it.each(["file", "symlink"])("rejects an elevation-host CUA driver %s before signing", (kind) => {
+ const tempRoot = tempDirs.make(`openclaw-codesign-elevation-cua-${kind}-`);
+ const app = path.join(tempRoot, "Fake.app");
+ const binDir = path.join(tempRoot, "bin");
+ const resources = path.join(app, "Contents", "Resources");
+ mkdirSync(path.join(app, "Contents", "MacOS"), { recursive: true });
+ mkdirSync(resources, { recursive: true });
+ mkdirSync(binDir);
+ writeFileSync(path.join(app, "Contents", "MacOS", "OpenClaw"), "#!/bin/sh\n");
+ const cuaDriver = path.join(resources, "cua-driver");
+ if (kind === "file") {
+ writeFileSync(cuaDriver, "driver\n");
+ } else {
+ symlinkSync("/missing/cua-driver", cuaDriver);
+ }
+ installElevationFakeCodesign(binDir);
+
+ const result = spawnSync("bash", [scriptPath, app], {
+ cwd: process.cwd(),
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ OPENCLAW_MAC_SIGNING_VARIANT: "elevation-host",
+ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
+ SIGN_IDENTITY: "Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)",
+ TMPDIR: tempRoot,
+ },
+ });
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("must not contain bundled CUA driver");
+ });
+
it("consumes complete codesign metadata under pipefail before validating authority", () => {
const tempRoot = tempDirs.make("openclaw-codesign-elevation-metadata-");
const app = path.join(tempRoot, "Fake.app");
diff --git a/test/scripts/mac-elevation-host.test.ts b/test/scripts/mac-elevation-host.test.ts
index fb016a26b7b7..79a06cb99564 100644
--- a/test/scripts/mac-elevation-host.test.ts
+++ b/test/scripts/mac-elevation-host.test.ts
@@ -57,6 +57,45 @@ function receiptDigestArgs(receiptPath: string): string[] {
return ["--receipt-sha256", sha256(readFileSync(receiptPath))];
}
+function quarantinedElevationAppPath(stateDir: string): string | undefined {
+ const container = readdirSync(stateDir).find((name) =>
+ name.startsWith("elevation-host.quarantined-app."),
+ );
+ return container ? path.join(stateDir, container, "OpenClaw.app") : undefined;
+}
+
+function preservedCuaAppPath(harness: {
+ appPath: string;
+ env: NodeJS.ProcessEnv;
+ stateDir: string;
+}): string | undefined {
+ const quarantined = quarantinedElevationAppPath(harness.stateDir);
+ if (quarantined) {
+ const driver = path.join(quarantined, "Contents", "Resources", "cua-driver");
+ if (existsSync(driver) || lstatSync(driver, { throwIfNoEntry: false })?.isSymbolicLink()) {
+ return quarantined;
+ }
+ }
+ const home = harness.env.HOME!;
+ for (const entry of readdirSync(home)) {
+ if (
+ !entry.startsWith(`${path.basename(harness.appPath)}.rollback-elevation-host-`) &&
+ !entry.startsWith(`${path.basename(harness.appPath)}.failed-elevation-host-`)
+ ) {
+ continue;
+ }
+ const container = path.join(home, entry);
+ const candidate = entry.includes(".failed-elevation-host-")
+ ? path.join(container, "OpenClaw.app")
+ : container;
+ const driver = path.join(candidate, "Contents", "Resources", "cua-driver");
+ if (existsSync(driver) || lstatSync(driver, { throwIfNoEntry: false })?.isSymbolicLink()) {
+ return candidate;
+ }
+ }
+ return undefined;
+}
+
function runInstaller(
installerPath: string,
args: string[],
@@ -536,7 +575,13 @@ function createArtifactVerificationHarness() {
' : >"$TEST_PENDING_KILL_MARKER"',
' kill -KILL "$PPID"',
"fi",
+ 'if [ "${1:-}" = "--elevation-sync-file" ] && [ "${TEST_CORRUPT_ROLLBACK_PLIST_BACKUP_ON_SYNC:-0}" = "1" ] && echo "${2:-}" | grep -q \'elevation-host[.]previous-plist[.]\'; then',
+ " printf '%s\\n' corrupt >\"$2\"",
+ "fi",
'if [ "${1:-}" = "--elevation-rename-exclusive" ]; then',
+ ' if [ "${TEST_FAIL_ELEVATION_QUARANTINE_RENAME:-0}" = "1" ] && echo "$3" | grep -q \'elevation-host[.]quarantined-launch-agent[.]\'; then',
+ " exit 7",
+ " fi",
' if [ "${TEST_DANGLING_ROLLBACK_DURING_MOVE:-0}" = "1" ] && echo "$3" | grep -q \'[.]rollback-elevation-host-\'; then',
' ln -s /missing/openclaw-rollback-target "$3"',
" fi",
@@ -570,6 +615,12 @@ function createArtifactVerificationHarness() {
" fi",
' if [ -e "$3" ] || [ -L "$3" ]; then exit 1; fi',
' /bin/mv "$2" "$3" || exit $?',
+ ' if [ "${TEST_REMOVE_CUA_DRIVER_AFTER_UNSAFE_ENTRY_MOVE:-0}" = "1" ] && echo "$3" | grep -q \'elevation-host[.]quarantined-app[.].*[/]OpenClaw[.]app$\' && [ -L "$3" ]; then',
+ ' /bin/rm -f -- "$(readlink "$3")/Contents/Resources/cua-driver"',
+ " fi",
+ ' if [ "${TEST_RELOAD_ELEVATION_AFTER_QUARANTINE:-0}" = "1" ] && echo "$3" | grep -q \'elevation-host[.]quarantined-launch-agent[.]\'; then',
+ " printf '%s\\n' elevation-loaded >\"$TEST_LAUNCH_STATE_FILE\"",
+ " fi",
' if [ "${TEST_KILL_AFTER_INITIAL_MIGRATION_CUSTODY:-0}" = "1" ] && echo "$3" | grep -q \'[.]custody[.]\'; then',
' kill -KILL "$PPID"',
" fi",
@@ -595,6 +646,12 @@ function createArtifactVerificationHarness() {
"APP_HELPER",
'printf helper >"$app/Contents/MacOS/openclaw-mlx-tts"',
'chmod 755 "$app/Contents/MacOS/OpenClaw" "$app/Contents/MacOS/openclaw-mlx-tts"',
+ 'case "${TEST_CUA_DRIVER_KIND:-none}" in',
+ ' file) mkdir -p "$app/Contents/Resources"; printf driver >"$app/Contents/Resources/cua-driver"; chmod 755 "$app/Contents/Resources/cua-driver" ;;',
+ ' symlink) mkdir -p "$app/Contents/Resources"; ln -s /missing/cua-driver "$app/Contents/Resources/cua-driver" ;;',
+ " none) ;;",
+ " *) exit 64 ;;",
+ "esac",
"",
].join("\n"),
);
@@ -613,6 +670,9 @@ function createArtifactVerificationHarness() {
'if [[ "$*" == *"--verify"* && "${TEST_FINAL_SIGNATURE_INVALID:-0}" == "1" && "$target" == "${TEST_INSTALLED_APP_PATH:-}" && -f "${TEST_LAUNCH_STATE_FILE:-}" && "$(tr -d \'\\n\' <"$TEST_LAUNCH_STATE_FILE")" == "elevation-loaded" ]]; then',
" exit 1",
"fi",
+ 'if [[ "$*" == *"--verify"* && "${TEST_CURRENT_CUA_SIGNATURE_INVALID:-0}" == "1" && "$target" == "${TEST_INSTALLED_APP_PATH:-}" && ( -e "$target/Contents/Resources/cua-driver" || -L "$target/Contents/Resources/cua-driver" ) ]]; then',
+ " exit 1",
+ "fi",
'if [[ "$*" == *"--verify"* && -e "$target/Contents/invalid-signature" ]]; then',
" exit 1",
"fi",
@@ -693,6 +753,8 @@ function createArtifactVerificationHarness() {
function createInstallRollbackHarness(
options: {
danglingRollbackDuringMove?: boolean;
+ corruptRollbackPlistBackupOnSync?: boolean;
+ currentCuaSignatureInvalid?: boolean;
failCurrentReceiptRestoreCopy?: boolean;
failAfterReceiptCommitMove?: boolean;
failRecoveryXattrRead?: boolean;
@@ -700,8 +762,11 @@ function createInstallRollbackHarness(
finalSignatureInvalid?: boolean;
failLsofInspection?: boolean;
failPgrepInspection?: boolean;
+ failUnsafeEntryIdentity?: boolean;
+ failUnsafeEntryMktemp?: boolean;
hupDuringCustody?: boolean;
launchdBootstrapFails?: boolean;
+ failElevationQuarantineRename?: boolean;
killDuringMigrationRestoreBootstrapOnce?: boolean;
killAfterMigrationRestoreBootstrapOnce?: boolean;
killAfterInitialMigrationCustody?: boolean;
@@ -709,7 +774,9 @@ function createInstallRollbackHarness(
killAfterRollbackAppCustody?: boolean;
migrationRestoreBootstrapFails?: boolean;
raceMigrationCustodyDestination?: boolean;
+ reloadElevationAfterQuarantine?: boolean;
removeInstalledExecutableAfterReadiness?: boolean;
+ existingElevationLoaded?: boolean;
recreateAppDuringDamagedCustody?: boolean;
recreateSourceDuringBootout?: boolean;
recreateSourceOnFailure?: boolean;
@@ -720,6 +787,8 @@ function createInstallRollbackHarness(
replaceMigrationSourceSameContentBeforeInitialCustody?: boolean;
restartAppDuringBootout?: boolean;
rollbackNonNativeSignatureInvalid?: boolean;
+ rollbackCuaDriverKind?: "file" | "symlink";
+ removeCuaDriverAfterUnsafeEntryMove?: boolean;
signalDuringCustody?: boolean;
signalDuringRecoveryAppMove?: boolean;
signalDuringReceiptCommit?: boolean;
@@ -728,6 +797,14 @@ function createInstallRollbackHarness(
symlinkDamagedAppBeforeCustody?: boolean;
sameSourceExistingApp?: boolean;
transientAppRestartReloadsJob?: boolean;
+ unsafeEntryEvidence?:
+ | "job"
+ | "plist"
+ | "plist-program"
+ | "receipt"
+ | "unrelated-plist"
+ | "unrelated-program"
+ | "unrelated-receipt";
} = {},
) {
const artifact = createArtifactVerificationHarness();
@@ -741,6 +818,7 @@ function createInstallRollbackHarness(
const label = "ai.openclaw.mac.node-fixture";
const launchAgentsDir = path.join(tempRoot, "Library", "LaunchAgents");
const sourcePlist = path.join(launchAgentsDir, `${label}.plist`);
+ const elevationPlist = path.join(launchAgentsDir, "ai.openclaw.mac.elevation-host.plist");
const launchStateFile = path.join(tempRoot, "launch-state");
const nodeGenerationFile = path.join(tempRoot, "node-generation");
mkdirSync(path.join(stateDir, "state"), { recursive: true });
@@ -750,6 +828,16 @@ function createInstallRollbackHarness(
writeAppInfoPlist(appPath, oldSourceCommit, oldPeekabooCommit);
writeExecutable(path.join(appPath, "Contents", "MacOS", "OpenClaw"), "#!/bin/sh\nexit 0\n");
writeFileSync(path.join(appPath, "Contents", "old-fixture"), "old\n", "utf8");
+ if (options.rollbackCuaDriverKind) {
+ const resources = path.join(appPath, "Contents", "Resources");
+ const cuaDriver = path.join(resources, "cua-driver");
+ mkdirSync(resources, { recursive: true });
+ if (options.rollbackCuaDriverKind === "file") {
+ writeExecutable(cuaDriver, "#!/bin/sh\nexit 0\n");
+ } else {
+ symlinkSync("/missing/cua-driver", cuaDriver);
+ }
+ }
const sourceContents = [
'',
'',
@@ -764,10 +852,48 @@ function createInstallRollbackHarness(
"",
"",
].join("\n");
- writeFileSync(sourcePlist, sourceContents, "utf8");
- writeFileSync(launchStateFile, "source-loaded\n", "utf8");
+ const elevationPlistContents = [
+ '',
+ '',
+ "Labelai.openclaw.mac.elevation-host",
+ `ProgramArguments${appPath}/Contents/MacOS/OpenClaw--elevation-host`,
+ "EnvironmentVariables",
+ `OPENCLAW_STATE_DIR${stateDir}`,
+ `OPENCLAW_CONFIG_PATH${configPath}`,
+ "",
+ "",
+ ].join("\n");
+ if (options.existingElevationLoaded) {
+ writeFileSync(elevationPlist, elevationPlistContents, "utf8");
+ writeFileSync(launchStateFile, "elevation-loaded\n", "utf8");
+ } else {
+ writeFileSync(sourcePlist, sourceContents, "utf8");
+ writeFileSync(launchStateFile, "source-loaded\n", "utf8");
+ }
writeFileSync(nodeGenerationFile, "0\n", "utf8");
writeExecutable(path.join(binDir, "defaults"), "#!/bin/sh\nprintf '%s\\n' primary\n");
+ writeExecutable(
+ path.join(binDir, "df"),
+ [
+ "#!/usr/bin/env bash",
+ 'if [[ "${TEST_FAIL_UNSAFE_ENTRY_IDENTITY:-0}" == "1" && "${!#}" == "${TEST_INSTALLED_APP_PATH:-}" ]]; then',
+ " exit 7",
+ "fi",
+ 'exec /bin/df "$@"',
+ "",
+ ].join("\n"),
+ );
+ writeExecutable(
+ path.join(binDir, "mktemp"),
+ [
+ "#!/usr/bin/env bash",
+ 'if [[ "${TEST_FAIL_UNSAFE_ENTRY_MKTEMP:-0}" == "1" && "$*" == *"elevation-host.quarantined-app."* ]]; then',
+ " exit 7",
+ "fi",
+ 'exec /usr/bin/mktemp "$@"',
+ "",
+ ].join("\n"),
+ );
if (options.replaceAuthenticatedRenameHelperBeforeUse) {
writeExecutable(
path.join(binDir, "shasum"),
@@ -912,6 +1038,8 @@ function createInstallRollbackHarness(
" fi",
' if [[ "$target" == */ai.openclaw.mac.elevation-host && "$state" == "elevation-loaded" ]]; then',
" printf '%s\\n' ' pid = 555555'",
+ " printf ' program = %s/Contents/MacOS/OpenClaw\\n' \"$TEST_INSTALLED_APP_PATH\"",
+ " printf '%s\\n' ' arguments = {' ' --elevation-host' ' }'",
" exit 0",
" fi",
" printf '%s\\n' 'Could not find service in domain' >&2",
@@ -932,6 +1060,27 @@ function createInstallRollbackHarness(
' plist="${3:-}"',
' if [[ "$plist" == *ai.openclaw.mac.elevation-host.plist ]]; then',
' if [[ "$TEST_LAUNCHD_BOOTSTRAP_FAILS" == "1" ]]; then',
+ ' if [[ -n "$TEST_UNSAFE_ENTRY_EVIDENCE" ]]; then',
+ ' rollback_app="$(find "$(dirname "$TEST_INSTALLED_APP_PATH")" -maxdepth 1 -type d -name "$(basename "$TEST_INSTALLED_APP_PATH").rollback-elevation-host-*" -print -quit)"',
+ ' [[ -n "$rollback_app" ]] || exit 71',
+ ' /bin/rm -rf -- "$TEST_INSTALLED_APP_PATH"',
+ ' if [[ "$TEST_UNSAFE_ENTRY_EVIDENCE" == unrelated-* ]]; then',
+ ' /bin/mv "$rollback_app" "$TEST_INSTALLED_APP_PATH"',
+ " else",
+ ' ln -s "$rollback_app" "$TEST_INSTALLED_APP_PATH"',
+ " fi",
+ ' pending_receipt="$TEST_STATE_DIR/elevation-host-install.pending.json"',
+ ' case "$TEST_UNSAFE_ENTRY_EVIDENCE" in',
+ ' job) /bin/rm -f -- "$TEST_ELEVATION_PLIST" "$pending_receipt"; printf \'%s\\n\' elevation-loaded >"$TEST_LAUNCH_STATE_FILE" ;;',
+ ' plist) /bin/rm -f -- "$pending_receipt"; printf \'%s\\n\' elevation-absent >"$TEST_LAUNCH_STATE_FILE" ;;',
+ ' plist-program) /bin/rm -f -- "$pending_receipt"; /usr/bin/plutil -insert Program -string "$TEST_INSTALLED_APP_PATH/Contents/MacOS/OpenClaw" "$TEST_ELEVATION_PLIST"; printf \'%s\\n\' elevation-absent >"$TEST_LAUNCH_STATE_FILE" ;;',
+ ' receipt) /bin/rm -f -- "$TEST_ELEVATION_PLIST"; printf \'%s\\n\' elevation-absent >"$TEST_LAUNCH_STATE_FILE" ;;',
+ ' unrelated-plist) /bin/rm -f -- "$pending_receipt"; /usr/bin/plutil -replace ProgramArguments.0 -string "$TEST_UNRELATED_APP_PATH/Contents/MacOS/OpenClaw" "$TEST_ELEVATION_PLIST"; printf \'%s\\n\' elevation-absent >"$TEST_LAUNCH_STATE_FILE" ;;',
+ ' unrelated-program) /bin/rm -f -- "$pending_receipt"; /usr/bin/plutil -insert Program -string "$TEST_UNRELATED_APP_PATH/Contents/MacOS/OpenClaw" "$TEST_ELEVATION_PLIST"; printf \'%s\\n\' elevation-absent >"$TEST_LAUNCH_STATE_FILE" ;;',
+ ' unrelated-receipt) /bin/rm -f -- "$TEST_ELEVATION_PLIST"; jq --arg appPath "$TEST_UNRELATED_APP_PATH" \'.appPath = $appPath\' "$pending_receipt" >"$pending_receipt.tmp"; /bin/mv "$pending_receipt.tmp" "$TEST_STATE_DIR/elevation-host-install.json"; /bin/rm -f -- "$pending_receipt"; printf \'%s\\n\' elevation-absent >"$TEST_LAUNCH_STATE_FILE" ;;',
+ " *) exit 72 ;;",
+ " esac",
+ " fi",
' if [[ "$TEST_RECREATE_SOURCE_ON_FAILURE" == "1" ]]; then',
" printf '%s\\n' replacement-owner >\"$TEST_SOURCE_PLIST\"",
" fi",
@@ -1000,6 +1149,8 @@ function createInstallRollbackHarness(
...artifact,
appPath,
configPath,
+ elevationPlist,
+ elevationPlistContents,
label,
launchStateFile,
sourceContents,
@@ -1008,15 +1159,26 @@ function createInstallRollbackHarness(
env: {
...artifact.env,
TEST_DANGLING_ROLLBACK_DURING_MOVE: options.danglingRollbackDuringMove ? "1" : "0",
+ TEST_CORRUPT_ROLLBACK_PLIST_BACKUP_ON_SYNC: options.corruptRollbackPlistBackupOnSync
+ ? "1"
+ : "0",
TEST_FAIL_CURRENT_RECEIPT_RESTORE_COPY: options.failCurrentReceiptRestoreCopy ? "1" : "0",
TEST_FAIL_AFTER_RECEIPT_COMMIT_MOVE: options.failAfterReceiptCommitMove ? "1" : "0",
+ TEST_FAIL_ELEVATION_QUARANTINE_RENAME: options.failElevationQuarantineRename ? "1" : "0",
TEST_FAIL_LSOF_INSPECTION: options.failLsofInspection ? "1" : "0",
TEST_FAIL_PGREP_INSPECTION: options.failPgrepInspection ? "1" : "0",
TEST_FAIL_RECOVERY_XATTR_READ: options.failRecoveryXattrRead ? "1" : "0",
+ TEST_FAIL_UNSAFE_ENTRY_IDENTITY: options.failUnsafeEntryIdentity ? "1" : "0",
+ TEST_FAIL_UNSAFE_ENTRY_MKTEMP: options.failUnsafeEntryMktemp ? "1" : "0",
TEST_FINAL_CDHASH_MISMATCH: options.finalCDHashMismatch ? "1" : "0",
TEST_FINAL_SIGNATURE_INVALID: options.finalSignatureInvalid ? "1" : "0",
TEST_INSTALLED_APP_PATH: appPath,
+ TEST_ELEVATION_PLIST: elevationPlist,
+ TEST_STATE_DIR: stateDir,
+ TEST_UNRELATED_APP_PATH: path.join(tempRoot, "UnrelatedOpenClaw.app"),
+ TEST_UNSAFE_ENTRY_EVIDENCE: options.unsafeEntryEvidence ?? "",
TEST_CUSTODY_SIGNAL: options.hupDuringCustody ? "HUP" : "TERM",
+ TEST_CURRENT_CUA_SIGNATURE_INVALID: options.currentCuaSignatureInvalid ? "1" : "0",
TEST_LAUNCHD_BOOTSTRAP_FAILS: options.launchdBootstrapFails === false ? "0" : "1",
TEST_LIVE_PID: String(process.pid),
TEST_LAUNCH_STATE_FILE: launchStateFile,
@@ -1037,6 +1199,7 @@ function createInstallRollbackHarness(
TEST_RECREATE_APP_DURING_DAMAGED_CUSTODY: options.recreateAppDuringDamagedCustody ? "1" : "0",
TEST_RECREATE_SOURCE_DURING_BOOTOUT: options.recreateSourceDuringBootout ? "1" : "0",
TEST_RECREATE_SOURCE_ON_FAILURE: options.recreateSourceOnFailure ? "1" : "0",
+ TEST_RELOAD_ELEVATION_AFTER_QUARANTINE: options.reloadElevationAfterQuarantine ? "1" : "0",
TEST_RENAME_HELPER_HASH_MARKER: path.join(tempRoot, "rename-helper-hash-marker"),
TEST_REPLACE_DAMAGED_APP_DIRECTORY_BEFORE_CUSTODY:
options.replaceDamagedAppDirectoryBeforeCustody ? "1" : "0",
@@ -1048,6 +1211,9 @@ function createInstallRollbackHarness(
options.replaceMigrationSourceSameContentBeforeInitialCustody ? "1" : "0",
TEST_REMOVE_INSTALLED_EXECUTABLE_AFTER_READINESS:
options.removeInstalledExecutableAfterReadiness ? "1" : "0",
+ TEST_REMOVE_CUA_DRIVER_AFTER_UNSAFE_ENTRY_MOVE: options.removeCuaDriverAfterUnsafeEntryMove
+ ? "1"
+ : "0",
TEST_RESTART_APP_DURING_BOOTOUT: options.restartAppDuringBootout ? "1" : "0",
TEST_ROLLBACK_NON_NATIVE_SIGNATURE_INVALID: options.rollbackNonNativeSignatureInvalid
? "1"
@@ -1299,6 +1465,47 @@ describe("mac elevation host command contract", () => {
},
);
+ it.skipIf(process.platform !== "darwin")(
+ "rejects CUA-bearing elevation artifacts before verify or install",
+ () => {
+ const verification = createArtifactVerificationHarness();
+ const rejectedVerify = runInstaller(
+ verification.installerPath,
+ [
+ "verify",
+ "--archive",
+ verification.archivePath,
+ "--receipt",
+ verification.receiptPath,
+ ...receiptDigestArgs(verification.receiptPath),
+ ],
+ { ...verification.env, TEST_CUA_DRIVER_KIND: "file" },
+ );
+ expect(rejectedVerify.status).toBe(1);
+ expect(rejectedVerify.stderr).toContain("must not contain bundled CUA driver");
+
+ const installation = createInstallRollbackHarness();
+ const rejectedInstall = runInstaller(
+ installation.installerPath,
+ [
+ "install",
+ "--archive",
+ installation.archivePath,
+ "--receipt",
+ installation.receiptPath,
+ ...receiptDigestArgs(installation.receiptPath),
+ "--app",
+ installation.appPath,
+ "--migrate-launch-agent",
+ installation.sourcePlist,
+ ],
+ { ...installation.env, TEST_CUA_DRIVER_KIND: "symlink" },
+ );
+ expect(rejectedInstall.status).toBe(1);
+ expect(rejectedInstall.stderr).toContain("must not contain bundled CUA driver");
+ },
+ );
+
it.skipIf(process.platform !== "darwin")(
"plans canonical node conversion without reading or copying its token",
() => {
@@ -1643,6 +1850,428 @@ describe("mac elevation host command contract", () => {
},
);
+ for (const rollbackCuaDriverKind of ["file", "symlink"] as const) {
+ it.skipIf(process.platform !== "darwin")(
+ `preserves but never restarts a ${rollbackCuaDriverKind} CUA-bearing elevation rollback`,
+ () => {
+ const harness = createInstallRollbackHarness({
+ existingElevationLoaded: true,
+ reloadElevationAfterQuarantine: true,
+ rollbackCuaDriverKind,
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--state-dir",
+ harness.stateDir,
+ "--config-path",
+ harness.configPath,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("could not bootstrap elevation host");
+ expect(result.stderr).toContain("Preserved previous elevation app with bundled CUA driver");
+ expect(result.stderr).toContain(
+ "Quarantined replacement for unsafe previous elevation LaunchAgent",
+ );
+ expect(result.stderr).toContain("automatic elevation-host rollback was incomplete");
+ expect(readFileSync(harness.launchStateFile, "utf8").trim()).toBe("elevation-absent");
+ const quarantinedApp = preservedCuaAppPath(harness);
+ expect(quarantinedApp).toBeDefined();
+ const rollbackDriver = lstatSync(
+ path.join(quarantinedApp!, "Contents", "Resources", "cua-driver"),
+ );
+ expect(rollbackDriver.isSymbolicLink()).toBe(rollbackCuaDriverKind === "symlink");
+ expect(existsSync(harness.elevationPlist)).toBe(false);
+ expect(
+ readdirSync(path.dirname(harness.elevationPlist)).filter((name) =>
+ name.startsWith("ai.openclaw.mac.elevation-host"),
+ ),
+ ).toEqual([]);
+ const previousPlist = readdirSync(harness.stateDir).find((name) =>
+ name.startsWith("elevation-host.previous-plist."),
+ );
+ expect(previousPlist).toBeDefined();
+ expect(readFileSync(path.join(harness.stateDir, previousPlist!), "utf8")).toBe(
+ harness.elevationPlistContents,
+ );
+ const quarantinedPlist = readdirSync(harness.stateDir).find((name) =>
+ name.startsWith("elevation-host.quarantined-launch-agent."),
+ );
+ expect(quarantinedPlist).toBeDefined();
+ expect(readFileSync(path.join(harness.stateDir, quarantinedPlist!), "utf8")).toContain(
+ "--elevation-host",
+ );
+ },
+ );
+ }
+
+ it.skipIf(process.platform !== "darwin")(
+ "removes the discoverable elevation plist when unsafe rollback quarantine fails",
+ () => {
+ const harness = createInstallRollbackHarness({
+ existingElevationLoaded: true,
+ failElevationQuarantineRename: true,
+ rollbackCuaDriverKind: "file",
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--state-dir",
+ harness.stateDir,
+ "--config-path",
+ harness.configPath,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain(
+ "Removed unquarantinable replacement for unsafe previous elevation LaunchAgent",
+ );
+ expect(readFileSync(harness.launchStateFile, "utf8").trim()).toBe("elevation-absent");
+ expect(existsSync(harness.elevationPlist)).toBe(false);
+ const previousPlist = readdirSync(harness.stateDir).find((name) =>
+ name.startsWith("elevation-host.previous-plist."),
+ );
+ expect(previousPlist).toBeDefined();
+ expect(readFileSync(path.join(harness.stateDir, previousPlist!), "utf8")).toBe(
+ harness.elevationPlistContents,
+ );
+ },
+ );
+
+ it.skipIf(process.platform !== "darwin")(
+ "quarantines a CUA-bearing elevation app when the recorded rollback destination is blocked",
+ () => {
+ const harness = createInstallRollbackHarness({
+ danglingRollbackDuringMove: true,
+ existingElevationLoaded: true,
+ rollbackCuaDriverKind: "file",
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--state-dir",
+ harness.stateDir,
+ "--config-path",
+ harness.configPath,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(existsSync(path.join(harness.appPath, "Contents", "Resources", "cua-driver"))).toBe(
+ false,
+ );
+ expect(existsSync(harness.elevationPlist)).toBe(false);
+ expect(readFileSync(harness.launchStateFile, "utf8").trim()).toBe("elevation-absent");
+ const quarantinedApp = quarantinedElevationAppPath(harness.stateDir);
+ expect(quarantinedApp).toBeDefined();
+ expect(existsSync(path.join(quarantinedApp!, "Contents", "Resources", "cua-driver"))).toBe(
+ true,
+ );
+ },
+ );
+
+ for (const evidence of ["job", "plist", "plist-program", "receipt"] as const) {
+ it.skipIf(process.platform !== "darwin")(
+ `quarantines a symlinked CUA app from exact ${evidence} ownership evidence`,
+ () => {
+ const harness = createInstallRollbackHarness({
+ rollbackCuaDriverKind: "file",
+ unsafeEntryEvidence: evidence,
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--migrate-launch-agent",
+ harness.sourcePlist,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("Quarantined CUA-bearing elevation app");
+ expect(lstatSync(harness.appPath, { throwIfNoEntry: false })).toBeUndefined();
+ expect(readFileSync(harness.launchStateFile, "utf8").trim()).toBe("elevation-absent");
+ const quarantinedApp = quarantinedElevationAppPath(harness.stateDir);
+ expect(quarantinedApp).toBeDefined();
+ expect(lstatSync(quarantinedApp!).isSymbolicLink()).toBe(true);
+ expect(existsSync(path.join(quarantinedApp!, "Contents", "Resources", "cua-driver"))).toBe(
+ true,
+ );
+ },
+ );
+ }
+
+ it.skipIf(process.platform !== "darwin")(
+ "keeps a symlink quarantined when its target changes after the move",
+ () => {
+ const harness = createInstallRollbackHarness({
+ removeCuaDriverAfterUnsafeEntryMove: true,
+ rollbackCuaDriverKind: "file",
+ unsafeEntryEvidence: "job",
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--migrate-launch-agent",
+ harness.sourcePlist,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("Quarantined CUA-bearing elevation app");
+ const quarantinedApp = quarantinedElevationAppPath(harness.stateDir);
+ expect(quarantinedApp).toBeDefined();
+ expect(lstatSync(quarantinedApp!).isSymbolicLink()).toBe(true);
+ },
+ );
+
+ for (const evidence of ["unrelated-plist", "unrelated-program", "unrelated-receipt"] as const) {
+ it.skipIf(process.platform !== "darwin")(
+ `preserves an app when only an ${evidence} remains`,
+ () => {
+ const harness = createInstallRollbackHarness({
+ rollbackCuaDriverKind: "file",
+ unsafeEntryEvidence: evidence,
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--migrate-launch-agent",
+ harness.sourcePlist,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).not.toContain("Quarantined CUA-bearing elevation app");
+ expect(lstatSync(harness.appPath).isDirectory()).toBe(true);
+ expect(quarantinedElevationAppPath(harness.stateDir)).toBeUndefined();
+ const stalePath =
+ evidence !== "unrelated-receipt"
+ ? harness.elevationPlist
+ : path.join(harness.stateDir, "elevation-host-install.json");
+ expect(existsSync(stalePath)).toBe(true);
+ },
+ );
+ }
+
+ for (const setupFailure of ["identity", "mktemp"] as const) {
+ it.skipIf(process.platform !== "darwin")(
+ `neutralizes launchd before unsafe app quarantine ${setupFailure} setup fails`,
+ () => {
+ const harness = createInstallRollbackHarness({
+ danglingRollbackDuringMove: true,
+ existingElevationLoaded: true,
+ failUnsafeEntryIdentity: setupFailure === "identity",
+ failUnsafeEntryMktemp: setupFailure === "mktemp",
+ rollbackCuaDriverKind: "file",
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--state-dir",
+ harness.stateDir,
+ "--config-path",
+ harness.configPath,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("automatic elevation-host rollback was incomplete");
+ expect(existsSync(harness.elevationPlist)).toBe(false);
+ expect(readFileSync(harness.launchStateFile, "utf8").trim()).toBe("elevation-absent");
+ expect(existsSync(path.join(harness.appPath, "Contents", "Resources", "cua-driver"))).toBe(
+ true,
+ );
+ },
+ );
+ }
+
+ it.skipIf(process.platform !== "darwin")(
+ "neutralizes unsafe rollback even when its plist evidence becomes corrupt",
+ () => {
+ const harness = createInstallRollbackHarness({
+ corruptRollbackPlistBackupOnSync: true,
+ existingElevationLoaded: true,
+ rollbackCuaDriverKind: "file",
+ });
+ const result = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--state-dir",
+ harness.stateDir,
+ "--config-path",
+ harness.configPath,
+ ],
+ harness.env,
+ );
+
+ expect(result.status).toBe(1);
+ expect(result.stderr).toContain("automatic elevation-host rollback was incomplete");
+ expect(readFileSync(harness.launchStateFile, "utf8").trim()).toBe("elevation-absent");
+ expect(existsSync(harness.appPath)).toBe(false);
+ expect(existsSync(harness.elevationPlist)).toBe(false);
+ expect(
+ readdirSync(harness.stateDir).some((name) =>
+ name.startsWith("elevation-host.quarantined-launch-agent."),
+ ),
+ ).toBe(true);
+ expect(
+ existsSync(path.join(preservedCuaAppPath(harness)!, "Contents", "Resources", "cua-driver")),
+ ).toBe(true);
+ },
+ );
+
+ it.skipIf(process.platform !== "darwin")(
+ "never restores a CUA-bearing current elevation job after explicit recovery fails",
+ () => {
+ const harness = createInstallRollbackHarness({
+ currentCuaSignatureInvalid: true,
+ launchdBootstrapFails: false,
+ migrationRestoreBootstrapFails: true,
+ });
+ const installed = runInstaller(
+ harness.installerPath,
+ [
+ "install",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--migrate-launch-agent",
+ harness.sourcePlist,
+ ],
+ harness.env,
+ );
+ expect(installed.status, installed.stderr).toBe(0);
+ const currentReceipt = readFileSync(
+ path.join(harness.stateDir, "elevation-host-install.json"),
+ "utf8",
+ );
+ const resources = path.join(harness.appPath, "Contents", "Resources");
+ mkdirSync(resources, { recursive: true });
+ writeExecutable(path.join(resources, "cua-driver"), "#!/bin/sh\nexit 0\n");
+
+ const recovered = runInstaller(
+ harness.installerPath,
+ [
+ "recover",
+ "--archive",
+ harness.archivePath,
+ "--receipt",
+ harness.receiptPath,
+ ...receiptDigestArgs(harness.receiptPath),
+ "--app",
+ harness.appPath,
+ "--state-dir",
+ harness.stateDir,
+ ],
+ harness.env,
+ );
+
+ expect(recovered.status).toBe(1);
+ expect(recovered.stderr).toContain("Preserved current elevation app with bundled CUA driver");
+ expect(recovered.stderr).toContain(
+ "recovery failed and the current OpenClaw installation could not be restored completely",
+ );
+ expect(readFileSync(harness.launchStateFile, "utf8").trim()).toBe("elevation-absent");
+ expect(existsSync(harness.elevationPlist)).toBe(false);
+ expect(existsSync(path.join(harness.appPath, "Contents", "Resources", "cua-driver"))).toBe(
+ false,
+ );
+ const preservedApp = preservedCuaAppPath(harness);
+ expect(preservedApp).toBeDefined();
+ expect(existsSync(path.join(preservedApp!, "Contents", "Resources", "cua-driver"))).toBe(
+ true,
+ );
+ expect(readFileSync(path.join(harness.stateDir, "elevation-host-install.json"), "utf8")).toBe(
+ currentReceipt,
+ );
+ const preservedCurrentPlist = readdirSync(harness.stateDir).find((name) =>
+ name.startsWith("elevation-host.recovery-current-plist."),
+ );
+ expect(preservedCurrentPlist).toBeDefined();
+ expect(readFileSync(path.join(harness.stateDir, preservedCurrentPlist!), "utf8")).toContain(
+ "--elevation-host",
+ );
+ },
+ );
+
it.skipIf(process.platform !== "darwin")(
"refuses to record an invalid existing app as rollback state",
() => {
diff --git a/test/scripts/package-mac-app.test.ts b/test/scripts/package-mac-app.test.ts
index 07bc7ab4eae6..24f4e55cda35 100644
--- a/test/scripts/package-mac-app.test.ts
+++ b/test/scripts/package-mac-app.test.ts
@@ -1680,6 +1680,27 @@ describe("package-mac-app plist stamping", () => {
);
});
+ it("omits the CUA driver only from elevation-host packages", () => {
+ const packageScript = readFileSync(scriptPath, "utf8");
+ const variantBlock = packageScript.slice(
+ packageScript.indexOf('SIGNING_VARIANT="${OPENCLAW_MAC_SIGNING_VARIANT:-standard}"'),
+ packageScript.indexOf("# OPENCLAW_SKIP_MLX_TTS"),
+ );
+ const cuaBlock = packageScript.slice(
+ packageScript.indexOf('if [[ "$SIGNING_VARIANT" == "elevation-host" ]]'),
+ packageScript.indexOf('echo "📦 Copying CLI installer"'),
+ );
+
+ expect(variantBlock).toContain("standard | elevation-host");
+ expect(variantBlock).toContain("Unknown OPENCLAW_MAC_SIGNING_VARIANT value");
+ expect(cuaBlock).toContain("Omitting embedded CUA driver from elevation-host package");
+ expect(cuaBlock).toContain("else");
+ expect(cuaBlock).toContain("Staging embedded CUA driver");
+ expect(cuaBlock).toContain(
+ '"$ROOT_DIR/scripts/stage-cua-driver-macos.sh" "$APP_ROOT/Contents/Resources/cua-driver"',
+ );
+ });
+
it("does not mask required Info.plist stamp failures", () => {
const script = readFileSync(scriptPath, "utf8");
const stampBlock = script.slice(