From 07bcf008bb4f7fd8f074dab2c01adda7d8026c86 Mon Sep 17 00:00:00 2001 From: OmarMcAdam Date: Thu, 22 Oct 2026 09:38:01 -0700 Subject: [PATCH 1/2] fix(security): standardize standalone runtimes on Node 24.22.1 Pin runtime images, CI and downloads to the October security release; preserve artifact verification and require patched Node 24 in standalone launchers. Update standalone Undici, add runtime baseline and Helm probe regression coverage, and document the separate rebuild/deployment and Electron follow-ups. --- .devcontainer/Dockerfile | 2 +- .devcontainer/Dockerfile.daytona-server | 28 ++++- .devcontainer/Dockerfile.daytona-vnc | 27 +++- .devcontainer/docker-compose.yml | 2 +- .devcontainer/node-runtime.test.mjs | 89 ++++++++++++++ .github/actions/setup-tests/action.yml | 2 +- .github/workflows/alpha-macos-aarch64.yml | 4 +- .github/workflows/api-contract.yml | 2 +- .github/workflows/build-electron-desktop.yml | 4 +- .github/workflows/celld-runner-deploy.yml | 2 +- .github/workflows/changelog.yml | 2 +- .github/workflows/ci-enterprise-mcp-mock.yml | 2 +- .github/workflows/ci-i18n.yml | 2 +- .github/workflows/ci-opencode-openwork.yml | 2 +- .github/workflows/ci-openwork-ui-mcp.yml | 4 +- .github/workflows/ci-sdk.yml | 2 +- .github/workflows/ci-tests.yml | 6 +- .github/workflows/codeql-result-coverage.yml | 2 +- .github/workflows/contributor-pr-test.yml | 6 +- .github/workflows/contributor-pr.yml | 2 +- .github/workflows/contributor-warden.yml | 4 +- .github/workflows/daytona-eval-image.yml | 4 +- .github/workflows/den-db-check.yml | 2 +- .github/workflows/den-db-migrate.yml | 2 +- .github/workflows/download-stats.yml | 2 +- .github/workflows/e2e-test-failure-alerts.yml | 2 +- .github/workflows/evidence-review-checks.yml | 4 +- .github/workflows/evidence-review.yml | 4 +- .github/workflows/features-check.yml | 2 +- .github/workflows/freestyle-cleanup.yml | 2 +- .github/workflows/freestyle-warm-dev.yml | 2 +- .github/workflows/freestyle-world-check.yml | 2 +- .github/workflows/opencode-agents.yml | 4 +- .github/workflows/pr-proof.yml | 6 +- .github/workflows/publish-ee-images.yml | 8 ++ .github/workflows/publish-opencode-plugin.yml | 2 +- .github/workflows/purge-model-catalog.yml | 2 +- .github/workflows/release-macos-aarch64.yml | 22 ++-- .github/workflows/review-app-deploy.yml | 2 +- .github/workflows/update-models.yml | 2 +- .github/workflows/warden-check-backfill.yml | 2 +- .nvmrc | 2 +- apps/app/package.json | 3 + apps/desktop/package.json | 2 +- apps/review/package.json | 3 + apps/server/README.md | 2 +- apps/server/bin/openwork-server.mjs | 2 +- apps/server/bin/platform.mjs | 14 +-- apps/server/package.json | 5 +- apps/server/scripts/publish-npm.mjs | 3 +- apps/server/test/node-runtime.test.mjs | 39 ++++++ docs/alchemy-effect-testkit-migration.md | 4 +- docs/computer-use/workbot.md | 10 +- docs/node-runtime-security.md | 63 ++++++++++ ee/apps/den-api/package.json | 5 +- ee/apps/den-api/src/env.ts | 2 +- ee/apps/den-gateway/package.json | 5 +- ee/apps/den-web/package.json | 3 + .../Dockerfile.daytona-snapshot | 4 +- ee/apps/diagnostics/package.json | 3 + ee/apps/gateway/package.json | 3 + ee/apps/headless-runner/README.md | 2 +- ee/apps/headless-runner/package.json | 2 +- ee/apps/landing/README.md | 4 +- ee/apps/landing/app/install.sh/route.ts | 35 ++++-- ee/apps/landing/app/start.md/route.ts | 2 +- ee/apps/landing/package.json | 3 + .../set-up-openwork-team/SKILL.md | 2 +- ee/apps/landing/public/llms.txt | 2 +- ee/apps/workbot/package.json | 2 +- .../headless-computer/Dockerfile.desktop | 4 +- .../headless-computer/src/image.test.ts | 18 +++ ee/packages/headless-computer/src/image.ts | 16 ++- evals/packages/hosts/src/sandbox-daytona.ts | 2 +- evals/packages/open-workplace/README.md | 2 +- evals/packages/open-workplace/package.json | 2 +- package.json | 4 + packages/computer-use/README.md | 2 +- packages/daytona-evidence/package.json | 2 +- packages/daytona-evidence/src/builder.ts | 8 +- .../use-openwork-from-an-ai-agent.mdx | 2 +- .../enterprise-mcp-mock-server/package.json | 2 +- packages/evidence-provider/package.json | 2 +- packages/openwork-bootstrap/README.md | 6 + packages/openwork-bootstrap/bin/openwork.mjs | 15 ++- .../agent-test-evidence-linux-container.mjs | 4 +- packages/openwork-bootstrap/package.json | 2 +- packages/openwork-bootstrap/start.md | 4 +- packages/openwork-bootstrap/test/README.md | 19 ++- .../test/fixtures/node-version.mjs | 2 + .../test/installer-runtime.test.mjs | 101 +++++++++++++++ .../test/node-runtime.test.mjs | 39 ++++++ packages/sandbox-daytona/src/live.test.ts | 2 +- packages/world/package.json | 2 +- packaging/docker/Dockerfile | 2 +- packaging/docker/Dockerfile.den | 2 +- packaging/docker/Dockerfile.den-gateway | 2 +- packaging/docker/Dockerfile.den-web | 2 +- packaging/docker/Dockerfile.gateway | 2 +- packaging/docker/Dockerfile.headless-runner | 2 +- packaging/docker/Dockerfile.microsandbox | 4 +- packaging/docker/Dockerfile.workbot | 2 +- packaging/docker/docker-compose.dev.yml | 2 +- packaging/helm/openwork-ee/README.md | 36 ++++++ .../templates/tests/env-probe-job.yaml | 5 + .../helm/openwork-ee/tests/node-runtime.mjs | 116 ++++++++++++++++++ pnpm-lock.yaml | 44 +++---- pnpm-workspace.yaml | 4 +- scripts/tests/node-runtime-baseline.test.mjs | 66 ++++++++++ 109 files changed, 885 insertions(+), 173 deletions(-) create mode 100644 .devcontainer/node-runtime.test.mjs create mode 100644 apps/server/test/node-runtime.test.mjs create mode 100644 docs/node-runtime-security.md create mode 100644 ee/packages/headless-computer/src/image.test.ts create mode 100644 packages/openwork-bootstrap/test/fixtures/node-version.mjs create mode 100644 packages/openwork-bootstrap/test/installer-runtime.test.mjs create mode 100644 packages/openwork-bootstrap/test/node-runtime.test.mjs create mode 100644 packaging/helm/openwork-ee/tests/node-runtime.mjs create mode 100644 scripts/tests/node-runtime-baseline.test.mjs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 97260536bb4..a0d3fb31569 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,7 +4,7 @@ # Xvfb + noVNC so you can see/steer the real Electron app # from your browser. -FROM node:20-bookworm +FROM node:24.22.1-bookworm # System deps for Electron / Chromium + virtual display + utils RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/.devcontainer/Dockerfile.daytona-server b/.devcontainer/Dockerfile.daytona-server index 708b1bdec0e..9893e1484d3 100644 --- a/.devcontainer/Dockerfile.daytona-server +++ b/.devcontainer/Dockerfile.daytona-server @@ -16,22 +16,46 @@ RUN apt-get update \ git \ procps \ sudo \ - && /usr/local/share/nvm/current/bin/npm install -g pnpm@10.27.0 bun@1.3.14 \ + xz-utils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* /root/.cache /root/.npm /tmp/* \ && printf 'daytona ALL=(ALL) NOPASSWD:ALL\n' >/etc/sudoers.d/daytona-openwork \ && chmod 0440 /etc/sudoers.d/daytona-openwork \ && install -d -o daytona -g daytona /workspace -WORKDIR /workspace +# Replace the inherited Node default, preserving Daytona's NVM current/bin launcher path. +# Official checksums: https://nodejs.org/dist/v24.22.1/SHASUMS256.txt +RUN set -eu; \ + case "$(dpkg --print-architecture)" in \ + amd64) node_arch=x64; node_sha256=6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6 ;; \ + arm64) node_arch=arm64; node_sha256=dbabf7a1cbf947e6059ffad2c27b80041db97dea1e553b62afdee7f13fca336e ;; \ + *) echo "Unsupported Node architecture" >&2; exit 1 ;; \ + esac; \ + node_tmp=$(mktemp -d); \ + curl -fsSL -o "$node_tmp/node.tar.xz" "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-$node_arch.tar.xz"; \ + echo "$node_sha256 $node_tmp/node.tar.xz" | sha256sum -c -; \ + node_prefix=/usr/local/share/nvm/versions/node/v24.22.1; \ + mkdir -p "$node_prefix" /usr/local/share/nvm/alias; \ + tar -xJf "$node_tmp/node.tar.xz" -C "$node_prefix" --strip-components=1 --no-same-owner; \ + rm -rf "$node_tmp" /usr/local/share/nvm/current; \ + ln -s "$node_prefix" /usr/local/share/nvm/current; \ + echo v24.22.1 > /usr/local/share/nvm/alias/default; \ + chown -R daytona:daytona "$node_prefix"; \ + test "$(/usr/local/share/nvm/current/bin/node --version)" = v24.22.1 ENV PATH="/usr/local/share/nvm/current/bin:$PATH" +RUN test "$(node --version)" = v24.22.1 \ + && /usr/local/share/nvm/current/bin/npm install -g pnpm@10.27.0 bun@1.3.14 \ + && /usr/local/share/nvm/current/bin/npm cache clean --force + +WORKDIR /workspace # Everything under /workspace is created as the daytona user so no later # chown layer is needed: a recursive chown in its own layer copy-on-write # duplicates the entire baked workspace and pushed the image past the 20 GB # snapshot limit. USER daytona +RUN test "$(node --version)" = v24.22.1 RUN git clone --depth 1 --branch dev https://github.com/different-ai/openwork.git . \ && mkdir -p /workspace/.openwork-daytona \ diff --git a/.devcontainer/Dockerfile.daytona-vnc b/.devcontainer/Dockerfile.daytona-vnc index fa0b5fefdf6..a28aceac5fc 100644 --- a/.devcontainer/Dockerfile.daytona-vnc +++ b/.devcontainer/Dockerfile.daytona-vnc @@ -8,10 +8,32 @@ FROM daytonaio/sandbox:0.6.0 USER root RUN apt-get update \ - && apt-get install -y --no-install-recommends ffmpeg \ + && apt-get install -y --no-install-recommends ca-certificates curl ffmpeg xz-utils \ && rm -rf /var/lib/apt/lists/* -RUN /usr/local/share/nvm/current/bin/npm install -g pnpm@10.27.0 bun +# Replace the inherited Node default, preserving Daytona's NVM current/bin launcher path. +# Official checksums: https://nodejs.org/dist/v24.22.1/SHASUMS256.txt +RUN set -eu; \ + case "$(dpkg --print-architecture)" in \ + amd64) node_arch=x64; node_sha256=6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6 ;; \ + arm64) node_arch=arm64; node_sha256=dbabf7a1cbf947e6059ffad2c27b80041db97dea1e553b62afdee7f13fca336e ;; \ + *) echo "Unsupported Node architecture" >&2; exit 1 ;; \ + esac; \ + node_tmp=$(mktemp -d); \ + curl -fsSL -o "$node_tmp/node.tar.xz" "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-$node_arch.tar.xz"; \ + echo "$node_sha256 $node_tmp/node.tar.xz" | sha256sum -c -; \ + node_prefix=/usr/local/share/nvm/versions/node/v24.22.1; \ + mkdir -p "$node_prefix" /usr/local/share/nvm/alias; \ + tar -xJf "$node_tmp/node.tar.xz" -C "$node_prefix" --strip-components=1 --no-same-owner; \ + rm -rf "$node_tmp" /usr/local/share/nvm/current; \ + ln -s "$node_prefix" /usr/local/share/nvm/current; \ + echo v24.22.1 > /usr/local/share/nvm/alias/default; \ + chown -R daytona:daytona "$node_prefix"; \ + test "$(/usr/local/share/nvm/current/bin/node --version)" = v24.22.1 + +ENV PATH="/usr/local/share/nvm/current/bin:$PATH" +RUN test "$(node --version)" = v24.22.1 \ + && /usr/local/share/nvm/current/bin/npm install -g pnpm@10.27.0 bun WORKDIR /workspace @@ -27,3 +49,4 @@ RUN git clone --depth 1 --branch dev https://github.com/different-ai/openwork.gi RUN chown -R daytona:daytona /workspace USER daytona +RUN test "$(node --version)" = v24.22.1 diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 410c78d3fa8..e7dc0f6f54e 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -4,7 +4,7 @@ # # Services: # - MySQL 8.4 for the Den database -# - Workspace container with Node 20, Electron deps, Xvfb, noVNC +# - Workspace container with Node 24.22.1, Electron deps, Xvfb, noVNC # # Ports: # - 5173 App Vite dev server (HMR) diff --git a/.devcontainer/node-runtime.test.mjs b/.devcontainer/node-runtime.test.mjs new file mode 100644 index 00000000000..49f7d52163d --- /dev/null +++ b/.devcontainer/node-runtime.test.mjs @@ -0,0 +1,89 @@ +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import { mkdtempSync, mkdirSync, readFileSync, readlinkSync, rmSync, writeFileSync, existsSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { test } from "node:test" + +const checksums = { + amd64: "6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6", + arm64: "dbabf7a1cbf947e6059ffad2c27b80041db97dea1e553b62afdee7f13fca336e", +} + +for (const file of ["Dockerfile.daytona-server", "Dockerfile.daytona-vnc"]) { + const source = readFileSync(new URL(file, import.meta.url), "utf8") + const block = source.match(/^RUN set -eu;[\s\S]*?(?=\n\n)/m)?.[0] + assert.ok(block, `${file} must explicitly install Node rather than inherit it`) + const script = block.slice("RUN ".length).replaceAll("\\\n", "\n") + + test(`${file} pins official artifacts and preserves Daytona's launcher path`, () => { + assert.equal(spawnSync("sh", ["-n"], { input: script }).status, 0) + assert.match(script, /https:\/\/nodejs\.org\/dist\/v24\.22\.1\/node-v24\.22\.1-linux-\$node_arch\.tar\.xz/) + for (const [arch, checksum] of Object.entries(checksums)) { + assert.ok(script.includes(`${arch}) node_arch=${arch === "amd64" ? "x64" : "arm64"}; node_sha256=${checksum}`)) + } + assert.ok(script.indexOf("sha256sum -c -") < script.indexOf("tar -xJf")) + assert.match(source, /ENV PATH="\/usr\/local\/share\/nvm\/current\/bin:\$PATH"/) + assert.ok(source.indexOf(block) < source.indexOf("/usr/local/share/nvm/current/bin/npm install")) + assert.match(source, /USER daytona\nRUN test "\$\(node --version\)" = v24\.22\.1/) + assert.match(source, /\bxz-utils\b/) + }) + + function runInstaller(arch, checksumFailure = false) { + const root = mkdtempSync(join(tmpdir(), "openwork-node-image-")) + const bin = join(root, "bin") + const nvm = join(root, "nvm") + const log = join(root, "events") + mkdirSync(bin) + const stub = (name, body) => writeFileSync(join(bin, name), `#!/bin/sh\nset -eu\n${body}\n`, { mode: 0o755 }) + stub("dpkg", 'echo "$TEST_ARCH"') + stub("curl", 'test "$1" = -fsSL; test "$2" = -o; echo "$4" >> "$TEST_LOG"; echo archive > "$3"') + stub("sha256sum", `read -r checksum archive + test "$checksum" = "$TEST_CHECKSUM" + test -f "$archive" + echo verified >> "$TEST_LOG" + test "$TEST_CHECKSUM_FAILURE" = 0`) + stub("tar", `test "$1" = -xJf; test "$3" = -C + echo extracted >> "$TEST_LOG" + mkdir -p "$4/bin" + printf '#!/bin/sh\\necho v24.22.1\\n' > "$4/bin/node" + chmod +x "$4/bin/node"`) + stub("chown", ":") + const result = spawnSync("sh", ["-c", script.replaceAll("/usr/local/share/nvm", nvm)], { + encoding: "utf8", + env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, TEST_ARCH: arch, TEST_LOG: log, + TEST_CHECKSUM: checksums[arch] ?? "", TEST_CHECKSUM_FAILURE: checksumFailure ? "1" : "0" }, + }) + return { root, nvm, result, events: existsSync(log) ? readFileSync(log, "utf8") : "" } + } + + for (const arch of Object.keys(checksums)) { + test(`${file} installs ${arch} and updates both NVM current and default`, () => { + const run = runInstaller(arch) + try { + assert.equal(run.result.status, 0, run.result.stderr) + assert.equal(readlinkSync(join(run.nvm, "current")), join(run.nvm, "versions/node/v24.22.1")) + assert.equal(readFileSync(join(run.nvm, "alias/default"), "utf8"), "v24.22.1\n") + assert.match(run.events, new RegExp(`node-v24\\.22\\.1-linux-${arch === "amd64" ? "x64" : "arm64"}\\.tar\\.xz`)) + assert.match(run.events, /verified\nextracted\n/) + } finally { rmSync(run.root, { recursive: true, force: true }) } + }) + } + + for (const [label, arch, failChecksum] of [["checksum mismatch", "amd64", true], ["unsupported architecture", "riscv64", false]]) { + test(`${file} fails closed on ${label} before extraction or activation`, () => { + const run = runInstaller(arch, failChecksum) + try { + assert.notEqual(run.result.status, 0) + assert.doesNotMatch(run.events, /extracted/) + assert.equal(existsSync(join(run.nvm, "current")), false) + } finally { rmSync(run.root, { recursive: true, force: true }) } + }) + } +} + +test("workspace devcontainer documentation matches the pinned runtime", () => { + const source = readFileSync(new URL("docker-compose.yml", import.meta.url), "utf8") + assert.match(source, /Workspace container with Node 24\.22\.1/) + assert.doesNotMatch(source, /Workspace container with Node 20/) +}) diff --git a/.github/actions/setup-tests/action.yml b/.github/actions/setup-tests/action.yml index aeb55404581..817c631a919 100644 --- a/.github/actions/setup-tests/action.yml +++ b/.github/actions/setup-tests/action.yml @@ -6,7 +6,7 @@ runs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/alpha-macos-aarch64.yml b/.github/workflows/alpha-macos-aarch64.yml index 0fb88532f6b..4a90bf21fd1 100644 --- a/.github/workflows/alpha-macos-aarch64.yml +++ b/.github/workflows/alpha-macos-aarch64.yml @@ -97,7 +97,7 @@ jobs: - name: Setup Node uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 @@ -318,7 +318,7 @@ jobs: - name: Setup Node uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 diff --git a/.github/workflows/api-contract.yml b/.github/workflows/api-contract.yml index 59a89161ee2..b46ef7e009e 100644 --- a/.github/workflows/api-contract.yml +++ b/.github/workflows/api-contract.yml @@ -52,7 +52,7 @@ jobs: - name: Setup Node uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 diff --git a/.github/workflows/build-electron-desktop.yml b/.github/workflows/build-electron-desktop.yml index 87a94a74dff..65607eb675d 100644 --- a/.github/workflows/build-electron-desktop.yml +++ b/.github/workflows/build-electron-desktop.yml @@ -48,7 +48,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup Bun uses: oven-sh/setup-bun@v2 @@ -94,7 +94,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup Bun uses: oven-sh/setup-bun@v2 diff --git a/.github/workflows/celld-runner-deploy.yml b/.github/workflows/celld-runner-deploy.yml index d51d478ba92..ece2c2a282a 100644 --- a/.github/workflows/celld-runner-deploy.yml +++ b/.github/workflows/celld-runner-deploy.yml @@ -92,7 +92,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 2d553121445..6aad737e2a3 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -70,7 +70,7 @@ jobs: if: steps.guard.outputs.stable == 'true' uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Gather release facts if: steps.guard.outputs.stable == 'true' && steps.dedupe.outputs.documented == 'false' diff --git a/.github/workflows/ci-enterprise-mcp-mock.yml b/.github/workflows/ci-enterprise-mcp-mock.yml index 6473b82aac2..712e1a79483 100644 --- a/.github/workflows/ci-enterprise-mcp-mock.yml +++ b/.github/workflows/ci-enterprise-mcp-mock.yml @@ -27,7 +27,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/ci-i18n.yml b/.github/workflows/ci-i18n.yml index fb4ef71dacd..5b1802ad4a4 100644 --- a/.github/workflows/ci-i18n.yml +++ b/.github/workflows/ci-i18n.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Run i18n audit run: node scripts/i18n-audit.mjs --ci diff --git a/.github/workflows/ci-opencode-openwork.yml b/.github/workflows/ci-opencode-openwork.yml index 8c19f189a13..caf208f87b7 100644 --- a/.github/workflows/ci-opencode-openwork.yml +++ b/.github/workflows/ci-opencode-openwork.yml @@ -34,7 +34,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 24.22.1 registry-url: https://registry.npmjs.org - name: Setup Bun for desktop channel tests diff --git a/.github/workflows/ci-openwork-ui-mcp.yml b/.github/workflows/ci-openwork-ui-mcp.yml index 75960226a49..b2e4fa153b4 100644 --- a/.github/workflows/ci-openwork-ui-mcp.yml +++ b/.github/workflows/ci-openwork-ui-mcp.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 24.22.1 registry-url: https://registry.npmjs.org - name: Setup pnpm @@ -62,7 +62,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 24.22.1 registry-url: https://registry.npmjs.org - name: Setup pnpm diff --git a/.github/workflows/ci-sdk.yml b/.github/workflows/ci-sdk.yml index 7484ff3df4e..f79e4e5d20e 100644 --- a/.github/workflows/ci-sdk.yml +++ b/.github/workflows/ci-sdk.yml @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 11.4.0 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index f3b25f409ce..f48bfeb4209 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -450,7 +450,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Validate models snapshot run: | @@ -518,7 +518,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Validate the docs build run: npx -y mint@4.2.868 validate - name: Check for broken links and images @@ -558,7 +558,7 @@ jobs: - name: Setup Node uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: diff --git a/.github/workflows/codeql-result-coverage.yml b/.github/workflows/codeql-result-coverage.yml index 85c801d80e6..6b595f92b7e 100644 --- a/.github/workflows/codeql-result-coverage.yml +++ b/.github/workflows/codeql-result-coverage.yml @@ -26,7 +26,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - name: Verify uploaded results for open pull requests env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/contributor-pr-test.yml b/.github/workflows/contributor-pr-test.yml index 6f6f856a98b..5604bcc3b44 100644 --- a/.github/workflows/contributor-pr-test.yml +++ b/.github/workflows/contributor-pr-test.yml @@ -52,7 +52,7 @@ jobs: sparse-checkout: .github/scripts - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - name: Check the commenter and bind the reviewed commit id: authorize env: @@ -102,7 +102,7 @@ jobs: sparse-checkout: .github/scripts - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - name: Approve the fork's waiting test runs env: GH_TOKEN: ${{ github.token }} @@ -150,7 +150,7 @@ jobs: sparse-checkout: .github/scripts - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - name: Mint the check token id: app uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 diff --git a/.github/workflows/contributor-pr.yml b/.github/workflows/contributor-pr.yml index 6e910de21ef..7a7b066557f 100644 --- a/.github/workflows/contributor-pr.yml +++ b/.github/workflows/contributor-pr.yml @@ -69,7 +69,7 @@ jobs: sparse-checkout: .github/scripts - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - name: List open forks to screen id: backfill env: diff --git a/.github/workflows/contributor-warden.yml b/.github/workflows/contributor-warden.yml index b810c8c9b64..fd26e7aadd1 100644 --- a/.github/workflows/contributor-warden.yml +++ b/.github/workflows/contributor-warden.yml @@ -80,7 +80,7 @@ jobs: - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 # Objects only. The head must still be the commit this run was asked to # review; a newer push gets its own run. @@ -165,7 +165,7 @@ jobs: STAGE: ${{ inputs.stage }} MERGE_BASE: ${{ steps.fetch.outputs.merge_base }} OPENAI_API_KEY: ${{ secrets.WARDEN_CONTRIBUTOR_OPENAI_API_KEY }} - IMAGE: node:24-bookworm@sha256:3d27e5c11e5786e309ec3e03f93ae536eb36e6e5eb3714d5eb3300a36157add0 + IMAGE: node:24.22.1-bookworm@sha256:5630d96570d4ca4a8523fda3d169d0114554d0f48293e1ae3db6be7c79b42a17 run: | if [ -z "$OPENAI_API_KEY" ]; then echo "::error::WARDEN_CONTRIBUTOR_OPENAI_API_KEY is not set; the Warden part of this check is incomplete." diff --git a/.github/workflows/daytona-eval-image.yml b/.github/workflows/daytona-eval-image.yml index f23d1415923..29ca81d957d 100644 --- a/.github/workflows/daytona-eval-image.yml +++ b/.github/workflows/daytona-eval-image.yml @@ -66,8 +66,8 @@ jobs: echo "file=ee/packages/headless-computer/Dockerfile.desktop" >> "$GITHUB_OUTPUT" echo "platforms=linux/amd64" >> "$GITHUB_OUTPUT" echo "tags=$image:computer-desktop-${GITHUB_SHA}" >> "$GITHUB_OUTPUT" - # Official Node 24 Bookworm AMD64 manifest; pin the build input. - echo "build_args=OPENWORK_COMPUTER_BASE_IMAGE=node:24-bookworm-slim@sha256:ec857ea6bb60cb3d0fa4d97495590c5fe7d51be8572c307b55957eeb06569cf7" >> "$GITHUB_OUTPUT" + # Official Node 24.22.1 Bookworm slim AMD64 manifest; pin the build input. + echo "build_args=OPENWORK_COMPUTER_BASE_IMAGE=node:24.22.1-bookworm-slim@sha256:2dd9a4f758e173b65fee15388f7706af6fb01ef50cf7a2c0538711d67a6981ae" >> "$GITHUB_OUTPUT" ;; eval-vnc) echo "context=.devcontainer" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/den-db-check.yml b/.github/workflows/den-db-check.yml index a69cb4d4af9..87fe79029d7 100644 --- a/.github/workflows/den-db-check.yml +++ b/.github/workflows/den-db-check.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/den-db-migrate.yml b/.github/workflows/den-db-migrate.yml index c20b0025831..3a806e2bc71 100644 --- a/.github/workflows/den-db-migrate.yml +++ b/.github/workflows/den-db-migrate.yml @@ -101,7 +101,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@v4 diff --git a/.github/workflows/download-stats.yml b/.github/workflows/download-stats.yml index 2430273b62b..a087f6cdfe0 100644 --- a/.github/workflows/download-stats.yml +++ b/.github/workflows/download-stats.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 # Keep the previous snapshot for deltas without writing to the branch. # A cache miss starts from the committed historical reports. diff --git a/.github/workflows/e2e-test-failure-alerts.yml b/.github/workflows/e2e-test-failure-alerts.yml index 2c8351d5ef0..85c7c9e6005 100644 --- a/.github/workflows/e2e-test-failure-alerts.yml +++ b/.github/workflows/e2e-test-failure-alerts.yml @@ -28,7 +28,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - name: Report failures or recovery to the team env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/evidence-review-checks.yml b/.github/workflows/evidence-review-checks.yml index 8577c69c6d9..94c902cbbb9 100644 --- a/.github/workflows/evidence-review-checks.yml +++ b/.github/workflows/evidence-review-checks.yml @@ -37,7 +37,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: ./.github/actions/setup-browser - name: Verify browser-only setup leaves OAuth handoff disabled shell: bash @@ -57,7 +57,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@v4 - run: | pnpm --filter @openwork/review-app... --filter @openwork/world... --filter @openwork/types... install --frozen-lockfile --ignore-scripts diff --git a/.github/workflows/evidence-review.yml b/.github/workflows/evidence-review.yml index ab7c1bb80b8..0b59f5f58c4 100644 --- a/.github/workflows/evidence-review.yml +++ b/.github/workflows/evidence-review.yml @@ -44,7 +44,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - name: Update the commit-bound evidence check env: GH_TOKEN: ${{ github.token }} @@ -81,7 +81,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@v4 - name: Install trusted publisher dependencies run: | diff --git a/.github/workflows/features-check.yml b/.github/workflows/features-check.yml index 3943a79ce04..5330f8d5c34 100644 --- a/.github/workflows/features-check.yml +++ b/.github/workflows/features-check.yml @@ -48,7 +48,7 @@ jobs: - name: Setup Node uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 diff --git a/.github/workflows/freestyle-cleanup.yml b/.github/workflows/freestyle-cleanup.yml index 31849724710..86eb631b6b2 100644 --- a/.github/workflows/freestyle-cleanup.yml +++ b/.github/workflows/freestyle-cleanup.yml @@ -39,7 +39,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@v4 with: version: 11.4.0 diff --git a/.github/workflows/freestyle-warm-dev.yml b/.github/workflows/freestyle-warm-dev.yml index 99fc3b27891..5fa5164c615 100644 --- a/.github/workflows/freestyle-warm-dev.yml +++ b/.github/workflows/freestyle-warm-dev.yml @@ -38,7 +38,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@v4 with: version: 11.4.0 diff --git a/.github/workflows/freestyle-world-check.yml b/.github/workflows/freestyle-world-check.yml index 7f62d0b76f9..04b4720f359 100644 --- a/.github/workflows/freestyle-world-check.yml +++ b/.github/workflows/freestyle-world-check.yml @@ -40,7 +40,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@v4 with: version: 11.4.0 diff --git a/.github/workflows/opencode-agents.yml b/.github/workflows/opencode-agents.yml index 59ed65e8a64..687978d39a1 100644 --- a/.github/workflows/opencode-agents.yml +++ b/.github/workflows/opencode-agents.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Install opencode run: | @@ -67,7 +67,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Install opencode run: | diff --git a/.github/workflows/pr-proof.yml b/.github/workflows/pr-proof.yml index cb81e669bc5..61576a2e148 100644 --- a/.github/workflows/pr-proof.yml +++ b/.github/workflows/pr-proof.yml @@ -33,7 +33,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - id: select env: GH_TOKEN: ${{ github.token }} @@ -81,7 +81,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@v4 with: version: 11.4.0 @@ -216,7 +216,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - uses: pnpm/action-setup@v4 - name: Install trusted judge dependencies run: pnpm --dir evals --filter @openwork/test-evidence... install --frozen-lockfile --ignore-scripts diff --git a/.github/workflows/publish-ee-images.yml b/.github/workflows/publish-ee-images.yml index 0f58364f1e2..89be7ad8eab 100644 --- a/.github/workflows/publish-ee-images.yml +++ b/.github/workflows/publish-ee-images.yml @@ -76,6 +76,14 @@ jobs: - name: Set up Helm uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: 24.22.1 + + - name: Check Node security baseline and Helm runtime probe + run: node --test scripts/tests/node-runtime-baseline.test.mjs packaging/helm/openwork-ee/tests/node-runtime.mjs + - name: Lint chart run: helm lint packaging/helm/openwork-ee diff --git a/.github/workflows/publish-opencode-plugin.yml b/.github/workflows/publish-opencode-plugin.yml index 78454a8e6f7..ee8611a4144 100644 --- a/.github/workflows/publish-opencode-plugin.yml +++ b/.github/workflows/publish-opencode-plugin.yml @@ -50,7 +50,7 @@ jobs: - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: - node-version: 24 + node-version: 24.22.1 registry-url: https://registry.npmjs.org - name: Setup Bun for desktop channel tests diff --git a/.github/workflows/purge-model-catalog.yml b/.github/workflows/purge-model-catalog.yml index 677174b890d..01ab704be1f 100644 --- a/.github/workflows/purge-model-catalog.yml +++ b/.github/workflows/purge-model-catalog.yml @@ -42,7 +42,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Build catalog run: node ee/apps/gateway/scripts/build-models.mjs diff --git a/.github/workflows/release-macos-aarch64.yml b/.github/workflows/release-macos-aarch64.yml index c59fe3bfb05..a91666971c1 100644 --- a/.github/workflows/release-macos-aarch64.yml +++ b/.github/workflows/release-macos-aarch64.yml @@ -152,7 +152,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 # The built-in GitHub Actions app cannot be a ruleset bypass actor # (GitHub rejects it), so fresh cuts push their tag as the org-owned @@ -474,7 +474,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Verify release tag if: needs.resolve-release.outputs.benchmark_only != 'true' @@ -634,7 +634,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup Bun uses: oven-sh/setup-bun@v2 @@ -1034,7 +1034,7 @@ jobs: if: needs.resolve-release.outputs.build_electron == 'true' uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Download Electron updater manifest artifacts if: needs.resolve-release.outputs.build_electron == 'true' @@ -1127,7 +1127,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Setup pnpm uses: pnpm/action-setup@v4 @@ -1217,19 +1217,19 @@ jobs: fail-fast: false matrix: include: - # 22.13.0 is the package's engines floor (node:sqlite without a flag). + # Verify every host at the package's Node 24.22.1 engines floor. - os: ubuntu-24.04 target: linux-x64 - node: "22.13.0" + node: "24.22.1" - os: macos-15-intel target: darwin-x64 - node: "24" + node: "24.22.1" - os: macos-15 target: darwin-arm64 - node: "24" + node: "24.22.1" - os: windows-2022 target: windows-x64 - node: "24" + node: "24.22.1" runs-on: ${{ matrix.os }} steps: - name: Checkout smoke script @@ -1285,7 +1285,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Download npm package uses: actions/download-artifact@v4 diff --git a/.github/workflows/review-app-deploy.yml b/.github/workflows/review-app-deploy.yml index 4e8ecdc5705..78e0c5a550f 100644 --- a/.github/workflows/review-app-deploy.yml +++ b/.github/workflows/review-app-deploy.yml @@ -51,7 +51,7 @@ jobs: persist-credentials: false - uses: actions/setup-node@v4 with: - node-version: 24 + node-version: 24.22.1 - name: Resolve the stable review alias id: alias run: | diff --git a/.github/workflows/update-models.yml b/.github/workflows/update-models.yml index 68e9bedaaad..77c50ce617e 100644 --- a/.github/workflows/update-models.yml +++ b/.github/workflows/update-models.yml @@ -47,7 +47,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.22.1 - name: Download and validate models run: | diff --git a/.github/workflows/warden-check-backfill.yml b/.github/workflows/warden-check-backfill.yml index 2b4486386af..daac1f3ced7 100644 --- a/.github/workflows/warden-check-backfill.yml +++ b/.github/workflows/warden-check-backfill.yml @@ -43,7 +43,7 @@ jobs: sparse-checkout: .github/scripts - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: - node-version: 24 + node-version: 24.22.1 - name: Mint the check token id: app uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 diff --git a/.nvmrc b/.nvmrc index a45fd52cc58..ce62013c39a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24 +24.22.1 diff --git a/apps/app/package.json b/apps/app/package.json index 49bd125d0f2..e493bc72158 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -2,6 +2,9 @@ "name": "@openwork/app", "private": true, "version": "0.0.0-dev", + "engines": { + "node": ">=24.22.1 <25" + }, "type": "module", "scripts": { "dev": "OPENWORK_DEV_MODE=1 vite", diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a2266ad7cdc..a12ae625f33 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -50,7 +50,7 @@ "jsonc-parser": "^3.2.1", "minimatch": "^10.2.5", "node-pty": "npm:@lydell/node-pty@1.2.0-beta.12", - "undici": "6.28.1", + "undici": "6.29.1", "yaml": "^2.9.0", "zod": "^4.3.6", "@openwork/computer-use": "workspace:*" diff --git a/apps/review/package.json b/apps/review/package.json index e1f1cc0e114..1631f9bdb84 100644 --- a/apps/review/package.json +++ b/apps/review/package.json @@ -2,6 +2,9 @@ "name": "@openwork/review-app", "private": true, "version": "0.0.0", + "engines": { + "node": ">=24.22.1 <25" + }, "scripts": { "dev": "next dev --hostname 127.0.0.1 --port ${REVIEW_PORT:-3011}", "build": "pnpm --filter \"@openwork/review-app^...\" --if-present run build && next build", diff --git a/apps/server/README.md b/apps/server/README.md index 8a0461fde86..191b4ea1f63 100644 --- a/apps/server/README.md +++ b/apps/server/README.md @@ -9,7 +9,7 @@ npm install -g openwork-server openwork-server --workspace /path/to/workspace --approval auto ``` -The npm package runs on Node.js 22.13 or newer on macOS, Linux, and Windows (arm64 or x64); Bun is not required at runtime. +The npm package requires stable Node.js 24.x, version 24.22.1 or newer within that major, on macOS, Linux, and Windows (arm64 or x64); Bun is not required at runtime. Older Node releases and other majors (including 25+) are rejected by the runtime gate. This security baseline also replaces the previous Node.js 22.13 minimum. ### Self-hosted web UI diff --git a/apps/server/bin/openwork-server.mjs b/apps/server/bin/openwork-server.mjs index 2be2723e890..7769057c731 100755 --- a/apps/server/bin/openwork-server.mjs +++ b/apps/server/bin/openwork-server.mjs @@ -29,7 +29,7 @@ const nodeBundle = fromPackage("./dist/openwork-server.mjs"); if (existsSync(nodeBundle)) { if (!nodeVersionSupported(process.versions.node)) { console.error( - `openwork-server needs Node.js ${MIN_NODE_VERSION} or newer (found ${process.versions.node}). Install a current Node.js LTS and try again.`, + `openwork-server needs Node.js 24.x, version ${MIN_NODE_VERSION} or newer within that major (found ${process.versions.node}). Install a supported Node.js 24 release and try again.`, ); process.exit(1); } diff --git a/apps/server/bin/platform.mjs b/apps/server/bin/platform.mjs index e381bf57eb0..a73b4d03117 100644 --- a/apps/server/bin/platform.mjs +++ b/apps/server/bin/platform.mjs @@ -16,13 +16,13 @@ export function serverBinaryName(platform, arch) { return `openwork-server-${target.target}${platform === "win32" ? ".exe" : ""}`; } -// node:sqlite (the server's runtime database) is available without a flag -// from Node 22.13 and 23.4. -export const MIN_NODE_VERSION = "22.13.0"; +// Security baseline: only patched releases on the supported Node 24 line. +// New majors must be reviewed explicitly, not accepted by a lower-bound check. +export const MIN_NODE_VERSION = "24.22.1"; export function nodeVersionSupported(version) { - const [major, minor] = version.replace(/^v/, "").split(".").map(Number); - if (major === 22) return minor >= 13; - if (major === 23) return minor >= 4; - return major > 23; + const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version); + if (!match) return false; + const [, major, minor, patch] = match.map(Number); + return major === 24 && (minor > 22 || (minor === 22 && patch >= 1)); } diff --git a/apps/server/package.json b/apps/server/package.json index 418aa730a36..5e47ed98ad5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -67,7 +67,7 @@ "htmlparser2": "8.0.2", "jsonc-parser": "^3.2.1", "minimatch": "^10.2.5", - "undici": "6.28.1", + "undici": "6.29.1", "yaml": "^2.9.0", "zod": "^4.3.6", "@openwork/computer-use": "workspace:*" @@ -84,5 +84,8 @@ "jpeg-js": "0.4.4", "typescript": "^5.6.3" }, + "engines": { + "node": ">=24.22.1 <25" + }, "packageManager": "pnpm@10.27.0" } diff --git a/apps/server/scripts/publish-npm.mjs b/apps/server/scripts/publish-npm.mjs index 1f9ce6c3aec..13944128d82 100644 --- a/apps/server/scripts/publish-npm.mjs +++ b/apps/server/scripts/publish-npm.mjs @@ -3,7 +3,6 @@ import { existsSync } from "node:fs"; import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { MIN_NODE_VERSION } from "../bin/platform.mjs"; // Dependencies the Node bundle leaves external (see build:npm-bundle); npm // installs them next to the package. @@ -77,7 +76,7 @@ export async function stageNpmPackage(packageRoot, outputRoot = resolve(packageR description: sourcePackage.description, type: sourcePackage.type, bin: sourcePackage.bin, - engines: { node: `>=${MIN_NODE_VERSION}` }, + engines: sourcePackage.engines, dependencies, repository: sourcePackage.repository, homepage: sourcePackage.homepage, diff --git a/apps/server/test/node-runtime.test.mjs b/apps/server/test/node-runtime.test.mjs new file mode 100644 index 00000000000..2a656d65d31 --- /dev/null +++ b/apps/server/test/node-runtime.test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, copyFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { MIN_NODE_VERSION, nodeVersionSupported } from "../bin/platform.mjs"; + +const accepted = ["24.22.1", "v24.22.1", "24.22.2", "24.23.0", "24.100.0"]; +const rejected = ["20.20.0", "22.13.0", "22.99.0", "23.4.0", "24.0.0", "24.18.0", "24.21.99", "24.22.0", "25.0.0", "26.0.0", "99.0.0", "24.22", "24.022.1", "24.22.1-rc.1", "24.22.1+build", "24.22.1.0", "garbage", ""]; + +test("the package engine and runtime minimum use the same security baseline", () => { + assert.equal(MIN_NODE_VERSION, "24.22.1"); + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); + assert.equal(pkg.engines.node, ">=24.22.1 <25"); +}); +for (const version of accepted) test(`accepts stable patched Node ${version}`, () => assert.equal(nodeVersionSupported(version), true)); +for (const version of rejected) test(`rejects Node ${JSON.stringify(version)}`, () => assert.equal(nodeVersionSupported(version), false)); + +test("published launcher rejects unsupported runtimes before importing its bundle", () => { + const dir = mkdtempSync(join(tmpdir(), "server-node-gate-")); + try { + mkdirSync(join(dir, "bin")); + mkdirSync(join(dir, "dist")); + for (const file of ["platform.mjs", "openwork-server.mjs"]) copyFileSync(new URL(`../bin/${file}`, import.meta.url), join(dir, "bin", file)); + writeFileSync(join(dir, "dist/openwork-server.mjs"), 'console.log("BUNDLE_IMPORTED")'); + const fixture = join(dir, "version.mjs"); + writeFileSync(fixture, 'Object.defineProperty(process.versions, "node", { value: process.env.TEST_NODE_VERSION })'); + for (const version of ["22.13.0", "24.18.0", "24.22.0", "24.22.1", "24.23.0", "25.0.0", "26.0.0"]) { + const result = spawnSync(process.execPath, ["--import", fixture, join(dir, "bin/openwork-server.mjs")], { encoding: "utf8", env: { ...process.env, TEST_NODE_VERSION: version } }); + assert.ifError(result.error); + assert.equal(result.status, nodeVersionSupported(version) ? 0 : 1, result.stderr); + assert.equal(result.stdout.includes("BUNDLE_IMPORTED"), nodeVersionSupported(version)); + if (!nodeVersionSupported(version)) assert.match(result.stderr, /Node.js 24.x, version 24.22.1/); + } + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/docs/alchemy-effect-testkit-migration.md b/docs/alchemy-effect-testkit-migration.md index 58624e8d40d..1c4c875ad68 100644 --- a/docs/alchemy-effect-testkit-migration.md +++ b/docs/alchemy-effect-testkit-migration.md @@ -147,7 +147,7 @@ reconciles. It must not be presented as universal drift detection. Layers rather than branches spread through resource constructors. 5. Determine with evidence whether Alchemy improves remote-resource recovery enough to justify its state and provider cost. -6. Keep pnpm and Node.js 22+ as the supported eval toolchain. Bun must not +6. Keep pnpm and stable Node.js 24.x (24.22.1 or newer within that major) as the supported eval toolchain. Bun must not become required. ## Non-goals @@ -404,7 +404,7 @@ No production lifecycle moves in this phase. isolated `evals/` workspace; do not depend on `@next` and do not add `@effect/vitest` to OpenWork's runner. 2. Use an Effect version satisfying Alchemy's reviewed - `>=4.0.0-beta.105` peer range. Verify pnpm + Node.js 22 execution, + `>=4.0.0-beta.105` peer range. Verify pnpm + stable Node.js 24.x (24.22.1 or newer within that major) execution, TypeScript 5.9, Vitest 3.2, ESM exports, and the existing eval tsconfig. Bun remains optional. 3. Prove an internal Effect Scope can return an existing `AsyncDisposable` diff --git a/docs/computer-use/workbot.md b/docs/computer-use/workbot.md index 6a4f8a3274e..6b8af8e77c4 100644 --- a/docs/computer-use/workbot.md +++ b/docs/computer-use/workbot.md @@ -22,9 +22,10 @@ with the existing `bash` and `look` tools available on the same machine. ## Prepare the headless runner -Build `ee/packages/headless-computer/Dockerfile.desktop` using a pinned -`node:24-bookworm-slim@sha256:…` base, push it to your registry, and create a -Daytona Linux VM snapshot from the resulting image digest. +Build `ee/packages/headless-computer/Dockerfile.desktop` using its digest-pinned +`node:24.22.1-bookworm-slim` default (or an explicitly verified pinned override), +push it to your registry, and create a Daytona Linux VM snapshot from the +resulting image digest. The **Daytona Eval Image** workflow offers a manual `computer-desktop` target: it publishes AMD64 to an immutable `computer-desktop-` tag without changing the existing eval image's `dev` tag. Its run summary records the digest. @@ -33,7 +34,6 @@ require an AMD64 image and an existing image reference for Linux VMs: ```sh docker build --platform linux/amd64 -f ee/packages/headless-computer/Dockerfile.desktop \ - --build-arg OPENWORK_COMPUTER_BASE_IMAGE="$PINNED_NODE_IMAGE" \ -t "$COMPUTER_IMAGE" . docker push "$COMPUTER_IMAGE" daytona snapshot create workbot-desktop --image "$COMPUTER_IMAGE_DIGEST" \ @@ -70,7 +70,7 @@ DAYTONA_TARGET= Keep `DAYTONA_API_KEY` in the runner's existing secret configuration. Build the runner normally; the headless computer build bundles the guest bridge and copies its startup helper. On first desktop use they are installed into the member's -existing computer. Older computers need the desktop packages and Node 22+ before +existing computer. Older computers need the desktop packages and patched Node 24.x (24.22.1 or newer) before they can open a stream; this change does not replace an existing VM or its files. ## Transport and rollout diff --git a/docs/node-runtime-security.md b/docs/node-runtime-security.md new file mode 100644 index 00000000000..9cd659f00dc --- /dev/null +++ b/docs/node-runtime-security.md @@ -0,0 +1,63 @@ +# Node runtime security baseline + +Standalone OpenWork Node runtimes use **Node 24.22.1** as the October 21, 2026 security baseline. Package engines accept stable patched Node 24 releases (`>=24.22.1 <25`); another major requires explicit compatibility and security review. + +Sources: [Node security announcement](https://nodejs.org/en/blog/vulnerability/october-2026-security-releases), [Node 24.22.1 release](https://nodejs.org/en/blog/release/v24.22.1/). + +## What is pinned + +- `.nvmrc`, CI standalone Node jobs, official Docker Node bases, sandbox defaults, and downloaded private runtimes use 24.22.1. +- Official tarball downloads retain SHA-256 verification. Immutable Docker image references retain verified digests. +- Native Render and Vercel application package manifests declare the bounded Node 24 engine range. +- Bootstrap and standalone npm server launchers reject unsupported runtimes. The installer can install a private supported Node without replacing system Node. +- Standalone Undici packages and overrides use 6.29.1 / 7.30.1. Node's bundled Undici and separately installed Undici are different copies. + +TypeScript/esbuild compilation targets describe output compatibility, not the Node runtime installed on the host. Third-party package engine requirements and historical benchmark runtime records are not deployment runtime pins. + +## Deployment checklist + +A source change alone does not patch an existing process, image, or VM snapshot. + +1. Build and test on the approved Node version, including native dependencies. +2. Publish new application images. Refresh approved immutable application image digests in deployment releases; do not retag old images and assume they are patched. +3. Rebuild computer and worker snapshots, promote their configuration pins, and replace affected instances through the normal lifecycle. +4. Roll out services and jobs. Verify the actual runtime in every relevant application image/process: + + ```sh + node -p 'JSON.stringify({node: process.version, undici: process.versions.undici, arch: process.arch})' + ``` + +5. Recheck service health, provider/MCP connections, streaming, and worker startup. Configuration readback alone is not runtime proof. + +### Render + +For native Node services, merge `NODE_VERSION=24.22.1` into existing environment variables. Never replace the entire environment just to set the runtime. Check environment-group overrides, service settings, build logs, and the deployed process version. Include maintenance cron jobs and explicitly review suspended services before reactivating them. + +For Docker services, the Dockerfile/application image selects Node; `NODE_VERSION` in the service environment does not replace the image's executable. Rebuild and redeploy the application image. + +Environment updates and repository changes must not be reported as a verified rollout until deployment and runtime checks complete. + +### Vercel + +Use project Node.js setting **24.x** and the app-root package engine range `>=24.22.1 <25`. Vercel manages the patch version; these settings do not by themselves prove a particular deployed patch. Check the new deployment's build logs/runtime and the provider's security rollout before declaring it remediated. Existing deployments still require verification. + +### Helm + +The chart selects **OpenWork application release images**, not standalone Node images. Keep `Chart.appVersion` and component/global image tags as OpenWork versions; never replace them with `24.22.1`. + +Deploy a newly built patched application release and run `helm test --logs`. The existing env-probe hook reports the actual Den API image's Node runtime and rejects versions outside the patched Node 24 baseline. This hook does not independently verify Den Web, Gateway, existing replicas, or snapshots; check those images/processes separately. See `packaging/helm/openwork-ee/README.md`. + +## Separate runtime tracks + +Electron embeds its own Node runtime. Updating standalone Node, `.nvmrc`, or CI does not patch a packaged desktop application. Require an Electron release containing these Node fixes or verified upstream security backports, then rebuild and ship the desktop. Check the embedded `process.versions`, not the developer's shell Node. OpenCode's Bun-compiled sidecars are a separate runtime track as well. + +## Regression checks + +Run under Node 24.22.1; Helm must be installed for chart rendering: + +```sh +pnpm test:node-runtime +pnpm --dir packages/openwork-bootstrap test +``` + +The image-publication workflow runs the source pin consistency checks and rendered Helm runtime-probe tests. These checks catch drift but do not prove a production deployment has been upgraded. diff --git a/ee/apps/den-api/package.json b/ee/apps/den-api/package.json index a8b55a41d66..9a208681d44 100644 --- a/ee/apps/den-api/package.json +++ b/ee/apps/den-api/package.json @@ -1,6 +1,9 @@ { "name": "@openwork-ee/den-api", "private": true, + "engines": { + "node": ">=24.22.1 <25" + }, "type": "module", "scripts": { "audit:pilot": "tsx --conditions=development scripts/audit-pilot.ts", @@ -93,7 +96,7 @@ "sharp": "0.35.5", "stripe": "^22.1.1", "typescript": "^5.5.4", - "undici": "7.29.1", + "undici": "7.30.1", "zod": "^4.3.6", "@openwork/documents": "workspace:*" }, diff --git a/ee/apps/den-api/src/env.ts b/ee/apps/den-api/src/env.ts index 770cbb88b43..5c0bfaedc28 100644 --- a/ee/apps/den-api/src/env.ts +++ b/ee/apps/den-api/src/env.ts @@ -930,7 +930,7 @@ export const env = { target: optionalString(parsed.DAYTONA_TARGET), snapshot: optionalString(parsed.DAYTONA_SNAPSHOT), sandboxClass: parsed.DAYTONA_SANDBOX_CLASS, - image: optionalString(parsed.DAYTONA_SANDBOX_IMAGE) ?? "node:20-bookworm", + image: optionalString(parsed.DAYTONA_SANDBOX_IMAGE) ?? "node:24.22.1-bookworm", resources: { cpu: Number(parsed.DAYTONA_SANDBOX_CPU ?? "2"), memory: Number(parsed.DAYTONA_SANDBOX_MEMORY ?? "4"), diff --git a/ee/apps/den-gateway/package.json b/ee/apps/den-gateway/package.json index ce6f792275c..1495f7a9d1c 100644 --- a/ee/apps/den-gateway/package.json +++ b/ee/apps/den-gateway/package.json @@ -1,6 +1,9 @@ { "name": "@openwork-ee/den-gateway", "private": true, + "engines": { + "node": ">=24.22.1 <25" + }, "type": "module", "scripts": { "dev": "NODE_OPTIONS=--conditions=development OPENWORK_DEV_MODE=1 tsx watch src/server.ts", @@ -13,7 +16,7 @@ "@openwork-ee/utils": "workspace:*", "dotenv": "^16.4.5", "hono": "4.13.11", - "undici": "7.29.1", + "undici": "7.30.1", "zod": "^4.3.6" }, "devDependencies": { diff --git a/ee/apps/den-web/package.json b/ee/apps/den-web/package.json index b4383520b7d..9e7635826c8 100644 --- a/ee/apps/den-web/package.json +++ b/ee/apps/den-web/package.json @@ -2,6 +2,9 @@ "name": "@openwork-ee/den-web", "private": true, "version": "0.0.0", + "engines": { + "node": ">=24.22.1 <25" + }, "scripts": { "dev": "OPENWORK_DEV_MODE=1 NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_API_KEY= next dev --hostname 0.0.0.0 --port 3005", "dev:local": "OPENWORK_DEV_MODE=1 NEXT_PUBLIC_POSTHOG_KEY= NEXT_PUBLIC_POSTHOG_API_KEY= next dev --hostname ${DEN_WEB_HOST:-0.0.0.0} --port ${DEN_WEB_PORT:-3005}", diff --git a/ee/apps/den-worker-runtime/Dockerfile.daytona-snapshot b/ee/apps/den-worker-runtime/Dockerfile.daytona-snapshot index fb8fc8df4e7..63c98d80337 100644 --- a/ee/apps/den-worker-runtime/Dockerfile.daytona-snapshot +++ b/ee/apps/den-worker-runtime/Dockerfile.daytona-snapshot @@ -2,7 +2,7 @@ # the target: bun compiles openwork-server via --target and the web build is # arch-independent. This avoids emulating the whole toolchain under QEMU # (node's libuv crashes under qemu-x86_64: uv__io_poll EEXIST assert). -FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS openwork-builder +FROM --platform=$BUILDPLATFORM node:24.22.1-bookworm-slim AS openwork-builder WORKDIR /src @@ -37,7 +37,7 @@ RUN set -eux; \ pnpm --filter @openwork/app build:web; \ pnpm --filter @openwork/documents build:collabora-theme -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim ARG OPENWORK_SERVER_VERSION=0.18.1 ARG OPENCODE_VERSION diff --git a/ee/apps/diagnostics/package.json b/ee/apps/diagnostics/package.json index 0627d908d0f..fd16e789ece 100644 --- a/ee/apps/diagnostics/package.json +++ b/ee/apps/diagnostics/package.json @@ -2,6 +2,9 @@ "name": "@openwork-ee/diagnostics", "private": true, "version": "0.0.0", + "engines": { + "node": ">=24.22.1 <25" + }, "scripts": { "dev": "next dev --hostname 0.0.0.0 --port ${DIAGNOSTICS_PORT:-3010}", "build": "next build", diff --git a/ee/apps/gateway/package.json b/ee/apps/gateway/package.json index 270d9b23fa1..7ccb77382a8 100644 --- a/ee/apps/gateway/package.json +++ b/ee/apps/gateway/package.json @@ -1,6 +1,9 @@ { "name": "@openwork-ee/gateway", "private": true, + "engines": { + "node": ">=24.22.1 <25" + }, "type": "module", "scripts": { "models:build": "node scripts/build-models.mjs", diff --git a/ee/apps/headless-runner/README.md b/ee/apps/headless-runner/README.md index 34b97595b58..ab02700c6c5 100644 --- a/ee/apps/headless-runner/README.md +++ b/ee/apps/headless-runner/README.md @@ -363,7 +363,7 @@ production runner runs on celld instead (see [Run on celld](#run-on-celld-no-dis `HEADLESS_FILES=s3` or `off`**: it refuses `disk` (`cells_require_s3_files`); see [OWNER-CELLS.md](./OWNER-CELLS.md). -On Render, create a **private service** so it has no public URL; only den-api and Workbot reach it. Use Node 22.13 or later. +On Render, create a **private service** so it has no public URL; only den-api and Workbot reach it. Use stable Node.js 24.x, version 24.22.1 or newer within that major. | Setting | Value | |---|---| diff --git a/ee/apps/headless-runner/package.json b/ee/apps/headless-runner/package.json index a40e59a63ca..152e08da532 100644 --- a/ee/apps/headless-runner/package.json +++ b/ee/apps/headless-runner/package.json @@ -4,7 +4,7 @@ "type": "module", "description": "Cheap headless agent sessions: OpenWork AI Gateway for the model, OpenWork MCP for tools, a small scratch filesystem, and optional kept files and a computer per conversation.", "engines": { - "node": ">=22.13" + "node": ">=24.22.1 <25" }, "scripts": { "dev": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx watch src/server.ts", diff --git a/ee/apps/landing/README.md b/ee/apps/landing/README.md index 66ca5dd9db1..2f11e956ec8 100644 --- a/ee/apps/landing/README.md +++ b/ee/apps/landing/README.md @@ -62,8 +62,8 @@ metadata does not require a new Plain field schema; update the API's allowlisted context inputs and it will be included in the JSON automatically. **Configure the schemas before deploying this change.** Plain rejects thread -fields whose keys/types do not match the workspace's schemas. Using Node 22.18+ -from `ee/apps/landing`: +fields whose keys/types do not match the workspace's schemas. Using stable Node.js +24.x (24.22.1 or newer within that major) from `ee/apps/landing`: 1. Preview the definitions with `pnpm plain:setup-fields`. This makes no API calls. 2. Set `PLAIN_SETUP_API_KEY` in your shell to a key with diff --git a/ee/apps/landing/app/install.sh/route.ts b/ee/apps/landing/app/install.sh/route.ts index 1e7ced45e5e..64cc480aa6b 100644 --- a/ee/apps/landing/app/install.sh/route.ts +++ b/ee/apps/landing/app/install.sh/route.ts @@ -20,8 +20,8 @@ export const dynamic = "force-static"; const installScript = `#!/usr/bin/env sh # OpenWork bootstrap installer. # Installs the \`openwork-bootstrap\` command (org setup CLI for agents) into a -# user-writable bin dir. No admin privileges, no npm, no npx. Uses Node.js 20+ -# when present; otherwise downloads the official Node.js build into the install +# user-writable bin dir. No admin privileges, no npm, no npx. Uses Node.js 24.x +# (24.22.1 or newer) when present; otherwise downloads the official build into the install # dir (checksum-verified, user-local). # # This does NOT install the OpenWork desktop app. To install the app: @@ -50,30 +50,35 @@ fi mkdir -p "$BIN_DIR" "$INSTALL_DIR" -# Use the system Node when it is 20+. Otherwise fetch the official Node.js -# release for this OS into $INSTALL_DIR/node (user-local, no admin rights), -# verified against nodejs.org's published SHA-256 checksums. +# Only patched stable releases on Node 24 are supported. Do not silently accept +# a future major. Keep this check aligned with the bootstrap CLI runtime gate. +node_supported() { + "$1" -e 'const m = /^v?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$/.exec(process.versions.node); if (!m) process.exit(1); const [, major, minor, patch] = m.map(Number); process.exit(major === 24 && (minor > 22 || (minor === 22 && patch >= 1)) ? 0 : 1)' 2>/dev/null +} + +# Otherwise fetch the official pinned Node.js release for this OS into +# $INSTALL_DIR/node (user-local, no admin rights), verified against +# nodejs.org's published SHA-256 checksums. NODE_BIN="node" -NODE_MAJOR="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))' 2>/dev/null || echo 0)" -if [ "\${NODE_MAJOR:-0}" -lt 20 ]; then +if ! node_supported "$NODE_BIN"; then case "$(uname -s)" in Darwin) NODE_OS=darwin ;; Linux) NODE_OS=linux ;; - *) echo "Node.js 20+ is required. Install it from https://nodejs.org/ and re-run this script." >&2; exit 1 ;; + *) echo "Node.js 24.x (24.22.1 or newer) is required. Install it from https://nodejs.org/ and re-run this script." >&2; exit 1 ;; esac case "$(uname -m)" in arm64|aarch64) NODE_ARCH=arm64 ;; x86_64|amd64) NODE_ARCH=x64 ;; - *) echo "Node.js 20+ is required. Install it from https://nodejs.org/ and re-run this script." >&2; exit 1 ;; + *) echo "Node.js 24.x (24.22.1 or newer) is required. Install it from https://nodejs.org/ and re-run this script." >&2; exit 1 ;; esac - NODE_DIST="\${OPENWORK_NODE_DIST_URL:-https://nodejs.org/dist/latest-v24.x}" - echo "Node.js 20+ not found; downloading a private copy from $NODE_DIST into $INSTALL_DIR/node ..." + NODE_DIST="\${OPENWORK_NODE_DIST_URL:-https://nodejs.org/dist/v24.22.1}" + echo "Node.js 24.x (24.22.1 or newer) not found; downloading a private copy from $NODE_DIST into $INSTALL_DIR/node ..." # shellcheck disable=SC2086 NODE_SUMS="$($DOWNLOAD "$NODE_DIST/SHASUMS256.txt")" - NODE_TARBALL="$(printf '%s\\n' "$NODE_SUMS" | awk '{print $2}' | grep -E "^node-v[0-9.]+-$NODE_OS-$NODE_ARCH\\.tar\\.gz$" | head -n 1)" + NODE_TARBALL="node-v24.22.1-$NODE_OS-$NODE_ARCH.tar.gz" NODE_SHA="$(printf '%s\\n' "$NODE_SUMS" | awk -v f="$NODE_TARBALL" '$2 == f {print $1}')" if [ -z "$NODE_TARBALL" ] || [ -z "$NODE_SHA" ]; then - echo "Could not find a Node.js build for $NODE_OS-$NODE_ARCH. Install Node.js 20+ from https://nodejs.org/ and re-run." >&2 + echo "Could not find a Node.js build for $NODE_OS-$NODE_ARCH. Install Node.js 24.x (24.22.1 or newer) from https://nodejs.org/ and re-run." >&2 exit 1 fi NODE_TMP="$(mktemp -d "\${TMPDIR:-/tmp}/openwork-node.XXXXXX")" @@ -94,6 +99,10 @@ if [ "\${NODE_MAJOR:-0}" -lt 20 ]; then tar -xzf "$NODE_TMP/$NODE_TARBALL" -C "$INSTALL_DIR/node" --strip-components 1 rm -rf "$NODE_TMP" NODE_BIN="$INSTALL_DIR/node/bin/node" + if ! node_supported "$NODE_BIN"; then + echo "Downloaded runtime is not a supported Node.js 24.x release (24.22.1 or newer); aborting." >&2 + exit 1 + fi echo "Using private Node.js $("$NODE_BIN" --version) at $NODE_BIN" fi diff --git a/ee/apps/landing/app/start.md/route.ts b/ee/apps/landing/app/start.md/route.ts index 35921bb1f8b..419bcc28925 100644 --- a/ee/apps/landing/app/start.md/route.ts +++ b/ee/apps/landing/app/start.md/route.ts @@ -13,7 +13,7 @@ const startMarkdown = [ "", "Install the `openwork-bootstrap` CLI into a user-writable bin dir.", "", - "Production path: download the script, inspect it, then run it. Do not pipe remote scripts directly into a shell. The script downloads the `openwork-bootstrap` CLI (a single dependency-free Node file) and installs it into `$HOME/.local/bin` - no npm or npx required. If Node.js 20+ is missing, the script downloads the official Node.js build into `$HOME/.openwork/bootstrap/node` (checksum-verified, no admin rights), so the user does not need to install anything first.", + "Production path: download the script, inspect it, then run it. Do not pipe remote scripts directly into a shell. The script downloads the `openwork-bootstrap` CLI (a single dependency-free Node file) and installs it into `$HOME/.local/bin` - no npm or npx required. If a supported stable Node.js 24.x (24.22.1 or newer within that major) is missing, the script downloads the official Node.js 24.22.1 build into `$HOME/.openwork/bootstrap/node` (checksum-verified, no admin rights), so the user does not need to install anything first.", "", "```bash", "curl -fsSLo /tmp/openwork-install.sh https://openworklabs.com/install.sh", diff --git a/ee/apps/landing/package.json b/ee/apps/landing/package.json index 7bc0c6addd5..adbfea8f8cc 100644 --- a/ee/apps/landing/package.json +++ b/ee/apps/landing/package.json @@ -2,6 +2,9 @@ "name": "@openwork-ee/landing", "private": true, "version": "0.0.0", + "engines": { + "node": ">=24.22.1 <25" + }, "scripts": { "dev": "OPENWORK_DEV_MODE=1 next dev --hostname 0.0.0.0", "check:agent-links": "node scripts/check-agent-links.mjs", diff --git a/ee/apps/landing/public/.well-known/agent-skills/set-up-openwork-team/SKILL.md b/ee/apps/landing/public/.well-known/agent-skills/set-up-openwork-team/SKILL.md index 86b1592efef..d278054bf0c 100644 --- a/ee/apps/landing/public/.well-known/agent-skills/set-up-openwork-team/SKILL.md +++ b/ee/apps/landing/public/.well-known/agent-skills/set-up-openwork-team/SKILL.md @@ -21,7 +21,7 @@ Walkthrough with screenshots: https://openworklabs.com/docs/cloud/team-quickstar ## Headless: let the agent set it up -When the user wants the agent to do the setup from a terminal, follow https://openworklabs.com/start.md. It installs the `openwork-bootstrap` CLI (needs Node.js 20+; it is not on npm) and creates a provisional workspace with a first skill. The human claims ownership later in the browser, so no password is created by the agent. +When the user wants the agent to do the setup from a terminal, follow https://openworklabs.com/start.md. It installs the `openwork-bootstrap` CLI (needs stable Node.js 24.x, version 24.22.1 or newer within that major; the installer fetches a private checksum-verified Node.js 24.22.1 when needed; it is not on npm) and creates a provisional workspace with a first skill. The human claims ownership later in the browser, so no password is created by the agent. ```sh curl -fsSLo /tmp/openwork-install.sh https://openworklabs.com/install.sh diff --git a/ee/apps/landing/public/llms.txt b/ee/apps/landing/public/llms.txt index 363954222a0..e70cea0d2c2 100644 --- a/ee/apps/landing/public/llms.txt +++ b/ee/apps/landing/public/llms.txt @@ -27,7 +27,7 @@ Do not run `npx openwork` or `npm install openwork`: the npm package named `open 4. Share a skill: `Plugin Directory` > `Create plugin` > `+ Skill`, add it to a Collection, and grant the Collection to a team. 5. Everyone installs the desktop app (Path 1) and clicks `Joining a team? Sign in`. - Walkthrough: [Team quickstart](https://openworklabs.com/docs/cloud/team-quickstart) -- Headless setup for agents (no password, the human claims the workspace later): [start.md](https://openworklabs.com/start.md), which uses the `openwork-bootstrap` CLI from https://openworklabs.com/install.sh (requires Node.js 20+; installs the bootstrap CLI, not the desktop app). +- Headless setup for agents (no password, the human claims the workspace later): [start.md](https://openworklabs.com/start.md), which uses the `openwork-bootstrap` CLI from https://openworklabs.com/install.sh (requires stable Node.js 24.x, version 24.22.1 or newer within that major; the installer fetches a private checksum-verified Node.js 24.22.1 when needed; installs the bootstrap CLI, not the desktop app). - Skill: [set-up-openwork-team](https://openworklabs.com/.well-known/agent-skills/set-up-openwork-team/SKILL.md) ### Path 3: "Inside my agent" — connect the OpenWork MCP Gateway diff --git a/ee/apps/workbot/package.json b/ee/apps/workbot/package.json index da4486c0551..fed8e325a9f 100644 --- a/ee/apps/workbot/package.json +++ b/ee/apps/workbot/package.json @@ -4,7 +4,7 @@ "type": "module", "description": "Workbot on its own address: one ongoing chat per member, signed in with Den, answered on the headless runner.", "engines": { - "node": ">=22.13" + "node": ">=24.22.1 <25" }, "scripts": { "dev": "WORKBOT_DEV=1 NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx watch --ignore 'src/client/**' src/server/main.ts", diff --git a/ee/packages/headless-computer/Dockerfile.desktop b/ee/packages/headless-computer/Dockerfile.desktop index 7b5d5fc5646..2da44754eb6 100644 --- a/ee/packages/headless-computer/Dockerfile.desktop +++ b/ee/packages/headless-computer/Dockerfile.desktop @@ -1,6 +1,6 @@ # Build and push this image first; create a linux-vm Daytona snapshot from its registry digest. -# The base is a pinned node:24-bookworm-slim digest, supplied by the operator. -ARG OPENWORK_COMPUTER_BASE_IMAGE +# Default to the verified official Node 24.22.1 multi-platform image; operators may override with a pinned digest. +ARG OPENWORK_COMPUTER_BASE_IMAGE=node:24.22.1-bookworm-slim@sha256:1681862882da07ee5476fee047bcf34138833f1df42633bb25bcb0f438d5bf3d FROM ${OPENWORK_COMPUTER_BASE_IMAGE} AS desktop-vnc-builder USER root COPY ee/packages/headless-computer/scripts/build-desktop-vnc.sh /tmp/build-desktop-vnc.sh diff --git a/ee/packages/headless-computer/src/image.test.ts b/ee/packages/headless-computer/src/image.test.ts new file mode 100644 index 00000000000..b4adebb3472 --- /dev/null +++ b/ee/packages/headless-computer/src/image.test.ts @@ -0,0 +1,18 @@ +import assert from "node:assert/strict" +import { execFileSync } from "node:child_process" +import { test } from "node:test" +import { COMPUTER_INSTALL_SCRIPT, DESKTOP_COMPUTER_INSTALL_SCRIPT } from "./image.ts" + +for (const [name, script] of [["computer", COMPUTER_INSTALL_SCRIPT], ["desktop", DESKTOP_COMPUTER_INSTALL_SCRIPT]]) { + test(`${name} installs verified official Node 24.22.1 instead of distro Node`, () => { + assert.ok(script) + execFileSync("bash", ["-n"], { input: script }) + assert.doesNotMatch(script, /\bnodejs npm\b/) + assert.match(script, /https:\/\/nodejs\.org\/dist\/v24\.22\.1\/node-v24\.22\.1-linux-\$node_arch\.tar\.xz/) + assert.match(script, /amd64\) node_arch=x64; node_sha256=6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6/) + assert.match(script, /arm64\) node_arch=arm64; node_sha256=dbabf7a1cbf947e6059ffad2c27b80041db97dea1e553b62afdee7f13fca336e/) + assert.match(script, /\*\) echo "Unsupported Node architecture" >&2; exit 1/) + assert.ok(script.indexOf("sha256sum -c -") < script.indexOf("tar -xJf")) + assert.match(script, /test "\$\(node --version\)" = v24\.22\.1/) + }) +} diff --git a/ee/packages/headless-computer/src/image.ts b/ee/packages/headless-computer/src/image.ts index ac31ab789b4..6e2f0a738ef 100644 --- a/ee/packages/headless-computer/src/image.ts +++ b/ee/packages/headless-computer/src/image.ts @@ -26,9 +26,21 @@ export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq --no-install-recommends \\ ffmpeg imagemagick poppler-utils qpdf ghostscript tesseract-ocr pandoc \\ - jq ripgrep sqlite3 zip unzip p7zip-full file curl ca-certificates git \\ - nodejs npm fonts-dejavu-core fonts-liberation fonts-noto-core \\ + jq ripgrep sqlite3 zip unzip p7zip-full file curl ca-certificates git xz-utils \\ + fonts-dejavu-core fonts-liberation fonts-noto-core \\ ${officePackages} +# Official Node release checksums: https://nodejs.org/dist/v24.22.1/SHASUMS256.txt +case "$(dpkg --print-architecture)" in + amd64) node_arch=x64; node_sha256=6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6 ;; + arm64) node_arch=arm64; node_sha256=dbabf7a1cbf947e6059ffad2c27b80041db97dea1e553b62afdee7f13fca336e ;; + *) echo "Unsupported Node architecture" >&2; exit 1 ;; +esac +node_tmp=$(mktemp -d) +curl -fsSL -o "$node_tmp/node.tar.xz" "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-$node_arch.tar.xz" +echo "$node_sha256 $node_tmp/node.tar.xz" | sha256sum -c - +tar -xJf "$node_tmp/node.tar.xz" -C /usr/local --strip-components=1 --no-same-owner +rm -rf "$node_tmp" +test "$(node --version)" = v24.22.1 ${lockedPython ? `python3 -m pip install --no-cache-dir --quiet --break-system-packages --require-hashes --only-binary=:all: -r /usr/local/share/openwork-computer/requirements-desktop.lock` : `python3 -m pip install --no-cache-dir --quiet --break-system-packages \\ pandas numpy matplotlib openpyxl xlsxwriter python-docx python-pptx pdfplumber pypdf pillow opencv-python-headless requests beautifulsoup4 \\ || python3 -m pip install --no-cache-dir --quiet \\ diff --git a/evals/packages/hosts/src/sandbox-daytona.ts b/evals/packages/hosts/src/sandbox-daytona.ts index 3c201837f7c..395b25a1194 100644 --- a/evals/packages/hosts/src/sandbox-daytona.ts +++ b/evals/packages/hosts/src/sandbox-daytona.ts @@ -84,7 +84,7 @@ export function sharedDaytonaExec(provider: SandboxProvider, info: (id: string) export async function createWorldDaytonaExec(): Promise { const auth = await worldDaytonaAuth(); if (auth.mode === "cli") return defaultDaytonaExec; - const config: DaytonaProviderConfig = { ...auth, target: "us", snapshot: null, image: "node:20-bookworm", resources: { cpu: 2, memoryGb: 4, diskGb: 8 }, pollIntervalMs: 100, helperCreateTimeoutMs: 120_000, platform: { os: "linux", isolation: "container" } }; + const config: DaytonaProviderConfig = { ...auth, target: "us", snapshot: null, image: "node:24.22.1-bookworm", resources: { cpu: 2, memoryGb: 4, diskGb: 8 }, pollIntervalMs: 100, helperCreateTimeoutMs: 120_000, platform: { os: "linux", isolation: "container" } }; const client = createDaytonaClient(config); const provider = createDaytonaProvider(config, { client }); return sharedDaytonaExec(provider, async id => { diff --git a/evals/packages/open-workplace/README.md b/evals/packages/open-workplace/README.md index 2cabd749d2f..71433deaf73 100644 --- a/evals/packages/open-workplace/README.md +++ b/evals/packages/open-workplace/README.md @@ -22,7 +22,7 @@ node src/atlassian.mjs # Open Jira, Open Confluence APIs + MCP (PORT=3 ATLASSIAN_BASE_URL=http://127.0.0.1:3991 node src/atlassian-web.mjs # Open Jira / Open Confluence web UI (PORT=3992) ``` -No install step: each file uses Node built-ins only (Node 22+). +No install step: each file uses Node built-ins only (stable Node.js 24.x, version 24.22.1 or newer within that major). | Service | Web | MCP | REST | |---|---|---|---| diff --git a/evals/packages/open-workplace/package.json b/evals/packages/open-workplace/package.json index 9444652fe42..dc33f85a67d 100644 --- a/evals/packages/open-workplace/package.json +++ b/evals/packages/open-workplace/package.json @@ -16,6 +16,6 @@ "test": "node --test src/*.test.mjs" }, "engines": { - "node": ">=22" + "node": ">=24.22.1 <25" } } diff --git a/package.json b/package.json index 6f1fe1e8803..0b915abc4c1 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "@different-ai/openwork-workspace", "private": true, "version": "0.0.0", + "engines": { + "node": ">=24.22.1 <25" + }, "scripts": { "sdk:generate": "pnpm --filter @openwork/sdk generate", "sdk:check": "pnpm --filter @openwork/sdk generate:check && pnpm --filter @openwork/sdk typecheck", @@ -9,6 +12,7 @@ "den:contract": "node scripts/den-contract.mjs", "hooks:install": "node scripts/install-git-hooks.mjs", "test:contracts": "pnpm --filter @openwork-ee/den-api test:contract-tools", + "test:node-runtime": "node --test scripts/tests/node-runtime-baseline.test.mjs apps/server/test/node-runtime.test.mjs .devcontainer/node-runtime.test.mjs packaging/helm/openwork-ee/tests/node-runtime.mjs", "dev": "OPENWORK_DEV_MODE=1 OPENWORK_ELECTRON_REMOTE_DEBUG_PORT=${OPENWORK_ELECTRON_REMOTE_DEBUG_PORT:-9823} pnpm --filter @openwork/desktop dev", "dev:worktree": "OPENWORK_DEV_MODE=1 OPENWORK_DEV_PROFILE=auto OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN=${OPENWORK_ELECTRON_USE_MOCK_KEYCHAIN:-1} OPENWORK_ELECTRON_REMOTE_DEBUG_PORT=0 PORT=0 pnpm --filter @openwork/desktop dev", "dev:electron": "OPENWORK_DEV_MODE=1 pnpm --filter @openwork/desktop dev:electron", diff --git a/packages/computer-use/README.md b/packages/computer-use/README.md index 99b98f75e43..de9bd440e6e 100644 --- a/packages/computer-use/README.md +++ b/packages/computer-use/README.md @@ -162,7 +162,7 @@ Electron. They live in this package; the desktop imports its explicit exports. [Managed Daytona computer](../../docs/computer-use/managed-daytona.md) for its image, flags, lifecycle and current viewer limits. -In a dedicated VM with the desktop running, Node 22+ can run +In a dedicated VM with the desktop running, stable Node.js 24.x (24.22.1 or newer within that major) can run `scripts/prove-cloud-desktop.mjs`. Request its loopback `/run` endpoint, pause and resume the VM through the provider, then request `/after`. This checks a browser fill followed by a desktop keyboard save, shared takeover, screenshot diff --git a/packages/daytona-evidence/package.json b/packages/daytona-evidence/package.json index 56222244c21..845aa47df94 100644 --- a/packages/daytona-evidence/package.json +++ b/packages/daytona-evidence/package.json @@ -14,7 +14,7 @@ "./previews": "./src/previews.ts" }, "engines": { - "node": ">=24" + "node": ">=24.22.1 <25" }, "dependencies": { "@openwork/evidence-provider": "workspace:*", diff --git a/packages/daytona-evidence/src/builder.ts b/packages/daytona-evidence/src/builder.ts index cef40ad1e4c..236b173d07e 100644 --- a/packages/daytona-evidence/src/builder.ts +++ b/packages/daytona-evidence/src/builder.ts @@ -28,9 +28,9 @@ const BUILDER_KIND = "openwork-daytona-builder-v1"; const VM_SIZE = { cpu: 4, memory: 8, disk: 10 }; const ROOT = "/opt/openwork-preview"; -const NODE_VERSION = "v24.21.0"; -/** From https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (node-v24.21.0-linux-x64.tar.xz). */ -const NODE_SHA256 = "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6"; +const NODE_VERSION = "v24.22.1"; +/** From https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (node-v24.22.1-linux-x64.tar.xz). */ +const NODE_SHA256 = "6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6"; /** Node 24, Bun and git on plain Ubuntu; Freestyle's base image ships them. */ const BASE_RECIPE = `export DEBIAN_FRONTEND=noninteractive @@ -41,7 +41,7 @@ curl -fsSL -o /tmp/node.tar.xz https://nodejs.org/dist/${NODE_VERSION}/node-${NO echo "${NODE_SHA256} /tmp/node.tar.xz" | sha256sum -c - tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 --no-same-owner rm /tmp/node.tar.xz -node -e 'if (Number(process.versions.node.split(".")[0]) < 24) throw new Error("Node 24 or newer is required")' +test "$(node --version)" = ${NODE_VERSION} # openwork-server's build bundles its plugins with Bun. npm install --global bun@1.3.14 bun --version`; diff --git a/packages/docs/start-here/use-openwork-from-an-ai-agent.mdx b/packages/docs/start-here/use-openwork-from-an-ai-agent.mdx index 62a00abee06..18f7a7e5aa6 100644 --- a/packages/docs/start-here/use-openwork-from-an-ai-agent.mdx +++ b/packages/docs/start-here/use-openwork-from-an-ai-agent.mdx @@ -31,7 +31,7 @@ These links always point to the latest stable release. Then [run your first task 4. Share a skill: **Manage › Plugins › Create a plugin**, add the skill, then add the team under **Who can use it**. 5. Everyone installs the desktop app and clicks `Joining a team? Sign in`. -Full walkthrough: [Team quickstart](/cloud/team-quickstart). For a headless, agent-run setup, follow [start.md](https://openworklabs.com/start.md). It uses the `openwork-bootstrap` CLI from `https://openworklabs.com/install.sh`, which needs Node.js 20+ and doesn't install the desktop app. +Full walkthrough: [Team quickstart](/cloud/team-quickstart). For a headless, agent-run setup, follow [start.md](https://openworklabs.com/start.md). It uses the `openwork-bootstrap` CLI from `https://openworklabs.com/install.sh`, which needs Node.js 24.x (24.22.1 or newer within that major) and doesn't install the desktop app. ## Inside my agent: connect the MCP Gateway diff --git a/packages/enterprise-mcp-mock-server/package.json b/packages/enterprise-mcp-mock-server/package.json index ccfa5ccdd67..98fa3973e64 100644 --- a/packages/enterprise-mcp-mock-server/package.json +++ b/packages/enterprise-mcp-mock-server/package.json @@ -30,6 +30,6 @@ "typescript": "^5.6.3" }, "engines": { - "node": ">=20" + "node": ">=24.22.1 <25" } } diff --git a/packages/evidence-provider/package.json b/packages/evidence-provider/package.json index 85a1cc23ee1..2a04d422dc0 100644 --- a/packages/evidence-provider/package.json +++ b/packages/evidence-provider/package.json @@ -9,7 +9,7 @@ "./review": "./src/review.ts" }, "engines": { - "node": ">=24" + "node": ">=24.22.1 <25" }, "scripts": { "typecheck": "tsc -p tsconfig.json --noEmit --pretty false", diff --git a/packages/openwork-bootstrap/README.md b/packages/openwork-bootstrap/README.md index 48b279fd7b7..225738a4ef4 100644 --- a/packages/openwork-bootstrap/README.md +++ b/packages/openwork-bootstrap/README.md @@ -2,6 +2,12 @@ Script-installable `openwork-bootstrap` command for agent-first onboarding. +Requires stable Node.js 24.x, version 24.22.1 or newer within that major. +Older releases and other majors (including 25+) are rejected before any command +runs. This replaces the previous Node.js 20+ requirement. The installer reuses a +supported system Node or downloads a private official Node.js 24.22.1 build, +verified against its published SHA-256 checksum, without changing system Node. + This package is intentionally small and does not assume npm is the install channel. A bootstrap script can place `bin/openwork.mjs` on disk, then run: diff --git a/packages/openwork-bootstrap/bin/openwork.mjs b/packages/openwork-bootstrap/bin/openwork.mjs index 9da7ef8e090..a3eb49bb158 100644 --- a/packages/openwork-bootstrap/bin/openwork.mjs +++ b/packages/openwork-bootstrap/bin/openwork.mjs @@ -7,6 +7,16 @@ import { dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" const VERSION = "0.1.0" +const MIN_NODE_VERSION = "24.22.1" + +// Keep this file dependency-free: install.sh downloads it as a single file. +// Accept patched Node 24 releases only; future majors need an explicit review. +function nodeVersionSupported(version) { + const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version) + if (!match) return false + const [, major, minor, patch] = match.map(Number) + return major === 24 && (minor > 22 || (minor === 22 && patch >= 1)) +} // The installed command name. Keep it explicit so setup guides can distinguish // bootstrap actions from other OpenWork commands a user may already have. const COMMAND_NAME = "openwork-bootstrap" @@ -752,7 +762,7 @@ async function runDoctor(args) { const json = hasFlag(args.flags, "json") const checks = [] - checks.push({ name: "node", ok: Number(process.versions.node.split(".")[0]) >= 20, value: process.versions.node }) + checks.push({ name: "node", ok: nodeVersionSupported(process.versions.node), value: process.versions.node }) checks.push({ name: "installDir", ok: existsSync(installDir), value: installDir }) checks.push({ name: "binDir", ok: existsSync(binDir), value: binDir }) @@ -1771,6 +1781,9 @@ async function runCloud(args) { } async function main() { + if (!nodeVersionSupported(process.versions.node)) { + throw new Error(`openwork-bootstrap needs Node.js 24.x, version ${MIN_NODE_VERSION} or newer within that major (found ${process.versions.node}). Install a supported Node.js 24 release or re-run https://openworklabs.com/install.sh.`) + } const args = parseArgs(process.argv.slice(2)) if (hasFlag(args.flags, "help") || args.positionals[0] === "help") { printHelp() diff --git a/packages/openwork-bootstrap/evals/agent-test-evidence-linux-container.mjs b/packages/openwork-bootstrap/evals/agent-test-evidence-linux-container.mjs index 450e849416d..916254fc7ce 100644 --- a/packages/openwork-bootstrap/evals/agent-test-evidence-linux-container.mjs +++ b/packages/openwork-bootstrap/evals/agent-test-evidence-linux-container.mjs @@ -91,7 +91,7 @@ function dockerRun(script, options = {}) { "-e", `DEN_API_E2E_BASE_URL=${denBaseUrl}`, ...(options.env ?? []), - "node:20-bookworm-slim", + "node:24.22.1-bookworm-slim", "bash", "-lc", script, @@ -125,7 +125,7 @@ try { const linuxIdentity = dockerRun("node -p 'JSON.stringify({platform:process.platform,arch:process.arch,node:process.version})'") prove("The e2e runs inside Linux", { - action: "docker run node:20-bookworm-slim node -p process.platform", + action: "docker run node:24.22.1-bookworm-slim node -p process.platform", assert: "container reports platform linux", evidence: linuxIdentity, }, linuxIdentity.status === 0 && linuxIdentity.json?.platform === "linux") diff --git a/packages/openwork-bootstrap/package.json b/packages/openwork-bootstrap/package.json index 2861f26fbe6..9d03ee226d6 100644 --- a/packages/openwork-bootstrap/package.json +++ b/packages/openwork-bootstrap/package.json @@ -18,6 +18,6 @@ "check": "node --check bin/openwork.mjs && node --check evals/agent-test-evidence-openwork-bootstrap.mjs && node --check evals/agent-test-evidence-openwork-app-install.mjs && node --check evals/agent-test-evidence-linux-container.mjs" }, "engines": { - "node": ">=20" + "node": ">=24.22.1 <25" } } diff --git a/packages/openwork-bootstrap/start.md b/packages/openwork-bootstrap/start.md index 5d4832b86b1..144b88e7048 100644 --- a/packages/openwork-bootstrap/start.md +++ b/packages/openwork-bootstrap/start.md @@ -31,8 +31,8 @@ node packages/openwork-bootstrap/bin/openwork.mjs install \ In production, download the bootstrap script, inspect it, then run it. Do not pipe remote scripts directly into a shell. The script downloads the `openwork-bootstrap` CLI (a single dependency-free Node file) and installs it -into `$HOME/.local/bin` - no npm or npx required. If Node.js 20+ is missing, -the script downloads the official Node.js build into +into `$HOME/.local/bin` - no npm or npx required. If a supported stable Node.js 24.x (24.22.1 or newer within that major) is missing, +the script downloads the official Node.js 24.22.1 build into `$HOME/.openwork/bootstrap/node` (checksum-verified, no admin rights), so the user does not need to install anything first. diff --git a/packages/openwork-bootstrap/test/README.md b/packages/openwork-bootstrap/test/README.md index 450b8278396..ec5a480af6f 100644 --- a/packages/openwork-bootstrap/test/README.md +++ b/packages/openwork-bootstrap/test/README.md @@ -1,12 +1,25 @@ -# Device login tests +# Bootstrap CLI tests -Run the CLI protocol tests: +Run on a supported stable Node.js 24.x release (24.22.1 or newer within that +major). The CLI now rejects older releases before executing commands. + +Run the CLI tests: ```sh pnpm --filter openwork-bootstrap test ``` -These tests run the shipped CLI script in a child process. Each test has a separate HOME and credentials file. The fixture replaces network requests. It controls the clock. It records request times, not codes or tokens. +`node-runtime.test.mjs` checks the version floor, the Node 24-only upper bound, +prerelease/malformed version rejection, early command rejection, and `doctor`. +Its fixture substitutes version strings only inside test subprocesses. +`installer-runtime.test.mjs` executes the actual served POSIX shell installer +with isolated downloads and runtimes. It covers supported system Node reuse, +private Node fallback, the exact official 24.22.1 distribution URL and filename, +mirror overrides, checksum failures, and rejection of unsupported private Node. +It requires POSIX shell and tar and is skipped on Windows; no test downloads a +real runtime or changes the user's installation. + +The device-login tests run the shipped CLI script in a child process. Each test has a separate HOME and credentials file. The fixture replaces network requests. It controls the clock. It records request times, not codes or tokens. The tests check these conditions: diff --git a/packages/openwork-bootstrap/test/fixtures/node-version.mjs b/packages/openwork-bootstrap/test/fixtures/node-version.mjs new file mode 100644 index 00000000000..ce931335cd0 --- /dev/null +++ b/packages/openwork-bootstrap/test/fixtures/node-version.mjs @@ -0,0 +1,2 @@ +// Test-only version substitution; never changes the production acceptance gate. +Object.defineProperty(process.versions, "node", { value: process.env.TEST_NODE_VERSION }) diff --git a/packages/openwork-bootstrap/test/installer-runtime.test.mjs b/packages/openwork-bootstrap/test/installer-runtime.test.mjs new file mode 100644 index 00000000000..a1176e24859 --- /dev/null +++ b/packages/openwork-bootstrap/test/installer-runtime.test.mjs @@ -0,0 +1,101 @@ +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import { createHash } from "node:crypto" +import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { test } from "node:test" +import { fileURLToPath } from "node:url" +import { runInNewContext } from "node:vm" + +// Exercise the actual served shell script, including template-string escaping. +const route = readFileSync(new URL("../../../ee/apps/landing/app/install.sh/route.ts", import.meta.url), "utf8") +const script = runInNewContext(route.match(/const installScript = (`[\s\S]*?`);/)[1]) +const fixture = fileURLToPath(new URL("./fixtures/node-version.mjs", import.meta.url)) +const quote = (text) => `'${text.replaceAll("'", "'\\''")}'` +const installerTest = process.platform === "win32" ? test.skip : test + +function install(systemVersion, { privateVersion = "24.22.1", badChecksum = false, override = false } = {}) { + const dir = mkdtempSync(join(tmpdir(), "installer-node-test-")) + try { + const shims = join(dir, "shims") + const tree = join(dir, "tree") + mkdirSync(shims) + mkdirSync(join(tree, "node/bin"), { recursive: true }) + const wrapper = (version) => `#!/bin/sh\nTEST_NODE_VERSION=${quote(version)}; export TEST_NODE_VERSION\nexec ${quote(process.execPath)} --import ${quote(fixture)} "$@"\n` + const executable = (path, content) => { writeFileSync(path, content); chmodSync(path, 0o755) } + executable(join(shims, "node"), systemVersion === null ? "#!/bin/sh\nexit 127\n" : wrapper(systemVersion)) + executable(join(tree, "node/bin/node"), wrapper(privateVersion)) + const os = process.platform === "darwin" ? "darwin" : "linux" + const tarball = `node-v24.22.1-${os}-${process.arch}.tar.gz` + const archive = join(dir, tarball) + const packed = spawnSync("tar", ["-czf", archive, "-C", tree, "node"], { encoding: "utf8" }) + assert.ifError(packed.error) + assert.equal(packed.status, 0, packed.stderr) + const sha = badChecksum ? "0".repeat(64) : createHash("sha256").update(readFileSync(archive)).digest("hex") + const sums = join(dir, "sums") + // Include a newer major first to ensure the installer selects the pinned build. + writeFileSync(sums, `${"1".repeat(64)} node-v26.0.0-${os}-${process.arch}.tar.gz\n${sha} ${tarball}\n`) + const cli = join(dir, "cli.mjs") + writeFileSync(cli, 'console.log("CLI_INSTALLED");') + const trace = join(dir, "requests") + const dist = override ? "https://mirror.test/node/v24.22.1" : "https://nodejs.org/dist/v24.22.1" + executable(join(shims, "curl"), `#!/bin/sh +for url do :; done +printf '%s\\n' "$url" >> ${quote(trace)} +case "$url" in + ${quote(`${dist}/SHASUMS256.txt`)}) cat ${quote(sums)} ;; + ${quote(`${dist}/${tarball}`)}) cat ${quote(archive)} ;; + 'https://openworklabs.com/openwork-bootstrap.mjs') cat ${quote(cli)} ;; + *) echo "Unexpected download: $url" >&2; exit 1 ;; +esac +`) + const installer = join(dir, "install.sh") + writeFileSync(installer, script) + const result = spawnSync("sh", [installer], { + encoding: "utf8", timeout: 10_000, + env: { + ...process.env, PATH: `${shims}:${process.env.PATH}`, HOME: dir, + OPENWORK_BIN_DIR: join(dir, "bin"), OPENWORK_INSTALL_DIR: join(dir, "install"), + OPENWORK_BOOTSTRAP_CLI_URL: "https://openworklabs.com/openwork-bootstrap.mjs", + OPENWORK_NODE_DIST_URL: override ? dist : "", + }, + }) + assert.ifError(result.error) + return { ...result, requests: readFileSync(trace, "utf8").trim().split("\n"), dist, tarball } + } finally { + rmSync(dir, { recursive: true, force: true }) + } +} + +for (const version of ["24.22.1", "24.22.2", "24.23.0"]) installerTest(`installer reuses supported system Node ${version}`, () => { + const result = install(version) + assert.equal(result.status, 0, result.stderr) + assert.match(result.stdout, /CLI_INSTALLED/) + assert.deepEqual(result.requests, ["https://openworklabs.com/openwork-bootstrap.mjs"]) +}) +for (const version of [null, "20.20.0", "22.13.0", "23.4.0", "24.18.0", "24.21.99", "24.22.0", "25.0.0", "26.0.0", "24.22.1-rc.1", "garbage"]) installerTest(`installer replaces unsupported system Node ${version}`, () => { + const result = install(version) + assert.equal(result.status, 0, result.stderr) + assert.match(result.stdout, /CLI_INSTALLED/) + assert.deepEqual(result.requests, [`${result.dist}/SHASUMS256.txt`, `${result.dist}/${result.tarball}`, "https://openworklabs.com/openwork-bootstrap.mjs"]) +}) +installerTest("installer preserves the distribution mirror override and pinned filename", () => { + const result = install("24.22.0", { override: true }) + assert.equal(result.status, 0, result.stderr) + assert.equal(result.requests[0], "https://mirror.test/node/v24.22.1/SHASUMS256.txt") +}) +installerTest("installer aborts a checksum mismatch before extracting or running the CLI", () => { + const result = install("24.22.0", { badChecksum: true }) + assert.equal(result.status, 1) + assert.match(result.stderr, /checksum mismatch/) + assert.doesNotMatch(result.stdout, /CLI_INSTALLED/) + assert.equal(result.requests.length, 2) +}) +installerTest("installer checks the downloaded runtime too, before downloading the CLI", () => { + const result = install("24.22.0", { privateVersion: "26.0.0" }) + assert.equal(result.status, 1) + assert.match(result.stderr, /Downloaded runtime is not a supported Node.js 24.x/) + assert.doesNotMatch(result.stdout, /CLI_INSTALLED/) + assert.equal(result.requests.length, 2) +}) diff --git a/packages/openwork-bootstrap/test/node-runtime.test.mjs b/packages/openwork-bootstrap/test/node-runtime.test.mjs new file mode 100644 index 00000000000..b47ee783219 --- /dev/null +++ b/packages/openwork-bootstrap/test/node-runtime.test.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import { readFileSync } from "node:fs" +import { fileURLToPath } from "node:url" +import { test } from "node:test" + +const cli = fileURLToPath(new URL("../bin/openwork.mjs", import.meta.url)) +const fixture = fileURLToPath(new URL("./fixtures/node-version.mjs", import.meta.url)) +const accepted = ["24.22.1", "v24.22.1", "24.22.2", "24.23.0", "24.100.0"] +const rejected = ["20.20.0", "22.13.0", "22.99.0", "23.4.0", "24.0.0", "24.18.0", "24.21.99", "24.22.0", "25.0.0", "26.0.0", "99.0.0", "24.22", "24.022.1", "24.22.1-rc.1", "24.22.1+build", "24.22.1.0", "garbage", ""] + +function run(version, args) { + const result = spawnSync(process.execPath, ["--import", fixture, cli, ...args], { + encoding: "utf8", env: { ...process.env, TEST_NODE_VERSION: version }, + }) + assert.ifError(result.error) + return result +} + +test("package engine restricts Node to the patched supported major", () => { + const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) + assert.equal(pkg.engines.node, ">=24.22.1 <25") +}) +for (const version of accepted) test(`accepts stable patched Node ${version}`, () => { + const result = run(version, ["--version"]) + assert.equal(result.status, 0, result.stderr) + assert.equal(result.stdout.trim(), "0.1.0") +}) +for (const version of rejected) test(`rejects Node ${JSON.stringify(version)} before command execution`, () => { + const result = run(version, ["install", "--source", "/does-not-exist", "--json"]) + assert.equal(result.status, 1) + assert.equal(result.stdout, "") + assert.match(result.stderr, /Node.js 24.x, version 24.22.1/) +}) +test("doctor reports the supported Node version using the same gate", () => { + const result = run("24.22.1", ["doctor", "--install-dir", "/does-not-exist", "--bin-dir", "/does-not-exist", "--json"]) + const output = JSON.parse(result.stdout) + assert.deepEqual(output.checks.find((check) => check.name === "node"), { name: "node", ok: true, value: "24.22.1" }) +}) diff --git a/packages/sandbox-daytona/src/live.test.ts b/packages/sandbox-daytona/src/live.test.ts index 9586c4794c9..831e99e3cd8 100644 --- a/packages/sandbox-daytona/src/live.test.ts +++ b/packages/sandbox-daytona/src/live.test.ts @@ -5,7 +5,7 @@ const key = process.env.DAYTONA_API_KEY if (!key) throw new Error("test:live requires DAYTONA_API_KEY (creates and deletes real sandboxes)") for (const c of sandboxBlockConformanceCases(() => createDaytonaProvider({ apiKey: key, apiUrl: process.env.DAYTONA_API_URL ?? "https://app.daytona.io/api", - snapshot: process.env.SANDBOX_TEST_SNAPSHOT ?? "openwork-0.18.55", image: "node:20-bookworm", + snapshot: process.env.SANDBOX_TEST_SNAPSHOT ?? "openwork-0.18.55", image: "node:24.22.1-bookworm", resources: { cpu: 1, memoryGb: 1, diskGb: 4 }, pollIntervalMs: 100, helperCreateTimeoutMs: 60000, platform: { os: "linux", isolation: "container" }, }), ["run", "files"])) test(`Daytona: ${c.name}`, { timeout: 120000 }, c.run) diff --git a/packages/world/package.json b/packages/world/package.json index 0f1f17cc705..aa49c942299 100644 --- a/packages/world/package.json +++ b/packages/world/package.json @@ -14,7 +14,7 @@ "openwork-world": "./bin/world.mjs" }, "engines": { - "node": ">=24" + "node": ">=24.22.1 <25" }, "dependencies": { "@openwork/paths": "workspace:*" diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 86123592195..d986b4ded86 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim ARG OPENWORK_SERVER_VERSION=0.18.4 ARG OPENCODE_VERSION=1.17.11 diff --git a/packaging/docker/Dockerfile.den b/packaging/docker/Dockerfile.den index 89f5391eb9d..3f32d0ae7c7 100644 --- a/packaging/docker/Dockerfile.den +++ b/packaging/docker/Dockerfile.den @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.7 -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends g++ make python3 \ diff --git a/packaging/docker/Dockerfile.den-gateway b/packaging/docker/Dockerfile.den-gateway index 11529db831b..0f0a90c6715 100644 --- a/packaging/docker/Dockerfile.den-gateway +++ b/packaging/docker/Dockerfile.den-gateway @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.7 -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim RUN corepack enable diff --git a/packaging/docker/Dockerfile.den-web b/packaging/docker/Dockerfile.den-web index 299998d319d..5d9dd76f4f3 100644 --- a/packaging/docker/Dockerfile.den-web +++ b/packaging/docker/Dockerfile.den-web @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.7 -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim RUN corepack enable diff --git a/packaging/docker/Dockerfile.gateway b/packaging/docker/Dockerfile.gateway index 02095c5a13a..89086685740 100644 --- a/packaging/docker/Dockerfile.gateway +++ b/packaging/docker/Dockerfile.gateway @@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends g++ make python3 \ diff --git a/packaging/docker/Dockerfile.headless-runner b/packaging/docker/Dockerfile.headless-runner index e721323a2e5..7bd001c99d1 100644 --- a/packaging/docker/Dockerfile.headless-runner +++ b/packaging/docker/Dockerfile.headless-runner @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.7 # Headless runner: Den's private agent service for Slack replies, headless # Automations and Workbot. Same build as the documented Render deploy. -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim RUN corepack enable diff --git a/packaging/docker/Dockerfile.microsandbox b/packaging/docker/Dockerfile.microsandbox index 14db11ab23c..2fa2361a13f 100644 --- a/packaging/docker/Dockerfile.microsandbox +++ b/packaging/docker/Dockerfile.microsandbox @@ -1,4 +1,4 @@ -FROM node:22-bookworm-slim AS openwork-builder +FROM node:24.22.1-bookworm-slim AS openwork-builder WORKDIR /src @@ -14,7 +14,7 @@ RUN pnpm install --frozen-lockfile --trust-lockfile --filter openwork-server... && pnpm --filter openwork-server build \ && pnpm --filter openwork-server build:bin -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim ARG OPENWORK_SERVER_VERSION ARG OPENCODE_VERSION diff --git a/packaging/docker/Dockerfile.workbot b/packaging/docker/Dockerfile.workbot index dbb762b2297..0d05d9ec06b 100644 --- a/packaging/docker/Dockerfile.workbot +++ b/packaging/docker/Dockerfile.workbot @@ -1,7 +1,7 @@ # syntax=docker/dockerfile:1.7 # Workbot: the chat app whose turns run on the headless runner. Same build as # the documented Render deploy. -FROM node:22-bookworm-slim +FROM node:24.22.1-bookworm-slim RUN corepack enable diff --git a/packaging/docker/docker-compose.dev.yml b/packaging/docker/docker-compose.dev.yml index 9b97bfc4f50..15321b527ce 100644 --- a/packaging/docker/docker-compose.dev.yml +++ b/packaging/docker/docker-compose.dev.yml @@ -21,7 +21,7 @@ # OPENWORK_HOST_OPENCODE_DATA_DIR — import source for OpenCode auth/data x-shared: &shared - image: node:22-bookworm-slim + image: node:24.22.1-bookworm-slim working_dir: /app volumes: # Mount the entire repo so both services share node_modules + source diff --git a/packaging/helm/openwork-ee/README.md b/packaging/helm/openwork-ee/README.md index ea10cd87fee..3d49e56ac3e 100644 --- a/packaging/helm/openwork-ee/README.md +++ b/packaging/helm/openwork-ee/README.md @@ -223,6 +223,42 @@ which floats to the newest release on every pull; check with `helm show values oci://ghcr.io/different-ai/charts/openwork-ee --version X | grep -A1 '^image:'` and, when it prints `tag: latest`, also pass `--set image.tag=X`. +### Node.js security baseline and image upgrades + +Deploy newly published OpenWork application images built with **stable Node.js +24.x >=24.22.1**. Re-rendering or upgrading only the chart does not patch the +runtime inside an existing image. Pin the new OpenWork application release with +`image.tag` (or upgrade to its matching published chart), and review any +per-component image tag overrides, including legacy `inference.image.tag` and +canonical `gateway.image.tag`, so they do not retain older images. Update mirrored +images to the newly built release as well; do not merely retag an old image. + +`Chart.appVersion` is the **OpenWork application release**, not the Node.js +version. Do not set it or `image.tag` to `24.22.1` to request a runtime upgrade. + +After upgrading, run the existing test hook: + +```sh +helm test openwork-ee --logs +``` + +The `env-probe` Job uses the configured Den API image and reports its actual +Node.js version, platform, and architecture. It fails for older runtimes, +prereleases, or other Node.js major versions, in addition to its existing +environment and custom-CA checks. This is an operator-invoked post-deployment +check, not an install/upgrade admission gate. It proves the runtime in the probe's +Den API image only: separately verify the Den web, Gateway/inference, and any other +application images you deploy, especially with per-component overrides. Helm +rendering cannot inspect an image's runtime or prove that running pods have +pulled the new image. + +Local regression checks (no cluster required): + +```sh +node --test packaging/helm/openwork-ee/tests/node-runtime.mjs +bash packaging/helm/openwork-ee/tests/image-tag-and-migration-hook.sh +``` + Create a values file for the target environment: ```yaml diff --git a/packaging/helm/openwork-ee/templates/tests/env-probe-job.yaml b/packaging/helm/openwork-ee/templates/tests/env-probe-job.yaml index 1f74d7777f8..5a13e853983 100644 --- a/packaging/helm/openwork-ee/templates/tests/env-probe-job.yaml +++ b/packaging/helm/openwork-ee/templates/tests/env-probe-job.yaml @@ -73,6 +73,11 @@ spec: ]; const secretPattern = /(SECRET|PASSWORD|PRIVATE|TOKEN|KEY|DATABASE_URL|BOOTSTRAP_CODE|SHA256)/; const failures = []; + console.log(`Node.js runtime: ${process.version} (${process.platform}/${process.arch})`); + const nodeVersion = /^24\.(\d+)\.(\d+)$/.exec(process.versions.node); + if (!nodeVersion || !(Number(nodeVersion[1]) > 22 || (Number(nodeVersion[1]) === 22 && Number(nodeVersion[2]) >= 1))) { + failures.push(`Node.js: expected stable 24.x >=24.22.1, actual ${process.version}; deploy a new OpenWork application image release built with the patched runtime`); + } function compactPem(value) { return value.replace(/\s+/g, ""); diff --git a/packaging/helm/openwork-ee/tests/node-runtime.mjs b/packaging/helm/openwork-ee/tests/node-runtime.mjs new file mode 100644 index 00000000000..e8f8e31a745 --- /dev/null +++ b/packaging/helm/openwork-ee/tests/node-runtime.mjs @@ -0,0 +1,116 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import { test } from "node:test"; +import { runInNewContext } from "node:vm"; + +const chartDir = fileURLToPath(new URL("../", import.meta.url)); +const require = createRequire(import.meta.url); + +function render(...args) { + return execFileSync("helm", ["template", "runtime-test", chartDir, ...args], { encoding: "utf8" }); +} + +const rendered = render(); +const probe = rendered.split(/^---$/m).find((document) => document.includes("name: runtime-test-openwork-ee-env-probe")); +assert.ok(probe, "existing env-probe test hook is rendered"); +const source = /^ - \|\n((?: {14}[^\n]*\n|\n)+)/m.exec(probe)?.[1] + .split("\n").map((line) => line.slice(14)).join("\n"); +assert.ok(source, "extract the complete rendered Node.js script, not a copied version check"); + +function runProbe(version, overrides = {}) { + const output = []; + const errors = []; + const exit = new Error("process.exit"); + let status = 0; + const env = { + DATABASE_URL: "mysql://runtime-test:private-password@database/test", + DEN_DB_ENCRYPTION_KEY: "private-encryption-key", + BETTER_AUTH_SECRET: "private-auth-secret", + DEN_BASE_URL: "https://den.example.test", + DB_MODE: "mysql", + DEN_ORG_MODE: "single_org", + PROVISIONER_MODE: "disabled", + DEN_API_BASE: "http://den-api:8788", + DEN_AUTH_FALLBACK_BASE: "https://den.example.test", + DEN_AUTH_ORIGIN: "https://den.example.test", + OPENWORK_DEV_MODE: "false", + PORT: "8788", + NODE_OPTIONS: "", + ...overrides, + }; + try { + runInNewContext(source, { + require, + console: { + log: (line) => output.push(line), + error: (line) => errors.push(line), + }, + process: { + version: `v${version}`, + versions: { node: version }, + platform: "linux", + arch: "arm64", + env, + exit: (code) => { status = code; throw exit; }, + }, + }); + } catch (error) { + if (error !== exit) throw error; + } + return { status, output: output.join("\n"), errors: errors.join("\n") }; +} + +for (const version of ["24.22.1", "24.22.2", "24.22.10", "24.23.0", "24.100.0"]) { + test(`rendered probe accepts stable Node.js ${version}`, () => { + const result = runProbe(version); + assert.equal(result.status, 0, result.errors); + assert.match(result.output, /OpenWork Helm env probe passed/); + assert.ok(result.output.includes(`Node.js runtime: v${version} (linux/arm64)`)); + }); +} + +for (const version of [ + "20.20.0", "22.22.1", "23.22.1", "24.0.0", "24.9.9", "24.21.99", "24.22.0", + "24.22.1-rc.1", "24.23.0-nightly20261021", "24.22.1+custom", "25.0.0", "26.22.1", "invalid", +]) { + test(`rendered probe rejects Node.js ${version}`, () => { + const result = runProbe(version); + assert.equal(result.status, 1); + assert.match(result.errors, /expected stable 24\.x >=24\.22\.1/); + assert.ok(result.errors.includes(`actual v${version}`)); + assert.match(result.errors, /deploy a new OpenWork application image release/); + assert.ok(result.output.includes(`Node.js runtime: v${version} (linux/arm64)`)); + assert.doesNotMatch(result.output, /env probe passed/); + assert.doesNotMatch(result.errors, /private-password|private-encryption-key|private-auth-secret/); + }); +} + +test("runtime check retains environment checks and redacted diagnostics", () => { + const result = runProbe("24.22.1", { OPENWORK_ENV_PROBE_EXPECTED_DB_MODE: "planetscale" }); + assert.equal(result.status, 1); + assert.match(result.errors, /DB_MODE: expected "planetscale", actual "mysql"/); + assert.match(result.errors, /DATABASE_URL: /); + assert.doesNotMatch(result.errors, /private-password|private-encryption-key|private-auth-secret/); +}); + +test("existing test hook uses the same image tag precedence as Den API", () => { + const appVersion = /^appVersion: "([^"]+)"$/m.exec(readFileSync(`${chartDir}/Chart.yaml`, "utf8"))?.[1]; + assert.ok(appVersion); + for (const [args, tag] of [ + [[], appVersion], + [["--set", "image.tag=0.99.123"], "0.99.123"], + [["--set", "image.tag=0.99.123", "--set", "denApi.image.tag=0.99.124"], "0.99.124"], + ]) { + const documents = render(...args).split(/^---$/m); + const hook = documents.find((document) => document.includes("name: runtime-test-openwork-ee-env-probe")); + const api = documents.find((document) => document.includes("kind: Deployment") && document.includes("name: runtime-test-openwork-ee-den-api")); + assert.ok(hook?.includes(`image: "ghcr.io/different-ai/openwork-den-api:${tag}"`)); + assert.ok(api?.includes(`image: "ghcr.io/different-ai/openwork-den-api:${tag}"`)); + assert.match(hook, /"helm.sh\/hook": test/); + assert.match(hook, /expected stable 24\.x >=24\.22\.1/); + assert.match(hook, /Node\.js runtime:/); + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7fb06651a78..c1996f27df1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,8 +95,8 @@ overrides: source-map-js@>=1.0.0 <1.2.2: 1.2.2 tar@<7.5.22: 7.5.22 tmp@>=0.2.0 <0.2.7: 0.2.7 - undici@>=6.0.0 <6.28.1: 6.28.1 - undici@>=7.0.0 <7.29.1: 7.29.1 + undici@>=6.0.0 <6.29.1: 6.29.1 + undici@>=7.0.0 <7.30.1: 7.30.1 uuid@<11.1.1: 11.1.1 vite@>=6.0.0 <6.4.4: 6.4.4 ws@>=8.0.0 <8.21.0: 8.21.0 @@ -399,8 +399,8 @@ importers: specifier: npm:@lydell/node-pty@1.2.0-beta.12 version: '@lydell/node-pty@1.2.0-beta.12' undici: - specifier: 6.28.1 - version: 6.28.1 + specifier: 6.29.1 + version: 6.29.1 yaml: specifier: ^2.9.0 version: 2.9.0 @@ -506,8 +506,8 @@ importers: specifier: ^10.2.5 version: 10.2.5 undici: - specifier: 6.28.1 - version: 6.28.1 + specifier: 6.29.1 + version: 6.29.1 yaml: specifier: ^2.9.0 version: 2.9.0 @@ -762,8 +762,8 @@ importers: specifier: ^5.5.4 version: 5.9.3 undici: - specifier: 7.29.1 - version: 7.29.1 + specifier: 7.30.1 + version: 7.30.1 zod: specifier: ^4.3.6 version: 4.3.6 @@ -802,8 +802,8 @@ importers: specifier: 4.13.11 version: 4.13.11 undici: - specifier: 7.29.1 - version: 7.29.1 + specifier: 7.30.1 + version: 7.30.1 zod: specifier: ^4.3.6 version: 4.3.6 @@ -10350,12 +10350,12 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - undici@6.28.1: - resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} + undici@6.29.1: + resolution: {integrity: sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==} engines: {node: '>=18.17'} - undici@7.29.1: - resolution: {integrity: sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==} + undici@7.30.1: + resolution: {integrity: sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -11940,7 +11940,7 @@ snapshots: semver: 7.8.5 sumchecker: 3.0.1 optionalDependencies: - undici: 7.29.1 + undici: 7.30.1 transitivePeerDependencies: - supports-color @@ -13914,7 +13914,7 @@ snapshots: dependencies: progress: 2.0.3 proxy-from-env: 1.1.0 - undici: 6.28.1 + undici: 6.29.1 which: 2.0.2 optionalDependencies: '@sentry/cli-darwin': 3.6.0 @@ -15340,7 +15340,7 @@ snapshots: is-buffer: 2.0.5 is-node-process: 1.2.0 throttleit: 2.1.0 - undici: 6.28.1 + undici: 6.29.1 '@vercel/cli-config@0.2.4': dependencies: @@ -17254,7 +17254,7 @@ snapshots: ws: 8.21.0 yargs: 17.7.2 optionalDependencies: - undici: 7.29.1 + undici: 7.30.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -18226,7 +18226,7 @@ snapshots: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.35.5(@types/node@24.13.3) - undici: 7.29.1 + undici: 7.30.1 workerd: 1.20260926.1 ws: 8.21.0 youch: 4.1.0-beta.10 @@ -18532,7 +18532,7 @@ snapshots: semver: 7.8.5 tar: 7.5.22 tinyglobby: 0.2.17 - undici: 6.28.1 + undici: 6.29.1 which: 6.0.1 node-int64@0.4.0: {} @@ -20154,9 +20154,9 @@ snapshots: undici-types@7.19.2: {} - undici@6.28.1: {} + undici@6.29.1: {} - undici@7.29.1: {} + undici@7.30.1: {} unenv@2.0.0-rc.24: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d2eb7198f21..9d8d53af0a3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -119,8 +119,8 @@ overrides: "source-map-js@>=1.0.0 <1.2.2": 1.2.2 "tar@<7.5.22": 7.5.22 "tmp@>=0.2.0 <0.2.7": 0.2.7 - "undici@>=6.0.0 <6.28.1": 6.28.1 - "undici@>=7.0.0 <7.29.1": 7.29.1 + "undici@>=6.0.0 <6.29.1": 6.29.1 + "undici@>=7.0.0 <7.30.1": 7.30.1 "uuid@<11.1.1": 11.1.1 "vite@>=6.0.0 <6.4.4": 6.4.4 "ws@>=8.0.0 <8.21.0": 8.21.0 diff --git a/scripts/tests/node-runtime-baseline.test.mjs b/scripts/tests/node-runtime-baseline.test.mjs new file mode 100644 index 00000000000..10e91dc1613 --- /dev/null +++ b/scripts/tests/node-runtime-baseline.test.mjs @@ -0,0 +1,66 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { test } from "node:test"; + +const root = fileURLToPath(new URL("../../", import.meta.url)); +const read = (path) => readFileSync(new URL(`../../${path}`, import.meta.url), "utf8"); +const tracked = execFileSync("git", ["ls-files", "-z"], { cwd: root, encoding: "utf8" }).split("\0").filter(Boolean); +const baseline = "24.22.1"; +const engine = ">=24.22.1 <25"; + +// These are deployment roots, not dependencies' compatibility declarations. +const deploymentRoots = [ + "", "apps/app/", "apps/review/", "apps/server/", + "ee/apps/den-api/", "ee/apps/den-gateway/", "ee/apps/den-web/", + "ee/apps/diagnostics/", "ee/apps/gateway/", "ee/apps/headless-runner/", + "ee/apps/landing/", "ee/apps/workbot/", "evals/packages/open-workplace/", + "packages/openwork-bootstrap/", +]; + +test("local development and deployment package engines share the patched Node 24 baseline", () => { + assert.equal(read(".nvmrc").trim(), baseline); + for (const directory of deploymentRoots) { + const path = `${directory}package.json`; + assert.equal(JSON.parse(read(path)).engines?.node, engine, path); + } +}); + +test("all first-party Docker Node bases pin the security release", () => { + let count = 0; + for (const path of tracked.filter((path) => /(^|\/)Dockerfile(?:[./]|$)/.test(path))) { + for (const match of read(path).matchAll(/^FROM\s+(?:--platform=\S+\s+)?(node:\S+)/gm)) { + assert.match(match[1], /^node:24\.22\.1-bookworm(?:-slim)?(?:@sha256:[a-f0-9]{64})?$/, path); + count++; + } + } + assert.ok(count >= 10, "check actual runtime Dockerfiles, not an empty file set"); +}); + +test("GitHub standalone Node jobs and runtime matrices pin the security release", () => { + let count = 0; + for (const path of tracked.filter((path) => path.startsWith(".github/") && /\.ya?ml$/.test(path))) { + for (const match of read(path).matchAll(/^\s*node-version:\s*([^\n#]+)/gm)) { + const value = match[1].trim(); + if (value.startsWith("${{")) continue; + assert.equal(value.replace(/^["']|["']$/g, ""), baseline, path); + count++; + } + } + assert.ok(count >= 50, "cover all workflow/action runtime pins"); + const release = read(".github/workflows/release-macos-aarch64.yml"); + for (const match of release.matchAll(/^\s+node:\s*"([^"]+)"/gm)) assert.equal(match[1], baseline); +}); + +test("standalone Undici imports and overrides use the security releases", () => { + for (const [directory, version] of [ + ["apps/desktop", "6.29.1"], ["apps/server", "6.29.1"], + ["ee/apps/den-api", "7.30.1"], ["ee/apps/den-gateway", "7.30.1"], + ]) assert.equal(JSON.parse(read(`${directory}/package.json`)).dependencies.undici, version, directory); + const workspace = read("pnpm-workspace.yaml"); + assert.ok(workspace.includes('"undici@>=6.0.0 <6.29.1": 6.29.1')); + assert.ok(workspace.includes('"undici@>=7.0.0 <7.30.1": 7.30.1')); + const lock = read("pnpm-lock.yaml"); + assert.doesNotMatch(lock, /undici@(?:6\.28\.1|7\.29\.1)/); +}); From 80537e747df6522be77f62663e58668c39a48b78 Mon Sep 17 00:00:00 2001 From: OmarMcAdam Date: Thu, 22 Oct 2026 09:55:27 -0700 Subject: [PATCH 2/2] fix(security): pin setup-node in image publication workflow Resolve the security review by replacing the mutable v6 action ref with its verified full commit SHA. Add a regression check that rejects mutable setup-node refs in the package-write workflow; retain Warden's human-approval safeguard. --- .github/workflows/publish-ee-images.yml | 2 +- scripts/tests/node-runtime-baseline.test.mjs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-ee-images.yml b/.github/workflows/publish-ee-images.yml index 89be7ad8eab..d6d8157e65e 100644 --- a/.github/workflows/publish-ee-images.yml +++ b/.github/workflows/publish-ee-images.yml @@ -77,7 +77,7 @@ jobs: uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 - name: Set up Node - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: 24.22.1 diff --git a/scripts/tests/node-runtime-baseline.test.mjs b/scripts/tests/node-runtime-baseline.test.mjs index 10e91dc1613..f784e7b5747 100644 --- a/scripts/tests/node-runtime-baseline.test.mjs +++ b/scripts/tests/node-runtime-baseline.test.mjs @@ -53,6 +53,13 @@ test("GitHub standalone Node jobs and runtime matrices pin the security release" for (const match of release.matchAll(/^\s+node:\s*"([^"]+)"/gm)) assert.equal(match[1], baseline); }); +test("image publication uses an immutable setup-node action", () => { + const workflow = read(".github/workflows/publish-ee-images.yml"); + const refs = [...workflow.matchAll(/uses:\s*actions\/setup-node@([^\s#]+)/g)]; + assert.ok(refs.length > 0, "check the Node setup step before publication checks"); + for (const [, ref] of refs) assert.match(ref, /^[a-f0-9]{40}$/, "package-write workflows must not use mutable action refs"); +}); + test("standalone Undici imports and overrides use the security releases", () => { for (const [directory, version] of [ ["apps/desktop", "6.29.1"], ["apps/server", "6.29.1"],