import ByreAPI import Foundation import SwiftUI typealias Permission = Components.Schemas.Permission typealias UsageCommands = Components.Schemas.UsageCommands typealias TailnetInfo = Components.Schemas.Tailnet let serverHelper = Bundle.main.bundleURL.appending(path: "Contents/Helpers/byre").path private let service = "gui/\(getuid())/run.fc.byre" struct ServerState { var on = false var tailnet: TailnetInfo? var address: String? { guard let tailnet, tailnet.state == "Running", !tailnet.origin.isEmpty else { return nil } return tailnet.origin } } let noCertificates = "byre's Tailscale node is signed in, but the tailnet has no HTTPS certificates, so byre can't serve its API. Turn on HTTPS Certificates on the DNS page of Tailscale's admin console, https://login.tailscale.com/admin/dns, then turn the server off and on in the Server tab." struct Check: Decodable { let name: String let need: String let `for`: String let ok: Bool let command: String let fix: String let blocking: Bool } struct Checks: Decodable { let ready: Bool let loopback: String let checks: [Check] } struct ChecksView: View { let checks: [Check] var body: some View { VStack(alignment: .leading, spacing: 10) { ForEach(checks, id: \.name) { check in VStack(alignment: .leading, spacing: 2) { HStack(spacing: 8) { Text(check.name).font(Fonts.font(13, weight: 700)) status(check) } Text("\(need(check)), for \(check.for)").foregroundStyle(Palette.muted) if !check.ok { if !check.command.isEmpty { CommandBox(command: check.command) } if !check.fix.isEmpty { Text(check.fix) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } } } } } } private func status(_ check: Check) -> some View { if check.ok { return Text("Found").foregroundStyle(Palette.ok) } return Text("Missing").foregroundStyle(check.blocking ? Palette.danger : Palette.muted) } private func need(_ check: Check) -> String { switch check.need { case "required": return "Required" case "agent": return "One of claude, codex and agy required" default: return "Optional" } } } private struct CommandBox: View { let command: String @ViewState private var copied = false @ViewState private var failure: String? var body: some View { box if let failure { Text(failure) .foregroundStyle(Palette.danger) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } } private var box: some View { HStack(alignment: .firstTextBaseline, spacing: 8) { Text(verbatim: command) .font(Fonts.font(12)) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) Button(copied ? "Copied" : "Copy", action: copy) .buttonStyle(SmallLinkButtonStyle()) .help("Copy the command to the clipboard") Button("Run in Terminal", action: run) .buttonStyle(SmallLinkButtonStyle()) .help("Open Terminal and run the command") } .padding(.horizontal, 8) .padding(.vertical, 6) .background(Palette.raised, in: RoundedRectangle(cornerRadius: 6)) .overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border)) .padding(.vertical, 2) } private func copy() { NSPasteboard.general.clearContents() NSPasteboard.general.setString(command, forType: .string) copied = true Task { try? await Task.sleep(for: .milliseconds(1500)) copied = false } } private func run() { let shell = unsafe String(cString: getpwuid(getuid()).pointee.pw_shell) let file = FileManager.default.temporaryDirectory.appending(path: "byre-install-\(UUID().uuidString).command") do { try "#!\(shell) -l\n\(command)\n".write(to: file, atomically: true, encoding: .utf8) try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: file.path) failure = nil } catch { failure = errorText(error) return } NSWorkspace.shared.open([file], withApplicationAt: URL(filePath: "/System/Applications/Utilities/Terminal.app"), configuration: NSWorkspace.OpenConfiguration()) } } // replayd keys ScreenCaptureKit clients by executable path, so the check runs from its own copy beside client_hands' byre-hands. func checkLocalPermissions() async throws -> [Permission] { let dir = FileManager.default.temporaryDirectory.appending(path: "byre-hands-check-\(UUID().uuidString)") try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: dir) } let bin = dir.appending(path: "byre-hands") try FileManager.default.copyItem(at: Bundle.main.bundleURL.appending(path: "Contents/Helpers/byre-hands"), to: bin) let r = try await execute(bin.path, ["check"]) guard r.status == 0, let perms = try? JSONDecoder().decode([Permission].self, from: Data(r.output.utf8)) else { throw ProcessFailure(errorDescription: "byre-hands check failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))") } return perms } struct PermissionsCheck: View { let run: () async throws -> [Permission] @ViewState private var result: Result<[Permission], any Error>? @ViewState private var busy = false var body: some View { VStack(alignment: .leading, spacing: 10) { Button(busy ? "Checking…" : "Check permissions", action: check) .buttonStyle(WebButtonStyle()) .disabled(busy) switch result { case .success(let perms): ForEach(perms, id: \.name) { perm in row(perm) } case .failure(let error): Text(errorText(error)) .foregroundStyle(Palette.danger) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) case nil: EmptyView() } } .padding(.top, 4) } private func row(_ perm: Permission) -> some View { VStack(alignment: .leading, spacing: 2) { HStack(spacing: 8) { Text(perm.name).font(Fonts.font(13, weight: 700)) switch perm.status { case .granted: Text("Granted").foregroundStyle(Palette.ok) case .missing: Text("Missing").foregroundStyle(Palette.danger) case .unchecked: Text("Not checked").foregroundStyle(Palette.muted) } } Text(perm.detail) .foregroundStyle(Palette.muted) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) if perm.status == .missing, let url = URL(string: perm.settings) { Button("Open System Settings") { NSWorkspace.shared.open(url) } .buttonStyle(SmallLinkButtonStyle()) } } } private func check() { busy = true Task { do { result = .success(try await run()) } catch { result = .failure(error) } busy = false } } } private struct UsageCommandsForm: View { let api: API @ViewState private var saved: UsageCommands? @ViewState private var draft: UsageCommands? @ViewState private var error = "" @ViewState private var busy = false var body: some View { VStack(alignment: .leading, spacing: 8) { if draft != nil { Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 6) { row("claude", \.claude) row("codex", \.codex) row("antigravity", \.antigravity) } Button(busy ? "Saving…" : "Save", action: save) .buttonStyle(WebButtonStyle()) .disabled(busy || draft == saved) } if !error.isEmpty { Text(error) .foregroundStyle(Palette.danger) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } } .padding(.top, 4) .task { do { saved = try await api.client.getUsageCommands().ok.body.json draft = saved error = "" } catch { self.error = "Couldn't load usage commands: \(errorText(error))" } } } private func row(_ vendor: String, _ key: WritableKeyPath) -> some View { GridRow { Text(vendor) TextField("Built in", text: Binding(get: { draft?[keyPath: key] ?? "" }, set: { draft?[keyPath: key] = $0 })) .modifier(FieldStyle()) .onSubmit(save) } } private func save() { guard let draft, draft != saved, !busy else { return } busy = true Task { do { saved = try await api.client.updateUsageCommands(body: .json(draft)).ok.body.json self.draft = saved error = "" } catch { self.error = "Couldn't save usage commands: \(errorText(error))" } busy = false } } } struct ProcessFailure: LocalizedError { let errorDescription: String? } private func execute(_ path: String, _ args: [String], env: [String: String] = [:]) async throws -> (status: Int32, output: String) { try await Task.detached { let p = Process() p.executableURL = URL(filePath: path) p.arguments = args p.environment = ProcessInfo.processInfo.environment.merging(env) { $1 } let out = Pipe() p.standardOutput = out p.standardError = out try p.run() let data = out.fileHandleForReading.readDataToEndOfFile() p.waitUntilExit() return (p.terminationStatus, String(decoding: data, as: UTF8.self)) }.value } private func launchctl(_ args: String...) async throws { let r = try await execute("/bin/launchctl", args) if r.status != 0 { throw ProcessFailure(errorDescription: "launchctl \(args.joined(separator: " ")): \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))") } } enum Server { static func state(loopback: URL?) async -> ServerState { guard let r = try? await execute("/bin/launchctl", ["print", service]), r.status == 0 else { return ServerState() } guard let loopback else { return ServerState(on: true) } return ServerState(on: true, tailnet: try? await tailnet(loopback)) } static func tailnet(_ loopback: URL) async throws -> TailnetInfo { let credential = URL.applicationSupportDirectory.appending(path: "byre/loopback-token") let token = try String(contentsOf: credential, encoding: .utf8) var request = URLRequest(url: loopback.appending(path: "api/tailnet")) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw ProcessFailure(errorDescription: "\(loopback.absoluteString)/api/tailnet: \(String(decoding: data, as: UTF8.self))") } return try JSONDecoder().decode(TailnetInfo.self, from: data) } // byre install copies its PATH into the service, which needs the login shell's to find claude, codex, agy and npx. private static func helper(_ command: String) async throws -> (status: Int32, output: String) { let shell = unsafe String(cString: getpwuid(getuid()).pointee.pw_shell) return try await execute(shell, ["-l", "-c", "exec \"$BYRE_HELPER\" \(command)"], env: ["BYRE_HELPER": serverHelper]) } static func checks() async throws -> Checks { let r = try await helper("check") guard r.status == 0, let line = r.output.split(separator: "\n").last, let checks = try? JSONDecoder().decode(Checks.self, from: Data(line.utf8)) else { throw ProcessFailure(errorDescription: "byre check failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))") } return checks } static func install() async throws { let r = try await helper("install") guard r.status == 0 else { throw ProcessFailure(errorDescription: "byre install failed: \(r.output.trimmingCharacters(in: .whitespacesAndNewlines))") } } // Reinstalling a running service restarts it, so a state read right after could still be answered by the old one. static func turnOn() async throws { if await state(loopback: nil).on { return } try await install() } static func turnOff() async throws { try await launchctl("bootout", service) } } struct ServerView: View { @Environment(Overlays.self) private var overlays @ViewState private var state: ServerState? @ViewState private var checks: Result? @ViewState private var busy = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: 8) { Text("Server on \(macName)") .font(Fonts.font(13, weight: 700)) .padding(.top, 16) if let state { details(state) } Text("Tools on \(macName)") .font(Fonts.font(13, weight: 700)) .padding(.top, 16) switch checks { case .success(let checks): ChecksView(checks: checks.checks) case .failure(let error): Text(errorText(error)) .foregroundStyle(Palette.danger) .textSelection(.enabled) case nil: EmptyView() } if let address = state?.address, let origin = URL(string: address) { Text("Agents' permissions") .font(Fonts.font(13, weight: 700)) .padding(.top, 16) Text("The macOS privacy grants the server's agents use to see and control this Mac. Checking asks macOS for any that are missing.") .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) PermissionsCheck { try await API(origin: origin).client.checkPermissions().ok.body.json } Text("Usage commands") .font(Fonts.font(13, weight: 700)) .padding(.top, 16) Text("A shell command per vendor that prints its usage as JSON for the Status tab, run after each of that vendor's turns. Empty uses what the vendor itself reports.") .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) UsageCommandsForm(api: API(origin: origin)) } } .padding(.horizontal, 16) .padding(.top, 8) .padding(.bottom, 16) .frame(maxWidth: .infinity, alignment: .leading) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Palette.bg) .onAppear { Tailnet.shared.serviceWatchers += 1 } .onDisappear { Tailnet.shared.serviceWatchers -= 1 } .task { await refresh() while !Task.isCancelled { try? await Task.sleep(for: .seconds(2)) if let current = state, current.on, current.address == nil { state = await Server.state(loopback: loopback) } } } .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in Task { await refresh() } } } private var loopback: URL? { (try? checks?.get()).flatMap { URL(string: $0.loopback) } } @ViewBuilder private func details(_ state: ServerState) -> some View { if state.on { Text("On").foregroundStyle(Palette.ok) if let address = state.address { Text(address).textSelection(.enabled) } else if let tailnet = state.tailnet, tailnet.state == "Running" { Text(noCertificates) .foregroundStyle(Palette.danger) .textSelection(.enabled) .fixedSize(horizontal: false, vertical: true) } else if let tailnet = state.tailnet { Text("Tailscale node \(tailnet.hostname): \(tailnet.state)") .foregroundStyle(Palette.muted) } else { Text("Not answering at \(loopback?.absoluteString ?? "its loopback address") yet.") .foregroundStyle(Palette.muted) } button("Turn off", Server.turnOff) } else { Text("Off").foregroundStyle(Palette.muted) button("Turn on", Server.turnOn) } } private func button(_ label: String, _ action: @escaping () async throws -> Void) -> some View { Button(label) { busy = true Task { do { try await action() } catch { overlays.showAlert(errorText(error)) } await refresh() busy = false } } .buttonStyle(WebButtonStyle()) .disabled(busy) .padding(.top, 4) } private func refresh() async { checks = await check() state = await Server.state(loopback: loopback) } private func check() async -> Result { do { return .success(try await Server.checks()) } catch { return .failure(error) } } }