mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
fix: clear stale remote discovery endpoints (#21618) (thanks @bmendonca3)
This commit is contained in:
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Media: enforce inbound media byte limits during download/read across Discord, Telegram, Zalo, Microsoft Teams, and BlueBubbles to prevent oversized payload memory spikes before rejection. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/Control UI: block symlink-based out-of-root static file reads by enforcing realpath containment and file-identity checks when serving Control UI assets and SPA fallback `index.html`. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/MSTeams media: enforce allowlist checks for SharePoint reference attachment URLs and redirect targets during Graph-backed media fetches so redirect chains cannot escape configured media host boundaries. This ships in the next npm release. Thanks @tdjackey for reporting.
|
||||
- Security/macOS discovery: fail closed for unresolved discovery endpoints by clearing stale remote selection values, use resolved service host only for SSH target derivation, and keep remote URL config aligned with resolved endpoint availability. (#21618) Thanks @bmendonca3.
|
||||
- Chat/Usage/TUI: strip synthetic inbound metadata blocks (including `Conversation info` and trailing `Untrusted context` channel metadata wrappers) from displayed conversation history so internal prompt context no longer leaks into user-visible logs.
|
||||
- Security/Browser relay: harden extension relay auth token handling for `/extension` and `/cdp` pathways.
|
||||
- Cron: persist `delivered` state in cron job records so delivery failures remain visible in status and logs. (#19174) Thanks @simonemacario.
|
||||
|
||||
@@ -2,6 +2,17 @@ import Foundation
|
||||
import OpenClawDiscovery
|
||||
|
||||
enum GatewayDiscoveryHelpers {
|
||||
static func resolvedServiceHost(
|
||||
for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String?
|
||||
{
|
||||
self.resolvedServiceHost(gateway.serviceHost)
|
||||
}
|
||||
|
||||
static func resolvedServiceHost(_ host: String?) -> String? {
|
||||
guard let host = self.trimmed(host), !host.isEmpty else { return nil }
|
||||
return host
|
||||
}
|
||||
|
||||
static func serviceEndpoint(
|
||||
for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> (host: String, port: Int)?
|
||||
{
|
||||
@@ -12,15 +23,15 @@ enum GatewayDiscoveryHelpers {
|
||||
serviceHost: String?,
|
||||
servicePort: Int?) -> (host: String, port: Int)?
|
||||
{
|
||||
guard let host = self.trimmed(serviceHost), !host.isEmpty else { return nil }
|
||||
guard let host = self.resolvedServiceHost(serviceHost) else { return nil }
|
||||
guard let port = servicePort, port > 0, port <= 65535 else { return nil }
|
||||
return (host, port)
|
||||
}
|
||||
|
||||
static func sshTarget(for gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? {
|
||||
guard let endpoint = self.serviceEndpoint(for: gateway) else { return nil }
|
||||
guard let host = self.resolvedServiceHost(for: gateway) else { return nil }
|
||||
let user = NSUserName()
|
||||
var target = "\(user)@\(endpoint.host)"
|
||||
var target = "\(user)@\(host)"
|
||||
if gateway.sshPort != 22 {
|
||||
target += ":\(gateway.sshPort)"
|
||||
}
|
||||
|
||||
@@ -676,16 +676,16 @@ extension GeneralSettings {
|
||||
MacNodeModeCoordinator.shared.setPreferredGatewayStableID(gateway.stableID)
|
||||
|
||||
if self.state.remoteTransport == .direct {
|
||||
if let url = GatewayDiscoveryHelpers.directUrl(for: gateway) {
|
||||
self.state.remoteUrl = url
|
||||
}
|
||||
} else if let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) {
|
||||
self.state.remoteTarget = target
|
||||
self.state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
|
||||
} else {
|
||||
self.state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
|
||||
}
|
||||
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
|
||||
OpenClawConfigFile.setRemoteGatewayUrl(
|
||||
host: endpoint.host,
|
||||
port: endpoint.port)
|
||||
} else {
|
||||
OpenClawConfigFile.clearRemoteGatewayUrl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,16 +26,16 @@ extension OnboardingView {
|
||||
GatewayDiscoveryPreferences.setPreferredStableID(gateway.stableID)
|
||||
|
||||
if self.state.remoteTransport == .direct {
|
||||
if let url = GatewayDiscoveryHelpers.directUrl(for: gateway) {
|
||||
self.state.remoteUrl = url
|
||||
}
|
||||
} else if let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) {
|
||||
self.state.remoteTarget = target
|
||||
self.state.remoteUrl = GatewayDiscoveryHelpers.directUrl(for: gateway) ?? ""
|
||||
} else {
|
||||
self.state.remoteTarget = GatewayDiscoveryHelpers.sshTarget(for: gateway) ?? ""
|
||||
}
|
||||
if let endpoint = GatewayDiscoveryHelpers.serviceEndpoint(for: gateway) {
|
||||
OpenClawConfigFile.setRemoteGatewayUrl(
|
||||
host: endpoint.host,
|
||||
port: endpoint.port)
|
||||
} else {
|
||||
OpenClawConfigFile.clearRemoteGatewayUrl()
|
||||
}
|
||||
|
||||
self.state.connectionMode = .remote
|
||||
|
||||
@@ -223,6 +223,19 @@ enum OpenClawConfigFile {
|
||||
}
|
||||
}
|
||||
|
||||
static func clearRemoteGatewayUrl() {
|
||||
self.updateGatewayDict { gateway in
|
||||
guard var remote = gateway["remote"] as? [String: Any] else { return }
|
||||
guard remote["url"] != nil else { return }
|
||||
remote.removeValue(forKey: "url")
|
||||
if remote.isEmpty {
|
||||
gateway.removeValue(forKey: "remote")
|
||||
} else {
|
||||
gateway["remote"] = remote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func remoteGatewayUrl() -> URL? {
|
||||
let root = self.loadDict()
|
||||
guard let gateway = root["gateway"] as? [String: Any],
|
||||
|
||||
@@ -42,6 +42,21 @@ struct GatewayDiscoveryHelpersTests {
|
||||
#expect(parsed?.port == 2201)
|
||||
}
|
||||
|
||||
@Test func sshTargetAllowsMissingResolvedServicePort() {
|
||||
let gateway = self.makeGateway(
|
||||
serviceHost: "resolved.example.ts.net",
|
||||
servicePort: nil,
|
||||
sshPort: 2201)
|
||||
|
||||
guard let target = GatewayDiscoveryHelpers.sshTarget(for: gateway) else {
|
||||
Issue.record("expected ssh target")
|
||||
return
|
||||
}
|
||||
let parsed = CommandResolver.parseSSHTarget(target)
|
||||
#expect(parsed?.host == "resolved.example.ts.net")
|
||||
#expect(parsed?.port == 2201)
|
||||
}
|
||||
|
||||
@Test func sshTargetRejectsTxtOnlyGateways() {
|
||||
let gateway = self.makeGateway(
|
||||
serviceHost: nil,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Foundation
|
||||
import OpenClawDiscovery
|
||||
import SwiftUI
|
||||
import Testing
|
||||
@@ -25,4 +26,36 @@ struct OnboardingViewSmokeTests {
|
||||
let order = OnboardingView.pageOrder(for: .local, showOnboardingChat: false)
|
||||
#expect(!order.contains(8))
|
||||
}
|
||||
|
||||
@Test func selectRemoteGatewayClearsStaleSshTargetWhenEndpointUnresolved() async {
|
||||
let override = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
|
||||
.appendingPathComponent("openclaw.json")
|
||||
.path
|
||||
|
||||
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
|
||||
let state = AppState(preview: true)
|
||||
state.remoteTransport = .ssh
|
||||
state.remoteTarget = "user@old-host:2222"
|
||||
let view = OnboardingView(
|
||||
state: state,
|
||||
permissionMonitor: PermissionMonitor.shared,
|
||||
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName))
|
||||
let gateway = GatewayDiscoveryModel.DiscoveredGateway(
|
||||
displayName: "Unresolved",
|
||||
serviceHost: nil,
|
||||
servicePort: nil,
|
||||
lanHost: "txt-host.local",
|
||||
tailnetDns: "txt-host.ts.net",
|
||||
sshPort: 22,
|
||||
gatewayPort: 18789,
|
||||
cliPath: "/tmp/openclaw",
|
||||
stableID: UUID().uuidString,
|
||||
debugID: UUID().uuidString,
|
||||
isLocal: false)
|
||||
|
||||
view.selectRemoteGateway(gateway)
|
||||
#expect(state.remoteTarget.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,31 @@ struct OpenClawConfigFileTests {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Test
|
||||
func clearRemoteGatewayUrlRemovesOnlyUrlField() async {
|
||||
let override = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
|
||||
.appendingPathComponent("openclaw.json")
|
||||
.path
|
||||
|
||||
await TestIsolation.withEnvValues(["OPENCLAW_CONFIG_PATH": override]) {
|
||||
OpenClawConfigFile.saveDict([
|
||||
"gateway": [
|
||||
"remote": [
|
||||
"url": "wss://old-host:111",
|
||||
"token": "tok",
|
||||
],
|
||||
],
|
||||
])
|
||||
OpenClawConfigFile.clearRemoteGatewayUrl()
|
||||
let root = OpenClawConfigFile.loadDict()
|
||||
let remote = ((root["gateway"] as? [String: Any])?["remote"] as? [String: Any]) ?? [:]
|
||||
#expect((remote["url"] as? String) == nil)
|
||||
#expect((remote["token"] as? String) == "tok")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func stateDirOverrideSetsConfigPath() async {
|
||||
let dir = FileManager().temporaryDirectory
|
||||
|
||||
Reference in New Issue
Block a user