Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ updates:
day: monday
open-pull-requests-limit: 5

# The container image's own dependency set (docker/package.json + lock), which
# the Dockerfile installs with `npm ci`. Separate from the root manifest: these
# are the packages that ship INSIDE the image, not truecopy's own deps.
- package-ecosystem: npm
directory: /docker
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5

# The base image is pinned by digest, so nothing else would ever move it —
# without this, "pinned" quietly becomes "stuck on an unpatched node:22-slim".
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
day: monday
open-pull-requests-limit: 5

- package-ecosystem: github-actions
directory: /
schedule:
Expand Down
41 changes: 31 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,43 @@
# server (@modelcontextprotocol/server-everything) and pins its tools at build
# time, so at runtime tools/list returns a real, *vetted* tool set — a live
# demonstration of the gate passing a clean server through untouched.
FROM node:22-slim
#
# Every dependency of this image is pinned by HASH, not by tag: the base image by
# digest below, the npm tree by integrity hash in docker/package-lock.json. A tool
# that exists to make agents pin their supply chain has to pin its own. Dependabot
# watches both (`docker` + `/docker` npm in .github/dependabot.yml), so the pins
# get bumped deliberately instead of drifting silently.
FROM node:22-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3

# @askalf/truecopy pulls @askalf/redstamp from GitHub, so git (+ CA certs for
# HTTPS) must be present when npm resolves the dependency tree.
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Pin both versions so the image — and the lock generated from it — are
# reproducible. Bump these together with a rebuild.
ARG TRUECOPY_VERSION=0.9.0
ARG EVERYTHING_VERSION=2026.7.4
RUN npm i -g @askalf/truecopy@${TRUECOPY_VERSION} \
@modelcontextprotocol/server-everything@${EVERYTHING_VERSION}
# npm writes hosted-git `resolved` URLs as git+ssh:// whenever the lock is
# generated on a machine with a GitHub SSH remote. There is no SSH key in this
# build, so force the HTTPS transport for github.com — the commit SHA in the lock
# is what pins the dependency; the transport that fetches it is not a security
# property. Without this the build breaks on whoever regenerates the lock next.
RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com/

WORKDIR /app

# Install from a LOCKFILE, not `npm i <pkg>@<version>`. A version tag is a
# mutable label; docker/package-lock.json records the integrity hash of every
# registry tarball in the transitive tree (and the exact commit for the
# git-sourced redstamp dependency), so the image is byte-reproducible and a
# compromised republish cannot slip in. Bump docker/package.json and regenerate
# the lock together — never hand-edit it.
COPY docker/package.json docker/package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund

# The two CLIs (truecopy, mcp-server-everything) are now local to /app rather
# than globally installed, so put the local bin dir on PATH for the build steps,
# the ENTRYPOINT, and the downstream server this gate spawns.
ENV PATH=/app/node_modules/.bin:$PATH

COPY docker/pin-everything.mjs ./pin-everything.mjs

# Capture exactly the tools the downstream advertises and pin them into
Expand All @@ -38,9 +59,9 @@ RUN node pin-everything.mjs everything -- mcp-server-everything stdio > everythi
# uid 0: the process only needs to READ the lock and exec the downstream server,
# so a container escape or a compromised downstream should not land on root.
# node:22-slim ships an unprivileged `node` user (uid 1000). Everything the
# runtime touches is world-readable and built above as root -- the globally
# installed binaries under /usr/local and the lock at /app/truecopy.lock -- so
# nothing needs chown'ing, and the gate never writes at runtime.
# runtime touches is world-readable and built above as root -- /app/node_modules
# and the lock at /app/truecopy.lock -- so nothing needs chown'ing, and the gate
# never writes at runtime.
# The docker workflow asserts `id -u != 0` on every build so this cannot regress.
USER node

Expand Down
7 changes: 5 additions & 2 deletions Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Scoped ignore for the truecopy-mcp image ONLY (BuildKit <Dockerfile>.dockerignore).
# The image installs truecopy from npm and only needs the pin helper — keep the
# build context tiny and reproducible rather than copying the working tree in.
# The image installs truecopy from npm and only needs its dependency manifest +
# the pin helper — keep the build context tiny and reproducible rather than
# copying the working tree in.
# NOT a root .dockerignore: ClusterFuzzLite (.clusterfuzzlite/Dockerfile) builds
# from the same repo-root context and needs the full tree (see #76).
*
!docker/package.json
!docker/package-lock.json
!docker/pin-everything.mjs
Loading