From a996ea25d9e46dc5ea932e272a989dee3043e590 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 17 Aug 2026 17:01:23 -0700 Subject: [PATCH] fix(update): support npm before lifecycle allowlists (#125452) --- README.md | 8 ++-- docs/index.md | 3 +- docs/install/daytona.md | 4 +- docs/install/index.md | 11 ++---- docs/install/macos-vm.md | 4 +- docs/install/updating.md | 22 +++++------ docs/install/upstash.md | 4 +- docs/plan/cloud-workers.md | 2 +- docs/platforms/chromeos.md | 4 +- docs/platforms/linux.md | 2 +- docs/platforms/mac/bundled-gateway.md | 4 +- docs/platforms/mac/dev-setup.md | 4 +- docs/plugins/dependency-resolution.md | 6 +-- src/cli/update-cli.test.ts | 54 -------------------------- src/infra/package-update-steps.test.ts | 14 +++---- src/infra/update-global.test.ts | 12 ++++-- src/infra/update-global.ts | 21 +++------- src/infra/update-runner.test.ts | 21 ---------- 18 files changed, 56 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index e12f11fc8880..c74989cee8a9 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ Already manage Node.js? Install the published package instead (Node 22.22.3+, 24 npm install -g openclaw@latest --allow-scripts=openclaw ``` -That command is for npm 12 or npm 11.16+. On npm 11.12 and earlier, omit -`--allow-scripts=openclaw`. If you have npm 11.13–11.15, upgrade npm first. See -the [installation guide](https://docs.openclaw.ai/install) for the lifecycle -script contract, Docker, Nix, and other deployment paths. +That command is for npm 12 or npm 11.16+. On npm 11.15 and earlier, omit +`--allow-scripts=openclaw`. See the +[installation guide](https://docs.openclaw.ai/install) for the lifecycle script +contract, Docker, Nix, and other deployment paths. ## Quick start diff --git a/docs/index.md b/docs/index.md index 5a8f8f959c1f..520f26a8b862 100644 --- a/docs/index.md +++ b/docs/index.md @@ -148,8 +148,7 @@ The Gateway is the single source of truth for sessions, routing, and channel con npm install -g openclaw@latest --allow-scripts=openclaw ``` - On npm 11.12 and earlier, omit `--allow-scripts=openclaw`. Upgrade npm - 11.13–11.15 before installing. + On npm 11.15 and earlier, omit `--allow-scripts=openclaw`. diff --git a/docs/install/daytona.md b/docs/install/daytona.md index 94d6e938fa9b..3b2ac7cab38f 100644 --- a/docs/install/daytona.md +++ b/docs/install/daytona.md @@ -242,8 +242,8 @@ personal-number mode, and self-chat details: [WhatsApp](/channels/whatsapp). The snapshot's global npm tree is owned by root, so plain `openclaw update` cannot write to it. Update from the sandbox SSH session with: -The command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier, -omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +The command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier, +omit `--allow-scripts=openclaw`. ```bash sudo env "PATH=$PATH" npm install --global openclaw@latest --allow-scripts=openclaw diff --git a/docs/install/index.md b/docs/install/index.md index 95fb2251c1f9..9fa6469cb31f 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -82,9 +82,8 @@ If you already manage Node yourself: openclaw onboard --install-daemon ``` - On npm 11.12 and earlier, use the same command without - `--allow-scripts=openclaw`. Do not use npm 11.13–11.15 for this install; - upgrade to npm 11.16+ first. + On npm 11.15 and earlier, use the same command without + `--allow-scripts=openclaw`. npm 12 blocks unapproved package lifecycle scripts by default. The @@ -93,11 +92,9 @@ If you already manage Node yourself: they are not covered by allowScripts`. npm 11.16 accepts the option but otherwise only warns that the scripts are - `not yet covered by allowScripts` and still runs them. npm 11.12 and earlier + `not yet covered by allowScripts` and still runs them. npm 11.15 and earlier have neither the policy nor the option, so their command must be unflagged. - npm 11.13–11.15 also lack the option, but they are transitional upstream - releases outside this documented install contract; upgrade rather than - relying on their unflagged behavior. The `npm approve-scripts openclaw` + The `npm approve-scripts openclaw` command suggested by npm 11.16 does not work for a global install — it fails with `ENOMATCH No installed packages match: openclaw`. diff --git a/docs/install/macos-vm.md b/docs/install/macos-vm.md index 8c6bcbf3f76b..107cab990a64 100644 --- a/docs/install/macos-vm.md +++ b/docs/install/macos-vm.md @@ -116,8 +116,8 @@ Replace `youruser` with the account you created, and the IP with your VM's IP. ## 6) Install OpenClaw -Inside the VM, use the following command on npm 12 or npm 11.16+. On npm 11.12 -and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +Inside the VM, use the following command on npm 12 or npm 11.16+. On npm 11.15 +and earlier, omit `--allow-scripts=openclaw`. ```bash npm install -g openclaw@latest --allow-scripts=openclaw diff --git a/docs/install/updating.md b/docs/install/updating.md index 3f6b2399726f..cc286ff0ce45 100644 --- a/docs/install/updating.md +++ b/docs/install/updating.md @@ -49,8 +49,8 @@ successful core update; a direct `npm install -g openclaw@extended-stable --allow-scripts=openclaw` does not update `update.channel`, but a final extended-stable package version still checks only the verified `extended-stable` selector for update availability. -That direct command is for npm 12 or npm 11.16+. On npm 11.12 and earlier, -omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +That direct command is for npm 12 or npm 11.16+. On npm 11.15 and earlier, +omit `--allow-scripts=openclaw`. After the core swap, eligible official npm plugins with bare/default or `latest` intent converge to that exact core version. Exact pins and explicit non-`latest` tags, third-party plugins, and non-npm sources remain unchanged. @@ -157,8 +157,8 @@ curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method npm --ve ## Alternative: manual npm, pnpm, or bun -The npm command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier, -omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier, +omit `--allow-scripts=openclaw`. ```bash npm i -g openclaw@latest --allow-scripts=openclaw @@ -178,7 +178,7 @@ that Gateway. Replace `/usr/bin/npm` with the system npm that owns the root-owned global prefix on your host: The npm command below follows the same version contract: use the flag on npm 12 -or npm 11.16+, omit it on npm 11.12 and earlier, and upgrade npm 11.13–11.15. +or npm 11.16+, and omit it on npm 11.15 and earlier. ```bash openclaw gateway stop @@ -203,10 +203,10 @@ Node version during `preinstall`; only then does OpenClaw verify the packaged `dist` inventory and swap the clean package tree into the real global prefix. A packed completion guard is omitted from the expected inventory and removed only after `preinstall` succeeds, so skipped lifecycle scripts also fail before the -swap. The updater probes the owning npm before mutation. On npm 11.12 and -earlier it omits the unsupported lifecycle-policy flag; on npm 11.13–11.15 it -stops with upgrade guidance. On npm 12 and npm 11.16+, it approves only the -candidate OpenClaw lifecycle; transitive dependency scripts remain unapproved. +swap. The updater probes the owning npm before mutation. On npm 11.15 and +earlier it omits the unsupported lifecycle-policy flag. On npm 12 and npm +11.16+, it approves only the candidate OpenClaw lifecycle; transitive +dependency scripts remain unapproved. This avoids npm overlaying a new package onto stale files from the old one. If the install command fails, OpenClaw retries once with `--omit=optional`, which helps hosts where native optional dependencies cannot compile. @@ -445,8 +445,8 @@ automatically replacing the package again. If the CLI update path is unavailable, use the same package manager and install scope that own the current Gateway: -The npm command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier, -omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier, +omit `--allow-scripts=openclaw`. ```bash openclaw gateway stop diff --git a/docs/install/upstash.md b/docs/install/upstash.md index fe32a6b61c67..ba8f572a25b1 100644 --- a/docs/install/upstash.md +++ b/docs/install/upstash.md @@ -39,8 +39,8 @@ The keepalive options reduce idle tunnel drops during onboarding. ## Install OpenClaw -Inside the Box, use the following command on npm 12 or npm 11.16+. On npm 11.12 -and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +Inside the Box, use the following command on npm 12 or npm 11.16+. On npm 11.15 +and earlier, omit `--allow-scripts=openclaw`. ```bash sudo npm install -g openclaw --allow-scripts=openclaw diff --git a/docs/plan/cloud-workers.md b/docs/plan/cloud-workers.md index d0c77ebdad30..7975cafdea26 100644 --- a/docs/plan/cloud-workers.md +++ b/docs/plan/cloud-workers.md @@ -88,7 +88,7 @@ RPCs: `environments.create`, `environments.destroy`, extended `environments.list No bespoke worker artifact, and no dependence on npm availability: - Canonical install for all modes: a gateway-produced, content-hashed worker bundle (the gateway's own build output packed as a tarball), pushed over SSH and installed on the box. This covers dev builds and unreleased commits by construction. -- On npm 12 or npm 11.16+, `npm i -g openclaw@ --allow-scripts=openclaw` is an optimization when the gateway runs a released version; never `latest`. On npm 11.12 and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +- On npm 12 or npm 11.16+, `npm i -g openclaw@ --allow-scripts=openclaw` is an optimization when the gateway runs a released version; never `latest`. On npm 11.15 and earlier, omit `--allow-scripts=openclaw`. - Bootstrap is idempotent; a warm lease with a matching bundle hash skips install. Raw machines may need a networked toolchain phase (Node runtime) — part of the setup phase, closed afterwards. - Handshake verifies worker build hash, protocol feature set, and runtime compatibility. The existing gateway version/protocol checks are insufficient for this (SSH-tunneled nodes are exempted from exact-version rejection), so worker admission does its own exact-build check. diff --git a/docs/platforms/chromeos.md b/docs/platforms/chromeos.md index dbf3fb6a745d..0f274cae8be1 100644 --- a/docs/platforms/chromeos.md +++ b/docs/platforms/chromeos.md @@ -54,8 +54,8 @@ Full server guidance lives in the [Linux guide](/platforms/linux) and the On a single user Chromebook, use the native npm install (the installer script, or `npm i -g openclaw@latest --allow-scripts=openclaw` on npm 12 or npm -11.16+) rather than [Docker](/install/docker). On npm 11.12 and earlier, omit -`--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +11.16+) rather than [Docker](/install/docker). On npm 11.15 and earlier, omit +`--allow-scripts=openclaw`. Docker works inside Crostini, but Docker in Crostini adds friction: if you use the Claude Code CLI as your model runtime, it has to be installed and logged in diff --git a/docs/platforms/linux.md b/docs/platforms/linux.md index 2b503b36f424..78775449d5ad 100644 --- a/docs/platforms/linux.md +++ b/docs/platforms/linux.md @@ -130,7 +130,7 @@ Linux v1 uses one Canvas window. HTTP and HTTPS pages are renderable, but A2UI a The CLI remains the simplest option for a headless server, a VPS, or a remote Gateway: 1. Install Node 26 (recommended), or another supported release: Node 22.22.3+, Node 24.15+, or Node 25.9+. -2. On npm 12 or npm 11.16+, run `npm i -g openclaw@latest --allow-scripts=openclaw`. On npm 11.12 and earlier, omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +2. On npm 12 or npm 11.16+, run `npm i -g openclaw@latest --allow-scripts=openclaw`. On npm 11.15 and earlier, omit `--allow-scripts=openclaw`. 3. `openclaw onboard --install-daemon` 4. From your laptop: `ssh -N -L 18789:127.0.0.1:18789 @` 5. Open `http://127.0.0.1:18789/` and authenticate with the configured shared diff --git a/docs/platforms/mac/bundled-gateway.md b/docs/platforms/mac/bundled-gateway.md index 1a32655d4df5..23340b9da9f2 100644 --- a/docs/platforms/mac/bundled-gateway.md +++ b/docs/platforms/mac/bundled-gateway.md @@ -29,8 +29,8 @@ OpenClaw package. For a manual install, use Node 26 (recommended) or another supported release: Node 22.22.3+, Node 24.15+, or Node 25.9+. Install `openclaw` globally: -The command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier, -omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +The command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier, +omit `--allow-scripts=openclaw`. ```bash npm install -g openclaw@ --allow-scripts=openclaw diff --git a/docs/platforms/mac/dev-setup.md b/docs/platforms/mac/dev-setup.md index ac585d878cac..e216ea29c505 100644 --- a/docs/platforms/mac/dev-setup.md +++ b/docs/platforms/mac/dev-setup.md @@ -55,8 +55,8 @@ matching user-space CLI and runtime before starting the Gateway wizard. For manual development recovery, install the matching CLI yourself: -The npm command below is for npm 12 or npm 11.16+. On npm 11.12 and earlier, -omit `--allow-scripts=openclaw`; upgrade npm 11.13–11.15 first. +The npm command below is for npm 12 or npm 11.16+. On npm 11.15 and earlier, +omit `--allow-scripts=openclaw`. ```bash npm install -g openclaw@ --allow-scripts=openclaw diff --git a/docs/plugins/dependency-resolution.md b/docs/plugins/dependency-resolution.md index def508860c87..70b343138402 100644 --- a/docs/plugins/dependency-resolution.md +++ b/docs/plugins/dependency-resolution.md @@ -207,9 +207,9 @@ the repository root does not prepare bundled plugin dependencies. For the global npm row, use `npm install -g openclaw --allow-scripts=openclaw` on npm 12 or npm 11.16+. -On npm 11.12 and earlier, omit `--allow-scripts=openclaw`; upgrade npm -11.13–11.15 first. Plugin dependency convergence remains intentionally -script-disabled and continues to use the `--ignore-scripts` commands above. +On npm 11.15 and earlier, omit `--allow-scripts=openclaw`. Plugin dependency +convergence remains intentionally script-disabled and continues to use the +`--ignore-scripts` commands above. ## Legacy cleanup diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 5e9f2e7a734d..3a18abded921 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -4717,34 +4717,6 @@ describe("update-cli", () => { expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1); }); - it.each(["11.13.0", "11.15.9"])( - "refuses npm %s before stopping the managed gateway or cleaning update backups", - async (npmVersion) => { - const tempDir = await createTrackedTempDir("openclaw-update-npm-policy-"); - const { nodeModules, entryPath } = await setupInstalledPackageRoot(tempDir); - const backupDir = path.join(nodeModules, ".openclaw-interrupted"); - await fs.mkdir(backupDir, { recursive: true }); - mockRunningManagedGateway(["node", entryPath, "gateway", "run"]); - mockFileBackedPathExists(); - vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => { - if (argv[0] === "npm" && argv[1] === "--version") { - return commandResult({ stdout: `${npmVersion}\n` }); - } - if (argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") { - return commandResult({ stdout: `${nodeModules}\n` }); - } - return commandResult(); - }); - - await updateCommand({ yes: true }); - - expect(serviceStop).not.toHaveBeenCalled(); - expect(packageInstallCommandCall()).toBeUndefined(); - await expect(fs.access(backupDir)).resolves.toBeUndefined(); - expect(getErrorOutput()).toContain(`npm ${npmVersion} cannot safely approve`); - }, - ); - it("stops a running managed gateway before package replacement", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const processOnSpy = vi.spyOn(process, "on"); @@ -6929,32 +6901,6 @@ describe("update-cli", () => { expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1); }); - it.each(["11.13.0", "11.15.9"])( - "refuses npm %s package-to-dev updates before checkout or install", - async (npmVersion) => { - const packageRoot = createCaseDir("openclaw-npm-transition"); - mockPackageInstallStatus(packageRoot); - vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => { - if (argv[0] === "npm" && argv[1] === "--version") { - return commandResult({ stdout: `${npmVersion}\n` }); - } - if (argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") { - return commandResult({ stdout: `${path.dirname(packageRoot)}\n` }); - } - return commandResult(); - }); - - await updateCommand({ channel: "dev", yes: true, restart: false }); - - expect(runGatewayUpdate).not.toHaveBeenCalled(); - expect(commandCalls().some(([argv]) => argv[0] === "git")).toBe(false); - expect( - commandCalls().some(([argv]) => argv[0] === "npm" && argv[1] === "i" && argv[2] === "-g"), - ).toBe(false); - expect(getErrorOutput()).toContain(`npm ${npmVersion} cannot safely approve`); - }, - ); - it("explains why git updates cannot run with edited files", async () => { vi.mocked(defaultRuntime.log).mockClear(); vi.mocked(defaultRuntime.error).mockClear(); diff --git a/src/infra/package-update-steps.test.ts b/src/infra/package-update-steps.test.ts index d8062b35a74d..fa6033992d4b 100644 --- a/src/infra/package-update-steps.test.ts +++ b/src/infra/package-update-steps.test.ts @@ -163,18 +163,14 @@ describe("markPackagePostInstallDoctorAdvisory", () => { }); describe("npm lifecycle policy preflight", () => { - it.each([ - { version: "11.13.0", message: "Upgrade the owning npm to 11.16" }, - { version: "11.15.9", message: "Upgrade the owning npm to 11.16" }, - { version: null, message: "Unable to determine the owning npm version" }, - ])("stops before mutation for npm $version", async ({ version, message }) => { + it("stops before mutation when the owning npm version is unknown", async () => { const runStep = vi.fn(); const runCommand = vi.fn(); const installTarget = createNpmTarget("/tmp/npm-policy-test/lib/node_modules"); installTarget.npmOwner = { - version, - lifecyclePolicy: version ? "unsupported-transition" : null, - ...(version ? {} : { probeError: "version probe failed" }), + version: null, + lifecyclePolicy: null, + probeError: "version probe failed", }; const result = await runGlobalPackageUpdateSteps({ @@ -187,7 +183,7 @@ describe("npm lifecycle policy preflight", () => { }); expect(runCommand).not.toHaveBeenCalled(); - expect(result.failedStep?.stderrTail).toContain(message); + expect(result.failedStep?.stderrTail).toContain("Unable to determine the owning npm version"); expect(runStep).not.toHaveBeenCalled(); }); }); diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts index 2a7dee822594..375654b22887 100644 --- a/src/infra/update-global.test.ts +++ b/src/infra/update-global.test.ts @@ -213,9 +213,9 @@ describe("update global helpers", () => { it.each([ ["11.12.0", "unflagged"], - ["11.13.0", "unsupported-transition"], - ["11.14.0", "unsupported-transition"], - ["11.15.9", "unsupported-transition"], + ["11.13.0", "unflagged"], + ["11.14.0", "unflagged"], + ["11.15.9", "unflagged"], ["11.16.0", "allow-scripts"], ["12.0.0", "allow-scripts"], ] as const)("binds npm %s lifecycle policy to the owning executable", async (version, policy) => { @@ -775,6 +775,12 @@ describe("update global helpers", () => { ]); }); + it("omits npm's lifecycle allowlist before npm 11.16", () => { + expect( + globalInstallArgs("npm", "openclaw@latest", null, null, null, "unflagged"), + ).not.toContain("--allow-scripts=openclaw"); + }); + it("allows only the resolved npm candidate lifecycle identity", () => { expect(globalInstallArgs("npm", "/tmp/openclaw-2026.7.2.tgz")).toContain( "--allow-scripts=/tmp/openclaw-2026.7.2.tgz", diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index 964445781994..ad5962bf17e0 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -83,9 +83,9 @@ export type NpmGlobalPrefixLayout = { binDir: string; }; -type NpmLifecyclePolicy = "unsupported-transition" | "unflagged" | "allow-scripts"; +type NpmLifecyclePolicy = "unflagged" | "allow-scripts"; -type SupportedNpmLifecyclePolicy = Exclude; +type SupportedNpmLifecyclePolicy = NpmLifecyclePolicy; type NpmLifecyclePolicyGate = | { policy: SupportedNpmLifecyclePolicy | null; error: null } @@ -97,13 +97,9 @@ function resolveNpmLifecyclePolicy(version: string): NpmLifecyclePolicy | null { if (!parsed) { return null; } - if (parsed.major !== 11) { - return parsed.major >= 12 ? "allow-scripts" : "unflagged"; - } - if (parsed.minor <= 12) { - return "unflagged"; - } - return parsed.minor >= 16 ? "allow-scripts" : "unsupported-transition"; + return parsed.major >= 12 || (parsed.major === 11 && parsed.minor >= 16) + ? "allow-scripts" + : "unflagged"; } /** Resolves the owning npm policy once, before any update mutation. */ @@ -113,17 +109,10 @@ export function resolveNpmLifecyclePolicyGate( if (installTarget.manager !== "npm") { return { policy: null, error: null }; } - const version = installTarget.npmOwner?.version ?? ""; const policy = installTarget.npmOwner?.lifecyclePolicy ?? null; if (policy === "unflagged" || policy === "allow-scripts") { return { policy, error: null }; } - if (policy === "unsupported-transition") { - return { - policy: null, - error: `npm ${version} cannot safely approve OpenClaw lifecycle scripts. Upgrade the owning npm to 11.16 or newer before updating; no package changes were made.`, - }; - } return { policy: null, error: `Unable to determine the owning npm version before updating; no package changes were made.${installTarget.npmOwner?.probeError ? ` ${installTarget.npmOwner.probeError}` : ""}`, diff --git a/src/infra/update-runner.test.ts b/src/infra/update-runner.test.ts index 949dc3e49bfa..283fea002a5d 100644 --- a/src/infra/update-runner.test.ts +++ b/src/infra/update-runner.test.ts @@ -3013,27 +3013,6 @@ describe("runGatewayUpdate", () => { expect(await pathExists(staleDir)).toBe(false); }); - it("refuses unsupported npm before global update cleanup mutates backups", async () => { - const { nodeModules, pkgRoot } = await createGlobalPackageFixture(tempDir); - const backupDir = path.join(nodeModules, ".openclaw-interrupted"); - await fs.mkdir(backupDir, { recursive: true }); - const { runCommand } = createGlobalInstallHarness({ - pkgRoot, - npmRootOutput: nodeModules, - npmVersion: "11.15.9", - installCommand: npmGlobalInstallCommand("openclaw@latest"), - }); - - const result = await runWithCommand(runCommand, { cwd: pkgRoot }); - - expect(result).toMatchObject({ - status: "error", - reason: "unexpected-error", - }); - expect(result.steps.at(-1)?.name).toBe("npm lifecycle policy preflight"); - await expect(fs.access(backupDir)).resolves.toBeUndefined(); - }); - it("retries global npm update with --omit=optional when initial install fails", async () => { const nodeModules = path.join(tempDir, "node_modules"); const pkgRoot = path.join(nodeModules, "openclaw");