diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b20f346..99a0b5e36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ Docs: https://docs.clawd.bot ## 2026.1.22 (unreleased) +### Changes +- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster +- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. +- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early. +- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting +- Docs: add /model allowlist troubleshooting note. (#1405) +- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky. +- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla. +- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead). +- Signal: add typing indicators and DM read receipts via signal-cli. +- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. +- CLI: add `clawdbot update wizard` for interactive channel selection and restart prompts. https://docs.clawd.bot/cli/update +- macOS: add attach-only debug toggle + `--attach-only`/`--no-launchd` flag to skip launchd installs. + +### Breaking +- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. + ### Fixes - BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell. - Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla. diff --git a/apps/macos/Sources/Clawdbot/DebugSettings.swift b/apps/macos/Sources/Clawdbot/DebugSettings.swift index 542c69112..51886bc5d 100644 --- a/apps/macos/Sources/Clawdbot/DebugSettings.swift +++ b/apps/macos/Sources/Clawdbot/DebugSettings.swift @@ -16,6 +16,8 @@ struct DebugSettings: View { @State private var modelsError: String? private let gatewayManager = GatewayProcessManager.shared private let healthStore = HealthStore.shared + @State private var launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() + @State private var launchAgentWriteError: String? @State private var gatewayRootInput: String = GatewayProcessManager.shared.projectRootPath() @State private var sessionStorePath: String = SessionLoader.defaultStorePath @State private var sessionStoreSaveError: String? @@ -47,6 +49,7 @@ struct DebugSettings: View { VStack(alignment: .leading, spacing: 14) { self.header + self.launchdSection self.appInfoSection self.gatewaySection self.logsSection @@ -79,6 +82,39 @@ struct DebugSettings: View { } } + private var launchdSection: some View { + GroupBox("Gateway startup") { + VStack(alignment: .leading, spacing: 8) { + Toggle("Attach only (skip launchd install)", isOn: self.$launchAgentWriteDisabled) + .onChange(of: self.launchAgentWriteDisabled) { _, newValue in + self.launchAgentWriteError = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(newValue) + if self.launchAgentWriteError != nil { + self.launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() + return + } + if newValue { + Task { + _ = await GatewayLaunchAgentManager.set( + enabled: false, + bundlePath: Bundle.main.bundlePath, + port: GatewayEnvironment.gatewayPort()) + } + } + } + + Text("When enabled, Clawdbot won't install or manage \(gatewayLaunchdLabel). It will only attach to an existing Gateway.") + .font(.caption) + .foregroundStyle(.secondary) + + if let launchAgentWriteError { + Text(launchAgentWriteError) + .font(.caption) + .foregroundStyle(.red) + } + } + } + } + private var header: some View { VStack(alignment: .leading, spacing: 6) { Text("Debug") diff --git a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift index 154932c64..f0896e691 100644 --- a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift @@ -4,11 +4,46 @@ enum GatewayLaunchAgentManager { private static let logger = Logger(subsystem: "com.clawdbot", category: "gateway.launchd") private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent" + private static var disableLaunchAgentMarkerURL: URL { + FileManager().homeDirectoryForCurrentUser + .appendingPathComponent(self.disableLaunchAgentMarker) + } + private static var plistURL: URL { FileManager().homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist") } + static func isLaunchAgentWriteDisabled() -> Bool { + FileManager().fileExists(atPath: self.disableLaunchAgentMarkerURL.path) + } + + static func setLaunchAgentWriteDisabled(_ disabled: Bool) -> String? { + let marker = self.disableLaunchAgentMarkerURL + if disabled { + do { + try FileManager().createDirectory( + at: marker.deletingLastPathComponent(), + withIntermediateDirectories: true) + if !FileManager().fileExists(atPath: marker.path) { + FileManager().createFile(atPath: marker.path, contents: nil) + } + } catch { + return error.localizedDescription + } + return nil + } + + if FileManager().fileExists(atPath: marker.path) { + do { + try FileManager().removeItem(at: marker) + } catch { + return error.localizedDescription + } + } + return nil + } + static func isLoaded() async -> Bool { guard let loaded = await self.readDaemonLoaded() else { return false } return loaded @@ -66,12 +101,6 @@ enum GatewayLaunchAgentManager { } extension GatewayLaunchAgentManager { - private static func isLaunchAgentWriteDisabled() -> Bool { - let marker = FileManager().homeDirectoryForCurrentUser - .appendingPathComponent(self.disableLaunchAgentMarker) - return FileManager().fileExists(atPath: marker.path) - } - private static func readDaemonLoaded() async -> Bool? { let result = await self.runDaemonCommandResult( ["status", "--json", "--no-probe"], diff --git a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift index 33156f58f..a4e7d72a7 100644 --- a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift @@ -79,6 +79,11 @@ final class GatewayProcessManager { func ensureLaunchAgentEnabledIfNeeded() async { guard !CommandResolver.connectionModeIsRemote() else { return } + if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() { + self.appendLog("[gateway] launchd auto-enable skipped (attach-only)\n") + self.logger.info("gateway launchd auto-enable skipped (disable marker set)") + return + } let enabled = await GatewayLaunchAgentManager.isLoaded() guard !enabled else { return } let bundlePath = Bundle.main.bundleURL.path @@ -308,6 +313,15 @@ final class GatewayProcessManager { return } + if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() { + let message = "Launchd disabled; start the Gateway manually or disable attach-only." + self.status = .failed(message) + self.lastFailureReason = "launchd disabled" + self.appendLog("[gateway] launchd disabled; skipping auto-start\n") + self.logger.info("gateway launchd enable skipped (disable marker set)") + return + } + let bundlePath = Bundle.main.bundleURL.path let port = GatewayEnvironment.gatewayPort() self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n") diff --git a/apps/macos/Sources/Clawdbot/MenuBar.swift b/apps/macos/Sources/Clawdbot/MenuBar.swift index 9738c310b..26a467e91 100644 --- a/apps/macos/Sources/Clawdbot/MenuBar.swift +++ b/apps/macos/Sources/Clawdbot/MenuBar.swift @@ -3,6 +3,7 @@ import Darwin import Foundation import MenuBarExtraAccess import Observation +import OSLog import Security import SwiftUI @@ -10,6 +11,7 @@ import SwiftUI struct ClawdbotApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate @State private var state: AppState + private static let logger = Logger(subsystem: "com.clawdbot", category: "app") private let gatewayManager = GatewayProcessManager.shared private let controlChannel = ControlChannel.shared private let activityStore = WorkActivityStore.shared @@ -31,6 +33,7 @@ struct ClawdbotApp: App { init() { ClawdbotLogging.bootstrapIfNeeded() + Self.applyAttachOnlyOverrideIfNeeded() _state = State(initialValue: AppStateStore.shared) } @@ -91,6 +94,22 @@ struct ClawdbotApp: App { self.statusItem?.button?.appearsDisabled = paused || sleeping } + private static func applyAttachOnlyOverrideIfNeeded() { + let args = CommandLine.arguments + guard args.contains("--attach-only") || args.contains("--no-launchd") else { return } + if let error = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(true) { + Self.logger.error("attach-only flag failed: \(error, privacy: .public)") + return + } + Task { + _ = await GatewayLaunchAgentManager.set( + enabled: false, + bundlePath: Bundle.main.bundlePath, + port: GatewayEnvironment.gatewayPort()) + } + Self.logger.info("attach-only flag enabled") + } + private var isGatewaySleeping: Bool { if self.state.isPaused { return false } switch self.state.connectionMode { diff --git a/docs/platforms/mac/child-process.md b/docs/platforms/mac/child-process.md index b88b0dece..46e98c040 100644 --- a/docs/platforms/mac/child-process.md +++ b/docs/platforms/mac/child-process.md @@ -45,6 +45,13 @@ present. To reset manually: rm ~/.clawdbot/disable-launchagent ``` +## Attach-only mode + +To force the macOS app to **never install or manage launchd**, launch it with +`--attach-only` (or `--no-launchd`). This sets `~/.clawdbot/disable-launchagent`, +so the app only attaches to an already running Gateway. You can toggle the same +behavior in Debug Settings. + ## Remote mode Remote mode never starts a local Gateway. The app uses an SSH tunnel to the