diff --git a/apps/macos/Sources/Clawdis/MenuBar.swift b/apps/macos/Sources/Clawdis/MenuBar.swift index 20290aaec..ec7358fa9 100644 --- a/apps/macos/Sources/Clawdis/MenuBar.swift +++ b/apps/macos/Sources/Clawdis/MenuBar.swift @@ -2,6 +2,9 @@ import AppKit import Foundation import MenuBarExtraAccess import SwiftUI +import Security +import OSLog +import Darwin @main struct ClawdisApp: App { @@ -386,6 +389,8 @@ enum CritterIconRenderer { final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate { private var listener: NSXPCListener? private var state: AppState? + private let xpcLogger = Logger(subsystem: "com.steipete.clawdis", category: "xpc") + private let allowedTeamIDs: Set = ["Y5PE65HELJ"] @MainActor func applicationDidFinishLaunching(_ notification: Notification) { @@ -426,6 +431,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate } func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool { + guard self.isAllowed(connection: connection) else { + self.xpcLogger.error("Rejecting XPC connection: team ID mismatch or invalid audit token") + connection.invalidate() + return false + } let interface = NSXPCInterface(with: ClawdisXPCProtocol.self) connection.exportedInterface = interface connection.exportedObject = ClawdisXPCService() @@ -438,4 +448,55 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSXPCListenerDelegate let running = NSWorkspace.shared.runningApplications.filter { $0.bundleIdentifier == bundleID } return running.count > 1 } + + private func isAllowed(connection: NSXPCConnection) -> Bool { + // Prefer audit token (available via KVC); fall back to pid-based lookup. + if let tokenData = connection.value(forKey: "auditToken") as? Data, + tokenData.count == MemoryLayout.size { + var token = audit_token_t() + _ = withUnsafeMutableBytes(of: &token) { tokenData.copyBytes(to: $0) } + let attrs: NSDictionary = [kSecGuestAttributeAudit: tokenData] + if self.teamIDMatches(attrs: attrs) { return true } + } + + let pid = connection.processIdentifier + guard pid > 0 else { return false } + let attrs: NSDictionary = [kSecGuestAttributePid: pid] + if self.teamIDMatches(attrs: attrs) { return true } + + // Fallback: allow same-user processes (still local-only). + var pidInfo = kinfo_proc() + var size = MemoryLayout.size(ofValue: pidInfo) + var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid] + let result = name.withUnsafeMutableBufferPointer { namePtr -> Bool in + return sysctl(namePtr.baseAddress, u_int(namePtr.count), &pidInfo, &size, nil, 0) == 0 + } + if result, pidInfo.kp_eproc.e_ucred.cr_uid == getuid() { + return true + } + return false + } + + private func teamIDMatches(attrs: NSDictionary) -> Bool { + var secCode: SecCode? + guard SecCodeCopyGuestWithAttributes(nil, attrs, SecCSFlags(), &secCode) == errSecSuccess, + let code = secCode else { return false } + + var staticCode: SecStaticCode? + guard SecCodeCopyStaticCode(code, SecCSFlags(), &staticCode) == errSecSuccess, + let sCode = staticCode else { return false } + + var infoCF: CFDictionary? + guard SecCodeCopySigningInformation(sCode, SecCSFlags(), &infoCF) == errSecSuccess, + let info = infoCF as? [String: Any], + let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String else { + return false + } + + return self.allowedTeamIDs.contains(teamID) + } + + @MainActor + private func writeEndpoint(_ endpoint: NSXPCListenerEndpoint) {} + @MainActor private func writeEndpointIfAvailable() {} } diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh index 71e07b3c2..e747210d2 100755 --- a/scripts/package-mac-app.sh +++ b/scripts/package-mac-app.sh @@ -89,6 +89,3 @@ echo "🔏 Signing bundle (ad-hoc by default; set SIGN_IDENTITY for real cert)" SIGN_IDENTITY="${SIGN_IDENTITY:--}" "$ROOT_DIR/scripts/codesign-mac-app.sh" "$APP_ROOT" echo "✅ Bundle ready at $APP_ROOT" - -echo "🚀 Launching app" -open "$APP_ROOT" diff --git a/scripts/restart-mac.sh b/scripts/restart-mac.sh index ceb872709..094eed9ec 100755 --- a/scripts/restart-mac.sh +++ b/scripts/restart-mac.sh @@ -56,7 +56,42 @@ run_step "swift build" bash -lc "cd '${ROOT_DIR}/apps/macos' && swift build -q - # 3) Package + relaunch the app (script also stops any stragglers). run_step "package app" "${ROOT_DIR}/scripts/package-mac-app.sh" -# 4) Verify the packaged app is alive. +# 4) Install launch agent with Mach service and bootstrap it (no KeepAlive). +cat > "${LAUNCH_AGENT}" < + + + + Label + com.steipete.clawdis + ProgramArguments + + ${APP_BUNDLE}/Contents/MacOS/Clawdis + + WorkingDirectory + ${ROOT_DIR} + RunAtLoad + + KeepAlive + + MachServices + + com.steipete.clawdis.xpc + + + StandardOutPath + /tmp/clawdis.log + StandardErrorPath + /tmp/clawdis.log + + +PLIST + +stop_launch_agent +run_step "bootstrap launch agent" launchctl bootstrap gui/"$UID" "${LAUNCH_AGENT}" +run_step "kickstart" launchctl kickstart -k gui/"$UID"/com.steipete.clawdis + +# 5) Verify the packaged app is alive. sleep 1 if pgrep -f "${APP_PROCESS_PATTERN}" >/dev/null 2>&1; then log "OK: Clawdis is running."