Files
maksarsanjeev 52e1e8288b Security + upstream fixes
- Network core moves to a root-only folder; BroV talks to a narrow root helper (netctl.py) over a user-only socket; install script verifies the mihomo SHA256 and migrates keys (scripts/netcore)
- Hardened runtime, no get-task-allow; bypassPermissions removed from the chat; concealed clipboard items are not restored; DangerCheck knows core, LaunchAgents and hook paths; dropped-file copies expire after 7 days
- Ported from upstream Coucou: 1h crash fix (d05f22b), safe settings.json writes (918d30e), Escape/fold for pending approvals (6012900, 40e3ba8), auto-close delay + reopen (74984f2, ea244a7), full AskUserQuestion (52b1562)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:49:11 +03:00

154 lines
6.5 KiB
Swift

import AppKit
import SwiftUI
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem?
private(set) var islandController: IslandWindowController?
func applicationWillTerminate(_ notification: Notification) {
HotKeyCenter.shared.unregisterAll()
}
func applicationDidFinishLaunching(_ notification: Notification) {
// Ignore SIGPIPE — prevents crash when nb-hook closes socket before we write response
signal(SIGPIPE, SIG_IGN)
// Warm up Keychain cache on main thread BEFORE any poller or view touches it
_ = KeychainStore.shared
NSApp.setActivationPolicy(.accessory)
// One BroV at a time (launchd may start one while another is still quitting).
let me = ProcessInfo.processInfo.processIdentifier
if NSRunningApplication.runningApplications(withBundleIdentifier: Bundle.main.bundleIdentifier ?? "")
.contains(where: { $0.processIdentifier != me }) {
exit(0)
}
BroVLaunchAgent.ensure()
Self.pruneInbox()
setupMenuBarItem()
CodexUsageMonitor.shared.start()
AgentWatch.shared.start()
AgentAuth.shared.start()
setupIsland()
#if PHONE_LINK
CloudProbe.shared.startIfEnabled()
#endif
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
openIsland()
return true
}
/// Copies of dropped files (HookServer.supportDir/inbox) are kept 7 days, then removed.
private static func pruneInbox() {
let inbox = HookServer.supportDir.appendingPathComponent("inbox")
let fm = FileManager.default
guard let files = try? fm.contentsOfDirectory(at: inbox, includingPropertiesForKeys: [.contentModificationDateKey]) else { return }
let cutoff = Date().addingTimeInterval(-7 * 86400)
for f in files {
let d = (try? f.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantFuture
if d < cutoff { try? fm.removeItem(at: f) }
}
try? fm.setAttributes([.posixPermissions: 0o700], ofItemAtPath: inbox.path)
}
// MARK: - Menu bar
private func setupMenuBarItem() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
guard let button = statusItem?.button else { return }
button.image = NSImage(named: "MenuBarIcon") ?? NSImage(systemSymbolName: "circle.fill", accessibilityDescription: "BroV")
button.image?.size = NSSize(width: 24, height: 18)
button.image?.accessibilityDescription = "BroV"
button.image?.isTemplate = true
let menu = NSMenu()
menu.addItem(withTitle: "Открыть BroV", action: #selector(openIsland), keyEquivalent: "")
menu.addItem(.separator())
menu.addItem(withTitle: "Настройки…", action: #selector(openSettings), keyEquivalent: ",")
menu.addItem(.separator())
menu.addItem(withTitle: "Выйти", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
statusItem?.menu = menu
}
// MARK: - Actions
@objc private func openIsland() {
islandController?.fsm.openedExternally()
islandController?.expand(to: .overview)
}
private var settingsWindow: NSWindow?
@objc private func openSettingsFromNotification(_ notification: Notification) {
if let section = notification.object as? String {
UserDefaults.standard.set(section, forKey: "settingsSection")
}
openSettings()
}
@objc private func openSettings() {
// The island floats above every window; fold it away so it can't cover Settings.
if AppState.shared.mode == .expanded { islandController?.collapse() }
if let w = settingsWindow, w.isVisible {
placeBelowIsland(w)
w.makeKeyAndOrderFront(nil); NSApp.activate(ignoringOtherApps: true); return
}
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered, defer: false)
win.title = "Настройки — BroV"
let host = NSHostingView(rootView: SettingsView())
host.sizingOptions = [.minSize]
win.contentView = host
win.contentMinSize = NSSize(width: 640, height: 420)
win.isReleasedWhenClosed = false
placeBelowIsland(win)
settingsWindow = win
win.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
/// Centres the window horizontally and keeps its title bar clear of the island panel
/// (320 pt tall at the top of the notch screen), shrinking it to fit if needed.
private func placeBelowIsland(_ win: NSWindow) {
let screen = IslandWindowController.notchScreen() ?? NSScreen.main ?? win.screen
guard let screen else { win.center(); return }
let visible = screen.visibleFrame
let islandBottom = screen.frame.maxY - 320 - 12 // island panel height + margin
let top = min(visible.maxY, islandBottom)
var frame = win.frame
frame.size.height = min(frame.height, max(top - visible.minY - 12, win.minSize.height))
frame.origin.x = visible.midX - frame.width / 2
frame.origin.y = max(visible.minY + 12, top - frame.height)
win.setFrame(frame, display: true)
}
// MARK: - Island setup
private func setupIsland() {
islandController = IslandWindowController()
islandController?.showWindow(nil)
islandController?.fsm.launch()
HookServer.shared.start()
N8nPoller.shared.start()
VercelPoller.shared.start()
ResendPoller.shared.start()
GithubPoller.shared.start()
StripePoller.shared.start()
CalcomPoller.shared.start()
NotionPoller.shared.start()
NotificationCenter.default.addObserver(self, selector: #selector(openSettingsFromNotification(_:)),
name: .openFullSettings, object: nil)
// After the greeting ends, fly Mochi back to the desktop if it was there at last quit
NotificationCenter.default.addObserver(forName: .greetComplete, object: nil, queue: .main) { _ in
DesktopMochiController.shared.launchFlyIfNeeded()
}
#if !APPSTORE
_ = MusicController.shared
#endif
}
}