fix(macos): preserve launchd Node gateway in PortGuardian (#100867)

* fix(macos): preserve launchd Node gateway in PortGuardian

Co-authored-by: lsr911 <18230762+lsr911@users.noreply.github.com>

* fix(macos): tighten PortGuardian gateway matcher

---------

Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: lsr911 <18230762+lsr911@users.noreply.github.com>
This commit is contained in:
Vincent Koc
2026-07-06 05:00:43 -07:00
committed by GitHub
parent 56fe5d0459
commit aabf68606a
2 changed files with 78 additions and 0 deletions

View File

@@ -534,6 +534,7 @@ actor PortGuardian {
{
return true
}
if self.isNodeOpenClawGatewayCommand(full) { return true }
// If args are unavailable, treat a CLI listener as expected.
if cmd.contains("openclaw"), full == cmd { return true }
return false
@@ -542,6 +543,30 @@ actor PortGuardian {
}
}
private static func isNodeOpenClawGatewayCommand(_ fullCommand: String) -> Bool {
let tokens = fullCommand
.split(whereSeparator: \.isWhitespace)
.map { self.unquoteCommandToken(String($0)) }
guard tokens.count >= 3 else { return false }
guard URL(fileURLWithPath: tokens[0]).lastPathComponent.lowercased() == "node" else {
return false
}
return self.isOpenClawDistEntrypointToken(tokens[1])
&& tokens[2].lowercased() == "gateway"
}
private static func isOpenClawDistEntrypointToken(_ token: String) -> Bool {
let normalized = token.replacingOccurrences(of: "\\", with: "/").lowercased()
guard normalized.hasSuffix("/dist/index.js") else { return false }
return normalized
.split(separator: "/", omittingEmptySubsequences: true)
.contains("openclaw")
}
private static func unquoteCommandToken(_ token: String) -> String {
token.trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
}
private func probeGatewayHealthIfNeeded(
port: Int,
mode: AppState.ConnectionMode,

View File

@@ -0,0 +1,53 @@
import Foundation
import Testing
@testable import OpenClaw
struct PortGuardianIsExpectedTests {
@Test func `local mode preserves launchd node dist gateway command`() {
let fullCommand = """
/opt/homebrew/bin/node /opt/homebrew/lib/node_modules/openclaw/dist/index.js gateway --port 18789 --bind loopback
"""
#expect(PortGuardian._testIsExpected(
command: "node",
fullCommand: fullCommand,
port: 18789,
mode: .local))
}
@Test func `local mode preserves git checkout node dist gateway command`() {
let fullCommand = """
/usr/local/bin/node /Users/dev/Projects/openclaw/dist/index.js gateway --port 18789
"""
#expect(PortGuardian._testIsExpected(
command: "node",
fullCommand: fullCommand,
port: 18789,
mode: .local))
}
@Test func `local mode rejects similarly named node project`() {
#expect(!PortGuardian._testIsExpected(
command: "node",
fullCommand: "/usr/local/bin/node /tmp/openclaw-tools/dist/index.js gateway --port 18789",
port: 18789,
mode: .local))
}
@Test func `local mode rejects gateway appearing after another node argument`() {
#expect(!PortGuardian._testIsExpected(
command: "node",
fullCommand: "/usr/local/bin/node --inspect /tmp/openclaw/dist/index.js gateway --port 18789",
port: 18789,
mode: .local))
}
@Test func `local mode rejects node dist entrypoint without gateway subcommand`() {
#expect(!PortGuardian._testIsExpected(
command: "node",
fullCommand: "/opt/homebrew/bin/node /opt/homebrew/lib/node_modules/openclaw/dist/index.js doctor",
port: 18789,
mode: .local))
}
}