diff --git a/.github/scripts/node_base_arg.sh b/.github/scripts/node_base_arg.sh new file mode 100755 index 000000000..dad66671e --- /dev/null +++ b/.github/scripts/node_base_arg.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# Single parser for Node.js base images in Dockerfiles. CI uses it both to +# decide which images need a runtime `node --version` check and to enforce that +# every Node.js base is pinned as node:${NODE_VERSION}-. +# +# Usage: +# node_base_arg.sh [target] +# Print the NODE_VERSION ARG that (default: the last stage) +# is ultimately built FROM, or nothing if it is not Node-based. +# node_base_arg.sh --check ... +# Validate every stage of every file and print " NODE_VERSION" +# once per pin each file uses. +# +# Both modes scan every FROM in the file and exit 1 if any stage: +# - references a Node.js image (any registry or namespace whose repository is +# "node", e.g. node:, node@, library/node, mirror.gcr.io/library/node) +# not written as node:${NODE_VERSION}-; or +# - takes its base from a build arg (FROM ${VAR}) other than an allowlisted one, +# or uses any build arg other than ${NODE_VERSION} inside the reference +# (e.g. node${TAG}-slim, ${REG}/node:...), since that can hide an unpinned +# Node.js base. +# FROM lines continued with "\" are joined first, as Docker does; a FROM with no +# image, or an "# escape=" parser directive, is rejected. +# Known limitation: a line starting with FROM inside a RUN heredoc is parsed as +# an instruction. That can only add a false error, never hide a Node.js base. +# The first mode also fails if is not a stage in the file. +# Stage names are matched case-insensitively, as Docker does. +set -euo pipefail + +# Build args that may be used directly as a base image. BUILDER_IMAGE selects +# the openab builder in Dockerfile.unified and is never a Node.js runtime base. +# CI can override it, so only its declared ARG default is checked. +ALLOWED_VAR_BASES="BUILDER_IMAGE" + +parse() { # parse [target] + awk -v mode="$1" -v target="${3:-}" -v allowed=" $ALLOWED_VAR_BASES " ' + # Classify one FROM image reference. Returns "node ", "none", or + # "error ". Stage references inherit the referenced stage. + function classify(img, depth, key, ref, var, probe) { + key = tolower(img) + if (key in stage) return stage[key] + if (img ~ /^[$][{]?[A-Za-z_][A-Za-z0-9_]*[}]?$/) { + var = img; gsub(/[${}]/, "", var) + if (index(allowed, " " var " ")) { + # CI may override an allowed build arg, but its declared default must + # not be a Node.js base either (e.g. ARG BUILDER_IMAGE=node:22). + if (!(var in argdef) || argdef[var] == "") return "none" + if (depth > 8) return "error " img " default refers back to itself" + return classify(argdef[var], depth + 1) + } + return "error FROM " img " takes its base image from a build arg, which can hide an unpinned Node.js base; use node:${NODE_VERSION}- or a named stage (allowed build args:" allowed ")" + } + # Any other build arg in the reference (e.g. node${TAG}-slim, ${REG}/node:...) + # cannot be resolved statically, so it could hide an unpinned Node.js base. + probe = img; gsub(/[$][{]NODE[0-9]+_VERSION[}]/, "", probe) + if (probe ~ /[$]/) + return "error " img " uses a build arg that cannot be resolved statically; only ${NODE_VERSION} may appear in a base image reference" + ref = img + sub(/^.*\//, "", ref) # any registry/namespace: keep the repository name + if (match(ref, /^node:[$][{]NODE[0-9]+_VERSION[}]-./)) + return "node " substr(ref, 8, index(ref, "}") - 8) # text between "node:${" and "}" + if (ref ~ /^node:[$][{]NODE[0-9]+_VERSION[}]$/) + return "error " img " has no tag suffix; use node:${NODE_VERSION}- (e.g. -trixie-slim)" + if (tolower(ref) ~ /^node([:@]|$)/) + return "error " img " is not pinned; use node:${NODE_VERSION}-" + return "none" + } + function report(lineno, msg) { + print "::error file=" FILENAME ",line=" lineno "::" msg > "/dev/stderr" + failed = 1 + } + function from(line, lineno, f, n, i, img, name, class, eq, v) { + n = split(line, f) + if (toupper(f[1]) == "ARG") { # remember ARG defaults for allowed build args + for (i = 2; i <= n; i++) { + eq = index(f[i], "=") + if (eq == 0) continue + v = substr(f[i], eq + 1); gsub(/^"|"$/, "", v) + argdef[substr(f[i], 1, eq - 1)] = v + } + return + } + if (toupper(f[1]) != "FROM") return + img = ""; name = "" + for (i = 2; i <= n; i++) { + if (f[i] ~ /^--/) continue # skip --platform=... and other flags + if (img == "") { img = f[i]; continue } + if (toupper(f[i]) == "AS" && i < n) { name = tolower(f[i + 1]); break } + } + if (img == "") { report(lineno, "FROM without an image reference"); return } + class = classify(img, 0) + if (class ~ /^error / && !(tolower(img) in stage)) # report each bad base once, not per child stage + report(lineno, substr(class, 7)) + if (class ~ /^node /) used[substr(class, 6)] = 1 + if (name != "") stage[name] = class + last = class + } + # A parser directive can change the line-continuation character; refuse it + # rather than misread continued FROM lines. + tolower($0) ~ /^#[[:space:]]*escape[[:space:]]*=/ { + report(FNR, "the escape parser directive is not supported by node_base_arg.sh") + next + } + /^[[:space:]]*#/ { next } # comments, also inside continuations + { + # Docker joins lines ending in "\" before parsing an instruction. + line = $0 + if (buf == "") start = FNR + if (line ~ /\\[[:space:]]*$/) { sub(/\\[[:space:]]*$/, " ", line); buf = buf line; next } + from(buf line, start) + buf = "" + } + END { + if (buf != "") from(buf, start) + if (failed) exit 1 + if (mode == "check") { + for (arg in used) print FILENAME, arg + exit 0 + } + if (target == "") { + class = last + } else if (tolower(target) in stage) { + class = stage[tolower(target)] + } else { + print "::error::Stage \"" target "\" not found in " FILENAME > "/dev/stderr" + exit 1 + } + if (class ~ /^node /) print substr(class, 6) + } + ' "$2" +} + +if [ "${1:-}" = "--check" ]; then + shift + [ "$#" -gt 0 ] || { echo "usage: node_base_arg.sh --check ..." >&2; exit 2; } + rc=0 + for f in "$@"; do parse check "$f" || rc=1; done # report every bad file, then fail + exit "$rc" +fi + +[ "$#" -ge 1 ] && [ "$#" -le 2 ] || { echo "usage: node_base_arg.sh [target]" >&2; exit 2; } +parse resolve "$1" "${2:-}" diff --git a/.github/scripts/test_node_base_arg.sh b/.github/scripts/test_node_base_arg.sh new file mode 100755 index 000000000..ba7d825ca --- /dev/null +++ b/.github/scripts/test_node_base_arg.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Regression tests for node_base_arg.sh. Run from the repository root: +# .github/scripts/test_node_base_arg.sh +set -euo pipefail + +script=.github/scripts/node_base_arg.sh +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +failures=0 + +# expect [script args...] +# The Dockerfile body is written to a fixture; "@" in args is replaced by its path. +expect() { + local name=$1 want_rc=$2 want_out=$3 want_err=$4 body=$5 + shift 5 + local file="$tmp/$name.Dockerfile" args=() rc=0 out err + printf '%b' "$body" > "$file" + for a in "$@"; do args+=("${a//@/$file}"); done + out=$("$script" "${args[@]}" 2> "$tmp/err") || rc=$? + err=$(cat "$tmp/err") + if [ "$rc" != "$want_rc" ] || [ "$out" != "$want_out" ] || [[ "$err" != *"$want_err"* ]]; then + echo "FAIL $name: rc=$rc (want $want_rc) out=[$out] (want [$want_out]) err=[$err] (want *$want_err*)" + failures=$((failures + 1)) + else + echo "ok $name" + fi +} + +# Resolution of pinned bases +expect stage-chain 0 NODE22_VERSION '' 'FROM node:${NODE22_VERSION}-trixie-slim AS base-node\nFROM base-node AS claude\n' @ claude +expect last-stage 0 NODE24_VERSION '' 'FROM rust AS builder\nFROM node:${NODE24_VERSION}-trixie-slim\n' @ +expect flags-and-case 0 NODE22_VERSION '' 'from --platform=$BUILDPLATFORM node:${NODE22_VERSION}-trixie-slim as base\nFROM base As app\n' @ app +expect stage-name-case 0 NODE22_VERSION '' 'FROM node:${NODE22_VERSION}-slim AS Base\nFROM base AS app\n' @ app +expect target-case 0 NODE22_VERSION '' 'FROM node:${NODE22_VERSION}-slim AS App\n' @ app +expect registry-pinned 0 NODE22_VERSION '' 'FROM docker.io/library/node:${NODE22_VERSION}-slim AS app\n' @ app +expect digest-on-pin 0 NODE22_VERSION '' 'FROM node:${NODE22_VERSION}-slim@sha256:0000 AS app\n' @ app +expect non-node 0 '' '' 'FROM debian:trixie-slim AS base\nFROM base AS app\n' @ app +expect allowed-var 0 '' '' 'FROM ${BUILDER_IMAGE} AS builder\nFROM debian AS app\n' @ app +expect allowed-var-stage 0 '' '' 'FROM rust AS local_builder\nARG BUILDER_IMAGE=local_builder\nFROM ${BUILDER_IMAGE} AS builder\n' @ builder +expect allowed-var-node 1 '' 'is not pinned' 'ARG BUILDER_IMAGE=node:22\nFROM ${BUILDER_IMAGE} AS app\n' @ app +expect allowed-var-loop 1 '' 'refers back' 'ARG BUILDER_IMAGE=${BUILDER_IMAGE}\nFROM ${BUILDER_IMAGE} AS app\n' @ app +expect param-expansion 1 '' 'cannot be resolved' 'FROM ${BASE:-node:22-trixie-slim} AS app\n' @ app +expect bare-composite 1 '' 'cannot be resolved' 'FROM $BASE-node AS app\n' @ app + +# Fail closed on Node.js bases that are not pinned through the ARG +expect literal-tag 1 '' 'is not pinned' 'FROM node:22-trixie-slim AS app\n' @ app +expect digest-only 1 '' 'is not pinned' 'FROM node@sha256:0000 AS app\n' @ app +expect registry-tag 1 '' 'is not pinned' 'FROM docker.io/library/node:22-trixie-slim AS app\n' @ app +expect library-tag 1 '' 'is not pinned' 'FROM library/node:22-trixie-slim AS app\n' @ app +expect bare-node 1 '' 'is not pinned' 'FROM node AS app\n' @ app +expect missing-suffix 1 '' 'has no tag suffix' 'FROM node:${NODE22_VERSION} AS app\n' @ app +expect var-base 1 '' 'from a build arg' 'ARG BASE=node:22-slim\nFROM ${BASE} AS app\n' @ app +expect var-base-plain 1 '' 'from a build arg' 'ARG BASE=node:22-slim\nFROM $BASE AS app\n' @ app +expect var-in-tag 1 '' 'cannot be resolved' 'FROM node${TAG}-slim AS app\n' @ app +expect var-image-name 1 '' 'cannot be resolved' 'FROM ${IMG}:22 AS app\n' @ app +expect var-registry 1 '' 'cannot be resolved' 'FROM ${REG}/node:${NODE22_VERSION}-slim AS app\n' @ app +expect var-node-tag 1 '' 'cannot be resolved' 'FROM node:${TAG} AS app\n' @ app +expect hidden-stage 1 '' 'is not pinned' 'FROM node:22-slim AS Base\nFROM base AS mid\nFROM debian AS app\n' @ app +expect index-registry 1 '' 'is not pinned' 'FROM index.docker.io/library/node:22 AS app\n' @ app +expect mirror-registry 1 '' 'is not pinned' 'FROM mirror.gcr.io/library/node:22 AS app\n' @ app +expect ecr-mirror 1 '' 'is not pinned' 'FROM public.ecr.aws/docker/library/node:22 AS app\n' @ app +expect continued-bad 1 '' 'is not pinned' 'FROM \\\n node:22-slim AS app\n' @ app +expect continued-cmt 1 '' 'is not pinned' 'FROM --platform=linux/amd64 \\\n# a comment\n node:22-slim \\\n AS app\n' @ app +expect empty-from 1 '' 'without an image' 'FROM\n' @ +expect escape-directive 1 '' 'escape parser' '# escape=`\nFROM debian AS app\n' @ app +expect unknown-target 1 '' 'not found' 'FROM debian AS app\n' @ nope +expect continued-ok 0 NODE24_VERSION '' 'FROM \\\n node:${NODE24_VERSION}-trixie-slim \\\n AS app\n' @ app +expect other-repo 0 '' '' 'FROM ghcr.io/openabdev/openab-native:beta AS app\nFROM rust:1@sha256:00 AS b\n' @ app + +# --check mode: every stage, one line per pin used +expect check-pins 0 "$tmp/check-pins.Dockerfile NODE22_VERSION" '' \ + 'FROM node:${NODE22_VERSION}-slim AS a\nFROM a AS b\nFROM debian AS c\n' --check @ +expect check-bad 1 '' 'is not pinned' 'FROM debian AS a\nFROM --platform=linux/amd64 node:22 AS b\n' --check @ + +# Current repository Dockerfiles resolve exactly as the old hand-written allowlist did. +# repo_expect checks the exit status too, so a crash cannot pass as "not Node-based". +repo_expect() { # repo_expect + local out + if ! out=$("$script" "$1" "$2"); then + echo "FAIL repo $1 $2: exited non-zero"; failures=$((failures + 1)) + elif [ "$out" != "$3" ]; then + echo "FAIL repo $1 $2: got [$out], want [$3]"; failures=$((failures + 1)) + fi +} +for f in Dockerfile.unified Dockerfile.package; do + for t in claude codex copilot gemini mimocode opencode pi; do repo_expect "$f" "$t" NODE22_VERSION; done + repo_expect "$f" kimi NODE24_VERSION + for t in kiro cursor grok devin antigravity native agentcore; do repo_expect "$f" "$t" ''; done +done +echo "ok repository targets" + +[ "$failures" -eq 0 ] || { echo "$failures test(s) failed"; exit 1; } +echo "all node_base_arg.sh tests passed" diff --git a/.github/workflows/docker-smoke-test-unified.yml b/.github/workflows/docker-smoke-test-unified.yml index d1e701ddd..3e1bae060 100644 --- a/.github/workflows/docker-smoke-test-unified.yml +++ b/.github/workflows/docker-smoke-test-unified.yml @@ -10,6 +10,7 @@ on: - 'Cargo.*' - 'openab-agent/**' - 'agy-acp/**' + - '.github/scripts/node_base_arg.sh' concurrency: group: smoke-test-unified-${{ github.event.pull_request.number }} @@ -85,6 +86,21 @@ jobs: - name: Verify agent CLI exists run: docker run --rm --entrypoint which openab-unified-${{ matrix.target.name }}:test ${{ matrix.target.agent }} + - name: Verify Node.js runtime version + shell: bash + run: | + set -euo pipefail + # Derive the pin from the target stage's base image rather than an + # agent allowlist, so a new Node-based target cannot skip this check, + # and an unpinned FROM node: fails here. + arg=$(.github/scripts/node_base_arg.sh Dockerfile.unified ${{ matrix.target.name }}) + if [ -z "$arg" ]; then echo "Not a Node.js-based image; skipping."; exit 0; fi + expected=$(awk -F= -v key="ARG $arg" '$1 == key {print $2; exit}' Dockerfile.unified) + test -n "$expected" || { echo "::error::Failed to extract $arg from Dockerfile.unified"; exit 1; } + actual=$(docker run --rm --entrypoint node openab-unified-${{ matrix.target.name }}:test --version) + [ "$actual" = "v$expected" ] || { echo "::error::Node.js version mismatch: expected v$expected ($arg), got $actual"; exit 1; } + echo "✅ Node.js $actual" + - name: Verify Codex runtime versions if: matrix.target.name == 'codex' run: | diff --git a/.github/workflows/docker-smoke-test.yml b/.github/workflows/docker-smoke-test.yml index c7baa30e0..3d8f6cc4e 100644 --- a/.github/workflows/docker-smoke-test.yml +++ b/.github/workflows/docker-smoke-test.yml @@ -5,9 +5,14 @@ on: types: [opened, synchronize, reopened, ready_for_review] paths: - 'Dockerfile*' + # validate-packaged-pins checks Node.js pins in every tracked Dockerfile, + # including nested ones such as openshell/Dockerfile. + - '**/Dockerfile*' - 'src/**' - 'crates/**' - 'Cargo.*' + - '.github/scripts/node_base_arg.sh' + - '.github/scripts/test_node_base_arg.sh' concurrency: group: smoke-test-${{ github.event.pull_request.number }} @@ -54,6 +59,21 @@ jobs: - name: Verify agent CLI exists run: docker run --rm --entrypoint which openab-test${{ matrix.variant.suffix }} ${{ matrix.variant.agent }} + - name: Verify Node.js runtime version + shell: bash + run: | + set -euo pipefail + # Derive the pin from the Dockerfile's base image rather than an agent + # allowlist, so a new Node-based variant cannot skip this check, and an + # unpinned FROM node: fails here. + arg=$(.github/scripts/node_base_arg.sh ${{ matrix.variant.dockerfile }}) + if [ -z "$arg" ]; then echo "Not a Node.js-based image; skipping."; exit 0; fi + expected=$(awk -F= -v key="ARG $arg" '$1 == key {print $2; exit}' ${{ matrix.variant.dockerfile }}) + test -n "$expected" || { echo "::error::Failed to extract $arg from ${{ matrix.variant.dockerfile }}"; exit 1; } + actual=$(docker run --rm --entrypoint node openab-test${{ matrix.variant.suffix }} --version) + [ "$actual" = "v$expected" ] || { echo "::error::Node.js version mismatch: expected v$expected ($arg), got $actual"; exit 1; } + echo "✅ Node.js $actual" + - name: Verify Codex runtime versions if: matrix.variant.suffix == '-codex' run: | @@ -169,6 +189,21 @@ jobs: } } + # Every Dockerfile that builds on Node.js must carry the same pin as + # Dockerfile.unified, so a Node bump edits only Dockerfiles and a partial + # bump fails. node_base_arg.sh is the only Dockerfile parser for this: + # --check fails on any Node base not written as + # node:${NODE_VERSION}- (literal tag, digest-only, --platform, + # registry-qualified, or hidden behind a build arg or another stage). + .github/scripts/test_node_base_arg.sh + mapfile -t dockerfiles < <(git ls-files -- 'Dockerfile*' '*/Dockerfile*') + node_pins=$(.github/scripts/node_base_arg.sh --check "${dockerfiles[@]}") + [ -n "$node_pins" ] || { echo "::error::No Node.js-based Dockerfile found; the Node pin parity check would pass vacuously" >&2; exit 1; } + while read -r f arg; do + pin=$(grep -m1 "^ARG $arg=" Dockerfile.unified) || { echo "::error::$arg is not pinned in Dockerfile.unified" >&2; exit 1; } + [[ "$pin" =~ ^ARG\ NODE[0-9]+_VERSION=[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::Dockerfile.unified must pin a full Node.js version (got '$pin')" >&2; exit 1; } + grep -Fqx -- "$pin" "$f" || { echo "::error file=$f::$f must declare '$pin' to match Dockerfile.unified" >&2; exit 1; } + done <<< "$node_pins" assert_line 'ARG KIRO_CLI_VERSION=2.25.0' Dockerfile Dockerfile.package Dockerfile.unified assert_line 'ARG KIRO_SHA256_AMD64=3e924de9697717b99891bb5f7d2a4bd9c768b8bfa1c498b35b3d242948512984' Dockerfile Dockerfile.package Dockerfile.unified assert_line 'ARG KIRO_SHA256_ARM64=cc8a2df90cd354b569b81ed4653db4796c86a69866568b4eb7fa7906afc95309' Dockerfile Dockerfile.package Dockerfile.unified diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 0c0edac13..7065dec0c 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -328,7 +328,9 @@ jobs: # The BUILD_ARGS literals above are indented for readability; strip any # leading whitespace so each emitted line is a clean KEY=VALUE that # build-push-action's build-args parser accepts. Drop blank lines too. - printf '%s\n' "$BUILD_ARGS" | sed 's/^[[:space:]]*//' | grep -v '^$' > resolved/build_args + # Nightly is the Node canary: pass only the major so the base image + # floats to the latest patch instead of the pinned Dockerfile default. + printf '%s\n' "$BUILD_ARGS" NODE22_VERSION=22 NODE24_VERSION=24 | sed 's/^[[:space:]]*//' | grep -v '^$' > resolved/build_args printf '%s\n' "$REPORT" > resolved/report - name: Upload resolved build args @@ -418,6 +420,27 @@ jobs: no-cache: true outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + - name: Report installed Node.js version + # Nightly floats the Node.js patch (NODE_VERSION=, see resolve). + # Record what actually shipped so a red canary or a rollback can be + # triaged without pulling the image. Warn-only: the float is intentional. + env: + DIGEST: ${{ steps.build.outputs.digest }} + ARCH: ${{ matrix.platform.arch }} + VARIANT: ${{ matrix.variant }} + run: | + set -uo pipefail + arg=$(.github/scripts/node_base_arg.sh Dockerfile.package "$VARIANT") || { + echo "::warning::Could not resolve the Node.js base for $VARIANT"; exit 0; } + if [ -z "$arg" ]; then echo "$VARIANT is not Node.js-based; skipping."; exit 0; fi + IMAGE="${REGISTRY}/${IMAGE_NAME}@${DIGEST}" + if docker pull -q "$IMAGE" >/dev/null && VERSION=$(docker run --rm --entrypoint node "$IMAGE" --version); then + echo "Node.js ${VARIANT} ${ARCH}: ${VERSION}" + echo "- Node.js \`${VARIANT}\` (${ARCH}, ${arg}): \`${VERSION}\`" >> "$GITHUB_STEP_SUMMARY" + else + echo "::warning::Could not read node --version from $IMAGE" + fi + - name: Verify and report installed Kiro version if: matrix.variant == 'kiro' env: diff --git a/Dockerfile.claude b/Dockerfile.claude index a10d21abf..a5ac97dc3 100644 --- a/Dockerfile.claude +++ b/Dockerfile.claude @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -19,7 +23,7 @@ COPY src/ src/ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/lib.rs crates/openab-mcp/src/lib.rs crates/openab-cp/src/lib.rs && cargo build --release # --- Runtime stage --- -FROM node:22-trixie-slim +FROM node:${NODE22_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini bubblewrap socat unzip && rm -rf /var/lib/apt/lists/* # Install claude-agent-acp adapter and Claude Code CLI. diff --git a/Dockerfile.codex b/Dockerfile.codex index c73a21031..56bfe4c25 100644 --- a/Dockerfile.codex +++ b/Dockerfile.codex @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -19,7 +23,7 @@ COPY src/ src/ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/lib.rs crates/openab-mcp/src/lib.rs crates/openab-cp/src/lib.rs && cargo build --release # --- Runtime stage --- -FROM node:22-trixie-slim +FROM node:${NODE22_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini bubblewrap unzip && rm -rf /var/lib/apt/lists/* # Pre-install codex-acp and codex CLI globally diff --git a/Dockerfile.copilot b/Dockerfile.copilot index fa56a0ae0..628680cce 100644 --- a/Dockerfile.copilot +++ b/Dockerfile.copilot @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -19,7 +23,7 @@ COPY src/ src/ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/lib.rs crates/openab-mcp/src/lib.rs crates/openab-cp/src/lib.rs && cargo build --release # --- Runtime stage --- -FROM node:22-trixie-slim +FROM node:${NODE22_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini unzip && rm -rf /var/lib/apt/lists/* # Install GitHub Copilot CLI via npm (pinned version) diff --git a/Dockerfile.gemini b/Dockerfile.gemini index 0c2c7e9c2..ed5fe272b 100644 --- a/Dockerfile.gemini +++ b/Dockerfile.gemini @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -19,7 +23,7 @@ COPY src/ src/ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/lib.rs crates/openab-mcp/src/lib.rs crates/openab-cp/src/lib.rs && cargo build --release # --- Runtime stage --- -FROM node:22-trixie-slim +FROM node:${NODE22_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini unzip && rm -rf /var/lib/apt/lists/* # Install Gemini CLI (native ACP support via --acp) diff --git a/Dockerfile.kimi b/Dockerfile.kimi index e68cdd78e..466accd87 100644 --- a/Dockerfile.kimi +++ b/Dockerfile.kimi @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE24_VERSION=24) floats to the latest. +ARG NODE24_VERSION=24.22.1 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -23,7 +27,7 @@ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/li # Kimi Code CLI is published as @moonshot-ai/kimi-code and exposes the # `kimi` binary. Its native ACP adapter is started with `kimi acp`. # Keep the package version pinned so image rebuilds are reproducible. -FROM node:24-trixie-slim +FROM node:${NODE24_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl procps ripgrep tini bubblewrap socat unzip git \ && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.mimocode b/Dockerfile.mimocode index f5fdfe03e..705fb0fd6 100644 --- a/Dockerfile.mimocode +++ b/Dockerfile.mimocode @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -27,7 +31,7 @@ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/li # ACP is supported via `mimo acp` (same JSON-RPC/stdio protocol as OpenCode). # # Version is pinned for reproducible builds. -FROM node:22-trixie-slim +FROM node:${NODE22_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini unzip && rm -rf /var/lib/apt/lists/* # Install MiMo-Code diff --git a/Dockerfile.opencode b/Dockerfile.opencode index 4333e25ac..05a85d95b 100644 --- a/Dockerfile.opencode +++ b/Dockerfile.opencode @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -19,9 +23,9 @@ COPY src/ src/ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/lib.rs crates/openab-mcp/src/lib.rs crates/openab-cp/src/lib.rs && cargo build --release # --- Runtime stage --- -# node:22-trixie-slim mirrors the base image used by Dockerfile.claude, -# Dockerfile.codex, and Dockerfile.gemini, keeping the project on a single -# consistent runtime base. +# node:${NODE22_VERSION}-trixie-slim mirrors the base image used by every +# Node 22-based agent Dockerfile (all pinned via NODE22_VERSION), keeping the +# project on a single consistent runtime base. # # opencode is published to npm as `opencode-ai`; the npm package is a thin # postinstall wrapper (~9 KB) that downloads the same pre-compiled binary @@ -44,7 +48,7 @@ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/li # The upstream fix landed in 1.17.0 (sst/opencode#30332, # "fix(opencode): generate reasoning variants for all OpenRouter models"). # 1.17.3 is the latest 1.17.x verified by users to recover correctly. -FROM node:22-trixie-slim +FROM node:${NODE22_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini unzip && rm -rf /var/lib/apt/lists/* # Install opencode diff --git a/Dockerfile.package b/Dockerfile.package index c5016fd49..ffa44e3aa 100644 --- a/Dockerfile.package +++ b/Dockerfile.package @@ -6,6 +6,11 @@ # # Usage: docker build --target -f Dockerfile.package --build-context bins=./bin . +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Nightly passes just the major (22 / 24) to float to the latest. +ARG NODE22_VERSION=22.23.4 +ARG NODE24_VERSION=24.22.1 + # ============================================================================= # Stage: bins — pre-built binaries passed in via build context # ============================================================================= @@ -30,7 +35,7 @@ RUN useradd -m -s /bin/bash -u 1000 agent # ============================================================================= # Stage: base-node — shared runtime base for node-based agents # ============================================================================= -FROM node:22-trixie-slim AS base-node +FROM node:${NODE22_VERSION}-trixie-slim AS base-node RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl procps ripgrep tini bubblewrap socat unzip git \ && rm -rf /var/lib/apt/lists/* @@ -133,7 +138,7 @@ CMD ["openab", "run", "-c", "/etc/openab/config.toml"] # ============================================================================= # Target: kimi # ============================================================================= -FROM node:24-trixie-slim AS kimi +FROM node:${NODE24_VERSION}-trixie-slim AS kimi RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl procps ripgrep tini bubblewrap socat unzip git \ && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.pi b/Dockerfile.pi index ae5fb99cc..f219aba44 100644 --- a/Dockerfile.pi +++ b/Dockerfile.pi @@ -1,3 +1,7 @@ +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 + # --- Build stage --- FROM rust:1-bookworm AS builder WORKDIR /build @@ -19,7 +23,7 @@ COPY src/ src/ RUN touch src/main.rs crates/openab-core/src/lib.rs crates/openab-gateway/src/lib.rs crates/openab-mcp/src/lib.rs crates/openab-cp/src/lib.rs && cargo build --release # --- Runtime stage --- -FROM node:22-trixie-slim +FROM node:${NODE22_VERSION}-trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl git procps ripgrep tini bubblewrap socat unzip && rm -rf /var/lib/apt/lists/* # Install pi-acp adapter and Pi coding agent CLI. diff --git a/Dockerfile.unified b/Dockerfile.unified index 4b802e1d4..91aa279db 100644 --- a/Dockerfile.unified +++ b/Dockerfile.unified @@ -14,6 +14,11 @@ # Global ARG — must be declared before first FROM for use in FROM instructions ARG BUILDER_IMAGE=local_builder +# Pinned so the shipped Node.js version is explicit; bump it on Node security +# releases. Passing just the major (e.g. NODE22_VERSION=22) floats to the latest. +ARG NODE22_VERSION=22.23.4 +ARG NODE24_VERSION=24.22.1 + # ============================================================================= # Stage: local_builder — compile openab binary (unified mode) # Only executed during build-core or local dev. When BUILDER_IMAGE is overridden @@ -81,7 +86,7 @@ RUN useradd -m -s /bin/bash -u 1000 agent # ============================================================================= # Stage: base-node — shared runtime base for node-based agents # ============================================================================= -FROM node:22-trixie-slim AS base-node +FROM node:${NODE22_VERSION}-trixie-slim AS base-node RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl procps ripgrep tini bubblewrap socat unzip git \ && rm -rf /var/lib/apt/lists/* @@ -168,7 +173,7 @@ CMD ["openab", "run", "-c", "/etc/openab/config.toml"] # ============================================================================= # Target: kimi # ============================================================================= -FROM node:24-trixie-slim AS kimi +FROM node:${NODE24_VERSION}-trixie-slim AS kimi RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl procps ripgrep tini bubblewrap socat unzip git \ && rm -rf /var/lib/apt/lists/* diff --git a/docs/opencode.md b/docs/opencode.md index f02d538d3..38d647856 100644 --- a/docs/opencode.md +++ b/docs/opencode.md @@ -28,7 +28,7 @@ OpenCode supports [75+ LLM providers](https://opencode.ai/docs/providers/) via t docker build -f Dockerfile.opencode -t openab-opencode:latest . ``` -The image installs `opencode-ai` globally via npm on `node:22-bookworm-slim`. +The image installs `opencode-ai` globally via npm on `node:-trixie-slim`, where `NODE22_VERSION` is the pinned Node.js 22 release in `Dockerfile.opencode`. ## Helm Install