From 51d5f167705553ff8f8032516cae6d0bd9615951 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 04:38:42 +0000 Subject: [PATCH] refactor: remove mac attach-only setting --- apps/macos/Sources/Clawdbot/AppState.swift | 14 --------- .../Sources/Clawdbot/CommandResolver.swift | 4 --- .../Clawdbot/ConnectionModeCoordinator.swift | 3 +- apps/macos/Sources/Clawdbot/Constants.swift | 1 - .../Sources/Clawdbot/ControlChannel.swift | 6 ---- .../Sources/Clawdbot/DebugSettings.swift | 2 +- .../Clawdbot/GatewayAutostartPolicy.swift | 5 ++-- .../Clawdbot/GatewayProcessManager.swift | 10 ------- .../Sources/Clawdbot/GeneralSettings.swift | 5 ---- .../Sources/Clawdbot/MenuContentView.swift | 2 +- .../GatewayAutostartPolicyTests.swift | 13 ++------- docs/platforms/mac/child-process.md | 24 ++------------- docs/start/setup.md | 9 ++---- scripts/restart-mac.sh | 17 ++--------- src/commands/doctor.ts | 29 ++----------------- 15 files changed, 18 insertions(+), 126 deletions(-) diff --git a/apps/macos/Sources/Clawdbot/AppState.swift b/apps/macos/Sources/Clawdbot/AppState.swift index 0f55d9b37..81d66ddc3 100644 --- a/apps/macos/Sources/Clawdbot/AppState.swift +++ b/apps/macos/Sources/Clawdbot/AppState.swift @@ -182,14 +182,6 @@ final class AppState { } } - var attachExistingGatewayOnly: Bool { - didSet { - self.ifNotPreview { - UserDefaults.standard.set(self.attachExistingGatewayOnly, forKey: attachExistingGatewayOnlyKey) - } - } - } - var remoteTarget: String { didSet { self.ifNotPreview { UserDefaults.standard.set(self.remoteTarget, forKey: remoteTargetKey) } @@ -302,8 +294,6 @@ final class AppState { self.canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true self.peekabooBridgeEnabled = UserDefaults.standard .object(forKey: peekabooBridgeEnabledKey) as? Bool ?? true - self.attachExistingGatewayOnly = UserDefaults.standard.bool(forKey: attachExistingGatewayOnlyKey) - if !self.isPreview { Task.detached(priority: .utility) { [weak self] in let current = await LaunchAgentManager.status() @@ -604,7 +594,6 @@ extension AppState { state.remoteIdentity = "~/.ssh/id_ed25519" state.remoteProjectRoot = "~/Projects/clawdbot" state.remoteCliPath = "" - state.attachExistingGatewayOnly = false return state } } @@ -624,9 +613,6 @@ enum AppStateStore { UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true } - static var attachExistingGatewayOnly: Bool { - UserDefaults.standard.bool(forKey: attachExistingGatewayOnlyKey) - } } @MainActor diff --git a/apps/macos/Sources/Clawdbot/CommandResolver.swift b/apps/macos/Sources/Clawdbot/CommandResolver.swift index 4f7e054fe..f4fb5c2b1 100644 --- a/apps/macos/Sources/Clawdbot/CommandResolver.swift +++ b/apps/macos/Sources/Clawdbot/CommandResolver.swift @@ -405,10 +405,6 @@ enum CommandResolver { cliPath: cliPath) } - static var attachExistingGatewayOnly: Bool { - UserDefaults.standard.bool(forKey: attachExistingGatewayOnlyKey) - } - static func connectionModeIsRemote(defaults: UserDefaults = .standard) -> Bool { self.connectionSettings(defaults: defaults).mode == .remote } diff --git a/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift b/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift index 8bb4d255a..1f6d829f1 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift @@ -27,8 +27,7 @@ final class ConnectionModeCoordinator { GatewayProcessManager.shared.setActive(true) if GatewayAutostartPolicy.shouldEnsureLaunchAgent( mode: .local, - paused: paused, - attachExistingOnly: AppStateStore.attachExistingGatewayOnly) + paused: paused) { Task { await GatewayProcessManager.shared.ensureLaunchAgentEnabledIfNeeded() } } diff --git a/apps/macos/Sources/Clawdbot/Constants.swift b/apps/macos/Sources/Clawdbot/Constants.swift index b07394489..8f205f566 100644 --- a/apps/macos/Sources/Clawdbot/Constants.swift +++ b/apps/macos/Sources/Clawdbot/Constants.swift @@ -32,7 +32,6 @@ let peekabooBridgeEnabledKey = "clawdbot.peekabooBridgeEnabled" let deepLinkKeyKey = "clawdbot.deepLinkKey" let modelCatalogPathKey = "clawdbot.modelCatalogPath" let modelCatalogReloadKey = "clawdbot.modelCatalogReload" -let attachExistingGatewayOnlyKey = "clawdbot.gateway.attachExistingOnly" let cliInstallPromptedVersionKey = "clawdbot.cliInstallPromptedVersion" let heartbeatsEnabledKey = "clawdbot.heartbeatsEnabled" let debugFileLogEnabledKey = "clawdbot.debug.fileLogEnabled" diff --git a/apps/macos/Sources/Clawdbot/ControlChannel.swift b/apps/macos/Sources/Clawdbot/ControlChannel.swift index e8c475a90..78160a43a 100644 --- a/apps/macos/Sources/Clawdbot/ControlChannel.swift +++ b/apps/macos/Sources/Clawdbot/ControlChannel.swift @@ -212,12 +212,6 @@ final class ControlChannel { return "Gateway connection was closed; start the gateway (localhost:\(port)) and retry." case .cannotFindHost, .cannotConnectToHost: let isRemote = CommandResolver.connectionModeIsRemote() - if AppStateStore.attachExistingGatewayOnly, !isRemote { - return """ - Cannot reach gateway at localhost:\(port) and “Attach existing gateway only” is enabled. - Disable it in Debug Settings or start a gateway on that port. - """ - } if isRemote { return """ Cannot reach gateway at localhost:\(port). diff --git a/apps/macos/Sources/Clawdbot/DebugSettings.swift b/apps/macos/Sources/Clawdbot/DebugSettings.swift index 411067d7c..e5b9c4a4c 100644 --- a/apps/macos/Sources/Clawdbot/DebugSettings.swift +++ b/apps/macos/Sources/Clawdbot/DebugSettings.swift @@ -772,7 +772,7 @@ struct DebugSettings: View { } private var canRestartGateway: Bool { - self.state.connectionMode == .local && !self.state.attachExistingGatewayOnly + self.state.connectionMode == .local } private func configURL() -> URL { diff --git a/apps/macos/Sources/Clawdbot/GatewayAutostartPolicy.swift b/apps/macos/Sources/Clawdbot/GatewayAutostartPolicy.swift index 0df638418..27f60abad 100644 --- a/apps/macos/Sources/Clawdbot/GatewayAutostartPolicy.swift +++ b/apps/macos/Sources/Clawdbot/GatewayAutostartPolicy.swift @@ -7,9 +7,8 @@ enum GatewayAutostartPolicy { static func shouldEnsureLaunchAgent( mode: AppState.ConnectionMode, - paused: Bool, - attachExistingOnly: Bool) -> Bool + paused: Bool) -> Bool { - self.shouldStartGateway(mode: mode, paused: paused) && !attachExistingOnly + self.shouldStartGateway(mode: mode, paused: paused) } } diff --git a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift index 921033ba8..8b712b9cd 100644 --- a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift @@ -69,7 +69,6 @@ final class GatewayProcessManager { func ensureLaunchAgentEnabledIfNeeded() async { guard !CommandResolver.connectionModeIsRemote() else { return } - guard !AppStateStore.attachExistingGatewayOnly else { return } let enabled = await GatewayLaunchAgentManager.isLoaded() guard !enabled else { return } let bundlePath = Bundle.main.bundleURL.path @@ -97,15 +96,6 @@ final class GatewayProcessManager { if await self.attachExistingGatewayIfAvailable() { return } - // Respect debug toggle: only attach, never spawn, when enabled. - if AppStateStore.attachExistingGatewayOnly { - await MainActor.run { - self.status = .failed("Attach-only enabled; no gateway to attach") - self.appendLog("[gateway] attach-only enabled; not spawning local gateway\n") - self.logger.warning("gateway attach-only enabled; not spawning") - } - return - } await self.enableLaunchdGateway() } } diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 2ed77ee88..0faf179f3 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -204,11 +204,6 @@ struct GeneralSettings: View { if !self.isNixMode { self.gatewayInstallerCard } - SettingsToggleRow( - title: "Attach only", - subtitle: "Use this when the gateway runs externally; the mac app will only attach " + - "to an already-running gateway and won't start one locally.", - binding: self.$state.attachExistingGatewayOnly) TailscaleIntegrationSection( connectionMode: self.state.connectionMode, isPaused: self.state.isPaused) diff --git a/apps/macos/Sources/Clawdbot/MenuContentView.swift b/apps/macos/Sources/Clawdbot/MenuContentView.swift index cd4ae26c9..9846b5de6 100644 --- a/apps/macos/Sources/Clawdbot/MenuContentView.swift +++ b/apps/macos/Sources/Clawdbot/MenuContentView.swift @@ -279,7 +279,7 @@ struct MenuContent: View { Label("Send Test Notification", systemImage: "bell") } Divider() - if self.state.connectionMode == .local, !AppStateStore.attachExistingGatewayOnly { + if self.state.connectionMode == .local { Button { DebugActions.restartGateway() } label: { diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayAutostartPolicyTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayAutostartPolicyTests.swift index c381121c6..4d00b957f 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayAutostartPolicyTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayAutostartPolicyTests.swift @@ -13,19 +13,12 @@ struct GatewayAutostartPolicyTests { @Test func ensuresLaunchAgentWhenLocalAndNotAttachOnly() { #expect(GatewayAutostartPolicy.shouldEnsureLaunchAgent( mode: .local, - paused: false, - attachExistingOnly: false)) + paused: false)) #expect(!GatewayAutostartPolicy.shouldEnsureLaunchAgent( mode: .local, - paused: false, - attachExistingOnly: true)) - #expect(!GatewayAutostartPolicy.shouldEnsureLaunchAgent( - mode: .local, - paused: true, - attachExistingOnly: false)) + paused: true)) #expect(!GatewayAutostartPolicy.shouldEnsureLaunchAgent( mode: .remote, - paused: false, - attachExistingOnly: false)) + paused: false)) } } diff --git a/docs/platforms/mac/child-process.md b/docs/platforms/mac/child-process.md index 28910ce8a..584ba00e9 100644 --- a/docs/platforms/mac/child-process.md +++ b/docs/platforms/mac/child-process.md @@ -1,5 +1,5 @@ --- -summary: "Gateway lifecycle on macOS (launchd + attach-only)" +summary: "Gateway lifecycle on macOS (launchd)" read_when: - Integrating the mac app with the gateway lifecycle --- @@ -10,8 +10,7 @@ uses the external `clawdbot` CLI (no embedded runtime). This gives you reliable auto‑start at login and restart on crashes. Child‑process mode (Gateway spawned directly by the app) is **not in use** today. -If you need tighter coupling to the UI, use **Attach‑only** and run the Gateway -manually in a terminal. +If you need tighter coupling to the UI, run the Gateway manually in a terminal. ## Default behavior (launchd) @@ -30,35 +29,18 @@ launchctl bootout gui/$UID/com.clawdbot.gateway Replace the label with `com.clawdbot.` when running a named profile. -## Attach‑only (developer mode) - -Attach‑only tells the app to **connect to an existing Gateway** without spawning -one. This is ideal for local dev (hot‑reload, custom flags). - -Steps: - -1) Start the Gateway yourself: - ```bash - pnpm gateway:watch - ``` -2) In the macOS app: Debug Settings → Gateway → **Attach only**. - -The UI should show “Using existing gateway …” once connected. - ## Unsigned dev builds `scripts/restart-mac.sh --no-sign` is for fast local builds when you don’t have signing keys. To prevent launchd from pointing at an unsigned relay binary, it: - Writes `~/.clawdbot/disable-launchagent`. -- Sets `clawdbot.gateway.attachExistingOnly=true` in the macOS app defaults. -Signed runs of `scripts/restart-mac.sh` clear these overrides if the marker is +Signed runs of `scripts/restart-mac.sh` clear this override if the marker is present. To reset manually: ```bash rm ~/.clawdbot/disable-launchagent -defaults write com.clawdbot.mac clawdbot.gateway.attachExistingOnly -bool NO ``` ## Remote mode diff --git a/docs/start/setup.md b/docs/start/setup.md index 53787421f..503a9c44a 100644 --- a/docs/start/setup.md +++ b/docs/start/setup.md @@ -12,7 +12,7 @@ Last updated: 2026-01-01 ## TL;DR - **Tailoring lives outside the repo:** `~/clawd` (workspace) + `~/.clawdbot/clawdbot.json` (config). - **Stable workflow:** install the macOS app; let it run the bundled Gateway. -- **Bleeding edge workflow:** run the Gateway yourself via `pnpm gateway:watch`, then point the macOS app at it using **Debug Settings → Gateway → Attach only**. +- **Bleeding edge workflow:** run the Gateway yourself via `pnpm gateway:watch`, then let the macOS app attach in Local mode. ## Prereqs (from source) - Node `>=22` @@ -84,9 +84,7 @@ pnpm gateway:watch In **Clawdbot.app**: - Connection Mode: **Local** -- Settings → **Debug Settings** → **Gateway** → enable **Attach only** - -This makes the app **only connect to an already-running gateway** and **never spawn** its own. +The app will attach to the running gateway on the configured port. ### 3) Verify @@ -98,7 +96,6 @@ pnpm clawdbot health ``` ### Common footguns -- **Attach only enabled, but nothing is running:** app shows “Attach-only enabled; no gateway to attach”. - **Wrong port:** Gateway WS defaults to `ws://127.0.0.1:18789`; keep app + CLI on the same port. - **Where state lives:** - Credentials: `~/.clawdbot/credentials/` @@ -129,4 +126,4 @@ user service (no lingering needed). See [Gateway runbook](/gateway) for the syst - [Gateway configuration](/gateway/configuration) (config schema + examples) - [Discord](/providers/discord) and [Telegram](/providers/telegram) (reply tags + replyToMode settings) - [Clawdbot assistant setup](/start/clawd) -- [macOS app](/platforms/macos) (gateway lifecycle + “Attach only”) +- [macOS app](/platforms/macos) (gateway lifecycle) diff --git a/scripts/restart-mac.sh b/scripts/restart-mac.sh index 1618ad141..d75aac24d 100755 --- a/scripts/restart-mac.sh +++ b/scripts/restart-mac.sh @@ -91,13 +91,11 @@ for arg in "$@"; do log " CLAWDBOT_GATEWAY_WAIT_SECONDS=0 Wait time before gateway port check (unsigned only)" log "" log "Unsigned recovery:" - log " defaults write clawdbot.gateway.attachExistingOnly -bool YES" log " node dist/entry.js daemon install --force --runtime node" log " node dist/entry.js daemon restart" log "" log "Reset unsigned overrides:" log " rm ~/.clawdbot/disable-launchagent" - log " defaults write clawdbot.gateway.attachExistingOnly -bool NO" log "" log "Default behavior: Auto-detect signing keys, fallback to --no-sign if none found" exit 0 @@ -203,20 +201,9 @@ choose_app_bundle() { choose_app_bundle -APP_BUNDLE_ID="$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "${APP_BUNDLE}/Contents/Info.plist" 2>/dev/null || true)" - -# When unsigned, avoid the app overwriting the LaunchAgent while iterating. -if [ "$NO_SIGN" -eq 1 ]; then - if [[ -n "${APP_BUNDLE_ID}" ]]; then - run_step "set attach-existing-only" \ - /usr/bin/defaults write "${APP_BUNDLE_ID}" clawdbot.gateway.attachExistingOnly -bool YES - fi -elif [[ -f "${LAUNCHAGENT_DISABLE_MARKER}" ]]; then +# When signed, clear any previous launchagent override marker. +if [[ "$NO_SIGN" -ne 1 && -f "${LAUNCHAGENT_DISABLE_MARKER}" ]]; then run_step "clear launchagent disable marker" /bin/rm -f "${LAUNCHAGENT_DISABLE_MARKER}" - if [[ -n "${APP_BUNDLE_ID}" ]]; then - run_step "unset attach-existing-only" \ - /usr/bin/defaults write "${APP_BUNDLE_ID}" clawdbot.gateway.attachExistingOnly -bool NO - fi fi # 4) Launch the installed app in the foreground so the menu bar extra appears. diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index c700f440f..5b00ab973 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -133,30 +133,10 @@ function noteOpencodeProviderOverrides(cfg: ClawdbotConfig) { note(lines.join("\n"), "OpenCode Zen"); } -const MAC_APP_BUNDLE_ID = "com.clawdbot.mac"; -const MAC_ATTACH_EXISTING_ONLY_KEY = "clawdbot.gateway.attachExistingOnly"; - function resolveHomeDir(): string { return process.env.HOME ?? os.homedir(); } -async function readMacAttachExistingOnly(): Promise { - const result = await runCommandWithTimeout( - [ - "/usr/bin/defaults", - "read", - MAC_APP_BUNDLE_ID, - MAC_ATTACH_EXISTING_ONLY_KEY, - ], - { timeoutMs: 2000 }, - ).catch(() => null); - if (!result || result.code !== 0) return null; - const raw = result.stdout.trim().toLowerCase(); - if (["1", "true", "yes"].includes(raw)) return true; - if (["0", "false", "no"].includes(raw)) return false; - return null; -} - async function noteMacLaunchAgentOverrides() { if (process.platform !== "darwin") return; const markerPath = path.join( @@ -165,17 +145,12 @@ async function noteMacLaunchAgentOverrides() { "disable-launchagent", ); const hasMarker = fs.existsSync(markerPath); - const attachOnly = await readMacAttachExistingOnly(); - if (!hasMarker && attachOnly !== true) return; + if (!hasMarker) return; const lines = [ - hasMarker ? `- LaunchAgent writes are disabled via ${markerPath}.` : null, - attachOnly === true - ? `- macOS app is set to Attach-only (${MAC_APP_BUNDLE_ID}:${MAC_ATTACH_EXISTING_ONLY_KEY}=true).` - : null, + `- LaunchAgent writes are disabled via ${markerPath}.`, "- To restore default behavior:", ` rm ${markerPath}`, - ` defaults write ${MAC_APP_BUNDLE_ID} ${MAC_ATTACH_EXISTING_ONLY_KEY} -bool NO`, ].filter((line): line is string => Boolean(line)); note(lines.join("\n"), "Gateway (macOS)"); }