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
183 lines
6.0 KiB
Swift
183 lines
6.0 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import Observation
|
|
import OpenClawDiscovery
|
|
import os
|
|
|
|
/// Manages Tailscale integration and status checking.
|
|
@Observable
|
|
@MainActor
|
|
final class TailscaleService {
|
|
static let shared = TailscaleService()
|
|
|
|
/// Tailscale local API endpoint.
|
|
private static let tailscaleAPIEndpoint = "http://100.100.100.100/api/data"
|
|
|
|
/// API request timeout in seconds.
|
|
private static let apiTimeoutInterval: TimeInterval = 5.0
|
|
|
|
private let logger = Logger(subsystem: "ai.openclaw", category: "tailscale")
|
|
|
|
/// Indicates if the Tailscale app is installed on the system.
|
|
private(set) var isInstalled = false
|
|
|
|
/// Indicates if Tailscale is currently running.
|
|
private(set) var isRunning = false
|
|
|
|
/// The Tailscale hostname for this device (e.g., "my-mac.tailnet.ts.net").
|
|
private(set) var tailscaleHostname: String?
|
|
|
|
/// The Tailscale IPv4 address for this device.
|
|
private(set) var tailscaleIP: String?
|
|
|
|
/// Error message if status check fails.
|
|
private(set) var statusError: String?
|
|
|
|
private init() {
|
|
Task { await self.checkTailscaleStatus() }
|
|
}
|
|
|
|
#if DEBUG
|
|
init(
|
|
isInstalled: Bool,
|
|
isRunning: Bool,
|
|
tailscaleHostname: String? = nil,
|
|
tailscaleIP: String? = nil,
|
|
statusError: String? = nil)
|
|
{
|
|
self.isInstalled = isInstalled
|
|
self.isRunning = isRunning
|
|
self.tailscaleHostname = tailscaleHostname
|
|
self.tailscaleIP = tailscaleIP
|
|
self.statusError = statusError
|
|
}
|
|
#endif
|
|
|
|
func checkAppInstallation() -> Bool {
|
|
let installed = FileManager().fileExists(atPath: "/Applications/Tailscale.app")
|
|
self.logger.info("Tailscale app installed: \(installed)")
|
|
return installed
|
|
}
|
|
|
|
private struct TailscaleAPIResponse: Codable {
|
|
let status: String
|
|
let deviceName: String
|
|
let tailnetName: String
|
|
let iPv4: String?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case status = "Status"
|
|
case deviceName = "DeviceName"
|
|
case tailnetName = "TailnetName"
|
|
case iPv4 = "IPv4"
|
|
}
|
|
}
|
|
|
|
private func fetchTailscaleStatus() async -> TailscaleAPIResponse? {
|
|
guard let url = URL(string: Self.tailscaleAPIEndpoint) else {
|
|
self.logger.error("Invalid Tailscale API URL")
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
let configuration = URLSessionConfiguration.default
|
|
configuration.timeoutIntervalForRequest = Self.apiTimeoutInterval
|
|
let session = URLSession(configuration: configuration)
|
|
|
|
let (data, response) = try await session.data(from: url)
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
|
httpResponse.statusCode == 200
|
|
else {
|
|
self.logger.warning("Tailscale API returned non-200 status")
|
|
return nil
|
|
}
|
|
|
|
let decoder = JSONDecoder()
|
|
return try decoder.decode(TailscaleAPIResponse.self, from: data)
|
|
} catch {
|
|
self.logger.debug("Failed to fetch Tailscale status: \(String(describing: error))")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func checkTailscaleStatus() async {
|
|
let previousIP = self.tailscaleIP
|
|
self.isInstalled = self.checkAppInstallation()
|
|
if !self.isInstalled {
|
|
self.isRunning = false
|
|
self.tailscaleHostname = nil
|
|
self.tailscaleIP = nil
|
|
self.statusError = "Tailscale is not installed"
|
|
} else if let apiResponse = await fetchTailscaleStatus() {
|
|
self.isRunning = apiResponse.status.lowercased() == "running"
|
|
|
|
if self.isRunning {
|
|
let deviceName = apiResponse.deviceName
|
|
.lowercased()
|
|
.replacingOccurrences(of: " ", with: "-")
|
|
let tailnetName = apiResponse.tailnetName
|
|
.replacingOccurrences(of: ".ts.net", with: "")
|
|
.replacingOccurrences(of: ".tailscale.net", with: "")
|
|
|
|
self.tailscaleHostname = "\(deviceName).\(tailnetName).ts.net"
|
|
self.tailscaleIP = apiResponse.iPv4
|
|
self.statusError = nil
|
|
|
|
self.logger.info(
|
|
"Tailscale running host=\(self.tailscaleHostname ?? "nil") ip=\(self.tailscaleIP ?? "nil")")
|
|
} else {
|
|
self.tailscaleHostname = nil
|
|
self.tailscaleIP = nil
|
|
self.statusError = "Tailscale is not running"
|
|
}
|
|
} else {
|
|
self.isRunning = false
|
|
self.tailscaleHostname = nil
|
|
self.tailscaleIP = nil
|
|
self.statusError = "Please start the Tailscale app"
|
|
self.logger.info("Tailscale API not responding; app likely not running")
|
|
}
|
|
|
|
if self.tailscaleIP == nil, let fallback = TailscaleNetwork.detectTailnetIPv4() {
|
|
self.tailscaleIP = fallback
|
|
if !self.isRunning {
|
|
self.isRunning = true
|
|
}
|
|
self.statusError = nil
|
|
self.logger.info("Tailscale interface IP detected (fallback) ip=\(fallback, privacy: .public)")
|
|
}
|
|
|
|
if previousIP != self.tailscaleIP {
|
|
await GatewayEndpointStore.shared.refresh()
|
|
}
|
|
}
|
|
|
|
func openTailscaleApp() {
|
|
if let url = URL(string: "file:///Applications/Tailscale.app") {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
|
|
func openAppStore() {
|
|
if let url = URL(string: "https://apps.apple.com/us/app/tailscale/id1475387142") {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
|
|
func openDownloadPage() {
|
|
if let url = URL(string: "https://tailscale.com/download/macos") {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
|
|
func openSetupGuide() {
|
|
if let url = URL(string: "https://tailscale.com/kb/1017/install/") {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
|
|
nonisolated static func fallbackTailnetIPv4() -> String? {
|
|
TailscaleNetwork.detectTailnetIPv4()
|
|
}
|
|
}
|