Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
308 lines
10 KiB
Swift
308 lines
10 KiB
Swift
import AppKit
|
|
import OpenClawKit
|
|
import OSLog
|
|
|
|
final class PairingAlertHostWindow: NSWindow {
|
|
override var canBecomeKey: Bool {
|
|
true
|
|
}
|
|
|
|
override var canBecomeMain: Bool {
|
|
true
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class PairingAlertState {
|
|
var activeAlert: NSAlert?
|
|
var activeRequestId: String?
|
|
var alertHostWindow: NSWindow?
|
|
}
|
|
|
|
@MainActor
|
|
enum PairingAlertSupport {
|
|
enum PairingResolution: String {
|
|
case approved
|
|
case rejected
|
|
}
|
|
|
|
struct ButtonTitles {
|
|
var approve: String = "Approve"
|
|
var postpone: String = "Not Now"
|
|
var reject: String = "Reject"
|
|
|
|
init(approve: String = "Approve", postpone: String = "Not Now", reject: String = "Reject") {
|
|
self.approve = approve
|
|
self.postpone = postpone
|
|
self.reject = reject
|
|
}
|
|
}
|
|
|
|
struct PairingResolvedEvent: Codable {
|
|
let requestId: String
|
|
let decision: String
|
|
let ts: Double
|
|
}
|
|
|
|
static func endActiveAlert(activeAlert: inout NSAlert?, activeRequestId: inout String?) {
|
|
guard let alert = activeAlert else { return }
|
|
if let parent = alert.window.sheetParent {
|
|
parent.endSheet(alert.window, returnCode: .abort)
|
|
}
|
|
activeAlert = nil
|
|
activeRequestId = nil
|
|
}
|
|
|
|
static func endActiveAlert(state: PairingAlertState) {
|
|
self.endActiveAlert(activeAlert: &state.activeAlert, activeRequestId: &state.activeRequestId)
|
|
}
|
|
|
|
static func requireAlertHostWindow(alertHostWindow: inout NSWindow?) -> NSWindow {
|
|
if let alertHostWindow {
|
|
return alertHostWindow
|
|
}
|
|
|
|
let window = PairingAlertHostWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 520, height: 1),
|
|
styleMask: [.borderless],
|
|
backing: .buffered,
|
|
defer: false)
|
|
window.title = ""
|
|
window.isReleasedWhenClosed = false
|
|
window.level = .floating
|
|
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
window.isOpaque = false
|
|
window.hasShadow = false
|
|
window.backgroundColor = .clear
|
|
window.ignoresMouseEvents = true
|
|
|
|
alertHostWindow = window
|
|
return window
|
|
}
|
|
|
|
static func configureDefaultPairingAlert(
|
|
_ alert: NSAlert,
|
|
messageText: String,
|
|
informativeText: String,
|
|
buttonTitles: ButtonTitles = ButtonTitles(),
|
|
accessoryView: NSView? = nil)
|
|
{
|
|
alert.alertStyle = .informational
|
|
alert.messageText = messageText
|
|
alert.informativeText = informativeText
|
|
alert.accessoryView = accessoryView
|
|
alert.addButton(withTitle: buttonTitles.approve)
|
|
alert.addButton(withTitle: buttonTitles.postpone)
|
|
alert.addButton(withTitle: buttonTitles.reject)
|
|
if #available(macOS 11.0, *), alert.buttons.indices.contains(2) {
|
|
alert.buttons[2].hasDestructiveAction = true
|
|
}
|
|
}
|
|
|
|
static func beginCenteredSheet(
|
|
alert: NSAlert,
|
|
hostWindow: NSWindow,
|
|
completionHandler: @escaping (NSApplication.ModalResponse) -> Void)
|
|
{
|
|
let sheetSize = alert.window.frame.size
|
|
if let screen = hostWindow.screen ?? NSScreen.main {
|
|
let bounds = screen.visibleFrame
|
|
let x = bounds.midX - (sheetSize.width / 2)
|
|
let sheetOriginY = bounds.midY - (sheetSize.height / 2)
|
|
let hostY = sheetOriginY + sheetSize.height - hostWindow.frame.height
|
|
hostWindow.setFrameOrigin(NSPoint(x: x, y: hostY))
|
|
} else {
|
|
hostWindow.center()
|
|
}
|
|
hostWindow.makeKeyAndOrderFront(nil)
|
|
alert.beginSheetModal(for: hostWindow, completionHandler: completionHandler)
|
|
}
|
|
|
|
static func runPairingPushTask(
|
|
bufferingNewest: Int = 200,
|
|
loadPending: @escaping @MainActor () async -> Void,
|
|
handlePush: @escaping @MainActor (GatewayPush) -> Void) async
|
|
{
|
|
_ = try? await GatewayConnection.shared.refresh()
|
|
await loadPending()
|
|
await GatewayPushSubscription.consume(bufferingNewest: bufferingNewest, onPush: handlePush)
|
|
}
|
|
|
|
static func startPairingPushTask(
|
|
task: inout Task<Void, Never>?,
|
|
isStopping: inout Bool,
|
|
bufferingNewest: Int = 200,
|
|
loadPending: @escaping @MainActor () async -> Void,
|
|
handlePush: @escaping @MainActor (GatewayPush) -> Void)
|
|
{
|
|
guard task == nil else { return }
|
|
isStopping = false
|
|
task = Task {
|
|
await self.runPairingPushTask(
|
|
bufferingNewest: bufferingNewest,
|
|
loadPending: loadPending,
|
|
handlePush: handlePush)
|
|
}
|
|
}
|
|
|
|
static func beginPairingAlert(
|
|
messageText: String,
|
|
informativeText: String,
|
|
buttonTitles: ButtonTitles = ButtonTitles(),
|
|
accessoryView: NSView? = nil,
|
|
alertHostWindow: inout NSWindow?,
|
|
completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void) -> NSAlert
|
|
{
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
|
let alert = NSAlert()
|
|
self.configureDefaultPairingAlert(
|
|
alert,
|
|
messageText: messageText,
|
|
informativeText: informativeText,
|
|
buttonTitles: buttonTitles,
|
|
accessoryView: accessoryView)
|
|
|
|
let hostWindow = self.requireAlertHostWindow(alertHostWindow: &alertHostWindow)
|
|
self.beginCenteredSheet(alert: alert, hostWindow: hostWindow) { response in
|
|
completion(response, hostWindow)
|
|
}
|
|
return alert
|
|
}
|
|
|
|
static func presentPairingAlert(
|
|
requestId: String,
|
|
messageText: String,
|
|
informativeText: String,
|
|
buttonTitles: ButtonTitles = ButtonTitles(),
|
|
accessoryView: NSView? = nil,
|
|
activeAlert: inout NSAlert?,
|
|
activeRequestId: inout String?,
|
|
alertHostWindow: inout NSWindow?,
|
|
completion: @escaping (NSApplication.ModalResponse, NSWindow) -> Void)
|
|
{
|
|
activeRequestId = requestId
|
|
activeAlert = self.beginPairingAlert(
|
|
messageText: messageText,
|
|
informativeText: informativeText,
|
|
buttonTitles: buttonTitles,
|
|
accessoryView: accessoryView,
|
|
alertHostWindow: &alertHostWindow,
|
|
completion: completion)
|
|
}
|
|
|
|
static func presentPairingAlert<Request>(
|
|
request: Request,
|
|
requestId: String,
|
|
messageText: String,
|
|
informativeText: String,
|
|
buttonTitles: ButtonTitles = ButtonTitles(),
|
|
accessoryView: NSView? = nil,
|
|
state: PairingAlertState,
|
|
onResponse: @escaping @MainActor (NSApplication.ModalResponse, Request) async -> Void)
|
|
{
|
|
self.presentPairingAlert(
|
|
requestId: requestId,
|
|
messageText: messageText,
|
|
informativeText: informativeText,
|
|
buttonTitles: buttonTitles,
|
|
accessoryView: accessoryView,
|
|
activeAlert: &state.activeAlert,
|
|
activeRequestId: &state.activeRequestId,
|
|
alertHostWindow: &state.alertHostWindow,
|
|
completion: { response, hostWindow in
|
|
Task { @MainActor in
|
|
self.clearActivePairingAlert(state: state, hostWindow: hostWindow)
|
|
await onResponse(response, request)
|
|
}
|
|
})
|
|
}
|
|
|
|
static func clearActivePairingAlert(
|
|
activeAlert: inout NSAlert?,
|
|
activeRequestId: inout String?,
|
|
hostWindow: NSWindow)
|
|
{
|
|
activeRequestId = nil
|
|
activeAlert = nil
|
|
hostWindow.orderOut(nil)
|
|
}
|
|
|
|
static func clearActivePairingAlert(state: PairingAlertState, hostWindow: NSWindow) {
|
|
self.clearActivePairingAlert(
|
|
activeAlert: &state.activeAlert,
|
|
activeRequestId: &state.activeRequestId,
|
|
hostWindow: hostWindow)
|
|
}
|
|
|
|
static func stopPairingPrompter(
|
|
isStopping: inout Bool,
|
|
activeAlert: inout NSAlert?,
|
|
activeRequestId: inout String?,
|
|
task: inout Task<Void, Never>?,
|
|
queue: inout [some Any],
|
|
isPresenting: inout Bool,
|
|
alertHostWindow: inout NSWindow?)
|
|
{
|
|
isStopping = true
|
|
self.endActiveAlert(activeAlert: &activeAlert, activeRequestId: &activeRequestId)
|
|
task?.cancel()
|
|
task = nil
|
|
queue.removeAll(keepingCapacity: false)
|
|
isPresenting = false
|
|
activeRequestId = nil
|
|
alertHostWindow?.orderOut(nil)
|
|
alertHostWindow?.close()
|
|
alertHostWindow = nil
|
|
}
|
|
|
|
static func stopPairingPrompter(
|
|
isStopping: inout Bool,
|
|
task: inout Task<Void, Never>?,
|
|
queue: inout [some Any],
|
|
isPresenting: inout Bool,
|
|
state: PairingAlertState)
|
|
{
|
|
self.stopPairingPrompter(
|
|
isStopping: &isStopping,
|
|
activeAlert: &state.activeAlert,
|
|
activeRequestId: &state.activeRequestId,
|
|
task: &task,
|
|
queue: &queue,
|
|
isPresenting: &isPresenting,
|
|
alertHostWindow: &state.alertHostWindow)
|
|
}
|
|
|
|
static func approveRequest(
|
|
requestId: String,
|
|
kind: String,
|
|
logger: Logger,
|
|
action: @escaping () async throws -> Void) async -> Bool
|
|
{
|
|
do {
|
|
try await action()
|
|
logger.info("approved \(kind, privacy: .public) pairing requestId=\(requestId, privacy: .public)")
|
|
return true
|
|
} catch {
|
|
logger.error("approve failed requestId=\(requestId, privacy: .public)")
|
|
logger.error("approve failed: \(error.localizedDescription, privacy: .public)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
static func rejectRequest(
|
|
requestId: String,
|
|
kind: String,
|
|
logger: Logger,
|
|
action: @escaping () async throws -> Void) async
|
|
{
|
|
do {
|
|
try await action()
|
|
logger.info("rejected \(kind, privacy: .public) pairing requestId=\(requestId, privacy: .public)")
|
|
} catch {
|
|
logger.error("reject failed requestId=\(requestId, privacy: .public)")
|
|
logger.error("reject failed: \(error.localizedDescription, privacy: .public)")
|
|
}
|
|
}
|
|
}
|