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
1 change: 1 addition & 0 deletions docs/get-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ Use these details when your first-run path needs more control.
At any prompt, press Enter to accept the default shown in `[brackets]`, type `back` to return to the previous prompt, or type `exit` to quit.

If registered sandboxes already exist, the installer prepares the current NemoClaw CLI without replacing OpenShell, requires a fresh backup of every registered sandbox before it changes the gateway, and runs `nemoclaw upgrade-sandboxes --auto` after the host upgrade.
After backup, it retires the running gateway before replacing OpenShell only when the installed OpenShell version is outside the current release's supported range; an unknown installed version or an invalid or missing range stops the update without retiring the gateway, while any retirement failure stops the update with the sandbox backups preserved.
Successful recovery rebuilds stale sandboxes, restores validated backups for registered sandboxes that are not Ready, and skips generic onboarding rather than creating an additional sandbox or requesting a new provider credential.
If the recovery pass exits 0 but a recorded sandbox is not found on its own recorded gateway, such as after `nemoclaw uninstall` removed the gateway and Docker image while preserving `sandboxes.json`, the installer finishes with `Installation completed with warnings` and remediation guidance instead of claiming the sandbox was recovered.
For pre-fingerprint OpenClaw and Hermes registry entries, confirm that every listed sandbox used a NemoClaw-managed image before recovery onto the current managed image.
Expand Down
3 changes: 3 additions & 0 deletions docs/manage-sandboxes/update-sandboxes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_INSTALL_TAG=lkg bash

Before upgrade work, the installer prepares the current NemoClaw CLI without replacing OpenShell and requires a fresh backup of every registered sandbox.
If any sandbox is skipped or fails, the installer exits before it changes the gateway.
After backup, the installer compares the installed OpenShell version with the supported range declared by the prepared current source.
It retires the running gateway before replacing an out-of-range OpenShell installation, keeps the gateway when the installed version is supported, and stops without retiring it when the installed version or supported range cannot be validated.
If the installed OpenShell release cannot retire its gateway through a supported lifecycle command or the verified NemoClaw-owned gateway PID, the installer also stops after backup with the sandbox backups preserved.

After the host CLI and OpenShell update, the installer runs `$$nemoclaw upgrade-sandboxes --auto` to reconcile the existing sandboxes.

