Skip to content

Commit 003bb82

Browse files
fengjikuisteipete
andauthored
fix(macos): skip unresolved dashboard auth templates (#101375)
* fix(macos): skip unresolved auth env templates * fix(macos): align gateway auth env refs * fix(macos): resolve dashboard refs from service env * fix(macos): read generated gateway service env * chore: keep release changelog owner-only --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 735dbd2 commit 003bb82

5 files changed

Lines changed: 416 additions & 16 deletions

File tree

apps/macos/Sources/OpenClaw/GatewayEndpointStore.swift

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,16 @@ actor GatewayEndpointStore {
8484
env: [String: String],
8585
launchdSnapshot: LaunchAgentPlistSnapshot?) -> String?
8686
{
87+
let serviceEnv = launchdSnapshot?.environment ?? [:]
8788
let raw = env["OPENCLAW_GATEWAY_PASSWORD"] ?? ""
8889
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
8990
if !trimmed.isEmpty {
90-
if let configPassword = self.resolveConfigPassword(isRemote: isRemote, root: root),
91-
!configPassword.isEmpty
91+
if let configPassword = self.resolveConfigPassword(
92+
isRemote: isRemote,
93+
root: root,
94+
env: env,
95+
serviceEnv: serviceEnv),
96+
!configPassword.isEmpty
9297
{
9398
self.warnEnvOverrideOnce(
9499
kind: .password,
@@ -113,8 +118,11 @@ actor GatewayEndpointStore {
113118
let auth = gateway["auth"] as? [String: Any],
114119
let password = auth["password"] as? String
115120
{
116-
let pw = password.trimmingCharacters(in: .whitespacesAndNewlines)
117-
if !pw.isEmpty {
121+
if let pw = self.resolveLocalConfigAuthString(
122+
password,
123+
env: env,
124+
serviceEnv: serviceEnv)
125+
{
118126
return pw
119127
}
120128
}
@@ -126,7 +134,12 @@ actor GatewayEndpointStore {
126134
return nil
127135
}
128136

129-
private static func resolveConfigPassword(isRemote: Bool, root: [String: Any]) -> String? {
137+
private static func resolveConfigPassword(
138+
isRemote: Bool,
139+
root: [String: Any],
140+
env: [String: String] = [:],
141+
serviceEnv: [String: String] = [:]) -> String?
142+
{
130143
if isRemote {
131144
if let gateway = root["gateway"] as? [String: Any],
132145
let remote = gateway["remote"] as? [String: Any],
@@ -141,7 +154,7 @@ actor GatewayEndpointStore {
141154
let auth = gateway["auth"] as? [String: Any],
142155
let password = auth["password"] as? String
143156
{
144-
return password.trimmingCharacters(in: .whitespacesAndNewlines)
157+
return self.resolveLocalConfigAuthString(password, env: env, serviceEnv: serviceEnv)
145158
}
146159
return nil
147160
}
@@ -152,12 +165,17 @@ actor GatewayEndpointStore {
152165
env: [String: String],
153166
launchdSnapshot: LaunchAgentPlistSnapshot?) -> String?
154167
{
168+
let serviceEnv = launchdSnapshot?.environment ?? [:]
155169
let raw = env["OPENCLAW_GATEWAY_TOKEN"] ?? ""
156170
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
157171
if !trimmed.isEmpty {
158-
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
159-
!configToken.isEmpty,
160-
configToken != trimmed
172+
if let configToken = self.resolveConfigToken(
173+
isRemote: isRemote,
174+
root: root,
175+
env: env,
176+
serviceEnv: serviceEnv),
177+
!configToken.isEmpty,
178+
configToken != trimmed
161179
{
162180
self.warnEnvOverrideOnce(
163181
kind: .token,
@@ -167,8 +185,12 @@ actor GatewayEndpointStore {
167185
return trimmed
168186
}
169187

170-
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
171-
!configToken.isEmpty
188+
if let configToken = self.resolveConfigToken(
189+
isRemote: isRemote,
190+
root: root,
191+
env: env,
192+
serviceEnv: serviceEnv),
193+
!configToken.isEmpty
172194
{
173195
return configToken
174196
}
@@ -186,7 +208,12 @@ actor GatewayEndpointStore {
186208
return nil
187209
}
188210

189-
private static func resolveConfigToken(isRemote: Bool, root: [String: Any]) -> String? {
211+
private static func resolveConfigToken(
212+
isRemote: Bool,
213+
root: [String: Any],
214+
env: [String: String] = [:],
215+
serviceEnv: [String: String] = [:]) -> String?
216+
{
190217
if isRemote {
191218
return GatewayRemoteConfig.resolveTokenString(root: root)
192219
}
@@ -195,11 +222,52 @@ actor GatewayEndpointStore {
195222
let auth = gateway["auth"] as? [String: Any],
196223
let token = auth["token"] as? String
197224
{
198-
return token.trimmingCharacters(in: .whitespacesAndNewlines)
225+
return self.resolveLocalConfigAuthString(token, env: env, serviceEnv: serviceEnv)
226+
}
227+
return nil
228+
}
229+
230+
private static func resolveLocalConfigAuthString(
231+
_ raw: String,
232+
env: [String: String],
233+
serviceEnv: [String: String]) -> String?
234+
{
235+
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
236+
guard !trimmed.isEmpty else { return nil }
237+
guard let envName = self.envSecretRefName(trimmed) else {
238+
return trimmed
239+
}
240+
// Finder-launched apps cannot see gateway-service-only env values. Resolve
241+
// local refs from app env first, then the gateway LaunchAgent snapshot.
242+
for source in [env, serviceEnv] {
243+
let value = source[envName]?.trimmingCharacters(in: .whitespacesAndNewlines)
244+
if let value, !value.isEmpty {
245+
return value
246+
}
199247
}
200248
return nil
201249
}
202250

251+
private static func envSecretRefName(_ value: String) -> String? {
252+
let name: Substring
253+
if value.hasPrefix("${"), value.hasSuffix("}") {
254+
let nameStart = value.index(value.startIndex, offsetBy: 2)
255+
let nameEnd = value.index(before: value.endIndex)
256+
name = value[nameStart..<nameEnd]
257+
} else if value.hasPrefix("$") {
258+
let nameStart = value.index(after: value.startIndex)
259+
name = value[nameStart..<value.endIndex]
260+
} else {
261+
return nil
262+
}
263+
let candidate = String(name)
264+
return self.isValidEnvSecretRefID(candidate) ? candidate : nil
265+
}
266+
267+
private static func isValidEnvSecretRefID(_ value: String) -> Bool {
268+
value.range(of: #"^[A-Z][A-Z0-9_]{0,127}$"#, options: .regularExpression) != nil
269+
}
270+
203271
private static func warnEnvOverrideOnce(
204272
kind: EnvOverrideWarningKind,
205273
envVar: String,

apps/macos/Sources/OpenClaw/GatewayLaunchAgentManager.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ enum GatewayLaunchAgentManager {
1919
.appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist")
2020
}
2121

22+
private static var generatedEnvironmentDirectoryURL: URL {
23+
OpenClawPaths.stateDirURL.appendingPathComponent("service-env", isDirectory: true)
24+
}
25+
2226
static func isLaunchAgentWriteDisabled() -> Bool {
2327
if FileManager().fileExists(atPath: self.disableLaunchAgentMarkerURL.path) { return true }
2428
return false
@@ -91,7 +95,12 @@ enum GatewayLaunchAgentManager {
9195
}
9296

9397
static func launchdConfigSnapshot() -> LaunchAgentPlistSnapshot? {
94-
LaunchAgentPlist.snapshot(url: self.plistURL)
98+
let directory = self.generatedEnvironmentDirectoryURL
99+
return LaunchAgentPlist.snapshot(
100+
url: self.plistURL,
101+
generatedEnvironmentFileURL: directory.appendingPathComponent("\(gatewayLaunchdLabel).env"),
102+
generatedEnvironmentWrapperURL: directory.appendingPathComponent(
103+
"\(gatewayLaunchdLabel)-env-wrapper.sh"))
95104
}
96105

97106
static func launchdGatewayLogPath() -> String {

apps/macos/Sources/OpenClaw/Launchctl.swift

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ struct LaunchAgentPlistSnapshot: Equatable {
3939
}
4040

4141
enum LaunchAgentPlist {
42-
static func snapshot(url: URL) -> LaunchAgentPlistSnapshot? {
42+
static func snapshot(
43+
url: URL,
44+
generatedEnvironmentFileURL: URL? = nil,
45+
generatedEnvironmentWrapperURL: URL? = nil) -> LaunchAgentPlistSnapshot?
46+
{
4347
guard let data = try? Data(contentsOf: url) else { return nil }
4448
let rootAny: Any
4549
do {
@@ -52,7 +56,12 @@ enum LaunchAgentPlist {
5256
}
5357
guard let root = rootAny as? [String: Any] else { return nil }
5458
let programArguments = root["ProgramArguments"] as? [String] ?? []
55-
let env = root["EnvironmentVariables"] as? [String: String] ?? [:]
59+
let inlineEnvironment = root["EnvironmentVariables"] as? [String: String] ?? [:]
60+
let generatedEnvironment = self.readGeneratedEnvironment(
61+
programArguments: programArguments,
62+
fileURL: generatedEnvironmentFileURL,
63+
wrapperURL: generatedEnvironmentWrapperURL)
64+
let env = inlineEnvironment.merging(generatedEnvironment) { _, generated in generated }
5665
let stdoutPath = (root["StandardOutPath"] as? String)?
5766
.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty
5867
let stderrPath = (root["StandardErrorPath"] as? String)?
@@ -72,6 +81,46 @@ enum LaunchAgentPlist {
7281
password: password)
7382
}
7483

84+
private static func readGeneratedEnvironment(
85+
programArguments: [String],
86+
fileURL: URL?,
87+
wrapperURL: URL?) -> [String: String]
88+
{
89+
guard let fileURL, let wrapperURL else { return [:] }
90+
let filePath = fileURL.standardizedFileURL.path
91+
let wrapperPath = wrapperURL.standardizedFileURL.path
92+
let usesShellWrapper = programArguments.count >= 3 &&
93+
programArguments[0] == "/bin/sh" &&
94+
programArguments[1] == wrapperPath &&
95+
programArguments[2] == filePath
96+
let usesDirectWrapper = programArguments.count >= 2 &&
97+
programArguments[0] == wrapperPath &&
98+
programArguments[1] == filePath
99+
// Read only the canonical file when the LaunchAgent uses OpenClaw's generated wrapper.
100+
// This keeps arbitrary ProgramArguments paths from becoming app-readable secret sources.
101+
guard usesShellWrapper || usesDirectWrapper,
102+
FileManager.default.fileExists(atPath: wrapperPath),
103+
let content = try? String(contentsOf: fileURL, encoding: .utf8)
104+
else { return [:] }
105+
106+
var environment: [String: String] = [:]
107+
for rawLine in content.components(separatedBy: .newlines) {
108+
let line = rawLine.trimmingCharacters(in: .whitespacesAndNewlines)
109+
guard line.hasPrefix("export ") else { continue }
110+
let assignment = line.dropFirst("export ".count)
111+
guard let separator = assignment.firstIndex(of: "=") else { continue }
112+
let key = String(assignment[..<separator])
113+
guard key.range(of: #"^[A-Za-z_][A-Za-z0-9_]*$"#, options: .regularExpression) != nil else {
114+
continue
115+
}
116+
let rawValue = String(assignment[assignment.index(after: separator)...])
117+
guard rawValue.hasPrefix("'"), rawValue.hasSuffix("'") else { continue }
118+
environment[key] = String(rawValue.dropFirst().dropLast())
119+
.replacingOccurrences(of: #"'\''"#, with: "'")
120+
}
121+
return environment
122+
}
123+
75124
private static func extractFlagInt(_ args: [String], flag: String) -> Int? {
76125
guard let raw = self.extractFlagString(args, flag: flag) else { return nil }
77126
return Int(raw)

0 commit comments

Comments
 (0)