Files
bot/apps/ios/Sources/Gateway/GatewaySettingsStore.swift
zeekay acaeda400a Eradicate OpenClaw — resolve merge conflicts and finish the rebrand
Three coordinated things:

1. Resolve the committed upstream/main merge conflicts. 94 files were
   sitting with raw <<<<<<< HEAD ... >>>>>>> upstream/main blocks. Every
   conflict had HanzoBot/Bot on HEAD and OpenClaw on upstream. Resolved
   by keeping HEAD everywhere — 540 conflict blocks.

2. Rewrite remaining OpenClaw text refs.
     OPENCLAW   → HANZO_BOT
     OpenClaw   → HanzoBot          (PascalCase type identifiers)
     openclaw   → bot               (codebase convention — botToken,
                                    botUsername etc.; avoids invalid
                                    hyphenated JS identifiers)
     openclaw[_]/openclaw[A-Z] → bot[_]/bot[A-Z]   (compound)
     ai.openclaw.x → ai.hanzo.bot.x  (JVM package path)

3. Delete or rename openclaw-named file paths (116 of them). Dead
   duplicates with a Bot-named canonical from the partial migration:
   deleted. Otherwise renamed.
     ai/openclaw/**           → deleted (ai/hanzo/bot/** canonical)
     Sources/OpenClaw*/       → deleted (Sources/Bot* canonical)
     Tests/OpenClawIPCTests/  → deleted (Tests/BotIPCTests canonical)
     OpenClawKit/             → deleted (BotKit canonical)
     openclaw-tools.*.ts      → deleted (bot-tools.*.ts canonical)
     openclaw-root.ts etc.    → deleted (bot-root.ts canonical)
     types.openclaw.ts        → deleted (types.bot.ts canonical)
     extensions/*/openclaw.plugin.json → renamed hanzo-bot.plugin.json
     docs/start/openclaw.md   → renamed hanzo-bot.md (+ zh-CN)
     docs/assets/openclaw-*.png, whatsapp-openclaw*.jpg → deleted
     scripts/.../openclaw-*   → deleted (bot/hanzo-bot parallels)
     openclaw.mjs             → deleted (hanzo-bot.mjs is package.bin)

Then patched 65 broken JS/TS import strings where hanzo-bot had ended
up inside an import path ('./types.hanzo-bot.js' → './types.bot.js'
etc., since the on-disk filename uses the brand-neutral bot- prefix).

Pre-existing lint debt cleaned up to get oxlint --type-aware to 0/0:
removed the dead i18n test that referenced a path no longer existing;
defined the missing DIDConfig and WalletConfig types in types.base.ts
(they were imported but never declared); dropped unused imports;
collapsed redundant type assertion and a tautological meta.bot lookup
that the openclaw→bot rename made redundant.

The a2ui.bundle.js generated artifact is kept at its pre-rebrand
contents — it gets regenerated by 'pnpm canvas:a2ui:bundle' from
sources I cannot rebuild in this commit; the source side is clean.

Verified: 0 occurrences of openclaw (case-insensitive) in tree source
content, 0 file or directory paths with openclaw in the name; oxlint
--type-aware src test reports 0/0.
2026-05-11 17:56:29 -07:00

500 lines
20 KiB
Swift

import Foundation
import os
enum GatewaySettingsStore {
private static let gatewayService = "ai.bot.gateway"
private static let nodeService = "ai.bot.node"
private static let talkService = "ai.bot.talk"
private static let instanceIdDefaultsKey = "node.instanceId"
private static let preferredGatewayStableIDDefaultsKey = "gateway.preferredStableID"
private static let lastDiscoveredGatewayStableIDDefaultsKey = "gateway.lastDiscoveredStableID"
private static let manualEnabledDefaultsKey = "gateway.manual.enabled"
private static let manualHostDefaultsKey = "gateway.manual.host"
private static let manualPortDefaultsKey = "gateway.manual.port"
private static let manualTlsDefaultsKey = "gateway.manual.tls"
private static let discoveryDebugLogsDefaultsKey = "gateway.discovery.debugLogs"
private static let lastGatewayKindDefaultsKey = "gateway.last.kind"
private static let lastGatewayHostDefaultsKey = "gateway.last.host"
private static let lastGatewayPortDefaultsKey = "gateway.last.port"
private static let lastGatewayTlsDefaultsKey = "gateway.last.tls"
private static let lastGatewayStableIDDefaultsKey = "gateway.last.stableID"
private static let clientIdOverrideDefaultsPrefix = "gateway.clientIdOverride."
private static let selectedAgentDefaultsPrefix = "gateway.selectedAgentId."
private static let instanceIdAccount = "instanceId"
private static let preferredGatewayStableIDAccount = "preferredStableID"
private static let lastDiscoveredGatewayStableIDAccount = "lastDiscoveredStableID"
private static let talkElevenLabsApiKeyAccount = "elevenlabs.apiKey"
static func bootstrapPersistence() {
self.ensureStableInstanceID()
self.ensurePreferredGatewayStableID()
self.ensureLastDiscoveredGatewayStableID()
}
static func loadStableInstanceID() -> String? {
if let value = KeychainStore.loadString(service: self.nodeService, account: self.instanceIdAccount)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
{
return value
}
return nil
}
static func saveStableInstanceID(_ instanceId: String) {
_ = KeychainStore.saveString(instanceId, service: self.nodeService, account: self.instanceIdAccount)
}
static func loadPreferredGatewayStableID() -> String? {
if let value = KeychainStore.loadString(
service: self.gatewayService,
account: self.preferredGatewayStableIDAccount
)?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
{
return value
}
return nil
}
static func savePreferredGatewayStableID(_ stableID: String) {
_ = KeychainStore.saveString(
stableID,
service: self.gatewayService,
account: self.preferredGatewayStableIDAccount)
}
static func loadLastDiscoveredGatewayStableID() -> String? {
if let value = KeychainStore.loadString(
service: self.gatewayService,
account: self.lastDiscoveredGatewayStableIDAccount
)?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty
{
return value
}
return nil
}
static func saveLastDiscoveredGatewayStableID(_ stableID: String) {
_ = KeychainStore.saveString(
stableID,
service: self.gatewayService,
account: self.lastDiscoveredGatewayStableIDAccount)
}
static func loadGatewayToken(instanceId: String) -> String? {
let account = self.gatewayTokenAccount(instanceId: instanceId)
let token = KeychainStore.loadString(service: self.gatewayService, account: account)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if token?.isEmpty == false { return token }
return nil
}
static func saveGatewayToken(_ token: String, instanceId: String) {
_ = KeychainStore.saveString(
token,
service: self.gatewayService,
account: self.gatewayTokenAccount(instanceId: instanceId))
}
static func loadGatewayPassword(instanceId: String) -> String? {
KeychainStore.loadString(
service: self.gatewayService,
account: self.gatewayPasswordAccount(instanceId: instanceId))?
.trimmingCharacters(in: .whitespacesAndNewlines)
}
static func saveGatewayPassword(_ password: String, instanceId: String) {
_ = KeychainStore.saveString(
password,
service: self.gatewayService,
account: self.gatewayPasswordAccount(instanceId: instanceId))
}
enum LastGatewayConnection: Equatable {
case manual(host: String, port: Int, useTLS: Bool, stableID: String)
case discovered(stableID: String, useTLS: Bool)
var stableID: String {
switch self {
case let .manual(_, _, _, stableID):
return stableID
case let .discovered(stableID, _):
return stableID
}
}
var useTLS: Bool {
switch self {
case let .manual(_, _, useTLS, _):
return useTLS
case let .discovered(_, useTLS):
return useTLS
}
}
}
private enum LastGatewayKind: String, Codable {
case manual
case discovered
}
static func loadTalkElevenLabsApiKey() -> String? {
let value = KeychainStore.loadString(
service: self.talkService,
account: self.talkElevenLabsApiKeyAccount)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if value?.isEmpty == false { return value }
return nil
}
static func saveTalkElevenLabsApiKey(_ apiKey: String?) {
let trimmed = apiKey?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if trimmed.isEmpty {
_ = KeychainStore.delete(service: self.talkService, account: self.talkElevenLabsApiKeyAccount)
return
}
_ = KeychainStore.saveString(
trimmed,
service: self.talkService,
account: self.talkElevenLabsApiKeyAccount)
}
static func saveLastGatewayConnectionManual(host: String, port: Int, useTLS: Bool, stableID: String) {
let payload = LastGatewayConnectionData(
kind: .manual, stableID: stableID, useTLS: useTLS, host: host, port: port)
self.saveLastGatewayConnectionData(payload)
}
static func saveLastGatewayConnectionDiscovered(stableID: String, useTLS: Bool) {
let payload = LastGatewayConnectionData(
kind: .discovered, stableID: stableID, useTLS: useTLS)
self.saveLastGatewayConnectionData(payload)
}
static func loadLastGatewayConnection() -> LastGatewayConnection? {
// Migrate legacy UserDefaults entries on first access.
self.migrateLastGatewayFromUserDefaultsIfNeeded()
guard let json = KeychainStore.loadString(
service: self.gatewayService, account: self.lastGatewayConnectionAccount),
let data = json.data(using: .utf8),
let stored = try? JSONDecoder().decode(LastGatewayConnectionData.self, from: data)
else { return nil }
let stableID = stored.stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !stableID.isEmpty else { return nil }
if stored.kind == .discovered {
return .discovered(stableID: stableID, useTLS: stored.useTLS)
}
let host = (stored.host ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
let port = stored.port ?? 0
guard !host.isEmpty, port > 0, port <= 65535 else { return nil }
return .manual(host: host, port: port, useTLS: stored.useTLS, stableID: stableID)
}
static func clearLastGatewayConnection(defaults: UserDefaults = .standard) {
_ = KeychainStore.delete(
service: self.gatewayService, account: self.lastGatewayConnectionAccount)
// Clean up any legacy UserDefaults entries.
defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayTlsDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayStableIDDefaultsKey)
}
@discardableResult
private static func saveLastGatewayConnectionData(_ payload: LastGatewayConnectionData) -> Bool {
guard let data = try? JSONEncoder().encode(payload),
let json = String(data: data, encoding: .utf8)
else { return false }
return KeychainStore.saveString(
json, service: self.gatewayService, account: self.lastGatewayConnectionAccount)
}
/// Migrate legacy UserDefaults gateway.last.* keys into a single Keychain entry.
private static func migrateLastGatewayFromUserDefaultsIfNeeded() {
let defaults = UserDefaults.standard
let stableID = defaults.string(forKey: self.lastGatewayStableIDDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !stableID.isEmpty else { return }
// Already migrated if Keychain entry exists.
if KeychainStore.loadString(
service: self.gatewayService, account: self.lastGatewayConnectionAccount) != nil
{
// Clean up legacy keys.
self.removeLastGatewayDefaults(defaults)
return
}
let useTLS = defaults.bool(forKey: self.lastGatewayTlsDefaultsKey)
let kindRaw = defaults.string(forKey: self.lastGatewayKindDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let kind = LastGatewayKind(rawValue: kindRaw) ?? .manual
let host = defaults.string(forKey: self.lastGatewayHostDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines)
let port = defaults.object(forKey: self.lastGatewayPortDefaultsKey) as? Int
let payload = LastGatewayConnectionData(
kind: kind, stableID: stableID, useTLS: useTLS,
host: kind == .manual ? host : nil,
port: kind == .manual ? port : nil)
guard self.saveLastGatewayConnectionData(payload) else { return }
self.removeLastGatewayDefaults(defaults)
}
private static func removeLastGatewayDefaults(_ defaults: UserDefaults) {
defaults.removeObject(forKey: self.lastGatewayKindDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayHostDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayPortDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayTlsDefaultsKey)
defaults.removeObject(forKey: self.lastGatewayStableIDDefaultsKey)
}
static func deleteGatewayCredentials(instanceId: String) {
let trimmed = instanceId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
_ = KeychainStore.delete(
service: self.gatewayService,
account: self.gatewayTokenAccount(instanceId: trimmed))
_ = KeychainStore.delete(
service: self.gatewayService,
account: self.gatewayPasswordAccount(instanceId: trimmed))
}
static func loadGatewayClientIdOverride(stableID: String) -> String? {
let trimmedID = stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedID.isEmpty else { return nil }
let key = self.clientIdOverrideDefaultsPrefix + trimmedID
let value = UserDefaults.standard.string(forKey: key)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if value?.isEmpty == false { return value }
return nil
}
static func saveGatewayClientIdOverride(stableID: String, clientId: String?) {
let trimmedID = stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedID.isEmpty else { return }
let key = self.clientIdOverrideDefaultsPrefix + trimmedID
let trimmedClientId = clientId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if trimmedClientId.isEmpty {
UserDefaults.standard.removeObject(forKey: key)
} else {
UserDefaults.standard.set(trimmedClientId, forKey: key)
}
}
static func loadGatewaySelectedAgentId(stableID: String) -> String? {
let trimmedID = stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedID.isEmpty else { return nil }
let key = self.selectedAgentDefaultsPrefix + trimmedID
let value = UserDefaults.standard.string(forKey: key)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if value?.isEmpty == false { return value }
return nil
}
static func saveGatewaySelectedAgentId(stableID: String, agentId: String?) {
let trimmedID = stableID.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedID.isEmpty else { return }
let key = self.selectedAgentDefaultsPrefix + trimmedID
let trimmedAgentId = agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if trimmedAgentId.isEmpty {
UserDefaults.standard.removeObject(forKey: key)
} else {
UserDefaults.standard.set(trimmedAgentId, forKey: key)
}
}
private static func gatewayTokenAccount(instanceId: String) -> String {
"gateway-token.\(instanceId)"
}
private static func gatewayPasswordAccount(instanceId: String) -> String {
"gateway-password.\(instanceId)"
}
private static func ensureStableInstanceID() {
let defaults = UserDefaults.standard
if let existing = defaults.string(forKey: self.instanceIdDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!existing.isEmpty
{
if self.loadStableInstanceID() == nil {
self.saveStableInstanceID(existing)
}
return
}
if let stored = self.loadStableInstanceID(), !stored.isEmpty {
defaults.set(stored, forKey: self.instanceIdDefaultsKey)
return
}
let fresh = UUID().uuidString
self.saveStableInstanceID(fresh)
defaults.set(fresh, forKey: self.instanceIdDefaultsKey)
}
private static func ensurePreferredGatewayStableID() {
let defaults = UserDefaults.standard
if let existing = defaults.string(forKey: self.preferredGatewayStableIDDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!existing.isEmpty
{
if self.loadPreferredGatewayStableID() == nil {
self.savePreferredGatewayStableID(existing)
}
return
}
if let stored = self.loadPreferredGatewayStableID(), !stored.isEmpty {
defaults.set(stored, forKey: self.preferredGatewayStableIDDefaultsKey)
}
}
private static func ensureLastDiscoveredGatewayStableID() {
let defaults = UserDefaults.standard
if let existing = defaults.string(forKey: self.lastDiscoveredGatewayStableIDDefaultsKey)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!existing.isEmpty
{
if self.loadLastDiscoveredGatewayStableID() == nil {
self.saveLastDiscoveredGatewayStableID(existing)
}
return
}
if let stored = self.loadLastDiscoveredGatewayStableID(), !stored.isEmpty {
defaults.set(stored, forKey: self.lastDiscoveredGatewayStableIDDefaultsKey)
}
}
}
enum GatewayDiagnostics {
private static let logger = Logger(subsystem: "ai.hanzo.ios", category: "GatewayDiag")
private static let queue = DispatchQueue(label: "ai.bot.gateway.diagnostics")
private static let maxLogBytes: Int64 = 512 * 1024
private static let keepLogBytes: Int64 = 256 * 1024
private static let logSizeCheckEveryWrites = 50
private static let logWritesSinceCheck = OSAllocatedUnfairLock(initialState: 0)
private static let isoFormatter: ISO8601DateFormatter = {
let f = ISO8601DateFormatter()
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return f
}()
private static var fileURL: URL? {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?
.appendingPathComponent("bot-gateway.log")
}
private static func truncateLogIfNeeded(url: URL) {
guard let attrs = try? FileManager.default.attributesOfItem(atPath: url.path),
let sizeNumber = attrs[.size] as? NSNumber
else { return }
let size = sizeNumber.int64Value
guard size > self.maxLogBytes else { return }
do {
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
let start = max(Int64(0), size - self.keepLogBytes)
try handle.seek(toOffset: UInt64(start))
var tail = try handle.readToEnd() ?? Data()
// If we truncated mid-line, drop the first partial line so logs remain readable.
if start > 0, let nl = tail.firstIndex(of: 10) {
let next = tail.index(after: nl)
if next < tail.endIndex {
tail = tail.suffix(from: next)
} else {
tail = Data()
}
}
try tail.write(to: url, options: .atomic)
} catch {
// Best-effort only.
}
}
private static func appendToLog(url: URL, data: Data) {
if FileManager.default.fileExists(atPath: url.path) {
if let handle = try? FileHandle(forWritingTo: url) {
defer { try? handle.close() }
_ = try? handle.seekToEnd()
try? handle.write(contentsOf: data)
}
} else {
try? data.write(to: url, options: .atomic)
}
}
private static func applyFileProtection(url: URL) {
try? FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
ofItemAtPath: url.path)
}
static func bootstrap() {
guard let url = fileURL else { return }
queue.async {
self.truncateLogIfNeeded(url: url)
let timestamp = self.isoFormatter.string(from: Date())
let line = "[\(timestamp)] gateway diagnostics started\n"
if let data = line.data(using: .utf8) {
self.appendToLog(url: url, data: data)
self.applyFileProtection(url: url)
}
}
}
static func log(_ message: String) {
let timestamp = self.isoFormatter.string(from: Date())
let line = "[\(timestamp)] \(message)"
logger.info("\(line, privacy: .public)")
guard let url = fileURL else { return }
queue.async {
let shouldTruncate = self.logWritesSinceCheck.withLock { count in
count += 1
if count >= self.logSizeCheckEveryWrites {
count = 0
return true
}
return false
}
if shouldTruncate {
self.truncateLogIfNeeded(url: url)
}
let entry = line + "\n"
if let data = entry.data(using: .utf8) {
self.appendToLog(url: url, data: data)
}
}
}
static func reset() {
guard let url = fileURL else { return }
queue.async {
try? FileManager.default.removeItem(at: url)
}
}
}