Vendor OpenClaw source as Adolf fork baseline
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
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,152 @@
import CoreLocation
import Foundation
import OpenClawKit
import UIKit
typealias OpenClawCameraSnapResult = (format: String, base64: String, width: Int, height: Int)
typealias OpenClawCameraClipResult = (format: String, base64: String, durationMs: Int, hasAudio: Bool)
protocol CameraServicing: Sendable {
func listDevices() async -> [CameraController.CameraDeviceInfo]
func snap(params: OpenClawCameraSnapParams) async throws -> OpenClawCameraSnapResult
func clip(params: OpenClawCameraClipParams) async throws -> OpenClawCameraClipResult
}
protocol ScreenRecordingServicing: Sendable {
func record(
screenIndex: Int?,
durationMs: Int?,
fps: Double?,
includeAudio: Bool?,
outPath: String?) async throws -> String
}
@MainActor
protocol LocationServicing: Sendable {
func authorizationStatus() -> CLAuthorizationStatus
func accuracyAuthorization() -> CLAccuracyAuthorization
func ensureAuthorization(mode: OpenClawLocationMode) async -> CLAuthorizationStatus
func currentLocation(
params: OpenClawLocationGetParams,
desiredAccuracy: OpenClawLocationAccuracy,
maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation
func setBackgroundLocationUpdatesEnabled(_ enabled: Bool)
func setAuthorizationChangeHandler(
_ handler: @escaping @MainActor @Sendable (CLAuthorizationStatus) -> Void)
func startMonitoringSignificantLocationChanges(onUpdate: @escaping @Sendable (CLLocation) -> Void)
func stopMonitoringSignificantLocationChanges()
}
@MainActor
protocol DeviceStatusServicing: Sendable {
func status() async throws -> OpenClawDeviceStatusPayload
func info() -> OpenClawDeviceInfoPayload
}
protocol PhotosServicing: Sendable {
func latest(params: OpenClawPhotosLatestParams) async throws -> OpenClawPhotosLatestPayload
}
protocol ContactsServicing: Sendable {
func search(params: OpenClawContactsSearchParams) async throws -> OpenClawContactsSearchPayload
func add(params: OpenClawContactsAddParams) async throws -> OpenClawContactsAddPayload
}
protocol CalendarServicing: Sendable {
func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload
func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload
}
protocol RemindersServicing: Sendable {
func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload
func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload
}
protocol MotionServicing: Sendable {
func activities(params: OpenClawMotionActivityParams) async throws -> OpenClawMotionActivityPayload
func pedometer(params: OpenClawPedometerParams) async throws -> OpenClawPedometerPayload
}
struct WatchMessagingStatus: Equatable {
var supported: Bool
var paired: Bool
var appInstalled: Bool
var reachable: Bool
var activationState: String
}
struct WatchQuickReplyEvent: Equatable {
var replyId: String
var promptId: String
var actionId: String
var actionLabel: String?
var sessionKey: String?
var note: String?
var sentAtMs: Int?
var transport: String
}
struct WatchExecApprovalResolveEvent: Equatable {
var replyId: String
var approvalId: String
var decision: OpenClawWatchExecApprovalDecision
var sentAtMs: Int?
var transport: String
}
struct WatchExecApprovalSnapshotRequestEvent: Equatable {
var requestId: String
var sentAtMs: Int?
var transport: String
}
struct WatchAppSnapshotRequestEvent: Equatable {
var requestId: String
var sentAtMs: Int?
var transport: String
}
struct WatchAppCommandEvent: Codable, Equatable {
var commandId: String
var command: OpenClawWatchAppCommand
var sessionKey: String?
var gatewayStableID: String?
var text: String?
var sentAtMs: Int?
var transport: String
}
struct WatchNotificationSendResult: Equatable {
var deliveredImmediately: Bool
var queuedForDelivery: Bool
var transport: String
}
protocol WatchMessagingServicing: AnyObject, Sendable {
func status() async -> WatchMessagingStatus
func setStatusHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?)
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?)
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?)
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
func setAppSnapshotRequestHandler(_ handler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?)
func setAppCommandHandler(_ handler: (@Sendable (WatchAppCommandEvent) -> Void)?)
func sendNotification(
id: String,
params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult
func sendExecApprovalPrompt(
_ message: OpenClawWatchExecApprovalPromptMessage) async throws -> WatchNotificationSendResult
func sendExecApprovalResolved(
_ message: OpenClawWatchExecApprovalResolvedMessage) async throws -> WatchNotificationSendResult
func sendExecApprovalExpired(
_ message: OpenClawWatchExecApprovalExpiredMessage) async throws -> WatchNotificationSendResult
func syncExecApprovalSnapshot(
_ message: OpenClawWatchExecApprovalSnapshotMessage) async throws -> WatchNotificationSendResult
func syncAppSnapshot(
_ message: OpenClawWatchAppSnapshotMessage) async throws -> WatchNotificationSendResult
}
extension CameraController: CameraServicing {}
extension ScreenRecordService: ScreenRecordingServicing {}
extension LocationService: LocationServicing {}

