feat: profile-aware gateway service names (#671)
Thanks @bjesuiter. Co-authored-by: Benjamin Jesuiter <bjesuiter@gmail.com>
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -29,6 +29,7 @@ Label:
|
||||
|
||||
Plist location (per‑user):
|
||||
- `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
|
||||
(or `~/Library/LaunchAgents/com.clawdbot.<profile>.plist`)
|
||||
|
||||
Manager:
|
||||
- The macOS app owns LaunchAgent install/update in Local mode.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)}`);
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user