diff --git a/apps/macos/Sources/Clawdis/NotificationManager.swift b/apps/macos/Sources/Clawdis/NotificationManager.swift index 3f48da8c2..4650d3afe 100644 --- a/apps/macos/Sources/Clawdis/NotificationManager.swift +++ b/apps/macos/Sources/Clawdis/NotificationManager.swift @@ -1,9 +1,17 @@ import ClawdisIPC import Foundation +import Security import UserNotifications @MainActor struct NotificationManager { + private static let hasTimeSensitiveEntitlement: Bool = { + guard let task = SecTaskCreateFromSelf(nil) else { return false } + let key = "com.apple.developer.usernotifications.time-sensitive" as CFString + guard let val = SecTaskCopyValueForEntitlement(task, key, nil) else { return false } + return (val as? Bool) == true + }() + func send(title: String, body: String, sound: String?, priority: NotificationPriority? = nil) async -> Bool { let center = UNUserNotificationCenter.current() let status = await center.notificationSettings() @@ -29,7 +37,7 @@ struct NotificationManager { case .active: content.interruptionLevel = .active case .timeSensitive: - content.interruptionLevel = .timeSensitive + content.interruptionLevel = Self.hasTimeSensitiveEntitlement ? .timeSensitive : .active } } diff --git a/docs/clawdis-mac.md b/docs/clawdis-mac.md index b2970e7f3..6cdfd68a6 100644 --- a/docs/clawdis-mac.md +++ b/docs/clawdis-mac.md @@ -70,6 +70,7 @@ struct Response { ok: Bool; message?: String; payload?: Data } - `run -- cmd args... [--cwd] [--env KEY=VAL] [--timeout 30] [--needs-screen-recording]` - `status` - Sounds: supply any macOS alert name with `--sound` per notification; omit the flag to use the system default. There is no longer a persisted “default sound” in the app UI. +- Priority: `timeSensitive` is best-effort and falls back to `active` unless the app is signed with the Time Sensitive Notifications entitlement. - Internals: builds Request, connects via AsyncXPCConnection, prints Response as JSON to stdout. ## Integration with clawdis/Clawdis (Node/TS) diff --git a/scripts/codesign-mac-app.sh b/scripts/codesign-mac-app.sh index 25f000527..3a124d9f8 100755 --- a/scripts/codesign-mac-app.sh +++ b/scripts/codesign-mac-app.sh @@ -3,7 +3,9 @@ set -euo pipefail APP_BUNDLE="${1:-dist/Clawdis.app}" IDENTITY="${SIGN_IDENTITY:-}" -ENT_TMP=$(mktemp -t clawdis-entitlements) +ENT_TMP_BASE=$(mktemp -t clawdis-entitlements-base) +ENT_TMP_APP=$(mktemp -t clawdis-entitlements-app) +ENT_TMP_APP_BASE=$(mktemp -t clawdis-entitlements-app-base) if [ ! -d "$APP_BUNDLE" ]; then echo "App bundle not found: $APP_BUNDLE" >&2 @@ -44,7 +46,41 @@ fi echo "Using signing identity: $IDENTITY" -cat > "$ENT_TMP" <<'PLIST' +cat > "$ENT_TMP_BASE" <<'PLIST' + + + + + com.apple.security.hardened-runtime + + com.apple.security.cs.allow-jit + + com.apple.security.automation.apple-events + + com.apple.security.device.audio-input + + + +PLIST + +cat > "$ENT_TMP_APP_BASE" <<'PLIST' + + + + + com.apple.security.hardened-runtime + + com.apple.security.cs.allow-jit + + com.apple.security.automation.apple-events + + com.apple.security.device.audio-input + + + +PLIST + +cat > "$ENT_TMP_APP" <<'PLIST' @@ -63,12 +99,27 @@ cat > "$ENT_TMP" <<'PLIST' PLIST +# The time-sensitive entitlement is restricted and needs to be present in a +# matching provisioning profile when using Apple Development signing. +# Avoid breaking local debug builds by only enabling it when forced, or when +# using distribution-style identities. +APP_ENTITLEMENTS="$ENT_TMP_APP_BASE" +if [[ "${ENABLE_TIME_SENSITIVE_NOTIFICATIONS:-}" == "1" ]]; then + APP_ENTITLEMENTS="$ENT_TMP_APP" +elif [[ "$IDENTITY" == *"Developer ID Application"* ]] || [[ "$IDENTITY" == *"Apple Distribution"* ]]; then + APP_ENTITLEMENTS="$ENT_TMP_APP" +else + echo "Note: Time Sensitive Notifications entitlement disabled for this signing identity." + echo " To force it: ENABLE_TIME_SENSITIVE_NOTIFICATIONS=1 scripts/codesign-mac-app.sh " +fi + # clear extended attributes to avoid stale signatures xattr -cr "$APP_BUNDLE" 2>/dev/null || true sign_item() { local target="$1" - codesign --force --options runtime --timestamp=none --entitlements "$ENT_TMP" --sign "$IDENTITY" "$target" + local entitlements="$2" + codesign --force --options runtime --timestamp=none --entitlements "$entitlements" --sign "$IDENTITY" "$target" } sign_plain_item() { @@ -78,16 +129,16 @@ sign_plain_item() { # Sign main binary and CLI helper if present if [ -f "$APP_BUNDLE/Contents/MacOS/Clawdis" ]; then - echo "Signing main binary"; sign_item "$APP_BUNDLE/Contents/MacOS/Clawdis" + echo "Signing main binary"; sign_item "$APP_BUNDLE/Contents/MacOS/Clawdis" "$APP_ENTITLEMENTS" fi if [ -f "$APP_BUNDLE/Contents/MacOS/ClawdisCLI" ]; then - echo "Signing CLI helper"; sign_item "$APP_BUNDLE/Contents/MacOS/ClawdisCLI" + echo "Signing CLI helper"; sign_item "$APP_BUNDLE/Contents/MacOS/ClawdisCLI" "$ENT_TMP_BASE" fi # Sign bundled gateway payload (native addons, libvips dylibs) if [ -d "$APP_BUNDLE/Contents/Resources/Relay" ]; then find "$APP_BUNDLE/Contents/Resources/Relay" -type f \( -name "*.node" -o -name "*.dylib" \) -print0 | while IFS= read -r -d '' f; do - echo "Signing gateway payload: $f"; sign_item "$f" + echo "Signing gateway payload: $f"; sign_item "$f" "$ENT_TMP_BASE" done fi @@ -115,7 +166,7 @@ if [ -d "$APP_BUNDLE/Contents/Frameworks" ]; then fi # Finally sign the bundle -sign_item "$APP_BUNDLE" +sign_item "$APP_BUNDLE" "$APP_ENTITLEMENTS" -rm -f "$ENT_TMP" +rm -f "$ENT_TMP_BASE" "$ENT_TMP_APP_BASE" "$ENT_TMP_APP" echo "Codesign complete for $APP_BUNDLE"