Skip to content

Commit 01641b3

Browse files
committed
feat(doctor): audit supervisor config + docs
1 parent d0c4ce6 commit 01641b3

9 files changed

Lines changed: 310 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Doctor/Daemon: audit supervisor configs, recommend doctor from daemon status, and document user vs system services. (#?) — thanks @steipete
56
- Daemon: align generated systemd unit with docs for network-online + restart delay. (#479) — thanks @azade-c
67
- Outbound: default Telegram account selection for config-only tokens; remove heartbeat-specific accountId handling. (follow-up #516) — thanks @YuriNachos
78
- Cron: allow Telegram delivery targets with topic/thread IDs (e.g. `-100…:topic:123`). (#474) — thanks @mitschabaude-bot

docs/gateway/doctor.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ cat ~/.clawdbot/clawdbot.json
5151
- Sandbox image repair when sandboxing is enabled.
5252
- Legacy service migration and extra gateway detection.
5353
- Gateway runtime checks (service installed but not running; cached launchd label).
54+
- Supervisor config audit (launchd/systemd/schtasks) with optional repair.
5455
- Gateway port collision diagnostics (default `18789`).
5556
- Security warnings for open DM policies.
5657
- systemd linger check on Linux.
@@ -143,17 +144,23 @@ workspace.
143144
Doctor runs a health check and offers to restart the gateway when it looks
144145
unhealthy.
145146

146-
### 11) Gateway runtime + port diagnostics
147+
### 11) Supervisor config audit + repair
148+
Doctor checks the installed supervisor config (launchd/systemd/schtasks) for
149+
missing or outdated defaults (e.g., systemd network-online dependencies and
150+
restart delay). When it finds a mismatch, it recommends an update and can
151+
rewrite the service file/task to the current defaults.
152+
153+
### 12) Gateway runtime + port diagnostics
147154
Doctor inspects the daemon runtime (PID, last exit status) and warns when the
148155
service is installed but not actually running. It also checks for port collisions
149156
on the gateway port (default `18789`) and reports likely causes (gateway already
150157
running, SSH tunnel).
151158

152-
### 12) Config write + wizard metadata
159+
### 13) Config write + wizard metadata
153160
Doctor persists any config changes and stamps wizard metadata to record the
154161
doctor run.
155162

156-
### 13) Workspace tips (backup + memory system)
163+
### 14) Workspace tips (backup + memory system)
157164
Doctor suggests a workspace memory system when missing and prints a backup tip
158165
if the workspace is not already under git.
159166

docs/gateway/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ Bundled mac app:
189189
- `launchctl` only works if the LaunchAgent is installed; otherwise use `clawdbot daemon install` first.
190190

191191
## Supervision (systemd user unit)
192+
Clawdbot installs a **systemd user service** by default on Linux/WSL2. We
193+
recommend user services for single-user machines (simpler env, per-user config).
194+
Use a **system service** for multi-user or always-on servers (no lingering
195+
required, shared supervision).
196+
197+
`clawdbot daemon install` writes the user unit. `clawdbot doctor` audits the
198+
unit and can update it to match the current recommended defaults.
199+
192200
Create `~/.config/systemd/user/clawdbot-gateway.service`:
193201
```
194202
[Unit]

docs/platforms/linux.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ clawdbot doctor
5454
```
5555

5656
## System control (systemd user unit)
57-
Full unit example lives in the [Gateway runbook](/gateway). Minimal setup:
57+
Clawdbot installs a systemd **user** service by default. Use a **system**
58+
service for shared or always-on servers. The full unit example and guidance
59+
live in the [Gateway runbook](/gateway).
60+
61+
Minimal setup:
5862

5963
Create `~/.config/systemd/user/clawdbot-gateway.service`:
6064

src/cli/daemon-cli.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import { resolveGatewayLogPaths } from "../daemon/launchd.js";
3333
import { findLegacyGatewayServices } from "../daemon/legacy.js";
3434
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
3535
import { resolveGatewayService } from "../daemon/service.js";
36+
import type { ServiceConfigAudit } from "../daemon/service-audit.js";
37+
import { auditGatewayServiceConfig } from "../daemon/service-audit.js";
3638
import { callGateway } from "../gateway/call.js";
3739
import { resolveGatewayBindHost } from "../gateway/net.js";
3840
import {
@@ -89,6 +91,7 @@ type DaemonStatus = {
8991
cachedLabel?: boolean;
9092
missingUnit?: boolean;
9193
};
94+
configAudit?: ServiceConfigAudit;
9295
};
9396
config?: {
9497
cli: ConfigSummary;
@@ -343,6 +346,10 @@ async function gatherDaemonStatus(opts: {
343346
service.readCommand(process.env).catch(() => null),
344347
service.readRuntime(process.env).catch(() => undefined),
345348
]);
349+
const configAudit = await auditGatewayServiceConfig({
350+
env: process.env,
351+
command,
352+
});
346353

347354
const serviceEnv = command?.environment ?? undefined;
348355
const mergedDaemonEnv = {
@@ -484,6 +491,7 @@ async function gatherDaemonStatus(opts: {
484491
notLoadedText: service.notLoadedText,
485492
command,
486493
runtime,
494+
configAudit,
487495
},
488496
config: {
489497
cli: cliConfigSummary,
@@ -538,6 +546,16 @@ function printDaemonStatus(status: DaemonStatus, opts: { json: boolean }) {
538546
if (daemonEnvLines.length > 0) {
539547
defaultRuntime.log(`Daemon env: ${daemonEnvLines.join(" ")}`);
540548
}
549+
if (service.configAudit?.issues.length) {
550+
defaultRuntime.error(
551+
"Service config looks out of date or non-standard.",
552+
);
553+
for (const issue of service.configAudit.issues) {
554+
const detail = issue.detail ? ` (${issue.detail})` : "";
555+
defaultRuntime.error(`Service config issue: ${issue.message}${detail}`);
556+
}
557+
defaultRuntime.error('Recommendation: run "clawdbot doctor".');
558+
}
541559
if (status.config) {
542560
const cliCfg = `${status.config.cli.path}${status.config.cli.exists ? "" : " (missing)"}${status.config.cli.valid ? "" : " (invalid)"}`;
543561
defaultRuntime.log(`Config (cli): ${cliCfg}`);

src/commands/doctor-gateway-services.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "../daemon/legacy.js";
1616
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
1717
import { resolveGatewayService } from "../daemon/service.js";
18+
import { auditGatewayServiceConfig } from "../daemon/service-audit.js";
1819
import type { RuntimeEnv } from "../runtime.js";
1920
import {
2021
DEFAULT_GATEWAY_DAEMON_RUNTIME,
@@ -23,6 +24,18 @@ import {
2324
} from "./daemon-runtime.js";
2425
import type { DoctorOptions, DoctorPrompter } from "./doctor-prompter.js";
2526

27+
function detectGatewayRuntime(
28+
programArguments: string[] | undefined,
29+
): GatewayDaemonRuntime {
30+
const first = programArguments?.[0];
31+
if (first) {
32+
const base = path.basename(first).toLowerCase();
33+
if (base === "bun" || base === "bun.exe") return "bun";
34+
if (base === "node" || base === "node.exe") return "node";
35+
}
36+
return DEFAULT_GATEWAY_DAEMON_RUNTIME;
37+
}
38+
2639
export async function maybeMigrateLegacyGatewayService(
2740
cfg: ClawdbotConfig,
2841
mode: "local" | "remote",
@@ -112,6 +125,83 @@ export async function maybeMigrateLegacyGatewayService(
112125
});
113126
}
114127

128+
export async function maybeRepairGatewayServiceConfig(
129+
cfg: ClawdbotConfig,
130+
mode: "local" | "remote",
131+
runtime: RuntimeEnv,
132+
prompter: DoctorPrompter,
133+
) {
134+
if (resolveIsNixMode(process.env)) {
135+
note("Nix mode detected; skip service updates.", "Gateway");
136+
return;
137+
}
138+
139+
if (mode === "remote") {
140+
note("Gateway mode is remote; skipped local service audit.", "Gateway");
141+
return;
142+
}
143+
144+
const service = resolveGatewayService();
145+
const command = await service.readCommand(process.env).catch(() => null);
146+
if (!command) return;
147+
148+
const audit = await auditGatewayServiceConfig({
149+
env: process.env,
150+
command,
151+
});
152+
if (audit.issues.length === 0) return;
153+
154+
note(
155+
audit.issues
156+
.map((issue) =>
157+
issue.detail ? `- ${issue.message} (${issue.detail})` : `- ${issue.message}`,
158+
)
159+
.join("\n"),
160+
"Gateway service config",
161+
);
162+
163+
const repair = await prompter.confirmSkipInNonInteractive({
164+
message: "Update gateway service config to the recommended defaults now?",
165+
initialValue: true,
166+
});
167+
if (!repair) return;
168+
169+
const devMode =
170+
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
171+
process.argv[1]?.endsWith(".ts");
172+
const port = resolveGatewayPort(cfg, process.env);
173+
const runtimeChoice = detectGatewayRuntime(command.programArguments);
174+
const { programArguments, workingDirectory } =
175+
await resolveGatewayProgramArguments({
176+
port,
177+
dev: devMode,
178+
runtime: runtimeChoice,
179+
});
180+
const environment: Record<string, string | undefined> = {
181+
PATH: process.env.PATH,
182+
CLAWDBOT_PROFILE: process.env.CLAWDBOT_PROFILE,
183+
CLAWDBOT_STATE_DIR: process.env.CLAWDBOT_STATE_DIR,
184+
CLAWDBOT_CONFIG_PATH: process.env.CLAWDBOT_CONFIG_PATH,
185+
CLAWDBOT_GATEWAY_PORT: String(port),
186+
CLAWDBOT_GATEWAY_TOKEN:
187+
cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
188+
CLAWDBOT_LAUNCHD_LABEL:
189+
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
190+
};
191+
192+
try {
193+
await service.install({
194+
env: process.env,
195+
stdout: process.stdout,
196+
programArguments,
197+
workingDirectory,
198+
environment,
199+
});
200+
} catch (err) {
201+
runtime.error(`Gateway service update failed: ${String(err)}`);
202+
}
203+
}
204+
115205
export async function maybeScanExtraGatewayServices(options: DoctorOptions) {
116206
const extraServices = await findExtraGatewayServices(process.env, {
117207
deep: options.deep,

src/commands/doctor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from "./doctor-format.js";
3131
import {
3232
maybeMigrateLegacyGatewayService,
33+
maybeRepairGatewayServiceConfig,
3334
maybeScanExtraGatewayServices,
3435
} from "./doctor-gateway-services.js";
3536
import {
@@ -157,6 +158,12 @@ export async function doctorCommand(
157158
prompter,
158159
);
159160
await maybeScanExtraGatewayServices(options);
161+
await maybeRepairGatewayServiceConfig(
162+
cfg,
163+
resolveMode(cfg),
164+
runtime,
165+
prompter,
166+
);
160167

161168
await noteSecurityWarnings(cfg);
162169

0 commit comments

Comments
 (0)