View File

@@ -0,0 +1,84 @@
import Foundation
import UserNotifications
struct NotificationSnapshot: @unchecked Sendable {
let identifier: String
let userInfo: [AnyHashable: Any]
}
enum NotificationAuthorizationStatus {
case notDetermined
case denied
case authorized
case provisional
case ephemeral
}
protocol NotificationCentering: Sendable {
func authorizationStatus() async -> NotificationAuthorizationStatus
func add(_ request: UNNotificationRequest) async throws
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async
func deliveredNotifications() async -> [NotificationSnapshot]
}
struct LiveNotificationCenter: NotificationCentering, @unchecked Sendable {
private let center: UNUserNotificationCenter
init(center: UNUserNotificationCenter = .current()) {
self.center = center
}
func authorizationStatus() async -> NotificationAuthorizationStatus {
let settings = await self.center.notificationSettings()
return switch settings.authorizationStatus {
case .authorized:
.authorized
case .provisional:
.provisional
case .ephemeral:
.ephemeral
case .denied:
.denied
case .notDetermined:
.notDetermined
@unknown default:
.denied
}
}
func add(_ request: UNNotificationRequest) async throws {
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
self.center.add(request) { error in
if let error {
cont.resume(throwing: error)
} else {
cont.resume(returning: ())
}
}
}
}
func removePendingNotificationRequests(withIdentifiers identifiers: [String]) async {
guard !identifiers.isEmpty else { return }
self.center.removePendingNotificationRequests(withIdentifiers: identifiers)
}
func removeDeliveredNotifications(withIdentifiers identifiers: [String]) async {
guard !identifiers.isEmpty else { return }
self.center.removeDeliveredNotifications(withIdentifiers: identifiers)
}
func deliveredNotifications() async -> [NotificationSnapshot] {
await withCheckedContinuation { continuation in
self.center.getDeliveredNotifications { notifications in
continuation.resume(
returning: notifications.map { notification in
NotificationSnapshot(
identifier: notification.request.identifier,
userInfo: notification.request.content.userInfo)
})
}
}
}
}

View File

