mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(macos): keep elevation host CUA-free (#125408)
* fix(macos): isolate elevation host from CUA * fix(macos): fail closed on unsafe elevation rollback * fix(macos): quarantine unsafe elevation state before recovery * fix(macos): bind elevation recovery ownership
This commit is contained in:
committed by
GitHub
parent
a996ea25d9
commit
04c9924c45
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
|
||||
+337
-34
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<plist version="1.0"><dict>',
|
||||
@@ -764,10 +852,48 @@ function createInstallRollbackHarness(
|
||||
"</dict></dict></plist>",
|
||||
"",
|
||||
].join("\n");
|
||||
writeFileSync(sourcePlist, sourceContents, "utf8");
|
||||
writeFileSync(launchStateFile, "source-loaded\n", "utf8");
|
||||
const elevationPlistContents = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<plist version="1.0"><dict>',
|
||||
"<key>Label</key><string>ai.openclaw.mac.elevation-host</string>",
|
||||
`<key>ProgramArguments</key><array><string>${appPath}/Contents/MacOS/OpenClaw</string><string>--elevation-host</string></array>`,
|
||||
"<key>EnvironmentVariables</key><dict>",
|
||||
`<key>OPENCLAW_STATE_DIR</key><string>${stateDir}</string>`,
|
||||
`<key>OPENCLAW_CONFIG_PATH</key><string>${configPath}</string>`,
|
||||
"</dict></dict></plist>",
|
||||
"",
|
||||
].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",
|
||||
() => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user