Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 50 additions & 23 deletions src/lib/actions/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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",
),
);
});
Expand All @@ -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",
),
);
});
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
21 changes: 17 additions & 4 deletions src/lib/actions/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -80,27 +88,32 @@ 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;
if (agent === "hermes") {
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(),
};
}

Expand Down
Loading