diff --git a/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift index 079fdc8e7..7be78c407 100644 --- a/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift @@ -26,15 +26,24 @@ actor GatewayEndpointStore { mode: { await MainActor.run { AppStateStore.shared.connectionMode } }, token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] }, password: { - // First check environment variable let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PASSWORD"] ?? "" let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { return trimmed } - // Then check config file based on connection mode let root = ClawdisConfigFile.loadDict() - // Check gateway.auth.password (for local gateway auth) + if CommandResolver.connectionModeIsRemote() { + if let gateway = root["gateway"] as? [String: Any], + let remote = gateway["remote"] as? [String: Any], + let password = remote["password"] as? String + { + let pw = password.trimmingCharacters(in: .whitespacesAndNewlines) + if !pw.isEmpty { + return pw + } + } + return nil + } if let gateway = root["gateway"] as? [String: Any], let auth = gateway["auth"] as? [String: Any], let password = auth["password"] as? String @@ -44,16 +53,6 @@ actor GatewayEndpointStore { return pw } } - // Check gateway.remote.password (for remote gateway auth) - if let gateway = root["gateway"] as? [String: Any], - let remote = gateway["remote"] as? [String: Any], - let password = remote["password"] as? String - { - let pw = password.trimmingCharacters(in: .whitespacesAndNewlines) - if !pw.isEmpty { - return pw - } - } return nil }, localPort: { GatewayEnvironment.gatewayPort() }, diff --git a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift index 9cc01ffef..7d0f04995 100644 --- a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift @@ -72,15 +72,17 @@ enum GatewayLaunchAgentManager { sips """ if let token { + let escapedToken = self.escapePlistValue(token) envEntries += """ CLAWDIS_GATEWAY_TOKEN - \(token) + \(escapedToken) """ } if let password { + let escapedPassword = self.escapePlistValue(password) envEntries += """ CLAWDIS_GATEWAY_PASSWORD - \(password) + \(escapedPassword) """ } let plist = """ @@ -171,6 +173,15 @@ enum GatewayLaunchAgentManager { return nil } + private static func escapePlistValue(_ raw: String) -> String { + raw + .replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">") + .replacingOccurrences(of: "\"", with: """) + .replacingOccurrences(of: "'", with: "'") + } + private struct LaunchctlResult { let status: Int32 let output: String diff --git a/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift b/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift index 64f117ec2..6061fc8f9 100644 --- a/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift @@ -29,6 +29,7 @@ import Testing let store = GatewayEndpointStore(deps: .init( mode: { mode.get() }, token: { "t" }, + password: { nil }, localPort: { 1234 }, remotePortIfRunning: { nil }, ensureRemoteTunnel: { 18789 })) @@ -44,6 +45,7 @@ import Testing let store = GatewayEndpointStore(deps: .init( mode: { mode.get() }, token: { nil }, + password: { nil }, localPort: { 18789 }, remotePortIfRunning: { nil }, ensureRemoteTunnel: { 18789 })) @@ -58,6 +60,7 @@ import Testing let store = GatewayEndpointStore(deps: .init( mode: { mode.get() }, token: { "tok" }, + password: { "pw" }, localPort: { 1 }, remotePortIfRunning: { 5555 }, ensureRemoteTunnel: { 5555 })) @@ -69,13 +72,14 @@ import Testing _ = try await store.ensureRemoteControlTunnel() let next = await iterator.next() - guard case let .ready(mode, url, token) = next else { + guard case let .ready(mode, url, token, password) = next else { Issue.record("expected .ready after ensure, got \(String(describing: next))") return } #expect(mode == .remote) #expect(url.absoluteString == "ws://127.0.0.1:5555") #expect(token == "tok") + #expect(password == "pw") } @Test func unconfiguredModeRejectsConfig() async { @@ -83,6 +87,7 @@ import Testing let store = GatewayEndpointStore(deps: .init( mode: { mode.get() }, token: { nil }, + password: { nil }, localPort: { 18789 }, remotePortIfRunning: { nil }, ensureRemoteTunnel: { 18789 })) diff --git a/src/gateway/call.ts b/src/gateway/call.ts index e17f8203f..edecfa285 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -46,7 +46,7 @@ export async function callGateway( (typeof opts.password === "string" && opts.password.trim().length > 0 ? opts.password.trim() : undefined) || - (process.env.CLAWDIS_GATEWAY_PASSWORD?.trim()) || + process.env.CLAWDIS_GATEWAY_PASSWORD?.trim() || (typeof remote?.password === "string" && remote.password.trim().length > 0 ? remote.password.trim() : undefined); diff --git a/src/gateway/hooks-mapping.ts b/src/gateway/hooks-mapping.ts index 81c001878..0b2ee8132 100644 --- a/src/gateway/hooks-mapping.ts +++ b/src/gateway/hooks-mapping.ts @@ -18,7 +18,13 @@ export type HookMappingResolved = { messageTemplate?: string; textTemplate?: string; deliver?: boolean; - channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage"; + channel?: + | "last" + | "whatsapp" + | "telegram" + | "discord" + | "signal" + | "imessage"; to?: string; thinking?: string; timeoutSeconds?: number; @@ -50,7 +56,13 @@ export type HookAction = wakeMode: "now" | "next-heartbeat"; sessionKey?: string; deliver?: boolean; - channel?: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage"; + channel?: + | "last" + | "whatsapp" + | "telegram" + | "discord" + | "signal" + | "imessage"; to?: string; thinking?: string; timeoutSeconds?: number; @@ -86,7 +98,7 @@ type HookTransformResult = Partial<{ name: string; sessionKey: string; deliver: boolean; - channel: "last" | "whatsapp" | "telegram" | "discord"; + channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage"; to: string; thinking: string; timeoutSeconds: number;