@@ -0,0 +1,437 @@
import Foundation
import OSLog
@preconcurrency import WatchConnectivity
private struct WatchConnectivityTransportCallbacks {
var statusUpdateHandler: (@Sendable (WatchMessagingStatus) -> Void)?
var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
var execApprovalSnapshotRequestHandler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
var appSnapshotRequestHandler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?
var appCommandHandler: (@Sendable (WatchAppCommandEvent) -> Void)?
}
private func sendReachableWatchMessage(_ payload: [String: Any], with session: WCSession) async throws {
// WatchConnectivity replies arrive on its own queue. Keep this continuation explicitly
// nonisolated so Swift 6 does not inherit a caller actor (for example MainActor) into the
// Objective-C callback boundary and trap on the reply callback executor check.
try await withCheckedThrowingContinuation(isolation: nil) { (continuation: CheckedContinuation<Void, Error>) in
session.sendMessage(
payload,
replyHandler: { _ in
continuation.resume(returning: ())
},
errorHandler: { error in
continuation.resume(throwing: error)
})
}
}
final class WatchConnectivityTransport: NSObject, @unchecked Sendable {
private nonisolated static let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "watch.messaging")
private let session: WCSession?
private let callbacksLock = NSLock()
private var callbacks = WatchConnectivityTransportCallbacks()
override init() {
if WCSession.isSupported() {
self.session = WCSession.default
} else {
self.session = nil
}
super.init()
if let session = self.session {
session.delegate = self
session.activate()
}
}
nonisolated static func isSupportedOnDevice() -> Bool {
WCSession.isSupported()
}
nonisolated static func currentStatusSnapshot() -> WatchMessagingStatus {
guard WCSession.isSupported() else {
return WatchMessagingStatus(
supported: false,
paired: false,
appInstalled: false,
reachable: false,
activationState: "unsupported")
}
return self.status(for: WCSession.default)
}
func status() async -> WatchMessagingStatus {
await self.ensureActivated()
return self.currentStatusSnapshot()
}
func currentStatusSnapshot() -> WatchMessagingStatus {
guard let session = self.session else {
return WatchMessagingStatus(
supported: false,
paired: false,
appInstalled: false,
reachable: false,
activationState: "unsupported")
}
return Self.status(for: session)
}
func setStatusUpdateHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?) {
self.updateCallbacks { $0.statusUpdateHandler = handler }
}
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?) {
self.updateCallbacks { $0.replyHandler = handler }
}
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?) {
self.updateCallbacks { $0.execApprovalResolveHandler = handler }
}
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
{
self.updateCallbacks { $0.execApprovalSnapshotRequestHandler = handler }
}
func setAppSnapshotRequestHandler(_ handler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?) {
self.updateCallbacks { $0.appSnapshotRequestHandler = handler }
}
func setAppCommandHandler(_ handler: (@Sendable (WatchAppCommandEvent) -> Void)?) {
self.updateCallbacks { $0.appCommandHandler = handler }
}
func sendPayload(_ payload: [String: Any]) async throws -> WatchNotificationSendResult {
await self.ensureActivated()
let session = try self.requireReadySession()
if session.isReachable {
do {
try await sendReachableWatchMessage(payload, with: session)
return WatchNotificationSendResult(
deliveredImmediately: true,
queuedForDelivery: false,
transport: "sendMessage")
} catch {
Self.logger.error("watch sendMessage failed: \(error.localizedDescription, privacy: .public)")
}
}
_ = session.transferUserInfo(payload)
return WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "transferUserInfo")
}
func sendSnapshotPayload(_ payload: [String: Any]) async throws -> WatchNotificationSendResult {
await self.ensureActivated()
let session = try self.requireReadySession()
if session.isReachable {
do {
try await sendReachableWatchMessage(payload, with: session)
return WatchNotificationSendResult(
deliveredImmediately: true,
queuedForDelivery: false,
transport: "sendMessage")
} catch {
Self.logger.error(
"watch snapshot sendMessage failed: \(error.localizedDescription, privacy: .public)")
}
}
do {
try session.updateApplicationContext(payload)
return WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "applicationContext")
} catch {
Self.logger.error(
"watch updateApplicationContext failed: \(error.localizedDescription, privacy: .public)")
_ = session.transferUserInfo(payload)
return WatchNotificationSendResult(
deliveredImmediately: false,
queuedForDelivery: true,
transport: "transferUserInfo")
}
}
private func updateCallbacks(_ update: (inout WatchConnectivityTransportCallbacks) -> Void) {
self.callbacksLock.lock()
defer { self.callbacksLock.unlock() }
update(&self.callbacks)
}
private func callbacksSnapshot() -> WatchConnectivityTransportCallbacks {
self.callbacksLock.lock()
defer { self.callbacksLock.unlock() }
return self.callbacks
}
private func requireReadySession() throws -> WCSession {
guard let session = self.session else {
throw WatchMessagingError.unsupported
}
let snapshot = Self.status(for: session)
guard snapshot.paired else {
throw WatchMessagingError.notPaired
}
guard snapshot.appInstalled else {
throw WatchMessagingError.watchAppNotInstalled
}
return session
}
private func ensureActivated() async {
guard let session = self.session else { return }
if session.activationState == .activated {
return
}
session.activate()
for _ in 0..<8 {
if session.activationState == .activated {
return
}
try? await Task.sleep(nanoseconds: 100_000_000)
}
}
private func emitStatusUpdate(_ snapshot: WatchMessagingStatus) {
guard let handler = self.callbacksSnapshot().statusUpdateHandler else {
return
}
Task { @MainActor in
handler(snapshot)
}
}
private func emitReply(_ event: WatchQuickReplyEvent) {
guard let handler = self.callbacksSnapshot().replyHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private func emitExecApprovalResolve(_ event: WatchExecApprovalResolveEvent) {
guard let handler = self.callbacksSnapshot().execApprovalResolveHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private func emitExecApprovalSnapshotRequest(_ event: WatchExecApprovalSnapshotRequestEvent) {
guard let handler = self.callbacksSnapshot().execApprovalSnapshotRequestHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private func emitAppSnapshotRequest(_ event: WatchAppSnapshotRequestEvent) {
guard let handler = self.callbacksSnapshot().appSnapshotRequestHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private func emitAppCommand(_ event: WatchAppCommandEvent) {
guard let handler = self.callbacksSnapshot().appCommandHandler else {
return
}
Task { @MainActor in
handler(event)
}
}
private nonisolated static func status(for session: WCSession) -> WatchMessagingStatus {
WatchMessagingStatus(
supported: true,
paired: session.isPaired,
appInstalled: session.isWatchAppInstalled,
reachable: session.isReachable,
activationState: self.activationStateLabel(session.activationState))
}
private nonisolated static func activationStateLabel(_ state: WCSessionActivationState) -> String {
switch state {
case .notActivated:
"notActivated"
case .inactive:
"inactive"
case .activated:
"activated"
@unknown default:
"unknown"
}
}
}
extension WatchConnectivityTransport: WCSessionDelegate {
func session(
_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: (any Error)?)
{
GatewayDiagnostics.log(
"watch messaging: activation complete "
+ "state=\(Self.activationStateLabel(activationState)) "
+ "error=\(error?.localizedDescription ?? "none")")
if let error {
Self.logger.error("watch activation failed: \(error.localizedDescription, privacy: .public)")
} else {
Self.logger.debug(
"watch activation state=\(Self.activationStateLabel(activationState), privacy: .public)")
}
self.emitStatusUpdate(Self.status(for: session))
}
func sessionDidBecomeInactive(_: WCSession) {}
func sessionDidDeactivate(_ session: WCSession) {
GatewayDiagnostics.log("watch messaging: session did deactivate; reactivating")
session.activate()
self.emitStatusUpdate(Self.status(for: session))
}
func session(_: WCSession, didReceiveMessage message: [String: Any]) {
let type = (message["type"] as? String) ?? "unknown"
GatewayDiagnostics.log("watch messaging: didReceiveMessage type=\(type)")
if let event = WatchMessagingPayloadCodec.parseQuickReplyPayload(message, transport: "sendMessage") {
self.emitReply(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalResolvePayload(
message,
transport: "sendMessage")
{
self.emitExecApprovalResolve(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalSnapshotRequestPayload(
message,
transport: "sendMessage")
{
self.emitExecApprovalSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppSnapshotRequestPayload(
message,
transport: "sendMessage")
{
self.emitAppSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppCommandPayload(
message,
transport: "sendMessage")
{
self.emitAppCommand(event)
}
}
func session(
_: WCSession,
didReceiveMessage message: [String: Any],
replyHandler: @escaping ([String: Any]) -> Void)
{
let type = (message["type"] as? String) ?? "unknown"
GatewayDiagnostics.log("watch messaging: didReceiveMessageWithReply type=\(type)")
if let event = WatchMessagingPayloadCodec.parseQuickReplyPayload(message, transport: "sendMessage") {
replyHandler(["ok": true])
self.emitReply(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalResolvePayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitExecApprovalResolve(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalSnapshotRequestPayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitExecApprovalSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppSnapshotRequestPayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitAppSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppCommandPayload(
message,
transport: "sendMessage")
{
replyHandler(["ok": true])
self.emitAppCommand(event)
return
}
replyHandler(["ok": false, "error": "unsupported_payload"])
}
func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
let type = (userInfo["type"] as? String) ?? "unknown"
GatewayDiagnostics.log("watch messaging: didReceiveUserInfo type=\(type)")
if let event = WatchMessagingPayloadCodec.parseQuickReplyPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitReply(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalResolvePayload(
userInfo,
transport: "transferUserInfo")
{
self.emitExecApprovalResolve(event)
return
}
if let event = WatchMessagingPayloadCodec.parseExecApprovalSnapshotRequestPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitExecApprovalSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppSnapshotRequestPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitAppSnapshotRequest(event)
return
}
if let event = WatchMessagingPayloadCodec.parseAppCommandPayload(
userInfo,
transport: "transferUserInfo")
{
self.emitAppCommand(event)
}
}
func sessionReachabilityDidChange(_ session: WCSession) {
GatewayDiagnostics.log(
"watch messaging: reachability changed "
+ "reachable=\(session.isReachable) paired=\(session.isPaired) "
+ "installed=\(session.isWatchAppInstalled)")
self.emitStatusUpdate(Self.status(for: session))
}
}

View File

@@ -0,0 +1,310 @@
import Foundation
import OpenClawKit
enum WatchMessagingPayloadCodec {
static func nowMs() -> Int {
Int(Date().timeIntervalSince1970 * 1000)
}
static func nonEmpty(_ value: String?) -> String? {
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? nil : trimmed
}
static func encodeNotificationPayload(
id: String,
params: OpenClawWatchNotifyParams) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.notify.rawValue,
"id": id,
"title": params.title,
"body": params.body,
"priority": params.priority?.rawValue ?? OpenClawNotificationPriority.active.rawValue,
"sentAtMs": self.nowMs(),
]
if let promptId = nonEmpty(params.promptId) {
payload["promptId"] = promptId
}
if let sessionKey = nonEmpty(params.sessionKey) {
payload["sessionKey"] = sessionKey
}
if let kind = nonEmpty(params.kind) {
payload["kind"] = kind
}
if let details = nonEmpty(params.details) {
payload["details"] = details
}
if let expiresAtMs = params.expiresAtMs {
payload["expiresAtMs"] = expiresAtMs
}
if let risk = params.risk {
payload["risk"] = risk.rawValue
}
if let actions = params.actions, !actions.isEmpty {
payload["actions"] = actions.map { action in
var encoded: [String: Any] = [
"id": action.id,
"label": action.label,
]
if let style = nonEmpty(action.style) {
encoded["style"] = style
}
return encoded
}
}
return payload
}
static func encodeExecApprovalItem(_ item: OpenClawWatchExecApprovalItem) -> [String: Any] {
var payload: [String: Any] = [
"id": item.id,
"commandText": item.commandText,
"allowedDecisions": item.allowedDecisions.map(\.rawValue),
]
if let commandPreview = nonEmpty(item.commandPreview) {
payload["commandPreview"] = commandPreview
}
if let host = nonEmpty(item.host) {
payload["host"] = host
}
if let nodeId = nonEmpty(item.nodeId) {
payload["nodeId"] = nodeId
}
if let agentId = nonEmpty(item.agentId) {
payload["agentId"] = agentId
}
if let expiresAtMs = item.expiresAtMs {
payload["expiresAtMs"] = expiresAtMs
}
if let risk = item.risk {
payload["risk"] = risk.rawValue
}
return payload
}
static func encodeExecApprovalPromptPayload(
_ message: OpenClawWatchExecApprovalPromptMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalPrompt.rawValue,
"approval": self.encodeExecApprovalItem(message.approval),
]
if let sentAtMs = message.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
if let deliveryId = nonEmpty(message.deliveryId) {
payload["deliveryId"] = deliveryId
}
if message.resetResolvingState == true {
payload["resetResolvingState"] = true
}
return payload
}
static func encodeExecApprovalResolvedPayload(
_ message: OpenClawWatchExecApprovalResolvedMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalResolved.rawValue,
"approvalId": message.approvalId,
]
if let decision = message.decision {
payload["decision"] = decision.rawValue
}
if let resolvedAtMs = message.resolvedAtMs {
payload["resolvedAtMs"] = resolvedAtMs
}
if let source = nonEmpty(message.source) {
payload["source"] = source
}
return payload
}
static func encodeExecApprovalExpiredPayload(
_ message: OpenClawWatchExecApprovalExpiredMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalExpired.rawValue,
"approvalId": message.approvalId,
"reason": message.reason.rawValue,
]
if let expiredAtMs = message.expiredAtMs {
payload["expiredAtMs"] = expiredAtMs
}
return payload
}
static func encodeExecApprovalSnapshotPayload(
_ message: OpenClawWatchExecApprovalSnapshotMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.execApprovalSnapshot.rawValue,
"approvals": message.approvals.map(self.encodeExecApprovalItem),
]
if let sentAtMs = message.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
if let snapshotId = nonEmpty(message.snapshotId) {
payload["snapshotId"] = snapshotId
}
return payload
}
static func encodeAppSnapshotPayload(
_ message: OpenClawWatchAppSnapshotMessage) -> [String: Any]
{
var payload: [String: Any] = [
"type": OpenClawWatchPayloadType.appSnapshot.rawValue,
"gatewayStatusText": message.gatewayStatusText,
"gatewayConnected": message.gatewayConnected,
"agentName": message.agentName,
"sessionKey": message.sessionKey,
"talkStatusText": message.talkStatusText,
"talkEnabled": message.talkEnabled,
"talkListening": message.talkListening,
"talkSpeaking": message.talkSpeaking,
"pendingApprovalCount": message.pendingApprovalCount,
]
if let agentAvatarURL = nonEmpty(message.agentAvatarURL) {
payload["agentAvatarUrl"] = agentAvatarURL
}
if let agentAvatarText = nonEmpty(message.agentAvatarText) {
payload["agentAvatarText"] = agentAvatarText
}
if let gatewayStableID = nonEmpty(message.gatewayStableID) {
payload["gatewayStableID"] = gatewayStableID
}
if let sentAtMs = message.sentAtMs {
payload["sentAtMs"] = sentAtMs
}
if let chatItems = message.chatItems {
payload["chatItems"] = chatItems.map { item in
var encoded: [String: Any] = [
"id": item.id,
"role": item.role,
"text": item.text,
]
if let timestampMs = item.timestampMs {
encoded["timestampMs"] = timestampMs
}
return encoded
}
}
if let chatStatusText = nonEmpty(message.chatStatusText) {
payload["chatStatusText"] = chatStatusText
}
if let snapshotId = nonEmpty(message.snapshotId) {
payload["snapshotId"] = snapshotId
}
return payload
}
static func parseQuickReplyPayload(
_ payload: [String: Any],
transport: String) -> WatchQuickReplyEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.reply.rawValue else {
return nil
}
guard let actionId = nonEmpty(payload["actionId"] as? String) else {
return nil
}
let promptId = self.nonEmpty(payload["promptId"] as? String) ?? "unknown"
let replyId = self.nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString
let actionLabel = self.nonEmpty(payload["actionLabel"] as? String)
let sessionKey = self.nonEmpty(payload["sessionKey"] as? String)
let note = self.nonEmpty(payload["note"] as? String)
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchQuickReplyEvent(
replyId: replyId,
promptId: promptId,
actionId: actionId,
actionLabel: actionLabel,
sessionKey: sessionKey,
note: note,
sentAtMs: sentAtMs,
transport: transport)
}
static func parseExecApprovalResolvePayload(
_ payload: [String: Any],
transport: String) -> WatchExecApprovalResolveEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.execApprovalResolve.rawValue else {
return nil
}
guard let approvalId = nonEmpty(payload["approvalId"] as? String),
let rawDecision = nonEmpty(payload["decision"] as? String),
let decision = OpenClawWatchExecApprovalDecision(rawValue: rawDecision)
else {
return nil
}
let replyId = self.nonEmpty(payload["replyId"] as? String) ?? UUID().uuidString
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchExecApprovalResolveEvent(
replyId: replyId,
approvalId: approvalId,
decision: decision,
sentAtMs: sentAtMs,
transport: transport)
}
static func parseExecApprovalSnapshotRequestPayload(
_ payload: [String: Any],
transport: String) -> WatchExecApprovalSnapshotRequestEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.execApprovalSnapshotRequest.rawValue else {
return nil
}
let requestId = self.nonEmpty(payload["requestId"] as? String) ?? UUID().uuidString
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchExecApprovalSnapshotRequestEvent(
requestId: requestId,
sentAtMs: sentAtMs,
transport: transport)
}
static func parseAppSnapshotRequestPayload(
_ payload: [String: Any],
transport: String) -> WatchAppSnapshotRequestEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.appSnapshotRequest.rawValue else {
return nil
}
let requestId = self.nonEmpty(payload["requestId"] as? String) ?? UUID().uuidString
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchAppSnapshotRequestEvent(
requestId: requestId,
sentAtMs: sentAtMs,
transport: transport)
}
static func parseAppCommandPayload(
_ payload: [String: Any],
transport: String) -> WatchAppCommandEvent?
{
guard (payload["type"] as? String) == OpenClawWatchPayloadType.appCommand.rawValue else {
return nil
}
guard let rawCommand = nonEmpty(payload["command"] as? String),
let command = OpenClawWatchAppCommand(rawValue: rawCommand)
else {
return nil
}
let commandId = self.nonEmpty(payload["commandId"] as? String) ?? UUID().uuidString
let sessionKey = self.nonEmpty(payload["sessionKey"] as? String)
let gatewayStableID = self.nonEmpty(payload["gatewayStableID"] as? String)
let text = self.nonEmpty(payload["text"] as? String)
let sentAtMs = (payload["sentAtMs"] as? Int) ?? (payload["sentAtMs"] as? NSNumber)?.intValue
return WatchAppCommandEvent(
commandId: commandId,
command: command,
sessionKey: sessionKey,
gatewayStableID: gatewayStableID,
text: text,
sentAtMs: sentAtMs,
transport: transport)
}
}

View File

@@ -0,0 +1,205 @@
import Foundation
import OpenClawKit
enum WatchMessagingError: LocalizedError {
case unsupported
case notPaired
case watchAppNotInstalled
var errorDescription: String? {
switch self {
case .unsupported:
"WATCH_UNAVAILABLE: WatchConnectivity is not supported on this device"
case .notPaired:
"WATCH_UNAVAILABLE: no paired Apple Watch"
case .watchAppNotInstalled:
"WATCH_UNAVAILABLE: OpenClaw watch companion app is not installed"
}
}
}
@MainActor
final class WatchMessagingService: @preconcurrency WatchMessagingServicing {
private let transport: WatchConnectivityTransport
private var statusHandler: (@Sendable (WatchMessagingStatus) -> Void)?
private var lastEmittedStatus: WatchMessagingStatus?
private var replyHandler: (@Sendable (WatchQuickReplyEvent) -> Void)?
private var execApprovalResolveHandler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?
private var execApprovalSnapshotRequestHandler: (
@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?
private var appSnapshotRequestHandler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?
private var appCommandHandler: (@Sendable (WatchAppCommandEvent) -> Void)?
init(transport: WatchConnectivityTransport = WatchConnectivityTransport()) {
self.transport = transport
self.transport.setStatusUpdateHandler { [weak self] snapshot in
Task { @MainActor [weak self] in
self?.emitStatusIfChanged(snapshot)
}
}
self.transport.setReplyHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitReply(event)
}
}
self.transport.setExecApprovalResolveHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitExecApprovalResolve(event)
}
}
self.transport.setExecApprovalSnapshotRequestHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitExecApprovalSnapshotRequest(event)
}
}
self.transport.setAppSnapshotRequestHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitAppSnapshotRequest(event)
}
}
self.transport.setAppCommandHandler { [weak self] event in
Task { @MainActor [weak self] in
self?.emitAppCommand(event)
}
}
}
nonisolated static func isSupportedOnDevice() -> Bool {
WatchConnectivityTransport.isSupportedOnDevice()
}
nonisolated static func currentStatusSnapshot() -> WatchMessagingStatus {
WatchConnectivityTransport.currentStatusSnapshot()
}
func status() async -> WatchMessagingStatus {
await self.transport.status()
}
func setStatusHandler(_ handler: (@Sendable (WatchMessagingStatus) -> Void)?) {
self.statusHandler = handler
guard let handler else {
self.lastEmittedStatus = nil
GatewayDiagnostics.log("watch messaging: cleared status handler")
return
}
let snapshot = self.transport.currentStatusSnapshot()
self.lastEmittedStatus = snapshot
GatewayDiagnostics.log(
"watch messaging: set status handler "
+ "supported=\(snapshot.supported) paired=\(snapshot.paired) "
+ "appInstalled=\(snapshot.appInstalled) reachable=\(snapshot.reachable) "
+ "activation=\(snapshot.activationState)")
handler(snapshot)
}
func setReplyHandler(_ handler: (@Sendable (WatchQuickReplyEvent) -> Void)?) {
self.replyHandler = handler
}
func setExecApprovalResolveHandler(_ handler: (@Sendable (WatchExecApprovalResolveEvent) -> Void)?) {
self.execApprovalResolveHandler = handler
}
func setExecApprovalSnapshotRequestHandler(
_ handler: (@Sendable (WatchExecApprovalSnapshotRequestEvent) -> Void)?)
{
self.execApprovalSnapshotRequestHandler = handler
}
func setAppSnapshotRequestHandler(_ handler: (@Sendable (WatchAppSnapshotRequestEvent) -> Void)?) {
self.appSnapshotRequestHandler = handler
}
func setAppCommandHandler(_ handler: (@Sendable (WatchAppCommandEvent) -> Void)?) {
self.appCommandHandler = handler
}
func sendNotification(
id: String,
params: OpenClawWatchNotifyParams) async throws -> WatchNotificationSendResult
{
let payload = WatchMessagingPayloadCodec.encodeNotificationPayload(id: id, params: params)
return try await self.transport.sendPayload(payload)
}
func sendExecApprovalPrompt(
_ message: OpenClawWatchExecApprovalPromptMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendPayload(
WatchMessagingPayloadCodec.encodeExecApprovalPromptPayload(message))
}
func sendExecApprovalResolved(
_ message: OpenClawWatchExecApprovalResolvedMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendPayload(
WatchMessagingPayloadCodec.encodeExecApprovalResolvedPayload(message))
}
func sendExecApprovalExpired(
_ message: OpenClawWatchExecApprovalExpiredMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendPayload(
WatchMessagingPayloadCodec.encodeExecApprovalExpiredPayload(message))
}
func syncExecApprovalSnapshot(
_ message: OpenClawWatchExecApprovalSnapshotMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendSnapshotPayload(
WatchMessagingPayloadCodec.encodeExecApprovalSnapshotPayload(message))
}
func syncAppSnapshot(
_ message: OpenClawWatchAppSnapshotMessage) async throws -> WatchNotificationSendResult
{
try await self.transport.sendSnapshotPayload(
WatchMessagingPayloadCodec.encodeAppSnapshotPayload(message))
}
private func emitStatusIfChanged(_ snapshot: WatchMessagingStatus) {
guard snapshot != self.lastEmittedStatus else {
return
}
self.lastEmittedStatus = snapshot
GatewayDiagnostics.log(
"watch messaging: status "
+ "supported=\(snapshot.supported) paired=\(snapshot.paired) "
+ "appInstalled=\(snapshot.appInstalled) reachable=\(snapshot.reachable) "
+ "activation=\(snapshot.activationState)")
self.statusHandler?(snapshot)
}
private func emitReply(_ event: WatchQuickReplyEvent) {
self.replyHandler?(event)
}
private func emitExecApprovalResolve(_ event: WatchExecApprovalResolveEvent) {
self.execApprovalResolveHandler?(event)
}
private func emitExecApprovalSnapshotRequest(_ event: WatchExecApprovalSnapshotRequestEvent) {
GatewayDiagnostics.log(
"watch messaging: snapshot request "
+ "id=\(event.requestId) transport=\(event.transport) "
+ "sentAtMs=\(event.sentAtMs ?? -1)")
self.execApprovalSnapshotRequestHandler?(event)
}
private func emitAppSnapshotRequest(_ event: WatchAppSnapshotRequestEvent) {
GatewayDiagnostics.log(
"watch messaging: app snapshot request "
+ "id=\(event.requestId) transport=\(event.transport) "
+ "sentAtMs=\(event.sentAtMs ?? -1)")
self.appSnapshotRequestHandler?(event)
}
private func emitAppCommand(_ event: WatchAppCommandEvent) {
GatewayDiagnostics.log(
"watch messaging: app command "
+ "id=\(event.commandId) command=\(event.command.rawValue) "
+ "transport=\(event.transport)")
self.appCommandHandler?(event)
}
}