# syntax=docker/dockerfile:1 # Digest-pinned base image keeps builds reproducible; bump deliberately. # T-164 round 180: the previous pin (a17d50af…) had drifted a week behind its own tag # `node:22-bookworm-slim`, which now points at 43ac6c60… (last_updated 2026-09-23). Measured, not # assumed: the Debian tracker lists CVE-2026-86145 / -89157 / -89161 as resolved in bookworm with # fixed_version 10.42-1+deb12u1 (bookworm-security), while this pin shipped libpcre2-8-0 10.42-1 # from the base repo — those three HIGH findings were a missing security update, not an unfixed # upstream. Refreshing the pin is what picks the update up. FROM node:22-bookworm-slim@sha256:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c AS runtime WORKDIR /app ENV NODE_ENV=production LABEL org.opencontainers.image.source="https://github.com/happy520ai/unified-ai-system" LABEL org.opencontainers.image.licenses="Apache-2.0" # T-164 round 181: the shipped base carries libpcre2-8-0 10.42-1+deb12u1, which is inside # CVE-2026-103111's vulnerable range (PCRE2 before 10.49, OSV 7.6 High; the 10-04 Trivy SARIF # upload opened exactly this one alert). Measured against the Debian tracker rather than # guessed: the fix exists in bookworm-security as 10.42-1+deb12u2 (DLA-4816-1) while bookworm # main still ships 10.42-1, and the pinned node image (last_updated 2026-09-23) predates that # upload -- today `node:22-bookworm-slim` still resolves to this very digest, so refreshing # the pin cannot pick the fix up. Upgrading this one library from the security pocket is the # fix; it is not a scanner workaround. Deliberately scoped to the single package: a blanket # apt-get upgrade would move the rest of the OS tree away from the digest pin's promise. # 2026-10-13: the 10-13 Trivy SARIF opened seven perl-base alerts (CVE-2026-8376, -42496, -13221 # CRITICAL; -57433, -57432, -48962, -42497 HIGH) against the base's 5.36.0-7+deb12u3; Trivy reports the # fix as 5.36.0-7+deb12u4 in bookworm-security, so perl-base is pinned up the same targeted way. # 2026-11-07: the 11-06 Trivy scan of the published gateway image (first scan after the #209 fix) # opened exactly two HIGH alerts, both CVE-2026-109518 in apt / libapt-pkg6.0 2.6.1. Measured inside # this very digest, not guessed: `apt-cache policy` in the pinned base offers 2.6.2 as the candidate # from bookworm-security, which is exactly the fix version Trivy names, so both packages are pinned # up the same targeted way as libpcre2 and perl-base before them. RUN apt-get update \ && apt-get install -y --no-install-recommends libpcre2-8-0=10.42-1+deb12u2 perl-base=5.36.0-7+deb12u4 apt=2.6.2 libapt-pkg6.0=2.6.2 \ && rm -rf /var/lib/apt/lists/* # 直接安装 pnpm(不经 corepack):qemu 跨架构构建下 corepack 的 tarball # 下载会确定性失败(exit 255),npm 的网络栈不受影响;同时运行时也 # 不再有 corepack 下载横幅污染 stdout。 # # T-164 round 180: pnpm is installed BY the image's bundled npm, so that npm's dependency tree is on # the real build path -- which is why Trivy's `library` findings were not decorative. Measured against # the registry rather than guessed: npm 10.9.9 already ships tar ^7.5.22 (clearing the one CRITICAL, # CVE-2026-59873) but still pacote ^19.0.1, and CVE-2026-9496 is fixed only at pacote 21.5.1. 11.21.0 # is the smallest line that clears both (tar ^7.5.22, pacote ^21.5.1) and its engines range # (^20.17.0 || >=22.9.0) covers the 22.23.x this base image ships. Pinned exactly, never @latest: a # floating version would make the image non-reproducible, which is the property the digest pin above # exists to keep. RUN npm install -g npm@11.21.0 RUN npm install -g pnpm@11.19.0 # npm has now done the only job it has in this image. pnpm@11.19.0 declares no runtime # dependencies at all (measured: `dependencies: {}` in its package.json), and neither runtime stage # below invokes npm -- they run `node` against the built tree. So npm's vendored tree is carried for # no reason, and it is the last thing Trivy was flagging: with npm 11.21.0 pinned above, brace-expansion # 5.0.9 and undici 6.28.0 are the only HIGH/CRITICAL findings left, and BOTH npm lines that exist # (11.21.0 and 12.2.0) bundle exactly those two versions, so no version bump can clear them. Removing # the tool is the honest fix -- it is not a scanner workaround: the vulnerable code simply is not in # the shipped artifact any more. If a future stage needs npm, re-add it deliberately. RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx # pnpm 的 verify-deps-before-run 会在项目根(/app,root 属主、node 只读) # 写 _tmp_* 哈希文件,非 root 运行 `pnpm gateway demo` 时偶发 EACCES。 # 容器内依赖由 --frozen-lockfile 在构建期锁定,运行期无需再校验。 ENV npm_config_verify_deps_before_run=false COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/ai-gateway-service/package.json apps/ai-gateway-service/package.json COPY apps/agent-console/package.json apps/agent-console/package.json COPY packages packages RUN pnpm install --frozen-lockfile \ --filter @unified-ai-system/ai-gateway-service... \ --filter @unified-ai-system/agent-console... \ --filter @unified-ai-system/mcp-server... # Same discipline as the npm strip above, applied to pnpm once its one job (the frozen install) is # done. pnpm's published bundle carries its own vendored node_modules, and Trivy reads undici 6.28.0 # out of `pnpm/dist/node_modules` and `pnpm/artifacts/exe/dist/node_modules` -- that was the last # HIGH/CRITICAL pair left after the base pin and the npm pin. Both runtime stages below run `node` # against the built tree and never invoke pnpm, and the CI smoke tests invoke `node` directly too # (tools/verify-public-clone.mjs and the docker smoke step), so nothing in this image needs it. # Note what this does NOT claim: it does not fix pnpm's undici, it stops shipping it. The build above # still ran pnpm at the repo's pinned version. If a future stage needs pnpm in the image, re-add it # deliberately and say why. RUN rm -rf /usr/local/lib/node_modules/pnpm /usr/local/bin/pnpm /usr/local/bin/pnpx /usr/local/bin/pnpm.cmd 2>/dev/null || true # 2026-11-07: the 11-06 Trivy scan also warned that pnpm's registry METADATA cache # (root/.cache/pnpm/v11/metadata/*.jsonl, 15 MB files like wrangler.jsonl) is baked into the # shipped image -- build-time state with no runtime purpose that bloats every pull and feeds # the scanner noise. The tools strips above already ended pnpm's job; its cache, its # content-addressable store, and its state end with it. node_modules keeps working across # this removal because pnpm hardlinks store files into the tree and the same layer holds # both, so the inodes stay alive through node_modules after the store entries are gone. RUN rm -rf /root/.cache/pnpm /root/.npm /root/.local/share/pnpm /root/.local/state/pnpm COPY apps/ai-gateway-service apps/ai-gateway-service COPY apps/agent-console apps/agent-console COPY tools/terminal-demo.mjs tools/terminal-demo.mjs COPY tools/mcp-smoke.mjs tools/mcp-smoke.mjs COPY tools/build-runtime-identity.mjs tools/build-runtime-identity.mjs # This value is a build declaration, not proof of the source revision. # The manifest separately fingerprints the source bytes shipped in this image. # pnpm install artifacts can carry root-only permission bits; the shipped # source tree must stay readable by the non-root runtime user so in-container # verification can re-hash it. Read/execute bits only - file bytes unchanged. RUN chmod -R a+rX apps packages tools package.json pnpm-lock.yaml pnpm-workspace.yaml ARG UAI_DECLARED_REVISION="" RUN node tools/build-runtime-identity.mjs --declared-revision "$UAI_DECLARED_REVISION" # 运行时状态目录(审计日志、请求日志、企业存储)归 node 所有; # 容器内进程以非 root 运行,缺这一步会在只读 /app 上 EACCES。 RUN mkdir -p .data/audit .data/request-logs .data/enterprise .data/knowledge apps/ai-gateway-service/.data \ && chown -R node:node .data apps/ai-gateway-service/.data # pnpm 11 默认开启 verify-deps-before-run,且镜像里没有仓库的 .npmrc。 # 依赖在构建期已由 --frozen-lockfile 锁定,运行期复核只会以非 root 身份 # 触发重装/清库。在镜像内的 .npmrc 显式关闭(pnpm 最权威的配置源)。 RUN printf 'verify-deps-before-run=false\n' > /app/.npmrc # Runtime state is restricted to explicit node-owned mounts. The application # root remains root-owned so deployments can enforce a read-only rootfs. VOLUME ["/app/.data", "/app/apps/ai-gateway-service/.data"] FROM runtime AS mcp LABEL org.opencontainers.image.description="Credential-free Unified AI System MCP server" LABEL io.modelcontextprotocol.server.name="io.github.happy520ai/unified-ai-system" USER node CMD ["node", "packages/mcp-server/src/index.js"] FROM runtime AS gateway ENV AI_GATEWAY_SERVICE_HOST=0.0.0.0 ENV AI_GATEWAY_SERVICE_PORT=3100 LABEL org.opencontainers.image.description="Terminal-first, self-hosted AI gateway" USER node EXPOSE 3100 # Keep enterprise authentication enabled for a bare `docker run` without # storing a secret-looking boolean in an ENV layer. An explicit runtime value # (including `false`) wins, and `exec` preserves Node as the signal-receiving # process. CMD ["sh", "-c", "PME_ENTERPRISE_AUTH_ENABLED=${PME_ENTERPRISE_AUTH_ENABLED:-true} exec node apps/ai-gateway-service/src/index.js"]