diff --git a/CHANGELOG.md b/CHANGELOG.md index 451513098ff..3ff497a4771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai - Setup/import: honor non-interactive `--import-from` onboarding flags by running the migration import path instead of silently completing normal setup without importing anything. Thanks @vincentkoc. - Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram. - Doctor/plugins: keep plain `doctor --non-interactive` from installing bundled plugin runtime dependencies, so headless health checks report missing deps while `doctor --fix` remains the explicit repair path. Thanks @vincentkoc. +- Doctor/gateway: require an interactive confirmation before installing or rewriting the Gateway service, so `doctor --fix --non-interactive` can repair plugin/config drift without replacing the operator's launchd/systemd service from a temporary environment. Thanks @vincentkoc. - Plugins/runtime-deps: include packaged OpenClaw identity in bundled plugin loader cache keys, so same-path package upgrades stop reusing stale versioned runtime-deps mirrors. Fixes #75045. Thanks @sahilsatralkar. - Plugin SDK: restore reply-prefix and reply-pipeline helpers on the deprecated root/compat SDK surface so external plugins still using `openclaw/plugin-sdk` do not fail message dispatch after update. Fixes #75171. Thanks @zhangxiliang. - Plugins/runtime-deps: prune inactive same-package versioned runtime-deps roots after bundled dependency repair, so upgrades do not leave old `openclaw--` package caches behind after doctor runs. Thanks @vincentkoc. diff --git a/src/commands/doctor-gateway-daemon-flow.test.ts b/src/commands/doctor-gateway-daemon-flow.test.ts index 37034f55c87..b3c8292a32b 100644 --- a/src/commands/doctor-gateway-daemon-flow.test.ts +++ b/src/commands/doctor-gateway-daemon-flow.test.ts @@ -185,6 +185,10 @@ describe("maybeRepairGatewayDaemon", () => { async function runNonInteractiveUpdateRepair() { process.env.OPENCLAW_UPDATE_IN_PROGRESS = "1"; + await runNonInteractiveRepair(); + } + + async function runNonInteractiveRepair() { const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; await maybeRepairGatewayDaemon({ cfg: { gateway: {} }, @@ -256,6 +260,20 @@ describe("maybeRepairGatewayDaemon", () => { expect(service.restart).not.toHaveBeenCalled(); }); + it("skips gateway install during non-interactive doctor repairs", async () => { + setPlatform("linux"); + service.isLoaded.mockResolvedValue(false); + + await runNonInteractiveRepair(); + + expect(service.install).not.toHaveBeenCalled(); + expect(service.restart).not.toHaveBeenCalled(); + expect(note).toHaveBeenCalledWith( + expect.stringContaining("openclaw gateway install"), + "Gateway", + ); + }); + it("skips gateway restart during non-interactive update repairs", async () => { setPlatform("linux"); diff --git a/src/commands/doctor-gateway-daemon-flow.ts b/src/commands/doctor-gateway-daemon-flow.ts index fa0d4d922d8..6210e039b39 100644 --- a/src/commands/doctor-gateway-daemon-flow.ts +++ b/src/commands/doctor-gateway-daemon-flow.ts @@ -199,9 +199,16 @@ export async function maybeRepairGatewayDaemon(params: { { message: "Install gateway service now?", initialValue: true, + requiresInteractiveConfirmation: true, }, serviceRepairPolicy, ); + if (!install) { + note( + `Run ${formatCliCommand("openclaw gateway install")} when you want to install the gateway service.`, + "Gateway", + ); + } if (install) { const daemonRuntime = await params.prompter.select( { diff --git a/src/commands/doctor-gateway-services.test.ts b/src/commands/doctor-gateway-services.test.ts index 556a6f8aabc..1077dec26b8 100644 --- a/src/commands/doctor-gateway-services.test.ts +++ b/src/commands/doctor-gateway-services.test.ts @@ -664,7 +664,7 @@ describe("maybeRepairGatewayServiceConfig", () => { expect(mocks.stage).not.toHaveBeenCalled(); }); - it("repairs entrypoint mismatch in non-interactive fix mode", async () => { + it("skips entrypoint rewrite in non-interactive fix mode", async () => { setupGatewayEntrypointRepairScenario({ currentEntrypoint: "/Users/test/Library/npm/node_modules/openclaw/dist/entry.js", installEntrypoint: "/Users/test/Library/npm/node_modules/openclaw/dist/index.js", @@ -680,8 +680,12 @@ describe("maybeRepairGatewayServiceConfig", () => { expect.stringContaining("Gateway service entrypoint does not match the current install."), "Gateway service config", ); + expect(mocks.note).toHaveBeenCalledWith( + expect.stringContaining("openclaw gateway install --force"), + "Gateway service config", + ); expect(mocks.stage).not.toHaveBeenCalled(); - expect(mocks.install).toHaveBeenCalledTimes(1); + expect(mocks.install).not.toHaveBeenCalled(); }); it("stages service config repairs during non-interactive update repairs", async () => { diff --git a/src/commands/doctor-gateway-services.ts b/src/commands/doctor-gateway-services.ts index 7ff29877cce..228aa1405cb 100644 --- a/src/commands/doctor-gateway-services.ts +++ b/src/commands/doctor-gateway-services.ts @@ -509,19 +509,32 @@ export async function maybeRepairGatewayServiceConfig( return; } - const repair = needsAggressive - ? await prompter.confirmAggressiveAutoFix({ - message: "Overwrite gateway service config with current defaults now?", - initialValue: prompter.shouldForce, - }) - : await prompter.confirmAutoFix({ - message: "Update gateway service config to the recommended defaults now?", - initialValue: true, + const updateRepairMode = isDoctorUpdateRepairMode(prompter.repairMode); + const repairMessage = needsAggressive + ? "Overwrite gateway service config with current defaults now?" + : "Update gateway service config to the recommended defaults now?"; + const repair = updateRepairMode + ? needsAggressive + ? await prompter.confirmAggressiveAutoFix({ + message: repairMessage, + initialValue: prompter.shouldForce, + }) + : await prompter.confirmAutoFix({ + message: repairMessage, + initialValue: true, + }) + : await prompter.confirmRuntimeRepair({ + message: repairMessage, + initialValue: needsAggressive ? prompter.shouldForce : true, + requiresInteractiveConfirmation: true, }); if (!repair) { + note( + "Run `openclaw gateway install --force` when you want to replace the gateway service definition.", + "Gateway service config", + ); return; } - const updateRepairMode = isDoctorUpdateRepairMode(prompter.repairMode); const serviceEmbeddedToken = readEmbeddedGatewayToken(command); const gatewayTokenForRepair = expectedGatewayToken ?? serviceEmbeddedToken; const configuredGatewayToken =