Files
openclaw/apps/macos/Sources/OpenClawIPC/IPC.swift

417 lines
17 KiB
Swift
Raw Normal View History

import CoreGraphics
2025-12-05 23:13:53 +01:00
import Foundation
// MARK: - Capabilities
public enum Capability: String, Codable, CaseIterable, Sendable {
2025-12-07 02:34:21 +01:00
/// AppleScript / Automation access to control other apps (TCC Automation).
case appleScript
2025-12-05 23:13:53 +01:00
case notifications
case accessibility
case screenRecording
case microphone
case speechRecognition
case camera
2026-01-04 00:54:44 +01:00
case location
2025-12-05 23:13:53 +01:00
}
2025-12-14 00:48:58 +00:00
public enum CameraFacing: String, Codable, Sendable {
case front
case back
}
2025-12-05 23:13:53 +01:00
// MARK: - Requests
/// Notification interruption level (maps to UNNotificationInterruptionLevel)
public enum NotificationPriority: String, Codable, Sendable {
2025-12-12 21:18:46 +00:00
case passive // silent, no wake
case active // default
case timeSensitive // breaks through Focus modes
}
/// Notification delivery mechanism.
public enum NotificationDelivery: String, Codable, Sendable {
/// Use macOS notification center (UNUserNotificationCenter).
case system
/// Use an in-app overlay/toast (no Notification Center history).
case overlay
/// Prefer system; fall back to overlay when system isn't available.
case auto
}
// MARK: - Canvas geometry
/// Optional placement hints for the Canvas panel.
/// Values are in screen coordinates (same as `NSWindow` frame).
public struct CanvasPlacement: Codable, Sendable {
public var x: Double?
public var y: Double?
public var width: Double?
public var height: Double?
public init(x: Double? = nil, y: Double? = nil, width: Double? = nil, height: Double? = nil) {
self.x = x
self.y = y
self.width = width
self.height = height
}
}
2025-12-17 10:37:35 +01:00
// MARK: - Canvas show result
public enum CanvasShowStatus: String, Codable, Sendable {
/// Panel was shown, but no navigation occurred (no target passed and session already existed).
case shown
2025-12-17 11:35:06 +01:00
/// Target was a direct URL (http(s) or file).
2025-12-17 10:37:35 +01:00
case web
/// Local canvas target resolved to an existing file.
case ok
/// Local canvas target did not resolve to a file (404 page).
case notFound
/// Local scaffold fallback (e.g., no index.html present).
2025-12-17 10:37:35 +01:00
case welcome
}
public struct CanvasShowResult: Codable, Sendable {
2026-01-30 03:15:10 +01:00
/// Session directory on disk (e.g. `~/Library/Application Support/OpenClaw/canvas/<session>/`).
2025-12-17 10:37:35 +01:00
public var directory: String
/// Target as provided by the caller (may be nil/empty).
public var target: String?
/// Target actually navigated to (nil when no navigation occurred; defaults to "/" for a newly created session).
public var effectiveTarget: String?
public var status: CanvasShowStatus
/// URL that was loaded (nil when no navigation occurred).
public var url: String?
public init(
directory: String,
target: String?,
effectiveTarget: String?,
status: CanvasShowStatus,
url: String?)
{
self.directory = directory
self.target = target
self.effectiveTarget = effectiveTarget
self.status = status
self.url = url
}
}
2025-12-17 11:35:06 +01:00
// MARK: - Canvas A2UI
public enum CanvasA2UICommand: String, Codable, Sendable {
case pushJSONL
case reset
}
2025-12-05 23:13:53 +01:00
public enum Request: Sendable {
case notify(
2026-01-15 14:40:57 +00:00
title: String,
body: String,
sound: String?,
priority: NotificationPriority?,
delivery: NotificationDelivery?)
2025-12-05 23:13:53 +01:00
case ensurePermissions([Capability], interactive: Bool)
case runShell(
2026-01-15 14:40:57 +00:00
command: [String],
cwd: String?,
env: [String: String]?,
timeoutSec: Double?,
needsScreenRecording: Bool)
2025-12-05 23:13:53 +01:00
case status
case agent(message: String, thinking: String?, session: String?, deliver: Bool, to: String?)
case rpcStatus
case canvasPresent(session: String, path: String?, placement: CanvasPlacement?)
case canvasHide(session: String)
case canvasEval(session: String, javaScript: String)
case canvasSnapshot(session: String, outPath: String?)
2025-12-17 11:35:06 +01:00
case canvasA2UI(session: String, command: CanvasA2UICommand, jsonl: String?)
2025-12-12 21:18:46 +00:00
case nodeList
case nodeDescribe(nodeId: String)
2025-12-12 21:18:46 +00:00
case nodeInvoke(nodeId: String, command: String, paramsJSON: String?)
2025-12-14 00:48:58 +00:00
case cameraSnap(facing: CameraFacing?, maxWidth: Int?, quality: Double?, outPath: String?)
case cameraClip(facing: CameraFacing?, durationMs: Int?, includeAudio: Bool, outPath: String?)
2025-12-19 03:16:25 +01:00
case screenRecord(screenIndex: Int?, durationMs: Int?, fps: Double?, includeAudio: Bool, outPath: String?)
2025-12-05 23:13:53 +01:00
}
// MARK: - Responses
public struct Response: Codable, Sendable {
public var ok: Bool
public var message: String?
/// Optional payload (PNG bytes, stdout text, etc.).
public var payload: Data?
public init(ok: Bool, message: String? = nil, payload: Data? = nil) {
self.ok = ok
self.message = message
self.payload = payload
}
}
// MARK: - Codable conformance for Request
extension Request: Codable {
private enum CodingKeys: String, CodingKey {
case type
case title, body, sound, priority, delivery
2025-12-05 23:13:53 +01:00
case caps, interactive
case command, cwd, env, timeoutSec, needsScreenRecording
case message, thinking, session, deliver, to
case rpcStatus
case path
case javaScript
case outPath
case screenIndex
case fps
2025-12-17 11:35:06 +01:00
case canvasA2UICommand
case jsonl
2025-12-14 00:48:58 +00:00
case facing
case maxWidth
case quality
case durationMs
case includeAudio
case placement
2025-12-12 21:18:46 +00:00
case nodeId
case nodeCommand
case paramsJSON
2025-12-05 23:13:53 +01:00
}
private enum Kind: String, Codable {
case notify
case ensurePermissions
case runShell
case status
case agent
case rpcStatus
case canvasPresent
case canvasHide
case canvasEval
case canvasSnapshot
2025-12-17 11:35:06 +01:00
case canvasA2UI
2025-12-12 21:18:46 +00:00
case nodeList
case nodeDescribe
2025-12-12 21:18:46 +00:00
case nodeInvoke
2025-12-14 00:48:58 +00:00
case cameraSnap
case cameraClip
case screenRecord
2025-12-05 23:13:53 +01:00
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .notify(title, body, sound, priority, delivery):
2025-12-05 23:13:53 +01:00
try container.encode(Kind.notify, forKey: .type)
try container.encode(title, forKey: .title)
try container.encode(body, forKey: .body)
try container.encodeIfPresent(sound, forKey: .sound)
try container.encodeIfPresent(priority, forKey: .priority)
try container.encodeIfPresent(delivery, forKey: .delivery)
2025-12-06 04:03:48 +01:00
2025-12-05 23:13:53 +01:00
case let .ensurePermissions(caps, interactive):
try container.encode(Kind.ensurePermissions, forKey: .type)
try container.encode(caps, forKey: .caps)
try container.encode(interactive, forKey: .interactive)
2025-12-06 04:03:48 +01:00
2025-12-05 23:13:53 +01:00
case let .runShell(command, cwd, env, timeoutSec, needsSR):
try container.encode(Kind.runShell, forKey: .type)
try container.encode(command, forKey: .command)
try container.encodeIfPresent(cwd, forKey: .cwd)
try container.encodeIfPresent(env, forKey: .env)
try container.encodeIfPresent(timeoutSec, forKey: .timeoutSec)
try container.encode(needsSR, forKey: .needsScreenRecording)
2025-12-06 04:03:48 +01:00
2025-12-05 23:13:53 +01:00
case .status:
try container.encode(Kind.status, forKey: .type)
case let .agent(message, thinking, session, deliver, to):
try container.encode(Kind.agent, forKey: .type)
try container.encode(message, forKey: .message)
try container.encodeIfPresent(thinking, forKey: .thinking)
try container.encodeIfPresent(session, forKey: .session)
try container.encode(deliver, forKey: .deliver)
try container.encodeIfPresent(to, forKey: .to)
case .rpcStatus:
try container.encode(Kind.rpcStatus, forKey: .type)
case let .canvasPresent(session, path, placement):
try container.encode(Kind.canvasPresent, forKey: .type)
try container.encode(session, forKey: .session)
try container.encodeIfPresent(path, forKey: .path)
try container.encodeIfPresent(placement, forKey: .placement)
case let .canvasHide(session):
try container.encode(Kind.canvasHide, forKey: .type)
try container.encode(session, forKey: .session)
case let .canvasEval(session, javaScript):
try container.encode(Kind.canvasEval, forKey: .type)
try container.encode(session, forKey: .session)
try container.encode(javaScript, forKey: .javaScript)
case let .canvasSnapshot(session, outPath):
try container.encode(Kind.canvasSnapshot, forKey: .type)
try container.encode(session, forKey: .session)
try container.encodeIfPresent(outPath, forKey: .outPath)
2025-12-12 21:18:46 +00:00
2025-12-17 11:35:06 +01:00
case let .canvasA2UI(session, command, jsonl):
try container.encode(Kind.canvasA2UI, forKey: .type)
try container.encode(session, forKey: .session)
try container.encode(command, forKey: .canvasA2UICommand)
try container.encodeIfPresent(jsonl, forKey: .jsonl)
2025-12-12 21:18:46 +00:00
case .nodeList:
try container.encode(Kind.nodeList, forKey: .type)
case let .nodeDescribe(nodeId):
try container.encode(Kind.nodeDescribe, forKey: .type)
try container.encode(nodeId, forKey: .nodeId)
2025-12-12 21:18:46 +00:00
case let .nodeInvoke(nodeId, command, paramsJSON):
try container.encode(Kind.nodeInvoke, forKey: .type)
try container.encode(nodeId, forKey: .nodeId)
try container.encode(command, forKey: .nodeCommand)
try container.encodeIfPresent(paramsJSON, forKey: .paramsJSON)
2025-12-14 00:48:58 +00:00
case let .cameraSnap(facing, maxWidth, quality, outPath):
try container.encode(Kind.cameraSnap, forKey: .type)
try container.encodeIfPresent(facing, forKey: .facing)
try container.encodeIfPresent(maxWidth, forKey: .maxWidth)
try container.encodeIfPresent(quality, forKey: .quality)
try container.encodeIfPresent(outPath, forKey: .outPath)
case let .cameraClip(facing, durationMs, includeAudio, outPath):
try container.encode(Kind.cameraClip, forKey: .type)
try container.encodeIfPresent(facing, forKey: .facing)
try container.encodeIfPresent(durationMs, forKey: .durationMs)
try container.encode(includeAudio, forKey: .includeAudio)
try container.encodeIfPresent(outPath, forKey: .outPath)
2025-12-19 03:16:25 +01:00
case let .screenRecord(screenIndex, durationMs, fps, includeAudio, outPath):
try container.encode(Kind.screenRecord, forKey: .type)
try container.encodeIfPresent(screenIndex, forKey: .screenIndex)
try container.encodeIfPresent(durationMs, forKey: .durationMs)
try container.encodeIfPresent(fps, forKey: .fps)
2025-12-19 03:16:25 +01:00
try container.encode(includeAudio, forKey: .includeAudio)
try container.encodeIfPresent(outPath, forKey: .outPath)
2025-12-05 23:13:53 +01:00
}
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let kind = try container.decode(Kind.self, forKey: .type)
switch kind {
case .notify:
let title = try container.decode(String.self, forKey: .title)
let body = try container.decode(String.self, forKey: .body)
let sound = try container.decodeIfPresent(String.self, forKey: .sound)
let priority = try container.decodeIfPresent(NotificationPriority.self, forKey: .priority)
let delivery = try container.decodeIfPresent(NotificationDelivery.self, forKey: .delivery)
self = .notify(title: title, body: body, sound: sound, priority: priority, delivery: delivery)
2025-12-06 04:03:48 +01:00
2025-12-05 23:13:53 +01:00
case .ensurePermissions:
let caps = try container.decode([Capability].self, forKey: .caps)
let interactive = try container.decode(Bool.self, forKey: .interactive)
self = .ensurePermissions(caps, interactive: interactive)
2025-12-06 04:03:48 +01:00
2025-12-05 23:13:53 +01:00
case .runShell:
let command = try container.decode([String].self, forKey: .command)
let cwd = try container.decodeIfPresent(String.self, forKey: .cwd)
let env = try container.decodeIfPresent([String: String].self, forKey: .env)
let timeout = try container.decodeIfPresent(Double.self, forKey: .timeoutSec)
let needsSR = try container.decode(Bool.self, forKey: .needsScreenRecording)
self = .runShell(command: command, cwd: cwd, env: env, timeoutSec: timeout, needsScreenRecording: needsSR)
2025-12-06 04:03:48 +01:00
2025-12-05 23:13:53 +01:00
case .status:
self = .status
case .agent:
let message = try container.decode(String.self, forKey: .message)
let thinking = try container.decodeIfPresent(String.self, forKey: .thinking)
let session = try container.decodeIfPresent(String.self, forKey: .session)
let deliver = try container.decode(Bool.self, forKey: .deliver)
let to = try container.decodeIfPresent(String.self, forKey: .to)
self = .agent(message: message, thinking: thinking, session: session, deliver: deliver, to: to)
case .rpcStatus:
self = .rpcStatus
case .canvasPresent:
let session = try container.decode(String.self, forKey: .session)
let path = try container.decodeIfPresent(String.self, forKey: .path)
let placement = try container.decodeIfPresent(CanvasPlacement.self, forKey: .placement)
self = .canvasPresent(session: session, path: path, placement: placement)
case .canvasHide:
let session = try container.decode(String.self, forKey: .session)
self = .canvasHide(session: session)
case .canvasEval:
let session = try container.decode(String.self, forKey: .session)
let javaScript = try container.decode(String.self, forKey: .javaScript)
self = .canvasEval(session: session, javaScript: javaScript)
case .canvasSnapshot:
let session = try container.decode(String.self, forKey: .session)
let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
self = .canvasSnapshot(session: session, outPath: outPath)
2025-12-12 21:18:46 +00:00
2025-12-17 11:35:06 +01:00
case .canvasA2UI:
let session = try container.decode(String.self, forKey: .session)
let command = try container.decode(CanvasA2UICommand.self, forKey: .canvasA2UICommand)
let jsonl = try container.decodeIfPresent(String.self, forKey: .jsonl)
self = .canvasA2UI(session: session, command: command, jsonl: jsonl)
2025-12-12 21:18:46 +00:00
case .nodeList:
self = .nodeList
case .nodeDescribe:
let nodeId = try container.decode(String.self, forKey: .nodeId)
self = .nodeDescribe(nodeId: nodeId)
2025-12-12 21:18:46 +00:00
case .nodeInvoke:
let nodeId = try container.decode(String.self, forKey: .nodeId)
let command = try container.decode(String.self, forKey: .nodeCommand)
let paramsJSON = try container.decodeIfPresent(String.self, forKey: .paramsJSON)
self = .nodeInvoke(nodeId: nodeId, command: command, paramsJSON: paramsJSON)
2025-12-14 00:48:58 +00:00
case .cameraSnap:
let facing = try container.decodeIfPresent(CameraFacing.self, forKey: .facing)
let maxWidth = try container.decodeIfPresent(Int.self, forKey: .maxWidth)
let quality = try container.decodeIfPresent(Double.self, forKey: .quality)
let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
self = .cameraSnap(facing: facing, maxWidth: maxWidth, quality: quality, outPath: outPath)
case .cameraClip:
let facing = try container.decodeIfPresent(CameraFacing.self, forKey: .facing)
let durationMs = try container.decodeIfPresent(Int.self, forKey: .durationMs)
let includeAudio = (try? container.decode(Bool.self, forKey: .includeAudio)) ?? true
let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
self = .cameraClip(facing: facing, durationMs: durationMs, includeAudio: includeAudio, outPath: outPath)
case .screenRecord:
let screenIndex = try container.decodeIfPresent(Int.self, forKey: .screenIndex)
let durationMs = try container.decodeIfPresent(Int.self, forKey: .durationMs)
let fps = try container.decodeIfPresent(Double.self, forKey: .fps)
2025-12-19 03:16:25 +01:00
let includeAudio = (try? container.decode(Bool.self, forKey: .includeAudio)) ?? true
let outPath = try container.decodeIfPresent(String.self, forKey: .outPath)
2025-12-19 03:16:25 +01:00
self = .screenRecord(
screenIndex: screenIndex,
durationMs: durationMs,
fps: fps,
includeAudio: includeAudio,
outPath: outPath)
2025-12-05 23:13:53 +01:00
}
}
}
/// Shared transport settings
2026-01-30 03:15:10 +01:00
public let controlSocketPath: String = {
let home = FileManager().homeDirectoryForCurrentUser
return home
2026-01-30 03:15:10 +01:00
.appendingPathComponent("Library/Application Support/OpenClaw/control.sock")
.path
}()