diff --git a/docs/changelog/2026-08-12.mdx b/docs/changelog/2026-08-12.mdx index 8376696eef7..7c9f3f660e0 100644 --- a/docs/changelog/2026-08-12.mdx +++ b/docs/changelog/2026-08-12.mdx @@ -9,8 +9,11 @@ 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 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 077fadce632..12a87ab255c 100644 --- a/docs/inference/set-up-vllm.mdx +++ b/docs/inference/set-up-vllm.mdx @@ -295,7 +295,9 @@ 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 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 47ee13ac6ff..df43dfede51 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,24 @@ 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. -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. +## 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_GATEWAY_PORT= NEMOCLAW_OPENSHELL_UPGRADE_PREPARED=1 bash +``` + +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. +If the installation fails, rerun the same install-pipeline command to preserve `NEMOCLAW_GATEWAY_PORT` and `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED`. 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 +137,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/reference/commands.mdx b/docs/reference/commands.mdx index 9d77bc3d95f..1db00b0d34f 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -531,8 +531,14 @@ 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. +If the installation fails, rerun the same install-pipeline command to preserve `NEMOCLAW_GATEWAY_PORT` and `NEMOCLAW_OPENSHELL_UPGRADE_PREPARED`. 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/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..be024f024b9 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. + +### 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. 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. diff --git a/scripts/install.sh b/scripts/install.sh index dbf8ef74226..314ec8083f5 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/src/lib/onboard/managed-bootstrap/docker.test.ts b/src/lib/onboard/managed-bootstrap/docker.test.ts index dfb6699f632..ad29ac3ec0a 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); diff --git a/test/install-openshell-upgrade-prompt.test.ts b/test/install-openshell-upgrade-prompt.test.ts index 04c587bfc79..f812571ef33 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 }); @@ -107,6 +115,7 @@ function runPreinstallUpgradeGuard( currentMaxOpenshellVersion?: string; currentMinOpenshellVersion?: string; finishDeferAsPlain?: boolean; + finishGatewayPort?: string; finishInstallMode?: "managed" | "source" | "unset"; finishPreparedInstallSucceeds?: boolean; gatewayDestroySucceeds?: boolean; @@ -148,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"; @@ -246,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:-}" @@ -598,6 +609,18 @@ 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"; + 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)", () => { const { result, cliLog, openshellLog } = runPreinstallUpgradeGuard({ NON_INTERACTIVE: "1", @@ -900,13 +923,27 @@ esac`, expect(openshellLog).toBe("openshell install-mode force defer=\n"); }); - it("preserves prepared backups when the required OpenShell install fails after retirement (#8800)", () => { + it.each([ + { + 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) => { const { result, cliLog, openshellLog } = runPreinstallUpgradeGuard( { NON_INTERACTIVE: "1" }, { currentMaxOpenshellVersion: "0.0.101", currentMinOpenshellVersion: "0.0.101", finishPreparedInstallSucceeds: false, + finishGatewayPort: testCase.finishGatewayPort, finishInstallMode: "source", gatewayDestroySucceeds: false, gatewayProcessStopSucceeds: false, @@ -921,7 +958,8 @@ 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(testCase.expectedRetry); + 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="); }); diff --git a/test/support/connect-flow-test-harness.ts b/test/support/connect-flow-test-harness.ts index c5bf17699ff..f1440de5e08 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); @@ -275,17 +271,17 @@ export function createConnectHarness(options: ConnectHarnessOptions = {}): Conne const probeOllamaAuthProxyHealthSpy = vi .spyOn(ollamaProxy, "probeOllamaAuthProxyHealth") .mockReturnValue({ ok: true }); - if (typeof options.isWsl !== "boolean") { - const realIsWsl = platform.isWsl as (opts?: WslDetectionOptions) => boolean; - // Pin the platform gate for every isWsl consumer the harness loads: isWsl - // answers false off Linux before it reads WSL_DISTRO_NAME, so a case that - // stubs that variable cannot reach the WSL route on a macOS contributor - // 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) ?? {}) }), - ); - } + const realIsWsl = platform.isWsl as (opts?: WslDetectionOptions) => boolean; + // Pin the platform gate for every isWsl consumer the harness loads: isWsl + // answers false off Linux before it reads WSL_DISTRO_NAME, so a case that + // stubs that variable cannot reach the WSL route on a macOS contributor + // 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[]) => + typeof options.isWsl === "boolean" + ? options.isWsl + : realIsWsl({ platform: "linux", ...((args[0] as WslDetectionOptions | undefined) ?? {}) }), + ); const primaryRegistryEntry: SandboxEntry = { name: "alpha", agent: options.agentName ?? "openclaw",