Expand Down
5 changes: 4 additions & 1 deletion docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,10 @@ NEMOCLAW_SINGLE_SESSION=1 curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
```

When existing sandboxes were created with OpenShell earlier than `0.0.37`, the installer prompts before running the new automatic gateway upgrade path.
For scripted installs, set `NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE=1` to allow the installer to prepare the current CLI without replacing OpenShell, back up every registered sandbox with the current state manifest, retire the old gateway, install the supported OpenShell release, and recover the existing sandboxes.
For scripted installs, set `NEMOCLAW_ACCEPT_EXPERIMENTAL_OPENSHELL_UPGRADE=1` to allow the installer to prepare the current CLI without replacing OpenShell, back up every registered sandbox with the current state manifest, retire an installed gateway whose OpenShell version is outside the current release's supported range, install the supported OpenShell release, and recover the existing sandboxes.
The installer reads that supported range from the prepared current source and stops without retiring the gateway if the installed version is unknown or the range is missing or invalid.
When the installed OpenShell version is already supported, the installer keeps the running gateway through the host update.
If an out-of-range gateway cannot be retired through a supported lifecycle command or the verified NemoClaw-owned gateway PID, the installer stops after backup with the sandbox backups preserved.
If any registered sandbox cannot be backed up, the installer aborts before it changes the gateway.
When the registry contains a pre-fingerprint OpenClaw or Hermes entry with no recorded custom-image evidence, an interactive install asks you to confirm that the listed sandbox used a NemoClaw-managed image.
For a non-interactive install, set `NEMOCLAW_CONFIRM_LEGACY_MANAGED_RECREATE` to the exact JSON array of names printed by the installer, such as `["my-assistant","preserve-hermes"]`, only after verifying every named sandbox used a managed image.
Expand Down
88 changes: 82 additions & 6 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2004,6 +2004,19 @@ legacy_openshell_gateway_upgrade_needed() {
[[ -n "$version" ]] && ! version_gte "$version" "0.0.37"
}

resolve_current_openshell_version_range() {
local source_root="${NEMOCLAW_SOURCE_ROOT:-$(resolve_repo_root)}"
local blueprint="${source_root}/nemoclaw-blueprint/blueprint.yaml"
local min_version="" max_version=""
[ -f "$blueprint" ] || return 1
min_version="$(sed -nE 's/^min_openshell_version:[[:space:]]*["'"'"']([0-9]+\.[0-9]+\.[0-9]+)["'"'"'][[:space:]]*$/\1/p' "$blueprint")"
max_version="$(sed -nE 's/^max_openshell_version:[[:space:]]*["'"'"']([0-9]+\.[0-9]+\.[0-9]+)["'"'"'][[:space:]]*$/\1/p' "$blueprint")"
[[ "$min_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
[[ "$max_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
version_gte "$max_version" "$min_version" || return 1
printf '%s %s\n' "$min_version" "$max_version"
}

installer_non_interactive() {
[[ "${NON_INTERACTIVE:-}" == "1" || "${NEMOCLAW_NON_INTERACTIVE:-}" == "1" ]]
}
Expand Down Expand Up @@ -2186,6 +2199,55 @@ EOF
esac
}

stop_legacy_openshell_gateway_process() {
[ "$(uname -s)" = "Linux" ] || return 1

local gateway_port runtime_dir pid_file pid gateway_exe attempt
gateway_port="$(resolve_nemoclaw_gateway_port)" || return 2
if [ -n "${NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR:-}" ]; then
runtime_dir="${NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR}"
elif [ "$gateway_port" -eq 8080 ]; then
runtime_dir="${HOME}/.local/state/nemoclaw/openshell-docker-gateway"
else
runtime_dir="${HOME}/.local/state/nemoclaw/openshell-docker-gateway-${gateway_port}"
fi
pid_file="${runtime_dir}/openshell-gateway.pid"
[ -f "$pid_file" ] || return 1
if [ -L "$pid_file" ] || ! [ -O "$pid_file" ]; then
error "Refusing to retire the legacy OpenShell gateway from an untrusted PID file: ${pid_file}"
fi

IFS= read -r pid <"$pid_file" || return 2
[[ "$pid" =~ ^[1-9][0-9]*$ ]] \
|| error "Refusing to retire the legacy OpenShell gateway from an invalid PID file: ${pid_file}"
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pid_file"
return 0
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

gateway_exe="$(readlink "/proc/${pid}/exe" 2>/dev/null || true)"
[ "${gateway_exe##*/}" = "openshell-gateway" ] \
|| error "Refusing to stop PID ${pid}: the recorded process is not openshell-gateway."

kill "$pid" 2>/dev/null \
|| error "Could not stop the recorded legacy OpenShell gateway process ${pid}."
for attempt in {1..50}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.2
done
if kill -0 "$pid" 2>/dev/null; then
kill -KILL "$pid" 2>/dev/null \
|| error "Could not terminate the recorded legacy OpenShell gateway process ${pid}."
for attempt in {1..10}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.1
done
fi
kill -0 "$pid" 2>/dev/null \
&& error "The recorded legacy OpenShell gateway process ${pid} did not stop."
rm -f "$pid_file"
}

preinstall_backup_and_retire_legacy_gateway() {
local reg_file gateway_name
reg_file="$(nemoclaw_state_dir)/sandboxes.json"
Expand Down Expand Up @@ -2239,18 +2301,32 @@ preinstall_backup_and_retire_legacy_gateway() {
fi
export NEMOCLAW_RESTORE_LATEST_BACKUP_ON_RECREATE=1

# Current OpenShell builds are not compatible with pre-0.0.37 gateway state,
# and those CLIs no longer have lifecycle verbs for destroying that old gateway.
# Retire the old gateway while the old CLI can still do it, after backup.
if [[ -n "$old_openshell_version" ]] && ! version_gte "$old_openshell_version" "0.0.37"; then
# Retire a backed-up gateway before install-openshell replaces an out-of-range
# component set. Leaving the old gateway process alive makes the new CLI's
# schema preflight fail before sandbox recovery can recreate it.
local supported_range="" min_openshell_version="" max_openshell_version=""
if ! supported_range="$(resolve_current_openshell_version_range)"; then
error "Could not resolve the current OpenShell version range. Existing gateway and sandbox state were left unchanged after backup."
fi
read -r min_openshell_version max_openshell_version <<<"$supported_range"
[ -n "$old_openshell_version" ] \
|| error "Could not determine the installed OpenShell version. The installer stopped after backup without retiring the gateway."
if ! version_gte "$old_openshell_version" "$min_openshell_version" \
|| ! version_gte "$max_openshell_version" "$old_openshell_version"; then
info "Retiring OpenShell ${old_openshell_version} gateway before installing current OpenShell…"
if [ "$gateway_name" = "nemoclaw" ]; then
openshell gateway destroy -g "$gateway_name" >/dev/null 2>&1 \
|| openshell gateway destroy >/dev/null 2>&1 \
|| warn "Could not destroy the legacy OpenShell gateway before upgrade; onboarding will clean up stale runtime state."
|| { stop_legacy_openshell_gateway_process \
&& { openshell gateway remove "$gateway_name" >/dev/null 2>&1 \
|| warn "The legacy gateway process stopped, but its OpenShell registration could not be removed; onboarding will replace the stale registration."; }; } \
|| error "Could not retire the legacy OpenShell gateway after backup. The installer stopped with the sandbox backups preserved."
else
openshell gateway destroy -g "$gateway_name" >/dev/null 2>&1 \
|| warn "Could not destroy legacy gateway ${gateway_name} before upgrade; onboarding will clean up only that gateway's stale runtime state."
|| { stop_legacy_openshell_gateway_process \
&& { openshell gateway remove "$gateway_name" >/dev/null 2>&1 \
|| warn "Legacy gateway ${gateway_name} stopped, but its OpenShell registration could not be removed; onboarding will replace only that stale registration."; }; } \
|| error "Could not retire legacy gateway ${gateway_name} after backup. The installer stopped with the sandbox backups preserved."
fi
fi
}
Expand Down
9 changes: 6 additions & 3 deletions test/e2e/live/openshell-gateway-upgrade-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,16 @@ export function currentGatewayUpgradeInstallerArgs(
return options.interactive ? [installer] : [installer, ...NON_INTERACTIVE_INSTALLER_ARGS];
}

export function expectedLegacyRegistryVersion(nemoclawRef: string): string | undefined {
export function expectedLegacyRegistryMetadata(nemoclawRef: string): {
nemoclawVersion: string | undefined;
fromDockerfile: null | undefined;
} {
switch (nemoclawRef) {
case "v0.0.36":
case "v0.0.55":
return undefined;
return { nemoclawVersion: undefined, fromDockerfile: undefined };
case "v0.0.74":
return "0.0.74";
return { nemoclawVersion: "0.0.74", fromDockerfile: null };
default:
throw new Error(`Unsupported gateway-upgrade registry fixture: ${nemoclawRef}`);
}
Expand Down
36 changes: 22 additions & 14 deletions test/e2e/live/openshell-gateway-upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { REPO_ROOT } from "../fixtures/paths.ts";
import type { ShellProbeResult } from "../fixtures/shell-probe.ts";
import {
currentGatewayUpgradeInstallerArgs,
expectedLegacyRegistryVersion,
expectedLegacyRegistryMetadata,
oldGatewayUpgradeInstallerArgs,
upgradeGatewayCleanupScript,
upgradeGatewayStateCleanupScript,
Expand Down Expand Up @@ -555,10 +555,13 @@ git -C "$HOME/.nemoclaw/source" rev-parse --verify HEAD`,
sandboxes?: Record<string, { nemoclawVersion?: unknown; fromDockerfile?: unknown }>;
};
expect(oldRegistry.sandboxes?.[SURVIVOR_SANDBOX]).toBeDefined();
const expectedRegistryMetadata = expectedLegacyRegistryMetadata(OLD_NEMOCLAW_REF);
expect(oldRegistry.sandboxes?.[SURVIVOR_SANDBOX]?.nemoclawVersion).toBe(
expectedLegacyRegistryVersion(OLD_NEMOCLAW_REF),
expectedRegistryMetadata.nemoclawVersion,
);
expect(oldRegistry.sandboxes?.[SURVIVOR_SANDBOX]?.fromDockerfile).toBe(
expectedRegistryMetadata.fromDockerfile,
);
expect(oldRegistry.sandboxes?.[SURVIVOR_SANDBOX]?.fromDockerfile).toBeUndefined();
}

