fix(macos): check config file mode for gateway token/password resolution (#1022)
* fix: honor config gateway mode for credentials * chore: oxfmt doctor platform notes --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -73,6 +73,7 @@
|
|||||||
- Fix: keep background exec aborts from killing backgrounded sessions while honoring timeouts.
|
- Fix: keep background exec aborts from killing backgrounded sessions while honoring timeouts.
|
||||||
- Fix: use local auth for gateway security probe unless remote mode has a URL. (#1011) — thanks @ivanrvpereira.
|
- Fix: use local auth for gateway security probe unless remote mode has a URL. (#1011) — thanks @ivanrvpereira.
|
||||||
- Discord: truncate skill command descriptions for slash command limits. (#1018) — thanks @evalexpr.
|
- Discord: truncate skill command descriptions for slash command limits. (#1018) — thanks @evalexpr.
|
||||||
|
- macOS: resolve gateway token/password using config mode/remote URL, and warn when `launchctl setenv` overrides config. (#1022, #1021) — thanks @kkarimi.
|
||||||
|
|
||||||
## 2026.1.14-1
|
## 2026.1.14-1
|
||||||
|
|
||||||
|
|||||||
@@ -257,30 +257,8 @@ final class AppState {
|
|||||||
|
|
||||||
let configRoot = ClawdbotConfigFile.loadDict()
|
let configRoot = ClawdbotConfigFile.loadDict()
|
||||||
let configGateway = configRoot["gateway"] as? [String: Any]
|
let configGateway = configRoot["gateway"] as? [String: Any]
|
||||||
let configModeRaw = (configGateway?["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
let configMode: ConnectionMode? = switch configModeRaw {
|
|
||||||
case "local":
|
|
||||||
.local
|
|
||||||
case "remote":
|
|
||||||
.remote
|
|
||||||
default:
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
let configRemoteUrl = (configGateway?["remote"] as? [String: Any])?["url"] as? String
|
let configRemoteUrl = (configGateway?["remote"] as? [String: Any])?["url"] as? String
|
||||||
let configHasRemoteUrl = !(configRemoteUrl?
|
let resolvedConnectionMode = ConnectionModeResolver.resolve(root: configRoot).mode
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
.isEmpty ?? true)
|
|
||||||
|
|
||||||
let storedMode = UserDefaults.standard.string(forKey: connectionModeKey)
|
|
||||||
let resolvedConnectionMode: ConnectionMode = if let configMode {
|
|
||||||
configMode
|
|
||||||
} else if configHasRemoteUrl {
|
|
||||||
.remote
|
|
||||||
} else if let storedMode {
|
|
||||||
ConnectionMode(rawValue: storedMode) ?? .local
|
|
||||||
} else {
|
|
||||||
onboardingSeen ? .local : .unconfigured
|
|
||||||
}
|
|
||||||
self.connectionMode = resolvedConnectionMode
|
self.connectionMode = resolvedConnectionMode
|
||||||
|
|
||||||
let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? ""
|
let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? ""
|
||||||
|
|||||||
@@ -385,14 +385,8 @@ enum CommandResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func connectionSettings(defaults: UserDefaults = .standard) -> RemoteSettings {
|
static func connectionSettings(defaults: UserDefaults = .standard) -> RemoteSettings {
|
||||||
let modeRaw = defaults.string(forKey: connectionModeKey)
|
let root = ClawdbotConfigFile.loadDict()
|
||||||
let mode: AppState.ConnectionMode
|
let mode = ConnectionModeResolver.resolve(root: root, defaults: defaults).mode
|
||||||
if let modeRaw {
|
|
||||||
mode = AppState.ConnectionMode(rawValue: modeRaw) ?? .local
|
|
||||||
} else {
|
|
||||||
let seen = defaults.bool(forKey: "clawdbot.onboardingSeen")
|
|
||||||
mode = seen ? .local : .unconfigured
|
|
||||||
}
|
|
||||||
let target = defaults.string(forKey: remoteTargetKey) ?? ""
|
let target = defaults.string(forKey: remoteTargetKey) ?? ""
|
||||||
let identity = defaults.string(forKey: remoteIdentityKey) ?? ""
|
let identity = defaults.string(forKey: remoteIdentityKey) ?? ""
|
||||||
let projectRoot = defaults.string(forKey: remoteProjectRootKey) ?? ""
|
let projectRoot = defaults.string(forKey: remoteProjectRootKey) ?? ""
|
||||||
|
|||||||
50
apps/macos/Sources/Clawdbot/ConnectionModeResolver.swift
Normal file
50
apps/macos/Sources/Clawdbot/ConnectionModeResolver.swift
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum EffectiveConnectionModeSource: Sendable, Equatable {
|
||||||
|
case configMode
|
||||||
|
case configRemoteURL
|
||||||
|
case userDefaults
|
||||||
|
case onboarding
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EffectiveConnectionMode: Sendable, Equatable {
|
||||||
|
let mode: AppState.ConnectionMode
|
||||||
|
let source: EffectiveConnectionModeSource
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectionModeResolver {
|
||||||
|
static func resolve(
|
||||||
|
root: [String: Any],
|
||||||
|
defaults: UserDefaults = .standard) -> EffectiveConnectionMode
|
||||||
|
{
|
||||||
|
let gateway = root["gateway"] as? [String: Any]
|
||||||
|
let configModeRaw = (gateway?["mode"] as? String) ?? ""
|
||||||
|
let configMode = configModeRaw
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
|
||||||
|
switch configMode {
|
||||||
|
case "local":
|
||||||
|
return EffectiveConnectionMode(mode: .local, source: .configMode)
|
||||||
|
case "remote":
|
||||||
|
return EffectiveConnectionMode(mode: .remote, source: .configMode)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let remoteURLRaw = ((gateway?["remote"] as? [String: Any])?["url"] as? String) ?? ""
|
||||||
|
let remoteURL = remoteURLRaw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !remoteURL.isEmpty {
|
||||||
|
return EffectiveConnectionMode(mode: .remote, source: .configRemoteURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let storedModeRaw = defaults.string(forKey: connectionModeKey) {
|
||||||
|
let storedMode = AppState.ConnectionMode(rawValue: storedModeRaw) ?? .local
|
||||||
|
return EffectiveConnectionMode(mode: storedMode, source: .userDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
let seen = defaults.bool(forKey: "clawdbot.onboardingSeen")
|
||||||
|
return EffectiveConnectionMode(mode: seen ? .local : .unconfigured, source: .onboarding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ConcurrencyExtras
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
@@ -16,6 +17,13 @@ actor GatewayEndpointStore {
|
|||||||
static let shared = GatewayEndpointStore()
|
static let shared = GatewayEndpointStore()
|
||||||
private static let supportedBindModes: Set<String> = ["loopback", "tailnet", "lan", "auto"]
|
private static let supportedBindModes: Set<String> = ["loopback", "tailnet", "lan", "auto"]
|
||||||
private static let remoteConnectingDetail = "Connecting to remote gateway…"
|
private static let remoteConnectingDetail = "Connecting to remote gateway…"
|
||||||
|
private static let staticLogger = Logger(subsystem: "com.clawdbot", category: "gateway-endpoint")
|
||||||
|
private enum EnvOverrideWarningKind: Sendable {
|
||||||
|
case token
|
||||||
|
case password
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let envOverrideWarnings = LockIsolated((token: false, password: false))
|
||||||
|
|
||||||
struct Deps: Sendable {
|
struct Deps: Sendable {
|
||||||
let mode: @Sendable () async -> AppState.ConnectionMode
|
let mode: @Sendable () async -> AppState.ConnectionMode
|
||||||
@@ -30,16 +38,18 @@ actor GatewayEndpointStore {
|
|||||||
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
||||||
token: {
|
token: {
|
||||||
let root = ClawdbotConfigFile.loadDict()
|
let root = ClawdbotConfigFile.loadDict()
|
||||||
|
let isRemote = ConnectionModeResolver.resolve(root: root).mode == .remote
|
||||||
return GatewayEndpointStore.resolveGatewayToken(
|
return GatewayEndpointStore.resolveGatewayToken(
|
||||||
isRemote: CommandResolver.connectionModeIsRemote(),
|
isRemote: isRemote,
|
||||||
root: root,
|
root: root,
|
||||||
env: ProcessInfo.processInfo.environment,
|
env: ProcessInfo.processInfo.environment,
|
||||||
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
let root = ClawdbotConfigFile.loadDict()
|
let root = ClawdbotConfigFile.loadDict()
|
||||||
|
let isRemote = ConnectionModeResolver.resolve(root: root).mode == .remote
|
||||||
return GatewayEndpointStore.resolveGatewayPassword(
|
return GatewayEndpointStore.resolveGatewayPassword(
|
||||||
isRemote: CommandResolver.connectionModeIsRemote(),
|
isRemote: isRemote,
|
||||||
root: root,
|
root: root,
|
||||||
env: ProcessInfo.processInfo.environment,
|
env: ProcessInfo.processInfo.environment,
|
||||||
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
||||||
@@ -68,6 +78,14 @@ actor GatewayEndpointStore {
|
|||||||
let raw = env["CLAWDBOT_GATEWAY_PASSWORD"] ?? ""
|
let raw = env["CLAWDBOT_GATEWAY_PASSWORD"] ?? ""
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmed.isEmpty {
|
if !trimmed.isEmpty {
|
||||||
|
if let configPassword = self.resolveConfigPassword(isRemote: isRemote, root: root),
|
||||||
|
!configPassword.isEmpty
|
||||||
|
{
|
||||||
|
self.warnEnvOverrideOnce(
|
||||||
|
kind: .password,
|
||||||
|
envVar: "CLAWDBOT_GATEWAY_PASSWORD",
|
||||||
|
configKey: isRemote ? "gateway.remote.password" : "gateway.auth.password")
|
||||||
|
}
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
if isRemote {
|
if isRemote {
|
||||||
@@ -99,6 +117,26 @@ actor GatewayEndpointStore {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func resolveConfigPassword(isRemote: Bool, root: [String: Any]) -> String? {
|
||||||
|
if isRemote {
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let remote = gateway["remote"] as? [String: Any],
|
||||||
|
let password = remote["password"] as? String
|
||||||
|
{
|
||||||
|
return password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let auth = gateway["auth"] as? [String: Any],
|
||||||
|
let password = auth["password"] as? String
|
||||||
|
{
|
||||||
|
return password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private static func resolveGatewayToken(
|
private static func resolveGatewayToken(
|
||||||
isRemote: Bool,
|
isRemote: Bool,
|
||||||
root: [String: Any],
|
root: [String: Any],
|
||||||
@@ -108,6 +146,14 @@ actor GatewayEndpointStore {
|
|||||||
let raw = env["CLAWDBOT_GATEWAY_TOKEN"] ?? ""
|
let raw = env["CLAWDBOT_GATEWAY_TOKEN"] ?? ""
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmed.isEmpty {
|
if !trimmed.isEmpty {
|
||||||
|
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
|
||||||
|
!configToken.isEmpty
|
||||||
|
{
|
||||||
|
self.warnEnvOverrideOnce(
|
||||||
|
kind: .token,
|
||||||
|
envVar: "CLAWDBOT_GATEWAY_TOKEN",
|
||||||
|
configKey: isRemote ? "gateway.remote.token" : "gateway.auth.token")
|
||||||
|
}
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
if isRemote {
|
if isRemote {
|
||||||
@@ -139,6 +185,49 @@ actor GatewayEndpointStore {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func resolveConfigToken(isRemote: Bool, root: [String: Any]) -> String? {
|
||||||
|
if isRemote {
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let remote = gateway["remote"] as? [String: Any],
|
||||||
|
let token = remote["token"] as? String
|
||||||
|
{
|
||||||
|
return token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let auth = gateway["auth"] as? [String: Any],
|
||||||
|
let token = auth["token"] as? String
|
||||||
|
{
|
||||||
|
return token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func warnEnvOverrideOnce(
|
||||||
|
kind: EnvOverrideWarningKind,
|
||||||
|
envVar: String,
|
||||||
|
configKey: String)
|
||||||
|
{
|
||||||
|
let shouldWarn = Self.envOverrideWarnings.withValue { state in
|
||||||
|
switch kind {
|
||||||
|
case .token:
|
||||||
|
guard !state.token else { return false }
|
||||||
|
state.token = true
|
||||||
|
return true
|
||||||
|
case .password:
|
||||||
|
guard !state.password else { return false }
|
||||||
|
state.password = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard shouldWarn else { return }
|
||||||
|
Self.staticLogger.warning(
|
||||||
|
"\(envVar, privacy: .public) is set and overrides \(configKey, privacy: .public). " +
|
||||||
|
"If this is unintentional, clear it with: launchctl unsetenv \(envVar, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
private let deps: Deps
|
private let deps: Deps
|
||||||
private let logger = Logger(subsystem: "com.clawdbot", category: "gateway-endpoint")
|
private let logger = Logger(subsystem: "com.clawdbot", category: "gateway-endpoint")
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import Testing
|
|||||||
@testable import Clawdbot
|
@testable import Clawdbot
|
||||||
|
|
||||||
@Suite struct GatewayEndpointStoreTests {
|
@Suite struct GatewayEndpointStoreTests {
|
||||||
|
private func makeDefaults() -> UserDefaults {
|
||||||
|
let suiteName = "GatewayEndpointStoreTests.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suiteName)!
|
||||||
|
defaults.removePersistentDomain(forName: suiteName)
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
@Test func resolveGatewayTokenPrefersEnvAndFallsBackToLaunchd() {
|
@Test func resolveGatewayTokenPrefersEnvAndFallsBackToLaunchd() {
|
||||||
let snapshot = LaunchAgentPlistSnapshot(
|
let snapshot = LaunchAgentPlistSnapshot(
|
||||||
programArguments: [],
|
programArguments: [],
|
||||||
@@ -66,4 +73,70 @@ import Testing
|
|||||||
launchdSnapshot: snapshot)
|
launchdSnapshot: snapshot)
|
||||||
#expect(password == "launchd-pass")
|
#expect(password == "launchd-pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverPrefersConfigModeOverDefaults() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("remote", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"mode": " local ",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .local)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverTrimsConfigMode() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("local", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"mode": " remote ",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverFallsBackToDefaultsWhenMissingConfig() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("remote", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: [:], defaults: defaults)
|
||||||
|
#expect(resolved.mode == .remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverFallsBackToDefaultsOnUnknownConfig() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("local", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"mode": "staging",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .local)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverPrefersRemoteURLWhenModeMissing() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("local", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"remote": [
|
||||||
|
"url": " ws://umbrel:18789 ",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .remote)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,3 +21,14 @@ clawdbot doctor --repair
|
|||||||
clawdbot doctor --deep
|
clawdbot doctor --deep
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## macOS: `launchctl` env overrides
|
||||||
|
|
||||||
|
If you previously ran `launchctl setenv CLAWDBOT_GATEWAY_TOKEN ...` (or `...PASSWORD`), that value overrides your config file and can cause persistent “unauthorized” errors.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
launchctl getenv CLAWDBOT_GATEWAY_TOKEN
|
||||||
|
launchctl getenv CLAWDBOT_GATEWAY_PASSWORD
|
||||||
|
|
||||||
|
launchctl unsetenv CLAWDBOT_GATEWAY_TOKEN
|
||||||
|
launchctl unsetenv CLAWDBOT_GATEWAY_PASSWORD
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import { execFile } from "node:child_process";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
function resolveHomeDir(): string {
|
function resolveHomeDir(): string {
|
||||||
return process.env.HOME ?? os.homedir();
|
return process.env.HOME ?? os.homedir();
|
||||||
}
|
}
|
||||||
@@ -21,3 +26,47 @@ export async function noteMacLaunchAgentOverrides() {
|
|||||||
].filter((line): line is string => Boolean(line));
|
].filter((line): line is string => Boolean(line));
|
||||||
note(lines.join("\n"), "Gateway (macOS)");
|
note(lines.join("\n"), "Gateway (macOS)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function launchctlGetenv(name: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const result = await execFileAsync("/bin/launchctl", ["getenv", name], { encoding: "utf8" });
|
||||||
|
const value = String(result.stdout ?? "").trim();
|
||||||
|
return value.length > 0 ? value : undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasConfigGatewayCreds(cfg: ClawdbotConfig): boolean {
|
||||||
|
const localToken =
|
||||||
|
typeof cfg.gateway?.auth?.token === "string" ? cfg.gateway?.auth?.token.trim() : "";
|
||||||
|
const localPassword =
|
||||||
|
typeof cfg.gateway?.auth?.password === "string" ? cfg.gateway?.auth?.password.trim() : "";
|
||||||
|
const remoteToken =
|
||||||
|
typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway?.remote?.token.trim() : "";
|
||||||
|
const remotePassword =
|
||||||
|
typeof cfg.gateway?.remote?.password === "string" ? cfg.gateway?.remote?.password.trim() : "";
|
||||||
|
return Boolean(localToken || localPassword || remoteToken || remotePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function noteMacLaunchctlGatewayEnvOverrides(cfg: ClawdbotConfig) {
|
||||||
|
if (process.platform !== "darwin") return;
|
||||||
|
if (!hasConfigGatewayCreds(cfg)) return;
|
||||||
|
|
||||||
|
const envToken = await launchctlGetenv("CLAWDBOT_GATEWAY_TOKEN");
|
||||||
|
const envPassword = await launchctlGetenv("CLAWDBOT_GATEWAY_PASSWORD");
|
||||||
|
if (!envToken && !envPassword) return;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
"- launchctl environment overrides detected (can cause confusing unauthorized errors).",
|
||||||
|
envToken ? "- `CLAWDBOT_GATEWAY_TOKEN` is set; it overrides config tokens." : undefined,
|
||||||
|
envPassword
|
||||||
|
? "- `CLAWDBOT_GATEWAY_PASSWORD` is set; it overrides config passwords."
|
||||||
|
: undefined,
|
||||||
|
"- Clear overrides and restart the app/gateway:",
|
||||||
|
envToken ? " launchctl unsetenv CLAWDBOT_GATEWAY_TOKEN" : undefined,
|
||||||
|
envPassword ? " launchctl unsetenv CLAWDBOT_GATEWAY_PASSWORD" : undefined,
|
||||||
|
].filter((line): line is string => Boolean(line));
|
||||||
|
|
||||||
|
note(lines.join("\n"), "Gateway (macOS)");
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ import {
|
|||||||
maybeScanExtraGatewayServices,
|
maybeScanExtraGatewayServices,
|
||||||
} from "./doctor-gateway-services.js";
|
} from "./doctor-gateway-services.js";
|
||||||
import { noteSourceInstallIssues } from "./doctor-install.js";
|
import { noteSourceInstallIssues } from "./doctor-install.js";
|
||||||
import { noteMacLaunchAgentOverrides } from "./doctor-platform-notes.js";
|
import {
|
||||||
|
noteMacLaunchAgentOverrides,
|
||||||
|
noteMacLaunchctlGatewayEnvOverrides,
|
||||||
|
} from "./doctor-platform-notes.js";
|
||||||
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
import { createDoctorPrompter, type DoctorOptions } from "./doctor-prompter.js";
|
||||||
import { maybeRepairSandboxImages, noteSandboxScopeWarnings } from "./doctor-sandbox.js";
|
import { maybeRepairSandboxImages, noteSandboxScopeWarnings } from "./doctor-sandbox.js";
|
||||||
import { noteSecurityWarnings } from "./doctor-security.js";
|
import { noteSecurityWarnings } from "./doctor-security.js";
|
||||||
@@ -160,6 +163,7 @@ export async function doctorCommand(
|
|||||||
await maybeScanExtraGatewayServices(options);
|
await maybeScanExtraGatewayServices(options);
|
||||||
await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter);
|
await maybeRepairGatewayServiceConfig(cfg, resolveMode(cfg), runtime, prompter);
|
||||||
await noteMacLaunchAgentOverrides();
|
await noteMacLaunchAgentOverrides();
|
||||||
|
await noteMacLaunchctlGatewayEnvOverrides(cfg);
|
||||||
|
|
||||||
await noteSecurityWarnings(cfg);
|
await noteSecurityWarnings(cfg);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user