refactor: remove mac attach-only setting
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.<profile>` 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user