From a7be243b7ad1aad93062a2c93993d4c40fec0e15 Mon Sep 17 00:00:00 2001 From: Conrad Date: Tue, 27 Jan 2026 01:14:47 -0800 Subject: [PATCH] macOS: add remote gateway token field --- apps/macos/Sources/Moltbot/AppState.swift | 33 +++++++++++++++++++ .../Sources/Moltbot/GeneralSettings.swift | 19 +++++++++++ 2 files changed, 52 insertions(+) diff --git a/apps/macos/Sources/Moltbot/AppState.swift b/apps/macos/Sources/Moltbot/AppState.swift index 627e5851f..534f005a8 100644 --- a/apps/macos/Sources/Moltbot/AppState.swift +++ b/apps/macos/Sources/Moltbot/AppState.swift @@ -213,6 +213,10 @@ final class AppState { didSet { self.syncGatewayConfigIfNeeded() } } + var remoteToken: String { + didSet { self.syncGatewayConfigIfNeeded() } + } + var remoteIdentity: String { didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteIdentity, forKey: remoteIdentityKey) } } } @@ -278,6 +282,7 @@ final class AppState { let configRoot = MoltbotConfigFile.loadDict() let configRemoteUrl = GatewayRemoteConfig.resolveUrlString(root: configRoot) let configRemoteTransport = GatewayRemoteConfig.resolveTransport(root: configRoot) + let configRemoteToken = Self.resolveRemoteToken(root: configRoot) let resolvedConnectionMode = ConnectionModeResolver.resolve(root: configRoot).mode self.remoteTransport = configRemoteTransport self.connectionMode = resolvedConnectionMode @@ -293,6 +298,7 @@ final class AppState { self.remoteTarget = storedRemoteTarget } self.remoteUrl = configRemoteUrl ?? "" + self.remoteToken = configRemoteToken self.remoteIdentity = UserDefaults.standard.string(forKey: remoteIdentityKey) ?? "" self.remoteProjectRoot = UserDefaults.standard.string(forKey: remoteProjectRootKey) ?? "" self.remoteCliPath = UserDefaults.standard.string(forKey: remoteCliPathKey) ?? "" @@ -352,6 +358,16 @@ final class AppState { return trimmed } + private static func resolveRemoteToken(root: [String: Any]) -> String { + guard let gateway = root["gateway"] as? [String: Any], + let remote = gateway["remote"] as? [String: Any], + let token = remote["token"] as? String + else { + return "" + } + return token.trimmingCharacters(in: .whitespacesAndNewlines) + } + private func startConfigWatcher() { let configUrl = MoltbotConfigFile.url() self.configWatcher = ConfigFileWatcher(url: configUrl) { [weak self] in @@ -375,6 +391,7 @@ final class AppState { .trimmingCharacters(in: .whitespacesAndNewlines) .isEmpty ?? true) let remoteTransport = GatewayRemoteConfig.resolveTransport(root: root) + let remoteToken = Self.resolveRemoteToken(root: root) let desiredMode: ConnectionMode? = switch modeRaw { case "local": @@ -402,6 +419,9 @@ final class AppState { if remoteUrlText != self.remoteUrl { self.remoteUrl = remoteUrlText } + if remoteToken != self.remoteToken { + self.remoteToken = remoteToken + } let targetMode = desiredMode ?? self.connectionMode if targetMode == .remote, @@ -437,6 +457,7 @@ final class AppState { let remoteIdentity = self.remoteIdentity let remoteTransport = self.remoteTransport let remoteUrl = self.remoteUrl + let remoteToken = self.remoteToken let desiredMode: String? = switch connectionMode { case .local: "local" @@ -529,6 +550,17 @@ final class AppState { } } + let trimmedToken = remoteToken.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedToken.isEmpty { + if (remote["token"] as? String) != trimmedToken { + remote["token"] = trimmedToken + remoteChanged = true + } + } else if remote["token"] != nil { + remote.removeValue(forKey: "token") + remoteChanged = true + } + if remoteChanged { gateway["remote"] = remote changed = true @@ -684,6 +716,7 @@ extension AppState { state.canvasEnabled = true state.remoteTarget = "user@example.com" state.remoteUrl = "wss://gateway.example.ts.net" + state.remoteToken = "example-token" state.remoteIdentity = "~/.ssh/id_ed25519" state.remoteProjectRoot = "~/Projects/moltbot" state.remoteCliPath = "" diff --git a/apps/macos/Sources/Moltbot/GeneralSettings.swift b/apps/macos/Sources/Moltbot/GeneralSettings.swift index ea54786c7..c0fa10b45 100644 --- a/apps/macos/Sources/Moltbot/GeneralSettings.swift +++ b/apps/macos/Sources/Moltbot/GeneralSettings.swift @@ -145,6 +145,8 @@ struct GeneralSettings: View { self.remoteDirectRow } + self.remoteTokenRow + GatewayDiscoveryInlineList( discovery: self.gatewayDiscovery, currentTarget: self.state.remoteTarget, @@ -305,6 +307,23 @@ struct GeneralSettings: View { } } + private var remoteTokenRow: some View { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 10) { + Text("Gateway token") + .font(.callout.weight(.semibold)) + .frame(width: self.remoteLabelWidth, alignment: .leading) + SecureField("Shared gateway auth token", text: self.$state.remoteToken) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: .infinity) + } + Text("Must match gateway.auth.token (or gateway.remote.token) on the gateway host.") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.leading, self.remoteLabelWidth + 10) + } + } + private var controlStatusLine: String { switch ControlChannel.shared.state { case .connected: "Connected"