feat: profile-aware gateway service names (#671)

Thanks @bjesuiter.

Co-authored-by: Benjamin Jesuiter <bjesuiter@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-15 05:22:09 +00:00
parent 1fe8df85cb
commit 77cf40da87
17 changed files with 28 additions and 77 deletions

View File

@@ -4,6 +4,7 @@
### Changes ### Changes
- Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics. - 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. - 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. - 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`. - Security: add `clawdbot security audit` (`--deep`, `--fix`) and surface it in `status --all` and `doctor`.

View File

@@ -29,6 +29,7 @@ Label:
Plist location (peruser): Plist location (peruser):
- `~/Library/LaunchAgents/com.clawdbot.gateway.plist` - `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
(or `~/Library/LaunchAgents/com.clawdbot.<profile>.plist`)
Manager: Manager:
- The macOS app owns LaunchAgent install/update in Local mode. - The macOS app owns LaunchAgent install/update in Local mode.

View File

@@ -49,7 +49,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
const profile = process.env.CLAWDBOT_PROFILE; const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false; let loaded = false;
try { try {
loaded = await service.isLoaded({ env: process.env, profile }); loaded = await service.isLoaded({ profile });
} catch (err) { } catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1); defaultRuntime.exit(1);

View File

@@ -24,7 +24,7 @@ export async function runDaemonStart() {
const profile = process.env.CLAWDBOT_PROFILE; const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false; let loaded = false;
try { try {
loaded = await service.isLoaded({ env: process.env, profile }); loaded = await service.isLoaded({ profile });
} catch (err) { } catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1); defaultRuntime.exit(1);
@@ -38,11 +38,7 @@ export async function runDaemonStart() {
return; return;
} }
try { try {
await service.restart({ await service.restart({ profile, stdout: process.stdout });
env: process.env,
profile,
stdout: process.stdout,
});
} catch (err) { } catch (err) {
defaultRuntime.error(`Gateway start failed: ${String(err)}`); defaultRuntime.error(`Gateway start failed: ${String(err)}`);
for (const hint of renderGatewayServiceStartHints()) { for (const hint of renderGatewayServiceStartHints()) {
@@ -57,7 +53,7 @@ export async function runDaemonStop() {
const profile = process.env.CLAWDBOT_PROFILE; const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false; let loaded = false;
try { try {
loaded = await service.isLoaded({ env: process.env, profile }); loaded = await service.isLoaded({ profile });
} catch (err) { } catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1); defaultRuntime.exit(1);
@@ -68,7 +64,7 @@ export async function runDaemonStop() {
return; return;
} }
try { try {
await service.stop({ env: process.env, profile, stdout: process.stdout }); await service.stop({ profile, stdout: process.stdout });
} catch (err) { } catch (err) {
defaultRuntime.error(`Gateway stop failed: ${String(err)}`); defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
defaultRuntime.exit(1); defaultRuntime.exit(1);
@@ -85,7 +81,7 @@ export async function runDaemonRestart(): Promise<boolean> {
const profile = process.env.CLAWDBOT_PROFILE; const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false; let loaded = false;
try { try {
loaded = await service.isLoaded({ env: process.env, profile }); loaded = await service.isLoaded({ profile });
} catch (err) { } catch (err) {
defaultRuntime.error(`Gateway service check failed: ${String(err)}`); defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
defaultRuntime.exit(1); defaultRuntime.exit(1);
@@ -99,11 +95,7 @@ export async function runDaemonRestart(): Promise<boolean> {
return false; return false;
} }
try { try {
await service.restart({ await service.restart({ profile, stdout: process.stdout });
env: process.env,
profile,
stdout: process.stdout,
});
return true; return true;
} catch (err) { } catch (err) {
defaultRuntime.error(`Gateway restart failed: ${String(err)}`); defaultRuntime.error(`Gateway restart failed: ${String(err)}`);

View File

@@ -112,12 +112,7 @@ export async function gatherDaemonStatus(
): Promise<DaemonStatus> { ): Promise<DaemonStatus> {
const service = resolveGatewayService(); const service = resolveGatewayService();
const [loaded, command, runtime] = await Promise.all([ const [loaded, command, runtime] = await Promise.all([
service service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }).catch(() => false),
.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})
.catch(() => false),
service.readCommand(process.env).catch(() => null), service.readCommand(process.env).catch(() => null),
service.readRuntime(process.env).catch(() => undefined), service.readRuntime(process.env).catch(() => undefined),
]); ]);

View File

@@ -89,10 +89,7 @@ export async function maybeExplainGatewayServiceStop() {
const service = resolveGatewayService(); const service = resolveGatewayService();
let loaded: boolean | null = null; let loaded: boolean | null = null;
try { try {
loaded = await service.isLoaded({ loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
} catch { } catch {
loaded = null; loaded = null;
} }

View File

@@ -26,10 +26,7 @@ export async function maybeInstallDaemon(params: {
daemonRuntime?: GatewayDaemonRuntime; daemonRuntime?: GatewayDaemonRuntime;
}) { }) {
const service = resolveGatewayService(); const service = resolveGatewayService();
const loaded = await service.isLoaded({ const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
let shouldCheckLinger = false; let shouldCheckLinger = false;
let shouldInstall = true; let shouldInstall = true;
let daemonRuntime = params.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME; let daemonRuntime = params.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME;
@@ -47,7 +44,6 @@ export async function maybeInstallDaemon(params: {
); );
if (action === "restart") { if (action === "restart") {
await service.restart({ await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE, profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout, stdout: process.stdout,
}); });

View File

@@ -37,10 +37,7 @@ export async function maybeRepairGatewayDaemon(params: {
if (params.healthOk) return; if (params.healthOk) return;
const service = resolveGatewayService(); const service = resolveGatewayService();
const loaded = await service.isLoaded({ const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
let serviceRuntime: Awaited<ReturnType<typeof service.readRuntime>> | undefined; let serviceRuntime: Awaited<ReturnType<typeof service.readRuntime>> | undefined;
if (loaded) { if (loaded) {
serviceRuntime = await service.readRuntime(process.env).catch(() => undefined); serviceRuntime = await service.readRuntime(process.env).catch(() => undefined);
@@ -132,7 +129,6 @@ export async function maybeRepairGatewayDaemon(params: {
}); });
if (start) { if (start) {
await service.restart({ await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE, profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout, stdout: process.stdout,
}); });
@@ -155,7 +151,6 @@ export async function maybeRepairGatewayDaemon(params: {
}); });
if (restart) { if (restart) {
await service.restart({ await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE, profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout, stdout: process.stdout,
}); });

View File

@@ -89,10 +89,7 @@ export async function maybeMigrateLegacyGatewayService(
} }
const service = resolveGatewayService(); const service = resolveGatewayService();
const loaded = await service.isLoaded({ const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
if (loaded) { if (loaded) {
note(`Clawdbot ${service.label} already ${service.loadedText}.`, "Gateway"); note(`Clawdbot ${service.label} already ${service.loadedText}.`, "Gateway");
return; return;

View File

@@ -212,10 +212,7 @@ export async function doctorCommand(
const service = resolveGatewayService(); const service = resolveGatewayService();
let loaded = false; let loaded = false;
try { try {
loaded = await service.isLoaded({ loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
} catch { } catch {
loaded = false; loaded = false;
} }

View File

@@ -41,14 +41,14 @@ async function stopGatewayIfRunning(runtime: RuntimeEnv) {
const profile = process.env.CLAWDBOT_PROFILE; const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false; let loaded = false;
try { try {
loaded = await service.isLoaded({ env: process.env, profile }); loaded = await service.isLoaded({ profile });
} catch (err) { } catch (err) {
runtime.error(`Gateway service check failed: ${String(err)}`); runtime.error(`Gateway service check failed: ${String(err)}`);
return; return;
} }
if (!loaded) return; if (!loaded) return;
try { try {
await service.stop({ env: process.env, profile, stdout: process.stdout }); await service.stop({ profile, stdout: process.stdout });
} catch (err) { } catch (err) {
runtime.error(`Gateway stop failed: ${String(err)}`); runtime.error(`Gateway stop failed: ${String(err)}`);
} }

View File

@@ -133,12 +133,7 @@ export async function statusAllCommand(
try { try {
const service = resolveGatewayService(); const service = resolveGatewayService();
const [loaded, runtimeInfo, command] = await Promise.all([ const [loaded, runtimeInfo, command] = await Promise.all([
service service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }).catch(() => false),
.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})
.catch(() => false),
service.readRuntime(process.env).catch(() => undefined), service.readRuntime(process.env).catch(() => undefined),
service.readCommand(process.env).catch(() => null), service.readCommand(process.env).catch(() => null),
]); ]);

View File

@@ -10,12 +10,7 @@ export async function getDaemonStatusSummary(): Promise<{
try { try {
const service = resolveGatewayService(); const service = resolveGatewayService();
const [loaded, runtime, command] = await Promise.all([ const [loaded, runtime, command] = await Promise.all([
service service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }).catch(() => false),
.isLoaded({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})
.catch(() => false),
service.readRuntime(process.env).catch(() => undefined), service.readRuntime(process.env).catch(() => undefined),
service.readCommand(process.env).catch(() => null), service.readCommand(process.env).catch(() => null),
]); ]);

View File

@@ -58,7 +58,7 @@ async function stopAndUninstallService(runtime: RuntimeEnv): Promise<boolean> {
const profile = process.env.CLAWDBOT_PROFILE; const profile = process.env.CLAWDBOT_PROFILE;
let loaded = false; let loaded = false;
try { try {
loaded = await service.isLoaded({ env: process.env, profile }); loaded = await service.isLoaded({ profile });
} catch (err) { } catch (err) {
runtime.error(`Gateway service check failed: ${String(err)}`); runtime.error(`Gateway service check failed: ${String(err)}`);
return false; return false;
@@ -68,7 +68,7 @@ async function stopAndUninstallService(runtime: RuntimeEnv): Promise<boolean> {
return true; return true;
} }
try { try {
await service.stop({ env: process.env, profile, stdout: process.stdout }); await service.stop({ profile, stdout: process.stdout });
} catch (err) { } catch (err) {
runtime.error(`Gateway stop failed: ${String(err)}`); runtime.error(`Gateway stop failed: ${String(err)}`);
} }

View File

@@ -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_LAUNCH_AGENT_LABEL = "com.clawdbot.gateway";
export const GATEWAY_SYSTEMD_SERVICE_NAME = "clawdbot-gateway"; export const GATEWAY_SYSTEMD_SERVICE_NAME = "clawdbot-gateway";
export const GATEWAY_WINDOWS_TASK_NAME = "Clawdbot Gateway"; export const GATEWAY_WINDOWS_TASK_NAME = "Clawdbot Gateway";

View File

@@ -47,8 +47,7 @@ function resolveLaunchAgentPlistPathForLabel(
} }
export function resolveLaunchAgentPlistPath(env: Record<string, string | undefined>): string { export function resolveLaunchAgentPlistPath(env: Record<string, string | undefined>): string {
const label = const label = resolveLaunchAgentLabel({ env });
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
return resolveLaunchAgentPlistPathForLabel(env, label); return resolveLaunchAgentPlistPathForLabel(env, label);
} }
@@ -206,8 +205,7 @@ export async function readLaunchAgentRuntime(
env: Record<string, string | undefined>, env: Record<string, string | undefined>,
): Promise<GatewayServiceRuntime> { ): Promise<GatewayServiceRuntime> {
const domain = resolveGuiDomain(); const domain = resolveGuiDomain();
const label = const label = resolveLaunchAgentLabel({ env });
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
const res = await execLaunchctl(["print", `${domain}/${label}`]); const res = await execLaunchctl(["print", `${domain}/${label}`]);
if (res.code !== 0) { if (res.code !== 0) {
return { return {
@@ -309,6 +307,7 @@ export async function uninstallLaunchAgent({
stdout: NodeJS.WritableStream; stdout: NodeJS.WritableStream;
}): Promise<void> { }): Promise<void> {
const domain = resolveGuiDomain(); const domain = resolveGuiDomain();
const label = resolveLaunchAgentLabel({ env });
const plistPath = resolveLaunchAgentPlistPath(env); const plistPath = resolveLaunchAgentPlistPath(env);
await execLaunchctl(["bootout", domain, plistPath]); await execLaunchctl(["bootout", domain, plistPath]);
await execLaunchctl(["unload", plistPath]); await execLaunchctl(["unload", plistPath]);
@@ -322,8 +321,6 @@ export async function uninstallLaunchAgent({
const home = resolveHomeDir(env); const home = resolveHomeDir(env);
const trashDir = path.join(home, ".Trash"); const trashDir = path.join(home, ".Trash");
const label =
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
const dest = path.join(trashDir, `${label}.plist`); const dest = path.join(trashDir, `${label}.plist`);
try { try {
await fs.mkdir(trashDir, { recursive: true }); await fs.mkdir(trashDir, { recursive: true });
@@ -378,8 +375,7 @@ export async function installLaunchAgent({
await fs.mkdir(logDir, { recursive: true }); await fs.mkdir(logDir, { recursive: true });
const domain = resolveGuiDomain(); const domain = resolveGuiDomain();
const label = const label = resolveLaunchAgentLabel({ env });
env.CLAWDBOT_LAUNCHD_LABEL?.trim() || resolveGatewayLaunchAgentLabel(env.CLAWDBOT_PROFILE);
for (const legacyLabel of LEGACY_GATEWAY_LAUNCH_AGENT_LABELS) { for (const legacyLabel of LEGACY_GATEWAY_LAUNCH_AGENT_LABELS) {
const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(env, legacyLabel); const legacyPlistPath = resolveLaunchAgentPlistPathForLabel(env, legacyLabel);
await execLaunchctl(["bootout", domain, legacyPlistPath]); await execLaunchctl(["bootout", domain, legacyPlistPath]);

View File

@@ -112,10 +112,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
); );
} }
const service = resolveGatewayService(); const service = resolveGatewayService();
const loaded = await service.isLoaded({ const loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
});
if (loaded) { if (loaded) {
const action = (await prompter.select({ const action = (await prompter.select({
message: "Gateway service already installed", message: "Gateway service already installed",
@@ -127,7 +124,6 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
})) as "restart" | "reinstall" | "skip"; })) as "restart" | "reinstall" | "skip";
if (action === "restart") { if (action === "restart") {
await service.restart({ await service.restart({
env: process.env,
profile: process.env.CLAWDBOT_PROFILE, profile: process.env.CLAWDBOT_PROFILE,
stdout: process.stdout, stdout: process.stdout,
}); });
@@ -139,10 +135,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
if ( if (
!loaded || !loaded ||
(loaded && (loaded &&
(await service.isLoaded({ (await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE })) === false)
env: process.env,
profile: process.env.CLAWDBOT_PROFILE,
})) === false)
) { ) {
const devMode = const devMode =
process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts"); process.argv[1]?.includes(`${path.sep}src${path.sep}`) && process.argv[1]?.endsWith(".ts");