async function stageOldOpenShellInUserLocalBin(host: HostCliClient): Promise<string> {
Expand Down Expand Up @@ -654,12 +657,12 @@ async function installCurrentNemoclawUpgrade(
const resolvedRef = currentRefResult.stdout.trim();
expect(resolvedRef.length).toBeGreaterThan(0);
const exerciseOrdinaryUpgrade = OLD_NEMOCLAW_REF === "v0.0.55";
if (exerciseOrdinaryUpgrade) {
expect(
hiddenOldOpenShellDir,
"the v0.0.55 fixture must record the original OpenShell directory before hiding it",
).toBeTruthy();
}
const expectsLegacyManagedConfirmation =
expectedLegacyRegistryMetadata(OLD_NEMOCLAW_REF).nemoclawVersion === undefined;
expect(
!exerciseOrdinaryUpgrade || Boolean(hiddenOldOpenShellDir),
"the v0.0.55 fixture must record the original OpenShell directory before hiding it",
).toBe(true);
const baseCurrentEnv = liveEnv({
COMPATIBLE_API_KEY: "dummy",
GITHUB_TOKEN: process.env.GITHUB_TOKEN ?? "",
Expand Down Expand Up @@ -707,11 +710,16 @@ async function installCurrentNemoclawUpgrade(
);

const currentLog = fs.readFileSync(currentInstallLog, "utf8");
expect(currentLog).toContain(
exerciseOrdinaryUpgrade
? "Confirmed legacy managed-image recovery"
: "Confirmed 1 exact pre-fingerprint sandbox name(s)",
);
const expectedConfirmation = exerciseOrdinaryUpgrade
? "Confirmed legacy managed-image recovery"
: expectsLegacyManagedConfirmation
? "Confirmed 1 exact pre-fingerprint sandbox name(s)"
: null;
expect(
expectedConfirmation === null
? !currentLog.includes("exact pre-fingerprint sandbox name(s)")
: currentLog.includes(expectedConfirmation),
).toBe(true);
expect(currentLog).toContain("Pre-upgrade backup: 1 backed up, 0 failed, 0 skipped");
expect(currentLog).toContain("Existing sandboxes recovered; skipping generic onboarding");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "../../../tools/e2e/workflow-boundary.mts";
import {
currentGatewayUpgradeInstallerArgs,
expectedLegacyRegistryVersion,
expectedLegacyRegistryMetadata,
oldGatewayUpgradeInstallerArgs,
upgradeGatewayCleanupScript,
validateLegacyGatewayUpgradeFixture,
Expand Down Expand Up @@ -73,10 +73,14 @@ describe("OpenShell gateway upgrade workflow boundary", () => {
});

it("pins the registry metadata written by each historical release fixture", () => {
expect(expectedLegacyRegistryVersion("v0.0.36")).toBeUndefined();
expect(expectedLegacyRegistryVersion("v0.0.55")).toBeUndefined();
expect(expectedLegacyRegistryVersion("v0.0.74")).toBe("0.0.74");
expect(() => expectedLegacyRegistryVersion("v0.0.75")).toThrow(
const absentMetadata = { nemoclawVersion: undefined, fromDockerfile: undefined };
expect(expectedLegacyRegistryMetadata("v0.0.36")).toEqual(absentMetadata);
expect(expectedLegacyRegistryMetadata("v0.0.55")).toEqual(absentMetadata);
expect(expectedLegacyRegistryMetadata("v0.0.74")).toEqual({
nemoclawVersion: "0.0.74",
fromDockerfile: null,
});
expect(() => expectedLegacyRegistryMetadata("v0.0.75")).toThrow(
/Unsupported gateway-upgrade registry fixture/,
);
});
Expand Down
Loading
Loading