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
- 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`.

View File

@@ -29,6 +29,7 @@ Label:
Plist location (peruser):
- `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
(or `~/Library/LaunchAgents/com.clawdbot.<profile>.plist`)
Manager:
- 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;
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);

View File

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

View File

@@ -112,12 +112,7 @@ export async function gatherDaemonStatus(
): Promise<DaemonStatus> {
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),
]);

View File

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

View File

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

View File

@@ -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<ReturnType<typeof service.readRuntime>> | 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,
});

View File

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

View File

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

View File

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

View File

@@ -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),
]);

View File

@@ -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),
]);

View File

@@ -58,7 +58,7 @@ async function stopAndUninstallService(runtime: RuntimeEnv): Promise<boolean> {
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<boolean> {
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)}`);
}

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_SYSTEMD_SERVICE_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 {
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<string, string | undefined>,
): Promise<GatewayServiceRuntime> {
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<void> {
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]);

View File

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