diff --git a/src/lib/actions/update.test.ts b/src/lib/actions/update.test.ts index eba856d098c..689020387d2 100644 --- a/src/lib/actions/update.test.ts +++ b/src/lib/actions/update.test.ts @@ -47,6 +47,7 @@ describe("runUpdateAction", () => { expect(spawnSyncImpl).not.toHaveBeenCalled(); expect(log).toHaveBeenCalledWith(expect.stringContaining("Current NemoClaw version: 0.1.0")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Latest maintained version: 0.2.0")); + expect(log).toHaveBeenCalledWith(expect.stringContaining(NEMOCLAW_UPDATE_COMMAND)); }); it("renders NemoHermes branding and installer guidance for --check when the Hermes alias is active", async () => { @@ -68,7 +69,7 @@ describe("runUpdateAction", () => { expect(log).toHaveBeenCalledWith(expect.stringContaining("Current NemoHermes version: 0.1.0")); expect(log).toHaveBeenCalledWith( expect.stringContaining( - "curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_AGENT=hermes bash", + "curl -fsSL --proto '=https' --proto-redir '=https' https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_AGENT=hermes bash", ), ); }); @@ -94,7 +95,7 @@ describe("runUpdateAction", () => { ); expect(log).toHaveBeenCalledWith( expect.stringContaining( - "curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_AGENT=langchain-deepagents-code bash", + "curl -fsSL --proto '=https' --proto-redir '=https' https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_AGENT=langchain-deepagents-code bash", ), ); }); @@ -201,6 +202,29 @@ describe("runUpdateAction", () => { expect(log).toHaveBeenCalledWith(expect.stringContaining("reinstalling anyway (--fresh)")); }); + it("restricts the maintained installer fetch and redirects to HTTPS", async () => { + const spawnSyncImpl = vi.fn( + () => ({ status: 0, stdout: "", stderr: "", signal: null }) as never, + ); + + const result = await runUpdateAction( + { yes: true }, + { + currentVersion: () => "0.1.0", + getMaintainedTarget: () => maintainedTarget("0.2.0"), + isSourceCheckout: () => false, + log: vi.fn(), + spawnSyncImpl, + }, + ); + + expect(result.ranInstaller).toBe(true); + const [command, args] = spawnSyncImpl.mock.calls[0] as unknown as [string, readonly string[]]; + expect(command).toBe("bash"); + expect(args.at(-1)).toContain("--proto '=https'"); + expect(args.at(-1)).toContain("--proto-redir '=https'"); + }); + it("refuses --fresh when the maintained tag is older than the install, even with --yes (#8306)", async () => { const spawnSyncImpl = vi.fn(); const error = vi.fn(); @@ -351,29 +375,32 @@ describe("runUpdateAction", () => { ["1.0.0", "01.0.0", "maintained release component"], ["1.0.0-01", "1.0.0-1", "installed prerelease identifier"], ["1.0.0-1", "1.0.0-01", "maintained prerelease identifier"], - ])("fails closed on --fresh for a leading zero in the %s (%s; %s) (#8306)", async (currentVersion, latestVersion) => { - const spawnSyncImpl = vi.fn(); - const error = vi.fn(); + ])( + "fails closed on --fresh for a leading zero in the %s (%s; %s) (#8306)", + async (currentVersion, latestVersion) => { + const spawnSyncImpl = vi.fn(); + const error = vi.fn(); - const result = await runUpdateAction( - { fresh: true, yes: true }, - { - currentVersion: () => currentVersion, - error, - getMaintainedTarget: () => maintainedTarget(latestVersion), - isSourceCheckout: () => false, - log: vi.fn(), - spawnSyncImpl, - }, - ); + const result = await runUpdateAction( + { fresh: true, yes: true }, + { + currentVersion: () => currentVersion, + error, + getMaintainedTarget: () => maintainedTarget(latestVersion), + isSourceCheckout: () => false, + log: vi.fn(), + spawnSyncImpl, + }, + ); - expect(result.ranInstaller).toBe(false); - expect(result.status).toBe(1); - expect(result.updateAvailable).toBeNull(); - expect(spawnSyncImpl).not.toHaveBeenCalled(); - expect(error).toHaveBeenCalledWith(expect.stringContaining("Cannot order")); - expect(error).toHaveBeenCalledWith(expect.stringContaining("--allow-downgrade")); - }); + expect(result.ranInstaller).toBe(false); + expect(result.status).toBe(1); + expect(result.updateAvailable).toBeNull(); + expect(spawnSyncImpl).not.toHaveBeenCalled(); + expect(error).toHaveBeenCalledWith(expect.stringContaining("Cannot order")); + expect(error).toHaveBeenCalledWith(expect.stringContaining("--allow-downgrade")); + }, + ); it("fails closed on --fresh when the maintained tag cannot be resolved (#8306)", async () => { const spawnSyncImpl = vi.fn(); diff --git a/src/lib/actions/update.ts b/src/lib/actions/update.ts index 167f1bbc15b..c7eef1918d8 100644 --- a/src/lib/actions/update.ts +++ b/src/lib/actions/update.ts @@ -11,7 +11,15 @@ import { normalizeVersion } from "../domain/installer/version"; export const NEMOCLAW_INSTALLER_URL = "https://www.nvidia.com/nemoclaw.sh"; export const NEMOCLAW_REPO_URL = "https://github.com/NVIDIA/NemoClaw.git"; -export const NEMOCLAW_UPDATE_COMMAND = `curl -fsSL ${NEMOCLAW_INSTALLER_URL} | bash`; +/** + * Pipeline `nemoclaw update` runs to install the maintained build. + * `--proto '=https'` and `--proto-redir '=https'` hold the transfer on HTTPS for + * the initial request and for every redirect, so a downgrade redirect fails + * closed instead of piping plaintext-fetched bytes into `bash`. Same fetch + * hardening as `src/lib/onboard/install-ollama-linux.ts`. + */ +const NEMOCLAW_UPDATE_FETCH_COMMAND = `curl -fsSL --proto '=https' --proto-redir '=https' ${NEMOCLAW_INSTALLER_URL}`; +export const NEMOCLAW_UPDATE_COMMAND = `${NEMOCLAW_UPDATE_FETCH_COMMAND} | bash`; export const NEMOCLAW_MAINTAINED_INSTALL_TAG = "lkg"; type LogFn = (message?: string) => void; @@ -80,6 +88,11 @@ function trimOutput(value: string | Buffer | null | undefined): string { const UPDATE_BRANDING_AGENTS = ["openclaw", "hermes", "langchain-deepagents-code"] as const; +function maintainedUpdateCommand(agent?: (typeof UPDATE_BRANDING_AGENTS)[number]): string { + const agentAssignment = agent ? `NEMOCLAW_AGENT=${agent} ` : ""; + return `${NEMOCLAW_UPDATE_FETCH_COMMAND} | ${agentAssignment}bash`; +} + function updateBranding(env: NodeJS.ProcessEnv): UpdateBranding { const agent = resolveAgentNameAlias(env.NEMOCLAW_AGENT, UPDATE_BRANDING_AGENTS) ?? env.NEMOCLAW_AGENT; @@ -87,20 +100,20 @@ function updateBranding(env: NodeJS.ProcessEnv): UpdateBranding { return { cliName: "nemohermes", displayName: "NemoHermes", - maintainedUpdateCommand: `curl -fsSL ${NEMOCLAW_INSTALLER_URL} | NEMOCLAW_AGENT=hermes bash`, + maintainedUpdateCommand: maintainedUpdateCommand("hermes"), }; } if (agent === "langchain-deepagents-code") { return { cliName: "nemo-deepagents", displayName: "NemoDeepAgents", - maintainedUpdateCommand: `curl -fsSL ${NEMOCLAW_INSTALLER_URL} | NEMOCLAW_AGENT=langchain-deepagents-code bash`, + maintainedUpdateCommand: maintainedUpdateCommand("langchain-deepagents-code"), }; } return { cliName: "nemoclaw", displayName: "NemoClaw", - maintainedUpdateCommand: NEMOCLAW_UPDATE_COMMAND, + maintainedUpdateCommand: maintainedUpdateCommand(), }; }