diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b1512198..b83f58b31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changes - Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics. +- Daemon: support profile-aware service names for multi-gateway setups. (#671) — thanks @bjesuiter. - Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors. - Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging. - Security: add `clawdbot security audit` (`--deep`, `--fix`) and surface it in `status --all` and `doctor`. diff --git a/docs/platforms/mac/bundled-gateway.md b/docs/platforms/mac/bundled-gateway.md index c8972e5b5..ef11dcc14 100644 --- a/docs/platforms/mac/bundled-gateway.md +++ b/docs/platforms/mac/bundled-gateway.md @@ -29,6 +29,7 @@ Label: Plist location (per‑user): - `~/Library/LaunchAgents/com.clawdbot.gateway.plist` + (or `~/Library/LaunchAgents/com.clawdbot..plist`) Manager: - The macOS app owns LaunchAgent install/update in Local mode. diff --git a/src/cli/daemon-cli/install.ts b/src/cli/daemon-cli/install.ts index 8704af6d0..1d4957c32 100644 --- a/src/cli/daemon-cli/install.ts +++ b/src/cli/daemon-cli/install.ts @@ -49,7 +49,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) { const profile = process.env.CLAWDBOT_PROFILE; let loaded = false; try { - loaded = await service.isLoaded({ env: process.env, profile }); + loaded = await service.isLoaded({ profile }); } catch (err) { defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.exit(1); diff --git a/src/cli/daemon-cli/lifecycle.ts b/src/cli/daemon-cli/lifecycle.ts index 14fa3edd3..bbe1ed9c4 100644 --- a/src/cli/daemon-cli/lifecycle.ts +++ b/src/cli/daemon-cli/lifecycle.ts @@ -24,7 +24,7 @@ export async function runDaemonStart() { const profile = process.env.CLAWDBOT_PROFILE; let loaded = false; try { - loaded = await service.isLoaded({ env: process.env, profile }); + loaded = await service.isLoaded({ profile }); } catch (err) { defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.exit(1); @@ -38,11 +38,7 @@ export async function runDaemonStart() { return; } try { - await service.restart({ - env: process.env, - profile, - stdout: process.stdout, - }); + await service.restart({ profile, stdout: process.stdout }); } catch (err) { defaultRuntime.error(`Gateway start failed: ${String(err)}`); for (const hint of renderGatewayServiceStartHints()) { @@ -57,7 +53,7 @@ export async function runDaemonStop() { const profile = process.env.CLAWDBOT_PROFILE; let loaded = false; try { - loaded = await service.isLoaded({ env: process.env, profile }); + loaded = await service.isLoaded({ profile }); } catch (err) { defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.exit(1); @@ -68,7 +64,7 @@ export async function runDaemonStop() { return; } try { - await service.stop({ env: process.env, profile, stdout: process.stdout }); + await service.stop({ profile, stdout: process.stdout }); } catch (err) { defaultRuntime.error(`Gateway stop failed: ${String(err)}`); defaultRuntime.exit(1); @@ -85,7 +81,7 @@ export async function runDaemonRestart(): Promise { const profile = process.env.CLAWDBOT_PROFILE; let loaded = false; try { - loaded = await service.isLoaded({ env: process.env, profile }); + loaded = await service.isLoaded({ profile }); } catch (err) { defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.exit(1); @@ -99,11 +95,7 @@ export async function runDaemonRestart(): Promise { return false; } try { - await service.restart({ - env: process.env, - profile, - stdout: process.stdout, - }); + await service.restart({ profile, stdout: process.stdout }); return true; } catch (err) { defaultRuntime.error(`Gateway restart failed: ${String(err)}`); diff --git a/src/cli/daemon-cli/status.gather.ts b/src/cli/daemon-cli/status.gather.ts index f21391779..ad5223bed 100644 --- a/src/cli/daemon-cli/status.gather.ts +++ b/src/cli/daemon-cli/status.gather.ts @@ -112,12 +112,7 @@ export async function gatherDaemonStatus( ): Promise { const service = resolveGatewayService(); const [loaded, command, runtime] = await Promise.all([ - service - .isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }) - .catch(() => false), + service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }).catch(() => false), service.readCommand(process.env).catch(() => null), service.readRuntime(process.env).catch(() => undefined), ]); diff --git a/src/cli/gateway-cli/shared.ts b/src/cli/gateway-cli/shared.ts index c0cca3191..3af93d337 100644 --- a/src/cli/gateway-cli/shared.ts +++ b/src/cli/gateway-cli/shared.ts @@ -89,10 +89,7 @@ export async function maybeExplainGatewayServiceStop() { const service = resolveGatewayService(); let loaded: boolean | null = null; try { - loaded = await service.isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }); + loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }); } catch { loaded = null; } diff --git a/src/commands/configure.daemon.ts b/src/commands/configure.daemon.ts index 01f88919e..c05a60d30 100644 --- a/src/commands/configure.daemon.ts +++ b/src/commands/configure.daemon.ts @@ -26,10 +26,7 @@ export async function maybeInstallDaemon(params: { daemonRuntime?: GatewayDaemonRuntime; }) { const service = resolveGatewayService(); - const loaded = await service.isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }); + const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }); let shouldCheckLinger = false; let shouldInstall = true; let daemonRuntime = params.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME; @@ -47,7 +44,6 @@ export async function maybeInstallDaemon(params: { ); if (action === "restart") { await service.restart({ - env: process.env, profile: process.env.CLAWDBOT_PROFILE, stdout: process.stdout, }); diff --git a/src/commands/doctor-gateway-daemon-flow.ts b/src/commands/doctor-gateway-daemon-flow.ts index 6ce31df0d..7a6186054 100644 --- a/src/commands/doctor-gateway-daemon-flow.ts +++ b/src/commands/doctor-gateway-daemon-flow.ts @@ -37,10 +37,7 @@ export async function maybeRepairGatewayDaemon(params: { if (params.healthOk) return; const service = resolveGatewayService(); - const loaded = await service.isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }); + const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }); let serviceRuntime: Awaited> | undefined; if (loaded) { serviceRuntime = await service.readRuntime(process.env).catch(() => undefined); @@ -132,7 +129,6 @@ export async function maybeRepairGatewayDaemon(params: { }); if (start) { await service.restart({ - env: process.env, profile: process.env.CLAWDBOT_PROFILE, stdout: process.stdout, }); @@ -155,7 +151,6 @@ export async function maybeRepairGatewayDaemon(params: { }); if (restart) { await service.restart({ - env: process.env, profile: process.env.CLAWDBOT_PROFILE, stdout: process.stdout, }); diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index ad608d9ad..4cdce95c7 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -89,10 +89,7 @@ export async function maybeMigrateLegacyGatewayService( } const service = resolveGatewayService(); - const loaded = await service.isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }); + const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }); if (loaded) { note(`Clawdbot ${service.label} already ${service.loadedText}.`, "Gateway"); return; diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index f7857c12e..6ac40d485 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -212,10 +212,7 @@ export async function doctorCommand( const service = resolveGatewayService(); let loaded = false; try { - loaded = await service.isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }); + loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }); } catch { loaded = false; } diff --git a/src/commands/reset.ts b/src/commands/reset.ts index 6e6a4d09e..25e6bf428 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -41,14 +41,14 @@ async function stopGatewayIfRunning(runtime: RuntimeEnv) { const profile = process.env.CLAWDBOT_PROFILE; let loaded = false; try { - loaded = await service.isLoaded({ env: process.env, profile }); + loaded = await service.isLoaded({ profile }); } catch (err) { runtime.error(`Gateway service check failed: ${String(err)}`); return; } if (!loaded) return; try { - await service.stop({ env: process.env, profile, stdout: process.stdout }); + await service.stop({ profile, stdout: process.stdout }); } catch (err) { runtime.error(`Gateway stop failed: ${String(err)}`); } diff --git a/src/commands/status-all.ts b/src/commands/status-all.ts index 4c9a4667b..68610bccf 100644 --- a/src/commands/status-all.ts +++ b/src/commands/status-all.ts @@ -133,12 +133,7 @@ export async function statusAllCommand( try { const service = resolveGatewayService(); const [loaded, runtimeInfo, command] = await Promise.all([ - service - .isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }) - .catch(() => false), + service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }).catch(() => false), service.readRuntime(process.env).catch(() => undefined), service.readCommand(process.env).catch(() => null), ]); diff --git a/src/commands/status.daemon.ts b/src/commands/status.daemon.ts index 2fa5159c6..2fe3c6801 100644 --- a/src/commands/status.daemon.ts +++ b/src/commands/status.daemon.ts @@ -10,12 +10,7 @@ export async function getDaemonStatusSummary(): Promise<{ try { const service = resolveGatewayService(); const [loaded, runtime, command] = await Promise.all([ - service - .isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }) - .catch(() => false), + service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }).catch(() => false), service.readRuntime(process.env).catch(() => undefined), service.readCommand(process.env).catch(() => null), ]); diff --git a/src/commands/uninstall.ts b/src/commands/uninstall.ts index a7f014351..33831c913 100644 --- a/src/commands/uninstall.ts +++ b/src/commands/uninstall.ts @@ -58,7 +58,7 @@ async function stopAndUninstallService(runtime: RuntimeEnv): Promise { const profile = process.env.CLAWDBOT_PROFILE; let loaded = false; try { - loaded = await service.isLoaded({ env: process.env, profile }); + loaded = await service.isLoaded({ profile }); } catch (err) { runtime.error(`Gateway service check failed: ${String(err)}`); return false; @@ -68,7 +68,7 @@ async function stopAndUninstallService(runtime: RuntimeEnv): Promise { return true; } try { - await service.stop({ env: process.env, profile, stdout: process.stdout }); + await service.stop({ profile, stdout: process.stdout }); } catch (err) { runtime.error(`Gateway stop failed: ${String(err)}`); } diff --git a/src/daemon/constants.ts b/src/daemon/constants.ts index a33cb7cfd..4f7e8c565 100644 --- a/src/daemon/constants.ts +++ b/src/daemon/constants.ts @@ -1,3 +1,4 @@ +// Default service labels (for backward compatibility and when no profile specified) export const GATEWAY_LAUNCH_AGENT_LABEL = "com.clawdbot.gateway"; export const GATEWAY_SYSTEMD_SERVICE_NAME = "clawdbot-gateway"; export const GATEWAY_WINDOWS_TASK_NAME = "Clawdbot Gateway"; diff --git a/src/daemon/launchd.ts b/src/daemon/launchd.ts index 89a65eed8..9734bdec2 100644 --- a/src/daemon/launchd.ts +++ b/src/daemon/launchd.ts @@ -47,8 +47,7 @@ function resolveLaunchAgentPlistPathForLabel( } export function resolveLaunchAgentPlistPath(env: Record): string { - const label = - env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE); + const label = resolveLaunchAgentLabel({ env }); return resolveLaunchAgentPlistPathForLabel(env, label); } @@ -206,8 +205,7 @@ export async function readLaunchAgentRuntime( env: Record, ): Promise { const domain = resolveGuiDomain(); - const label = - env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE); + const label = resolveLaunchAgentLabel({ env }); const res = await execLaunchctl(["print", `${domain}/${label}`]); if (res.code !== 0) { return { @@ -309,6 +307,7 @@ export async function uninstallLaunchAgent({ stdout: NodeJS.WritableStream; }): Promise { const domain = resolveGuiDomain(); + const label = resolveLaunchAgentLabel({ env }); const plistPath = resolveLaunchAgentPlistPath(env); await execLaunchctl(["bootout", domain, plistPath]); await execLaunchctl(["unload", plistPath]); @@ -322,8 +321,6 @@ export async function uninstallLaunchAgent({ const home = resolveHomeDir(env); const trashDir = path.join(home, ".Trash"); - const label = - env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE); const dest = path.join(trashDir, `${label}.plist`); try { await fs.mkdir(trashDir, { recursive: true }); @@ -378,8 +375,7 @@ export async function installLaunchAgent({ await fs.mkdir(logDir, { recursive: true }); const domain = resolveGuiDomain(); - const label = - env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE); + const label = resolveLaunchAgentLabel({ env }); for (const legacyLabel of LEGACY_GATEWAY_LAUNCH_AGENT_LABELS) { const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(env, legacyLabel); await execLaunchctl(["bootout", domain, legacyPlistPath]); diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index 32bc1893a..a65310b88 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -112,10 +112,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption ); } const service = resolveGatewayService(); - const loaded = await service.isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - }); + const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }); if (loaded) { const action = (await prompter.select({ message: "Gateway service already installed", @@ -127,7 +124,6 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption })) as "restart" | "reinstall" | "skip"; if (action === "restart") { await service.restart({ - env: process.env, profile: process.env.CLAWDBOT_PROFILE, stdout: process.stdout, }); @@ -139,10 +135,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption if ( !loaded || (loaded && - (await service.isLoaded({ - env: process.env, - profile: process.env.CLAWDBOT_PROFILE, - })) === false) + (await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })) === false) ) { const devMode = process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts");