diff --git a/CHANGELOG.md b/CHANGELOG.md index b8fbcc5ad4a..7e82bb035a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -340,6 +340,7 @@ Docs: https://docs.openclaw.ai - Mattermost/Matrix: keep direct-message main-session route updates pinned to the configured DM owner so paired or temporarily allowed senders cannot redirect future shared-session replies. Thanks @vincentkoc. - Discord: keep SecretRef-backed bot tokens discoverable for message actions without resolving the token during schema generation, and resolve scoped channel SecretRefs before outbound agent message sends even when the tool is built from a config snapshot. Fixes #75324. Thanks @slideshow-dingo and @Conan-Scott. - Updates: run package post-install doctor repair with the managed Gateway service profile and state paths when a daemon is installed, so shell/profile mismatches no longer repair the caller state while the restarted Gateway keeps stale config. Thanks @vincentkoc. +- CLI/update: treat inherited Gateway service markers as origin hints and only block package replacement when the managed Gateway is still live, so self-updates can stop the service and continue safely. (#75729) Thanks @hxy91819. - Models/DeepInfra: declare DeepInfra manifest catalog discovery and derive its runtime fallback catalog from the manifest, restoring provider-filtered `models list --all --provider deepinfra` rows without duplicated static model data. Thanks @shakkernerd. - CLI/update: verify managed gateway restarts against the installed service port instead of the caller shell port, so package updates do not report a healthy daemon as failed when profiles use different gateway ports. Thanks @vincentkoc. - Gateway/agent: reject strict `openclaw agent --deliver` requests with missing delivery targets before starting the agent run, so users do not wait for a completed turn that cannot send anywhere. Thanks @vincentkoc. diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index f1d61e0f6dd..7e0fb3f9c07 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -184,7 +184,10 @@ vi.mock("../daemon/service.js", () => ({ ? (command.environment as NodeJS.ProcessEnv | undefined) : undefined), }; - const [loaded, runtime] = await Promise.all([serviceLoaded({ env }), serviceReadRuntime(env)]); + const [loaded, runtime] = await Promise.all([ + serviceLoaded({ env }).catch(() => false), + serviceReadRuntime(env).catch(() => undefined), + ]); return { installed: command !== null, loaded, @@ -1337,6 +1340,41 @@ describe("update-cli", () => { ); }); + it("refuses package updates from inherited gateway service env when runtime inspection is inconclusive", async () => { + mockPackageInstallStatus(createCaseDir("openclaw-update")); + serviceReadCommand.mockResolvedValue({ + programArguments: ["openclaw", "gateway", "run"], + environment: { + OPENCLAW_SERVICE_MARKER: "openclaw", + OPENCLAW_SERVICE_KIND: "gateway", + }, + }); + serviceReadRuntime.mockRejectedValueOnce(new Error("runtime probe failed")); + + await withEnvAsync( + { + OPENCLAW_SERVICE_MARKER: "openclaw", + OPENCLAW_SERVICE_KIND: "gateway", + }, + async () => { + await updateCommand({ yes: true }); + }, + ); + + expect(defaultRuntime.error).toHaveBeenCalledWith( + expect.stringContaining( + "Package updates cannot run from inside the gateway service process.", + ), + ); + expect(defaultRuntime.exit).toHaveBeenCalledWith(1); + expect(serviceStop).not.toHaveBeenCalled(); + expect(runGatewayUpdate).not.toHaveBeenCalled(); + expect(runCommandWithTimeout).not.toHaveBeenCalledWith( + ["npm", "i", "-g", "openclaw@latest", "--no-fund", "--no-audit", "--loglevel=error"], + expect.any(Object), + ); + }); + it("blocks package updates when the target requires a newer Node runtime", async () => { mockPackageInstallStatus(createCaseDir("openclaw-update")); vi.mocked(fetchNpmPackageTargetStatus).mockResolvedValue({ diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 2a52b31b011..deef66841f9 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -163,6 +163,7 @@ export function shouldUseLegacyProcessRestartAfterUpdate(params: { type PrePackageServiceStop = { stopped: boolean; inspected: boolean; + runtimeInspected: boolean; running: boolean; serviceEnv?: NodeJS.ProcessEnv; }; @@ -177,13 +178,14 @@ async function maybeStopManagedServiceBeforePackageUpdate(params: { service = resolveGatewayService(); serviceState = await readGatewayServiceState(service, { env: process.env }); } catch { - return { stopped: false, inspected: false, running: false }; + return { stopped: false, inspected: false, runtimeInspected: false, running: false }; } if (!serviceState.installed) { - return { stopped: false, inspected: true, running: false }; + return { stopped: false, inspected: true, runtimeInspected: true, running: false }; } + const runtimeInspected = Boolean(serviceState.runtime); if (!params.shouldRestart) { if (!params.jsonMode && serviceState.running) { defaultRuntime.log( @@ -195,20 +197,43 @@ async function maybeStopManagedServiceBeforePackageUpdate(params: { return { stopped: false, inspected: true, + runtimeInspected, running: serviceState.running, serviceEnv: serviceState.env, }; } + if (!runtimeInspected) { + return { + stopped: false, + inspected: true, + runtimeInspected: false, + running: false, + serviceEnv: serviceState.env, + }; + } + if (!serviceState.running) { - return { stopped: false, inspected: true, running: false, serviceEnv: serviceState.env }; + return { + stopped: false, + inspected: true, + runtimeInspected: true, + running: false, + serviceEnv: serviceState.env, + }; } if (!params.jsonMode) { defaultRuntime.log(theme.muted("Stopping managed gateway service before package update...")); } await service.stop({ env: serviceState.env, stdout: process.stdout }); - return { stopped: true, inspected: true, running: true, serviceEnv: serviceState.env }; + return { + stopped: true, + inspected: true, + runtimeInspected: true, + running: true, + serviceEnv: serviceState.env, + }; } async function maybeRestartServiceAfterFailedPackageUpdate(params: { @@ -256,10 +281,13 @@ function shouldBlockPackageUpdateFromGatewayServiceEnv(params: { if (!stopState?.inspected) { return true; } - if (!stopState.running) { + if (stopState.stopped) { return false; } - return !stopState.stopped; + if (!stopState.runtimeInspected) { + return true; + } + return stopState.running; } function formatCommandFailure(stdout: string, stderr: string): string {