From 6c25a2cd6bf808bbd29e2821a998ed618a627b19 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 13 Aug 2026 10:20:41 -0700 Subject: [PATCH 01/10] docs: apply v0.0.108 audit follow-ups Signed-off-by: Miyoung Choi --- docs/changelog/2026-08-12.mdx | 6 ++++-- docs/inference/set-up-vllm.mdx | 3 ++- docs/manage-sandboxes/update-sandboxes.mdx | 15 +++++++++++++-- docs/security/configure-corporate-ca-trust.mdx | 12 ------------ docs/security/credential-rotation.mdx | 7 ++++++- 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/changelog/2026-08-12.mdx b/docs/changelog/2026-08-12.mdx index 8376696eef7..b33999e3e34 100644 --- a/docs/changelog/2026-08-12.mdx +++ b/docs/changelog/2026-08-12.mdx @@ -9,8 +9,10 @@ NemoClaw v0.0.108 adds read-only host mounts and an Experimental Muse Glimmer pr It improves onboarding recovery, messaging credential rotation, inference validation, MCP registration, snapshots, and Hermes configuration. It also strengthens managed images, gateway credentials, runtime state, and release qualification. -- On Linux and Windows Subsystem for Linux 2 (WSL2), `nemoclaw onboard` and `nemoclaw rebuild` can now mount an existing host directory read-only beneath `/sandbox` with `--host-mount`. - NemoClaw validates each source, destination, and symlink boundary before mutation, persists accepted mounts for `rebuild` and interrupted `onboard --resume`, removes their declaration during `destroy`, and reports them in sandbox status. +- On Linux and Windows Subsystem for Linux 2 (WSL2), `nemoclaw onboard --host-mount ` can now expose an existing host directory read-only beneath `/sandbox`. + NemoClaw validates each source, destination, and symlink boundary before mutation. + It persists accepted mounts for `nemoclaw rebuild` and interrupted `nemoclaw onboard --resume`. + The `destroy` command removes their declaration, and sandbox status reports them. For more information, refer to [Understand Sandbox State](/user-guide/openclaw/manage-sandboxes/state-and-backups/understand-sandbox-state) and the [NemoClaw CLI Commands Reference](/user-guide/openclaw/reference/commands). Related change: [PR #8280](https://github.com/NVIDIA/NemoClaw/pull/8280). - The explicit-only Experimental Muse Glimmer managed vLLM profile now runs on one DGX Spark with its validated checkpoint, ARM64 image, parser settings, and resource limits. diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index 077fadce632..80b48c68019 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -295,7 +295,8 @@ Two behaviors surprise direct callers of the managed vLLM endpoint; the OpenClaw - **Read reasoning from `reasoning`, not `reasoning_content`.** The pinned vLLM runtime renamed the deprecated `reasoning_content` response field to `reasoning`, so `choices[].message.reasoning_content` is `null` even for prompts that reasoned heavily. - The reasoning is not discarded — read `choices[].message.reasoning` (and the `delta.reasoning` field when streaming). + The reasoning is not discarded. + Read `choices[].message.reasoning` and the `delta.reasoning` field when streaming. - **Send a generous `max_tokens` (at least `1024`, and more for hard prompts).** The reasoning trace counts against `max_tokens`. With a modest budget the model can spend the entire budget on reasoning before it reaches the answer, returning an empty `content` with `finish_reason=length`. diff --git a/docs/manage-sandboxes/update-sandboxes.mdx b/docs/manage-sandboxes/update-sandboxes.mdx index 47ee13ac6ff..5a616ba27f3 100644 --- a/docs/manage-sandboxes/update-sandboxes.mdx +++ b/docs/manage-sandboxes/update-sandboxes.mdx @@ -38,6 +38,10 @@ The `langchain-deepagents-code` agent manifest declares the expected Deep Agents +## Use the Automatic Upgrade Path + +Run the maintained installer to update the host CLI and reconcile registered sandboxes. + OpenShell 0.0.101 limits a routed sandbox name to 19 characters and rejects consecutive hyphens. A NemoClaw sandbox name must contain 1 to 19 characters. @@ -84,12 +88,17 @@ After the automatic path retires an out-of-range gateway, it forces installation This mandatory installation applies to source and managed install modes and cannot remain deferred after gateway retirement. If the forced installation fails, the installer does not stage a gateway service or start sandbox recovery, preserves the backups, and reports how to retry. -For any registered-sandbox upgrade that you already prepared manually, set `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` only after you back up every registered sandbox and retire the gateway. +## Resume a Manually Prepared Upgrade + +Use this path only after you back up every registered sandbox and retire the gateway. +Set `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` when you rerun the installer. This environment variable asserts that you completed those steps, so the installer skips the repeated backup and gateway-retirement phase before it checks whether OpenShell is installed or whether its version is in range. It reuses the latest backups, forces installation of the OpenShell version pinned by the prepared source, and starts recovery only after that installation succeeds. If the forced installation fails, rerun the installer with `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` to reuse the prepared state and retry the OpenShell installation. Outside these forced paths, an in-range source checkout preserves an existing OpenShell installation and installs OpenShell only when it is missing. +## Reconcile Registered Sandboxes + After the host CLI and OpenShell update, the installer runs `$$nemoclaw upgrade-sandboxes --auto` to reconcile the existing sandboxes. During installer-driven recovery, each stale or non-Ready sandbox requires a validated latest backup. @@ -121,7 +130,9 @@ This usually means a prior uninstall removed the gateway and Docker image while The installer finishes with `Installation completed with warnings` and tells you to run `$$nemoclaw destroy`, then `$$nemoclaw onboard`, instead of claiming recovery succeeded. -For a manual update flow, create a snapshot first: +### Use the Manual Update Flow + +Create a snapshot before a manual update. ```bash $$nemoclaw snapshot create --name pre-upgrade diff --git a/docs/security/configure-corporate-ca-trust.mdx b/docs/security/configure-corporate-ca-trust.mdx index 1074b16bce3..05ab102efe1 100644 --- a/docs/security/configure-corporate-ca-trust.mdx +++ b/docs/security/configure-corporate-ca-trust.mdx @@ -39,18 +39,6 @@ When onboarding supplies a corporate CA, the Dockerfile sets `NODE_EXTRA_CA_CERT This includes registry-backed npm remediation and runtime dependency installation that cross a TLS-inspecting proxy. The build does not retain either variable in the image configuration. -The required `reviewed-npm-audit` CI check materializes the dependency graph from the committed lock and verifies its registry signatures. -Any nonzero `npm audit signatures` status fails the check. -The pull request (PR) check must pass before merge. -The managed-image PR workflow loads its audit implementation from the base branch revision and evaluates the dependency files from the commit under review. -The image-build job requires that result before local builds and same-repository digest publication. -The base-image workflow requires its audit result before it builds or publishes any base image. -It also requires the result before it invokes managed-image publication. -Managed OpenClaw sandbox image builds do not repeat that Sigstore request. -The base provenance marker records package and advisory-audit metadata, not the CI signature result. -The final image reuses those runtimes only from a digest-pinned base in the official GHCR namespace. -A locally built base or mutable tag does not provide independent CI publication evidence. -Its marker cannot authorize reuse; the existing version checks reinstall the locked OpenClaw and mcporter runtimes or reject a newer base. When NemoClaw selects a corporate CA, it sets `NEMOCLAW_MANAGED_IMAGE_RUNTIME_USER=root` for the image build. The OpenClaw managed startup runtime creates `/run/nemoclaw/managed-startup-ca-bundle.pem` as `root:root` with mode `0444` before it starts agent commands as the `sandbox` user. The `sandbox` user can read the merged bundle but cannot modify or replace it. diff --git a/docs/security/credential-rotation.mdx b/docs/security/credential-rotation.mdx index 66722ba4a7e..affd504adce 100644 --- a/docs/security/credential-rotation.mdx +++ b/docs/security/credential-rotation.mdx @@ -36,7 +36,7 @@ Use `channels add`, `channels remove`, or `channels stop` to manage integrations | Credential | Supported rotation path | Sandbox impact | |---|---|---| | Inference API key | Rerun onboarding with the replacement value | The existing sandbox can normally be reused unless onboarding detects unrelated configuration drift | -| Active Telegram, Discord, Slack, WeChat, or Microsoft Teams credential | Rerun onboarding with the replacement value | Onboarding runs the channel's configured checks before automatically backing up supported workspace and manifest-declared state, recreating the sandbox, and restoring that backup; some channels cannot prove credential validity until the recreated runtime connects, and files outside the preserved state paths are not preserved | +| Active Telegram, Discord, Slack, WeChat, or Microsoft Teams credential | Rerun onboarding with the replacement value | Onboarding applies the [messaging rotation procedure](#rotate-a-messaging-credential), which recreates the sandbox. Some channels require verification after recreation. | | Brave or Tavily web search key | Rerun onboarding with the selected web search provider | Sandbox recreation required | ## Rotate an Inference API key @@ -63,6 +63,9 @@ For an interactive rotation, export the replacement key and run `$$nemoclaw onbo ## Rotate a Messaging Credential Supply the replacement credential and rerun onboarding for the existing sandbox. + +### Review the Recreation Boundary + For each supported active-channel credential that you supply, NemoClaw compares the replacement with the SHA-256 hash in the sandbox registry. NemoClaw runs each changed value through the checks configured for that channel before it backs up or replaces the sandbox. It then backs up supported workspace and manifest-declared state, destroys and recreates the sandbox, and restores the backup. @@ -73,6 +76,8 @@ For those channels, an invalid non-empty replacement can surface only after recr If the recorded channel state changes during rotation, onboarding stops before it replaces the sandbox. Rerun onboarding so it uses the updated channel state. +### Understand Credential Handling + Onboarding reads the replacement from the host environment and registers it with the sandbox-specific OpenShell messaging provider. The sandbox registry stores the credential hash, not the credential value. The `unset` command in each procedure removes the value from the current host shell after onboarding. From 8f64b3c625ba5d91cce85ecedb7ccc78e8661f66 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 13 Aug 2026 10:25:05 -0700 Subject: [PATCH 02/10] docs: address audit follow-up review Signed-off-by: Miyoung Choi --- docs/changelog/2026-08-12.mdx | 5 +++-- docs/inference/set-up-vllm.mdx | 3 ++- docs/manage-sandboxes/update-sandboxes.mdx | 7 ++++++- docs/security/credential-rotation.mdx | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/changelog/2026-08-12.mdx b/docs/changelog/2026-08-12.mdx index b33999e3e34..7c9f3f660e0 100644 --- a/docs/changelog/2026-08-12.mdx +++ b/docs/changelog/2026-08-12.mdx @@ -11,8 +11,9 @@ It also strengthens managed images, gateway credentials, runtime state, and rele - On Linux and Windows Subsystem for Linux 2 (WSL2), `nemoclaw onboard --host-mount ` can now expose an existing host directory read-only beneath `/sandbox`. NemoClaw validates each source, destination, and symlink boundary before mutation. - It persists accepted mounts for `nemoclaw rebuild` and interrupted `nemoclaw onboard --resume`. - The `destroy` command removes their declaration, and sandbox status reports them. + It persists accepted mounts across `nemoclaw rebuild` and interrupted onboarding resumed with `nemoclaw onboard --resume`. + The `nemoclaw destroy` command removes their declaration. + The `nemoclaw status` command reports them. For more information, refer to [Understand Sandbox State](/user-guide/openclaw/manage-sandboxes/state-and-backups/understand-sandbox-state) and the [NemoClaw CLI Commands Reference](/user-guide/openclaw/reference/commands). Related change: [PR #8280](https://github.com/NVIDIA/NemoClaw/pull/8280). - The explicit-only Experimental Muse Glimmer managed vLLM profile now runs on one DGX Spark with its validated checkpoint, ARM64 image, parser settings, and resource limits. diff --git a/docs/inference/set-up-vllm.mdx b/docs/inference/set-up-vllm.mdx index 80b48c68019..12a87ab255c 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -296,7 +296,8 @@ Two behaviors surprise direct callers of the managed vLLM endpoint; the OpenClaw - **Read reasoning from `reasoning`, not `reasoning_content`.** The pinned vLLM runtime renamed the deprecated `reasoning_content` response field to `reasoning`, so `choices[].message.reasoning_content` is `null` even for prompts that reasoned heavily. The reasoning is not discarded. - Read `choices[].message.reasoning` and the `delta.reasoning` field when streaming. + Read non-streaming reasoning from `choices[].message.reasoning`. + For streaming responses, read `choices[].delta.reasoning`. - **Send a generous `max_tokens` (at least `1024`, and more for hard prompts).** The reasoning trace counts against `max_tokens`. With a modest budget the model can spend the entire budget on reasoning before it reaches the answer, returning an empty `content` with `finish_reason=length`. diff --git a/docs/manage-sandboxes/update-sandboxes.mdx b/docs/manage-sandboxes/update-sandboxes.mdx index 5a616ba27f3..6444e102a33 100644 --- a/docs/manage-sandboxes/update-sandboxes.mdx +++ b/docs/manage-sandboxes/update-sandboxes.mdx @@ -91,7 +91,12 @@ If the forced installation fails, the installer does not stage a gateway service ## Resume a Manually Prepared Upgrade Use this path only after you back up every registered sandbox and retire the gateway. -Set `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` when you rerun the installer. +Set `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` on the `bash` side of the install pipeline when you rerun the installer. + +```bash +curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash +``` + This environment variable asserts that you completed those steps, so the installer skips the repeated backup and gateway-retirement phase before it checks whether OpenShell is installed or whether its version is in range. It reuses the latest backups, forces installation of the OpenShell version pinned by the prepared source, and starts recovery only after that installation succeeds. If the forced installation fails, rerun the installer with `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` to reuse the prepared state and retry the OpenShell installation. diff --git a/docs/security/credential-rotation.mdx b/docs/security/credential-rotation.mdx index affd504adce..be024f024b9 100644 --- a/docs/security/credential-rotation.mdx +++ b/docs/security/credential-rotation.mdx @@ -64,7 +64,7 @@ For an interactive rotation, export the replacement key and run `$$nemoclaw onbo Supply the replacement credential and rerun onboarding for the existing sandbox. -### Review the Recreation Boundary +### Understand the Recreation Boundary For each supported active-channel credential that you supply, NemoClaw compares the replacement with the SHA-256 hash in the sandbox registry. NemoClaw runs each changed value through the checks configured for that channel before it backs up or replaces the sandbox. From 498897817deed2b12f246d723da5b13e76557b03 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 13 Aug 2026 10:44:28 -0700 Subject: [PATCH 03/10] docs: preserve prepared upgrade gateway port Signed-off-by: Miyoung Choi --- docs/manage-sandboxes/update-sandboxes.mdx | 6 ++++-- docs/reference/commands.mdx | 6 ++++++ test/install-openshell-upgrade-prompt.test.ts | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/manage-sandboxes/update-sandboxes.mdx b/docs/manage-sandboxes/update-sandboxes.mdx index 6444e102a33..d0128e2513a 100644 --- a/docs/manage-sandboxes/update-sandboxes.mdx +++ b/docs/manage-sandboxes/update-sandboxes.mdx @@ -91,13 +91,15 @@ If the forced installation fails, the installer does not stage a gateway service ## Resume a Manually Prepared Upgrade Use this path only after you back up every registered sandbox and retire the gateway. +Set `NEMOCLAW_GATEWAY_PORT` to the port for the gateway that you retired. Set `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` on the `bash` side of the install pipeline when you rerun the installer. ```bash -curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash +curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_GATEWAY_PORT= NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash ``` -This environment variable asserts that you completed those steps, so the installer skips the repeated backup and gateway-retirement phase before it checks whether OpenShell is installed or whether its version is in range. +You can omit `NEMOCLAW_GATEWAY_PORT` when you prepared the default gateway on port `8080`. +`NEMOCLAW_OPENSHELL_UPGRADE_PREPARED` asserts that you completed those steps, so the installer skips the repeated backup and gateway-retirement phase before it checks whether OpenShell is installed or whether its version is in range. It reuses the latest backups, forces installation of the OpenShell version pinned by the prepared source, and starts recovery only after that installation succeeds. If the forced installation fails, rerun the installer with `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` to reuse the prepared state and retry the OpenShell installation. Outside these forced paths, an in-range source checkout preserves an existing OpenShell installation and installs OpenShell only when it is missing. diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index d206817d710..9e2e4c9713a 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -531,6 +531,12 @@ The confirmation permits those legacy entries to recover onto the current manage After successful recovery, the installer skips generic onboarding. For any registered-sandbox upgrade that you already prepared manually, set `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` only after backing up every registered sandbox and retiring the old gateway. This environment variable asserts that those steps are complete, so the installer skips the repeated backup and gateway-retirement phase before it checks whether OpenShell is installed or whether its version is in range. +For a non-default gateway, preserve the selected port on the `bash` side of the install pipeline. + +```bash +curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_GATEWAY_PORT= NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash +``` + It reuses the latest backups, forces the pinned OpenShell installation, and starts recovery only after that installation succeeds. If the installation fails, rerun with `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1` to reuse the prepared state and retry the OpenShell installation. diff --git a/test/install-openshell-upgrade-prompt.test.ts b/test/install-openshell-upgrade-prompt.test.ts index 04c587bfc79..4d6dd4cb8f0 100644 --- a/test/install-openshell-upgrade-prompt.test.ts +++ b/test/install-openshell-upgrade-prompt.test.ts @@ -9,6 +9,14 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; const INSTALLER_PAYLOAD = path.join(import.meta.dirname, "..", "scripts", "install.sh"); +const UPDATE_SANDBOXES_DOCS = path.join( + import.meta.dirname, + "..", + "docs", + "manage-sandboxes", + "update-sandboxes.mdx", +); +const COMMANDS_DOCS = path.join(import.meta.dirname, "..", "docs", "reference", "commands.mdx"); function writeExecutable(target: string, contents: string): void { fs.writeFileSync(target, contents, { mode: 0o755 }); @@ -598,6 +606,14 @@ esac`, expect(openshellLog).toBe(""); }); + it("documents the selected gateway port for a manually prepared upgrade", () => { + const preparedUpgradeCommand = + "curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_GATEWAY_PORT= NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash"; + + expect(fs.readFileSync(UPDATE_SANDBOXES_DOCS, "utf-8")).toContain(preparedUpgradeCommand); + expect(fs.readFileSync(COMMANDS_DOCS, "utf-8")).toContain(preparedUpgradeCommand); + }); + it("requires separate managed-image confirmation before preparing a backup (#6114)", () => { const { result, cliLog, openshellLog } = runPreinstallUpgradeGuard({ NON_INTERACTIVE: "1", From d5305b10d3274b14c35b89e6083a31727f6438ec Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 13 Aug 2026 10:51:14 -0700 Subject: [PATCH 04/10] docs: preserve gateway port on upgrade retry Signed-off-by: Miyoung Choi --- docs/manage-sandboxes/update-sandboxes.mdx | 2 +- docs/reference/commands.mdx | 2 +- test/install-openshell-upgrade-prompt.test.ts | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/manage-sandboxes/update-sandboxes.mdx b/docs/manage-sandboxes/update-sandboxes.mdx index d0128e2513a..df43dfede51 100644 --- a/docs/manage-sandboxes/update-sandboxes.mdx +++ b/docs/manage-sandboxes/update-sandboxes.mdx @@ -101,7 +101,7 @@ curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_GATEWAY_PORT= Prepared backup recovery for a legacy sandbox restores only the managed state directory recorded in its validated manifest, such as `/sandbox/.openclaw` or `/sandbox/.hermes`. diff --git a/test/install-openshell-upgrade-prompt.test.ts b/test/install-openshell-upgrade-prompt.test.ts index 4d6dd4cb8f0..0022ffbae36 100644 --- a/test/install-openshell-upgrade-prompt.test.ts +++ b/test/install-openshell-upgrade-prompt.test.ts @@ -609,9 +609,13 @@ esac`, it("documents the selected gateway port for a manually prepared upgrade", () => { const preparedUpgradeCommand = "curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_GATEWAY_PORT= NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash"; + const retryInstruction = + "If the installation fails, rerun the same install-pipeline command to preserve `NEMOCLAW_GATEWAY_PORT` and `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED`."; expect(fs.readFileSync(UPDATE_SANDBOXES_DOCS, "utf-8")).toContain(preparedUpgradeCommand); expect(fs.readFileSync(COMMANDS_DOCS, "utf-8")).toContain(preparedUpgradeCommand); + expect(fs.readFileSync(UPDATE_SANDBOXES_DOCS, "utf-8")).toContain(retryInstruction); + expect(fs.readFileSync(COMMANDS_DOCS, "utf-8")).toContain(retryInstruction); }); it("requires separate managed-image confirmation before preparing a backup (#6114)", () => { From b5e3fff26c8740046e1d8f3419ef6c7d8e581c4f Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 13 Aug 2026 11:09:39 -0700 Subject: [PATCH 05/10] fix(installer): preserve gateway port on retry Signed-off-by: Miyoung Choi --- scripts/install.sh | 7 ++++++- test/install-openshell-upgrade-prompt.test.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index 47ad79c89f1..a24394fc7cf 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -2457,7 +2457,12 @@ finish_nemoclaw_install() { [[ "$defer_was_exported" == true ]] && export NEMOCLAW_DEFER_OPENSHELL_INSTALL fi if [[ "$openshell_install_status" -ne 0 ]]; then - error "Could not install the OpenShell version pinned by the prepared source after retiring the gateway. The installer preserved the sandbox backups and did not start recovery. Rerun the installer with NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 to reuse the prepared upgrade state and retry the OpenShell install." + local retry_gateway_port retry_gateway_port_env="" + retry_gateway_port="$(resolve_nemoclaw_gateway_port)" + if [ "$retry_gateway_port" -ne 8080 ]; then + retry_gateway_port_env="NEMOCLAW_GATEWAY_PORT=${retry_gateway_port} " + fi + error "Could not install the OpenShell version pinned by the prepared source after retiring the gateway. The installer preserved the sandbox backups and did not start recovery. Rerun the installer with ${retry_gateway_port_env}NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 to reuse the prepared upgrade state and retry the OpenShell install." fi _OPENSHELL_INSTALL_REQUIRED_BEFORE_RECOVERY=false else diff --git a/test/install-openshell-upgrade-prompt.test.ts b/test/install-openshell-upgrade-prompt.test.ts index 0022ffbae36..106bd6f9a00 100644 --- a/test/install-openshell-upgrade-prompt.test.ts +++ b/test/install-openshell-upgrade-prompt.test.ts @@ -115,6 +115,7 @@ function runPreinstallUpgradeGuard( currentMaxOpenshellVersion?: string; currentMinOpenshellVersion?: string; finishDeferAsPlain?: boolean; + finishGatewayPort?: string; finishInstallMode?: "managed" | "source" | "unset"; finishPreparedInstallSucceeds?: boolean; gatewayDestroySucceeds?: boolean; @@ -156,6 +157,7 @@ function runPreinstallUpgradeGuard( const gatewayRemoveSucceeds = options.gatewayRemoveSucceeds === false ? "0" : "1"; const gatewayServiceStopSucceeds = options.gatewayServiceStopSucceeds === true ? "1" : "0"; const finishDeferAsPlain = options.finishDeferAsPlain === true ? "1" : "0"; + const finishGatewayPort = options.finishGatewayPort ?? ""; const finishInstallMode = options.finishInstallMode ?? ""; const finishPreparedInstallSucceeds = options.finishPreparedInstallSucceeds === false ? "0" : "1"; const openshellVersionCommandFails = options.openshellVersionCommandFails === true ? "1" : "0"; @@ -254,6 +256,7 @@ exit 0 } refresh_path() { :; } ensure_nemoclaw_shim() { :; } + [ -z "${finishGatewayPort}" ] || NEMOCLAW_GATEWAY_PORT="${finishGatewayPort}" finish_nemoclaw_install fi printf 'DEFER=%s\\n' "\${NEMOCLAW_DEFER_OPENSHELL_INSTALL:-}" @@ -920,13 +923,14 @@ esac`, expect(openshellLog).toBe("openshell install-mode force defer=\n"); }); - it("preserves prepared backups when the required OpenShell install fails after retirement (#8800)", () => { + it("preserves prepared backups and the selected port when OpenShell installation fails (#8800)", () => { const { result, cliLog, openshellLog } = runPreinstallUpgradeGuard( { NON_INTERACTIVE: "1" }, { currentMaxOpenshellVersion: "0.0.101", currentMinOpenshellVersion: "0.0.101", finishPreparedInstallSucceeds: false, + finishGatewayPort: "9123", finishInstallMode: "source", gatewayDestroySucceeds: false, gatewayProcessStopSucceeds: false, @@ -941,7 +945,9 @@ esac`, expect(result.status).not.toBe(0); expect(result.stdout + result.stderr).toContain("preserved the sandbox backups"); expect(result.stdout + result.stderr).toContain("did not start recovery"); - expect(result.stdout + result.stderr).toContain("NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1"); + expect(result.stdout + result.stderr).toContain( + "NEMOCLAW_GATEWAY_PORT=9123 NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1", + ); expect(cliLog.split(/\r?\n/)).toContain("current:backup-all"); expect(openshellLog).toContain("openshell install-mode force defer="); }); From 7e7668d5d1600bc4f78fdbcb38b24e2a73f1a322 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 13 Aug 2026 11:12:37 -0700 Subject: [PATCH 06/10] test(installer): cover default upgrade retry port Signed-off-by: Miyoung Choi --- test/install-openshell-upgrade-prompt.test.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test/install-openshell-upgrade-prompt.test.ts b/test/install-openshell-upgrade-prompt.test.ts index 106bd6f9a00..39b3e874ae7 100644 --- a/test/install-openshell-upgrade-prompt.test.ts +++ b/test/install-openshell-upgrade-prompt.test.ts @@ -923,14 +923,25 @@ esac`, expect(openshellLog).toBe("openshell install-mode force defer=\n"); }); - it("preserves prepared backups and the selected port when OpenShell installation fails (#8800)", () => { + it.each([ + { + expectedRetry: "NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1", + finishGatewayPort: undefined, + name: "the default gateway port", + }, + { + expectedRetry: "NEMOCLAW_GATEWAY_PORT=9123 NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1", + finishGatewayPort: "9123", + name: "a selected non-default gateway port", + }, + ])("preserves prepared backups and $name when OpenShell installation fails (#8800)", (testCase) => { const { result, cliLog, openshellLog } = runPreinstallUpgradeGuard( { NON_INTERACTIVE: "1" }, { currentMaxOpenshellVersion: "0.0.101", currentMinOpenshellVersion: "0.0.101", finishPreparedInstallSucceeds: false, - finishGatewayPort: "9123", + finishGatewayPort: testCase.finishGatewayPort, finishInstallMode: "source", gatewayDestroySucceeds: false, gatewayProcessStopSucceeds: false, @@ -945,9 +956,10 @@ esac`, expect(result.status).not.toBe(0); expect(result.stdout + result.stderr).toContain("preserved the sandbox backups"); expect(result.stdout + result.stderr).toContain("did not start recovery"); - expect(result.stdout + result.stderr).toContain( - "NEMOCLAW_GATEWAY_PORT=9123 NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1", - ); + expect(result.stdout + result.stderr).toContain(testCase.expectedRetry); + if (testCase.finishGatewayPort === undefined) { + expect(result.stdout + result.stderr).not.toContain("NEMOCLAW_GATEWAY_PORT="); + } expect(cliLog.split(/\r?\n/)).toContain("current:backup-all"); expect(openshellLog).toContain("openshell install-mode force defer="); }); From 07c9d56727a4e5bec8a49591f72eaefa863c1a27 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 13 Aug 2026 11:18:46 -0700 Subject: [PATCH 07/10] test(installer): keep retry cases linear Signed-off-by: Miyoung Choi --- test/install-openshell-upgrade-prompt.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/install-openshell-upgrade-prompt.test.ts b/test/install-openshell-upgrade-prompt.test.ts index 39b3e874ae7..f812571ef33 100644 --- a/test/install-openshell-upgrade-prompt.test.ts +++ b/test/install-openshell-upgrade-prompt.test.ts @@ -927,11 +927,13 @@ esac`, { expectedRetry: "NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1", finishGatewayPort: undefined, + forbiddenRetry: "NEMOCLAW_GATEWAY_PORT=", name: "the default gateway port", }, { expectedRetry: "NEMOCLAW_GATEWAY_PORT=9123 NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1", finishGatewayPort: "9123", + forbiddenRetry: "NEMOCLAW_GATEWAY_PORT=8080", name: "a selected non-default gateway port", }, ])("preserves prepared backups and $name when OpenShell installation fails (#8800)", (testCase) => { @@ -957,9 +959,7 @@ esac`, expect(result.stdout + result.stderr).toContain("preserved the sandbox backups"); expect(result.stdout + result.stderr).toContain("did not start recovery"); expect(result.stdout + result.stderr).toContain(testCase.expectedRetry); - if (testCase.finishGatewayPort === undefined) { - expect(result.stdout + result.stderr).not.toContain("NEMOCLAW_GATEWAY_PORT="); - } + expect(result.stdout + result.stderr).not.toContain(testCase.forbiddenRetry); expect(cliLog.split(/\r?\n/)).toContain("current:backup-all"); expect(openshellLog).toContain("openshell install-mode force defer="); }); From 593006027e3ba8fe3ef5780697c5785d08a26b65 Mon Sep 17 00:00:00 2001 From: prekshivyas Date: Thu, 13 Aug 2026 11:37:13 -0700 Subject: [PATCH 08/10] fix(onboard): preserve managed bootstrap reconnect window --- .../onboard/managed-bootstrap/docker.test.ts | 37 +++++++++++++++++++ src/lib/onboard/managed-bootstrap/docker.ts | 14 ++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/lib/onboard/managed-bootstrap/docker.test.ts b/src/lib/onboard/managed-bootstrap/docker.test.ts index dfb6699f632..4a0e66eb2d8 100644 --- a/src/lib/onboard/managed-bootstrap/docker.test.ts +++ b/src/lib/onboard/managed-bootstrap/docker.test.ts @@ -458,6 +458,43 @@ describe("Docker managed bootstrap adapter", () => { expect(fake.finalization).toMatchObject({ phase: "committed", commitReceipt }); }); + it("uses the Docker-GPU reconnect minimum instead of the shorter create timeout", async () => { + const fake = fixture(); + fake.deps.sleep = vi.fn(); + const adapter = createDockerManagedBootstrapAdapter(fake.deps); + const { handle, request, snapshot } = authority(); + const prepared = await adapter.prepareBootstrapReplacement({ + handle, + snapshot, + request, + replacementOptions: { values: {} }, + }); + const durable = durablePreparation(handle, snapshot, prepared); + const replacement = await adapter.activateBootstrapReplacement({ + handle, + snapshot, + prepared, + durablePreparation: durable, + }); + const dateNow = vi.spyOn(Date, "now").mockReturnValueOnce(0).mockReturnValue(2_000); + vi.mocked(fake.deps.runOpenshell).mockImplementationOnce(() => ({ status: 1 })).mockReturnValue({ + status: 0, + }); + + await expect( + adapter.awaitBootstrap({ + handle, + snapshot, + replacement, + timeoutSecs: 1, + }), + ).resolves.toMatchObject({ runtimeId: NEW_ID }); + + expect(fake.deps.runOpenshell).toHaveBeenCalledTimes(2); + expect(fake.deps.sleep).toHaveBeenCalledWith(2); + dateNow.mockRestore(); + }); + it("preserves commit validation failure details when the replacement cannot be quiesced", async () => { const fake = fixture({ sharedState: "pending", diff --git a/src/lib/onboard/managed-bootstrap/docker.ts b/src/lib/onboard/managed-bootstrap/docker.ts index 0c3a89e0855..656a437ff23 100644 --- a/src/lib/onboard/managed-bootstrap/docker.ts +++ b/src/lib/onboard/managed-bootstrap/docker.ts @@ -31,7 +31,10 @@ import type { DockerGpuPatchModeKind, DockerUlimit, } from "../docker-gpu-patch-types"; -import { waitForOpenShellSupervisorReconnect } from "../docker-gpu-supervisor-reconnect"; +import { + getDockerGpuSupervisorReconnectTimeoutSecs, + waitForOpenShellSupervisorReconnect, +} from "../docker-gpu-supervisor-reconnect"; import { openshellSandboxCommandEnvValue } from "../docker-startup-command-env"; import { OPENSHELL_MANAGED_BY_LABEL, @@ -3620,7 +3623,14 @@ export function createDockerManagedBootstrapAdapter( throw new Error("Managed bootstrap Docker replacement image content changed."); } assertReplacementBoundary(before, handle, snapshot); - if (!waitForOpenShellSupervisorReconnect(handle.sandbox.sandboxName, timeoutSecs, deps)) { + const supervisorReconnectTimeoutSecs = getDockerGpuSupervisorReconnectTimeoutSecs(timeoutSecs); + if ( + !waitForOpenShellSupervisorReconnect( + handle.sandbox.sandboxName, + supervisorReconnectTimeoutSecs, + deps, + ) + ) { throw new Error("Managed bootstrap Docker supervisor did not reconnect."); } const afterWaitJournal = deps.journalStore.load(journal.bootstrapIdentity); From 94bb73be3ead068914c21ec481fb4f0ef774fd38 Mon Sep 17 00:00:00 2001 From: prekshivyas Date: Thu, 13 Aug 2026 12:43:02 -0700 Subject: [PATCH 09/10] test(connect): preserve explicit WSL harness override --- ci/source-architecture-budget.json | 2 +- test/support/connect-flow-test-harness.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ci/source-architecture-budget.json b/ci/source-architecture-budget.json index c6d2f0527af..7394b419845 100644 --- a/ci/source-architecture-budget.json +++ b/ci/source-architecture-budget.json @@ -56,7 +56,7 @@ }, "allowedCycles": [], "maxRootFiles": { - "src/lib/onboard": 309, + "src/lib/onboard": 308, "src/lib/actions": 19, "src/lib/actions/sandbox": 183, "src/lib/state": 38, diff --git a/test/support/connect-flow-test-harness.ts b/test/support/connect-flow-test-harness.ts index bae208eaadb..0533186861f 100644 --- a/test/support/connect-flow-test-harness.ts +++ b/test/support/connect-flow-test-harness.ts @@ -183,10 +183,6 @@ export function createConnectHarness(options: ConnectHarnessOptions = {}): Conne const operation = args[1] as () => unknown; return { kind: "entered", value: await operation() }; }) as never); - if (typeof options.isWsl === "boolean") { - vi.spyOn(platform, "isWsl").mockReturnValue(options.isWsl); - } - const preflightVllmSpy = vi .spyOn(connectVllmPreflight, "preflightVllmModelEnvOrExit") .mockImplementation(() => undefined); @@ -282,7 +278,9 @@ export function createConnectHarness(options: ConnectHarnessOptions = {}): Conne // machine. With the gate pinned, the stubbed environment decides, on every // host, and a caller's own options still win over the pin (#8868). vi.spyOn(platform, "isWsl").mockImplementation((...args: unknown[]) => - realIsWsl({ platform: "linux", ...((args[0] as WslDetectionOptions | undefined) ?? {}) }), + typeof options.isWsl === "boolean" + ? options.isWsl + : realIsWsl({ platform: "linux", ...((args[0] as WslDetectionOptions | undefined) ?? {}) }), ); const primaryRegistryEntry: SandboxEntry = { name: "alpha", From 23ea8bda7176ce676ce91b1721749905abca1de6 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Thu, 13 Aug 2026 13:06:39 -0700 Subject: [PATCH 10/10] test(onboard): assert reconnect runner fixture Signed-off-by: Prekshi Vyas --- src/lib/onboard/managed-bootstrap/docker.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/onboard/managed-bootstrap/docker.test.ts b/src/lib/onboard/managed-bootstrap/docker.test.ts index 4a0e66eb2d8..ad29ac3ec0a 100644 --- a/src/lib/onboard/managed-bootstrap/docker.test.ts +++ b/src/lib/onboard/managed-bootstrap/docker.test.ts @@ -477,9 +477,9 @@ describe("Docker managed bootstrap adapter", () => { durablePreparation: durable, }); const dateNow = vi.spyOn(Date, "now").mockReturnValueOnce(0).mockReturnValue(2_000); - vi.mocked(fake.deps.runOpenshell).mockImplementationOnce(() => ({ status: 1 })).mockReturnValue({ - status: 0, - }); + vi.mocked(fake.deps.runOpenshell!) + .mockImplementationOnce(() => ({ status: 1 })) + .mockReturnValue({ status: 0 }); await expect( adapter.awaitBootstrap({