npxc 0.3.0

Sandboxed npm execution for MCP servers via Apple container
Documentation
ARG NODE_IMAGE=node:lts-slim
ARG PACKAGE_SPEC
# The `COPY --from` below pulls the prebuilt boringtun-cli binary from npxc's
# WireGuard base image (the guest runs userspace WireGuard because the container
# VM kernel has no WireGuard module). npxc substitutes the real image tag for
# the `__NPXC_WG_BASE_IMAGE__` placeholder when it writes this Dockerfile;
# `COPY --from` can't take a build ARG in this BuildKit frontend.

FROM ${NODE_IMAGE} AS builder
ARG PACKAGE_SPEC
WORKDIR /app
RUN npm init -y >/dev/null \
 && npm install --omit=dev --no-audit --no-fund "${PACKAGE_SPEC}"

# Compute an entrypoint shim from the package's "bin" field at build time.
# Single-quoted heredoc marker prevents shell from expanding ${abs} / "$@" /
# $NPXC_* tokens, which are JavaScript or guest-shell — not build-shell —
# expressions. The generated /app/entry.sh runs as root: if WireGuard config
# is present in the environment it starts a userspace WireGuard interface and
# routes all egress through the tunnel, then drops to the unprivileged `node`
# user to exec the server.
COPY <<'EOF' /app/gen-entry.js
const fs = require("fs"), path = require("path");
const spec = process.argv[2].replace(/@[^@/]+$/, "");
const pkgDir = path.join("/app/node_modules", spec);
const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
const bin = typeof pkg.bin === "string" ? pkg.bin
          : pkg.bin ? Object.values(pkg.bin)[0]
          : (() => { throw new Error("package has no bin entry"); })();
const abs = path.resolve(pkgDir, bin);
const lines = [
  "#!/bin/sh",
  "set -e",
  'if [ -n "$NPXC_WG_PRIVATE_KEY" ]; then',
  "  mkdir -p /run/wireguard",
  "  boringtun-cli --disable-drop-privileges wg0 >&2",
  '  i=0; while [ ! -S /run/wireguard/wg0.sock ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done',
  `  printf '%s' "$NPXC_WG_PRIVATE_KEY" | wg set wg0 private-key /dev/stdin`,
  '  wg set wg0 peer "$NPXC_WG_PEER_PUBLIC_KEY" endpoint "$NPXC_WG_ENDPOINT" allowed-ips 0.0.0.0/0,::/0 persistent-keepalive 25',
  '  ip address add "$NPXC_WG_ADDRESS" dev wg0',
  '  if [ -n "$NPXC_WG_ADDRESS6" ]; then ip -6 address add "$NPXC_WG_ADDRESS6" dev wg0 || true; fi',
  '  ip link set wg0 mtu "${NPXC_WG_MTU:-1380}" up',
  "  ip route replace default dev wg0",
  '  if [ -n "$NPXC_WG_ADDRESS6" ]; then ip -6 route replace default dev wg0 || true; fi',
  "fi",
  `exec setpriv --reuid 1000 --regid 1000 --clear-groups node ${abs} "$@"`,
];
fs.writeFileSync("/app/entry.sh", lines.join("\n") + "\n", { mode: 0o755 });
EOF
RUN node /app/gen-entry.js $PACKAGE_SPEC

# Strip docs/tests/sourcemaps.
RUN find /app/node_modules -type d \
      \( -name test -o -name tests -o -name __tests__ -o -name docs \) \
      -prune -exec rm -rf {} + 2>/dev/null || true \
 && find /app/node_modules -type f \
      \( -name '*.md' -o -name 'CHANGELOG*' -o -name '*.map' \) \
      -delete 2>/dev/null || true

FROM ${NODE_IMAGE} AS runtime
# wireguard-tools (`wg`) + iproute2 (`ip`) configure the tunnel; npm/npx are
# removed so the package cannot self-install at runtime. `setpriv` (util-linux,
# present in the base) drops privileges.
RUN apt-get update \
 && apt-get install -y --no-install-recommends wireguard-tools iproute2 \
 && rm -rf /usr/local/lib/node_modules/npm \
           /usr/local/bin/npm /usr/local/bin/npx \
 && apt-get clean && rm -rf /var/lib/apt/lists/*
# Userspace WireGuard daemon (kernel has no wireguard module), copied from the
# prebuilt base image rather than recompiled here.
COPY --from=__NPXC_WG_BASE_IMAGE__ /boringtun-cli /usr/local/bin/boringtun-cli
WORKDIR /app
COPY --from=builder --chown=node:node /app /app
WORKDIR /workspace
# The entrypoint starts as root (to start wg0 + configure networking when in
# tunnel mode) and drops to `node` via setpriv before exec'ing the server. With
# --cap-drop ALL and no --cap-add (non-tunnel mode), root holds no capabilities
# and the drop is immediate.
ENTRYPOINT ["/app/entry.sh"]