refactor: remove mac attach-only setting

This commit is contained in:
Peter Steinberger
2026-01-12 04:38:42 +00:00
parent 8e1cdf3a1f
commit 51d5f16770
15 changed files with 18 additions and 126 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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() }
}

View File

@@ -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"

View File

@@ -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).

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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: {

View File

@@ -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))
}
}

View File

@@ -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
autostart at login and restart on crashes.
Childprocess mode (Gateway spawned directly by the app) is **not in use** today.
If you need tighter coupling to the UI, use **Attachonly** 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.<profile>` when running a named profile.
## Attachonly (developer mode)
Attachonly tells the app to **connect to an existing Gateway** without spawning
one. This is ideal for local dev (hotreload, 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 dont 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

View File

@@ -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)

View File

@@ -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 <bundle-id> 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 <bundle-id> 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.

View File

@@ -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<boolean | null> {
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)");
}