mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 09:41:37 +00:00
fix(macos): respect OPENCLAW_STATE_DIR in openclaw-mac CLI config resolution (fixes #98591) (#98631)
* fix(macos): resolve OPENCLAW_STATE_DIR in openclaw-mac CLI config resolution Add shared resolveOpenClawConfigURL() to GatewayConfig.swift with precedence: OPENCLAW_CONFIG_PATH > OPENCLAW_STATE_DIR > default home. Switch ConfigureRemoteCommand and loadGatewayConfig to use it. Add focused tests for all three precedence paths. * fix(macos): honor state directory in openclaw-mac Use one normalized config resolver for connect, wizard, and configure-remote, with explicit config path precedence over the selected state directory. Co-authored-by: liuhao1024 <sunsky.lau@gmail.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -141,7 +141,7 @@ private func configureSSHRemote(
|
||||
userInfo: [NSLocalizedDescriptionKey: "SSH target must look like user@host[:port]"])
|
||||
}
|
||||
|
||||
let configURL = openClawConfigURL()
|
||||
let configURL = resolveOpenClawConfigURL()
|
||||
var root = try loadConfigRoot(from: configURL)
|
||||
var gateway = root["gateway"] as? [String: Any] ?? [:]
|
||||
var remote = gateway["remote"] as? [String: Any] ?? [:]
|
||||
@@ -201,7 +201,7 @@ private func configureDirectRemote(
|
||||
])
|
||||
}
|
||||
|
||||
let configURL = openClawConfigURL()
|
||||
let configURL = resolveOpenClawConfigURL()
|
||||
var root = try loadConfigRoot(from: configURL)
|
||||
var gateway = root["gateway"] as? [String: Any] ?? [:]
|
||||
var remote = gateway["remote"] as? [String: Any] ?? [:]
|
||||
@@ -234,15 +234,6 @@ private func configureDirectRemote(
|
||||
onboardingSkipped: true)
|
||||
}
|
||||
|
||||
private func openClawConfigURL() -> URL {
|
||||
if let raw = ProcessInfo.processInfo.environment["OPENCLAW_CONFIG_PATH"],
|
||||
!raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
{
|
||||
return URL(fileURLWithPath: NSString(string: raw).expandingTildeInPath)
|
||||
}
|
||||
return FileManager().homeDirectoryForCurrentUser.appendingPathComponent(".openclaw/openclaw.json")
|
||||
}
|
||||
|
||||
private func loadConfigRoot(from url: URL) throws -> [String: Any] {
|
||||
guard FileManager().isReadableFile(atPath: url.path) else { return [:] }
|
||||
let data = try Data(contentsOf: url)
|
||||
|
||||
@@ -19,13 +19,26 @@ struct GatewayEndpoint {
|
||||
let mode: String
|
||||
}
|
||||
|
||||
/// Keep standalone CLI reads and configure-remote writes on the same profile.
|
||||
/// An explicit config path wins; otherwise the selected state directory owns openclaw.json.
|
||||
func resolveOpenClawConfigURL() -> URL {
|
||||
if let configPath = openClawEnvironmentPath("OPENCLAW_CONFIG_PATH") {
|
||||
return URL(fileURLWithPath: NSString(string: configPath).expandingTildeInPath)
|
||||
}
|
||||
let stateDir = openClawEnvironmentPath("OPENCLAW_STATE_DIR").map {
|
||||
URL(fileURLWithPath: NSString(string: $0).expandingTildeInPath, isDirectory: true)
|
||||
} ?? FileManager().homeDirectoryForCurrentUser.appendingPathComponent(".openclaw", isDirectory: true)
|
||||
return stateDir.appendingPathComponent("openclaw.json")
|
||||
}
|
||||
|
||||
private func openClawEnvironmentPath(_ key: String) -> String? {
|
||||
guard let raw = ProcessInfo.processInfo.environment[key] else { return nil }
|
||||
let value = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return value.isEmpty ? nil : value
|
||||
}
|
||||
|
||||
func loadGatewayConfig() -> GatewayConfig {
|
||||
let home = FileManager().homeDirectoryForCurrentUser
|
||||
let candidates = [
|
||||
home.appendingPathComponent(".openclaw/openclaw.json"),
|
||||
]
|
||||
let url = candidates.first { FileManager().isReadableFile(atPath: $0.path) } ?? candidates[0]
|
||||
guard let data = try? Data(contentsOf: url) else { return GatewayConfig() }
|
||||
guard let data = try? Data(contentsOf: resolveOpenClawConfigURL()) else { return GatewayConfig() }
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
return GatewayConfig()
|
||||
}
|
||||
|
||||
@@ -198,6 +198,35 @@ struct ConfigureRemoteCommandTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func `configure remote writes state dir config when config path is unset`() async throws {
|
||||
let stateDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-configure-state-\(UUID().uuidString)", isDirectory: true)
|
||||
let configURL = stateDir.appendingPathComponent("openclaw.json")
|
||||
defer { try? FileManager().removeItem(at: stateDir) }
|
||||
|
||||
try await TestIsolation.withIsolatedState(env: [
|
||||
"OPENCLAW_CONFIG_PATH": nil,
|
||||
"OPENCLAW_STATE_DIR": stateDir.path,
|
||||
]) {
|
||||
try #require(resolveOpenClawConfigURL().path == configURL.path)
|
||||
let output = try configureRemote(
|
||||
.init(
|
||||
directUrl: "ws://192.168.0.203:18789",
|
||||
token: "state-dir-token"), // pragma: allowlist secret
|
||||
defaultsSuites: [])
|
||||
|
||||
#expect(output.configPath == configURL.path)
|
||||
let data = try Data(contentsOf: configURL)
|
||||
let root = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
let gateway = try #require(root["gateway"] as? [String: Any])
|
||||
let remote = try #require(gateway["remote"] as? [String: Any])
|
||||
#expect(gateway["mode"] as? String == "remote")
|
||||
#expect(remote["transport"] as? String == "direct")
|
||||
#expect(remote["url"] as? String == "ws://192.168.0.203:18789")
|
||||
#expect(remote["token"] as? String == "state-dir-token") // pragma: allowlist secret
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func `configure remote rejects plaintext public prefix bypass`() async {
|
||||
let configURL = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-configure-direct-reject-\(UUID().uuidString).json")
|
||||
@@ -213,3 +242,98 @@ struct ConfigureRemoteCommandTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suite(.serialized)
|
||||
struct GatewayConfigTests {
|
||||
@Test @MainActor func `config path wins when both config and state dir are set`() async throws {
|
||||
let rootDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-cli-config-precedence-\(UUID().uuidString)", isDirectory: true)
|
||||
let explicitConfigURL = rootDir.appendingPathComponent("explicit.json")
|
||||
let stateConfigURL = rootDir.appendingPathComponent("state/openclaw.json")
|
||||
defer { try? FileManager().removeItem(at: rootDir) }
|
||||
|
||||
try self.writeGatewayConfig(
|
||||
to: explicitConfigURL,
|
||||
port: 19101,
|
||||
remoteURL: "wss://explicit-config.example:19101",
|
||||
token: "explicit-config-token") // pragma: allowlist secret
|
||||
try self.writeGatewayConfig(
|
||||
to: stateConfigURL,
|
||||
port: 19102,
|
||||
remoteURL: "wss://state-config.example:19102",
|
||||
token: "state-config-token") // pragma: allowlist secret
|
||||
|
||||
await TestIsolation.withEnvValues([
|
||||
"OPENCLAW_CONFIG_PATH": explicitConfigURL.path,
|
||||
"OPENCLAW_STATE_DIR": stateConfigURL.deletingLastPathComponent().path,
|
||||
]) {
|
||||
let config = loadGatewayConfig()
|
||||
|
||||
#expect(config.port == 19101)
|
||||
#expect(config.remoteUrl == "wss://explicit-config.example:19101")
|
||||
#expect(config.remoteToken == "explicit-config-token") // pragma: allowlist secret
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func `config path trims surrounding whitespace and expands tilde`() async {
|
||||
let relativePath = ".openclaw-cli-config-\(UUID().uuidString)/openclaw.json"
|
||||
let expectedURL = FileManager().homeDirectoryForCurrentUser.appendingPathComponent(relativePath)
|
||||
|
||||
await TestIsolation.withEnvValues([
|
||||
"OPENCLAW_CONFIG_PATH": " ~/\(relativePath)\n",
|
||||
"OPENCLAW_STATE_DIR": "/tmp/openclaw-unused-state-dir",
|
||||
]) {
|
||||
#expect(resolveOpenClawConfigURL().path == expectedURL.path)
|
||||
}
|
||||
}
|
||||
|
||||
@Test @MainActor func `state dir trims surrounding whitespace for connect and wizard`() async throws {
|
||||
let stateDir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw cli state profile \(UUID().uuidString)", isDirectory: true)
|
||||
let configURL = stateDir.appendingPathComponent("openclaw.json")
|
||||
defer { try? FileManager().removeItem(at: stateDir) }
|
||||
|
||||
try self.writeGatewayConfig(
|
||||
to: configURL,
|
||||
port: 19201,
|
||||
remoteURL: "wss://state-profile.example:19202",
|
||||
remotePort: 19203,
|
||||
token: "state-profile-token") // pragma: allowlist secret
|
||||
|
||||
await TestIsolation.withEnvValues([
|
||||
"OPENCLAW_CONFIG_PATH": nil,
|
||||
"OPENCLAW_STATE_DIR": "\t\(stateDir.path) ",
|
||||
]) {
|
||||
let config = loadGatewayConfig()
|
||||
|
||||
#expect(config.mode == "remote")
|
||||
#expect(config.port == 19201)
|
||||
#expect(config.remoteUrl == "wss://state-profile.example:19202")
|
||||
#expect(config.remotePort == 19203)
|
||||
#expect(config.remoteToken == "state-profile-token") // pragma: allowlist secret
|
||||
}
|
||||
}
|
||||
|
||||
private func writeGatewayConfig(
|
||||
to url: URL,
|
||||
port: Int,
|
||||
remoteURL: String,
|
||||
remotePort: Int = 18789,
|
||||
token: String) throws
|
||||
{
|
||||
let root: [String: Any] = [
|
||||
"gateway": [
|
||||
"mode": "remote",
|
||||
"port": port,
|
||||
"remote": [
|
||||
"url": remoteURL,
|
||||
"remotePort": remotePort,
|
||||
"token": token,
|
||||
],
|
||||
],
|
||||
]
|
||||
try FileManager().createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])
|
||||
try data.write(to: url, options: [.atomic])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ openclaw-mac configure-remote \
|
||||
--token "$OPENCLAW_GATEWAY_TOKEN"
|
||||
```
|
||||
|
||||
Both forms write `~/.openclaw/openclaw.json`, mark onboarding complete, and let the app own the selected transport on next start. `--local-port`/`--remote-port` default to `18789`. Other flags: `--password`, `--identity <path>`, `--ssh-host-key-policy <strict|openssh>`, `--project-root <path>`, `--cli-path <path>`, `--json`. Run `openclaw-mac configure-remote --help` for the full reference.
|
||||
`openclaw-mac connect`, `wizard`, and `configure-remote` resolve the active config in this order: `OPENCLAW_CONFIG_PATH`, then `$OPENCLAW_STATE_DIR/openclaw.json`, then `~/.openclaw/openclaw.json`. Both configuration forms write that active file, mark onboarding complete, and let the app own the selected transport on next start. `--local-port`/`--remote-port` default to `18789`. Other flags: `--password`, `--identity <path>`, `--ssh-host-key-policy <strict|openssh>`, `--project-root <path>`, `--cli-path <path>`, `--json`. Run `openclaw-mac configure-remote --help` for the full reference.
|
||||
|
||||
To configure from the UI instead:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user