Files
mesh-lab/scripts/build-module-runtime.sh
T
jschoubben 87c4820130 anthropic bed: package a module's own npm deps, stage grant files readably
Two harness fixes the green end-to-end run needed:

- build-module-runtime.sh installs a module's non-@novox runtime deps under
  /app/modules/<module>/node_modules, so a module can carry a private dependency
  (the anthropic-manager seals with tweetnacl-sealedbox-js). The shared tree still
  answers @novox/* and common packages. A no-op for modules that declare none.

- stageIntoControl chmods the manager's 0600 adopt/refresh outputs to 0644 on the
  anchor host before docker cp, so the distroless mesh-control (non-root, no chmod)
  can read the staged file. What is staged is a sealed box or the access token,
  never a cleartext refresh token.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-07 02:47:50 +02:00

88 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build a per-module runtime image (novox/hq ADR 0052): the tool runtime carrying ONE module's
# compiled code, which serves that module's tools and runs its events/provisioner under the module's
# own scoped broker account. Generalises build-runtime-image.sh from the audit-logger to any module.
#
# build-module-runtime.sh <module> <output.tar>
# -> tags mesh-runtime-<module>:development and saves it to <output.tar>
set -euo pipefail
MODULE="${1:?usage: build-module-runtime.sh <module> <output.tar>}"
OUT="${2:?usage: build-module-runtime.sh <module> <output.tar>}"
HERE="$(cd "$(dirname "$0")/.." && pwd)"; ROOT="$(cd "$HERE/.." && pwd)"
MESH_TOOLS="${MESH_TOOLS:-$ROOT/mesh-tools}"
MESH_SDK="${MESH_SDK:-$ROOT/mesh-sdk}"
MESH_CATALOG="${MESH_CATALOG:-$ROOT/mesh-catalog}"
MOD="$MESH_CATALOG/modules/$MODULE"
TAG="${RUNTIME_TAG:-mesh-runtime-$MODULE:development}"
BASE="${RUNTIME_BASE:-node:22-bookworm-slim}"
[ -d "$MOD" ] || { echo "no module $MODULE at $MOD" >&2; exit 1; }
( cd "$MESH_SDK" && npm run build >/dev/null )
( cd "$MESH_TOOLS" && npm run build >/dev/null )
# Compile whichever of the module's entrypoints exist. Besides the serve-time entrypoints (tools,
# events, provisioner) and the run-once bootstrap, a module may carry scheduled/one-shot entrypoints
# it names in a `schedule`/`run-once` container's args (novox/hq ADR 0052/0053) — refresh/apply/usage
# for the anthropic model-access modules. tsc pulls in their imports, so leaf files they use are
# compiled with them.
SRCS=(); for f in \
client.ts index.ts tools/index.ts provisioner/index.ts bootstrap/index.ts \
adopt/index.ts refresh/index.ts apply/index.ts usage/index.ts; do
[ -f "$MOD/$f" ] && SRCS+=("$f")
done
TSC="$MESH_SDK/node_modules/.bin/tsc"; ( cd "$MOD" && "$TSC" "${SRCS[@]}" --module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist >/dev/null )
STAGE="$(mktemp -d)"; trap 'rm -rf "$STAGE"' EXIT
cp -r "$MESH_TOOLS/dist" "$STAGE/dist"
cp -rL "$MESH_TOOLS/node_modules" "$STAGE/node_modules"
mkdir -p "$STAGE/modules/$MODULE"; cp -r "$MOD/dist" "$STAGE/modules/$MODULE/dist"
cp "$MESH_TOOLS/package.json" "$STAGE/package.json"
# A module may declare its own third-party runtime deps (the anthropic-manager seals with
# tweetnacl-sealedbox-js). The shared node_modules copied above carries the common packages and
# @novox/* — but not a module's private deps. Install those under the module itself, so Node
# resolves them from /app/modules/<module>/node_modules and still falls back to the shared tree
# at /app/node_modules for @novox/* and everything common. Modules with no non-@novox deps are a
# no-op. (@novox/* are workspace deps with no registry to fetch from, so they are excluded here.)
MOD_DEPS="$(node -e 'const d=(require("'"$MOD"'/package.json").dependencies)||{};process.stdout.write(Object.keys(d).filter(k=>!k.startsWith("@novox/")).map(k=>k+"@"+d[k]).join(" "))')"
if [ -n "$MOD_DEPS" ]; then
# shellcheck disable=SC2086
npm install --prefix "$STAGE/modules/$MODULE" --omit=dev --no-save --no-package-lock --ignore-scripts $MOD_DEPS >/dev/null
fi
# The entrypoints the runtime loads: tools, events and (a provider's) provisioner, whichever exist.
ENTRIES=""; for e in tools/index.js index.js provisioner/index.js; do
[ -f "$STAGE/modules/$MODULE/dist/$e" ] && ENTRIES="${ENTRIES:+$ENTRIES,}/app/modules/$MODULE/dist/$e"
done
# A module whose code drives a CLI needs that CLI in the image — postgres shells out to `psql`, minio
# to `mc`. Everything else speaks a wire protocol or HTTP and needs nothing added.
EXTRA=""
case "$MODULE" in
postgres) EXTRA='RUN apt-get update && apt-get install -y --no-install-recommends postgresql-client && rm -rf /var/lib/apt/lists/*' ;;
minio) EXTRA='COPY --from=minio/mc:latest /usr/bin/mc /usr/bin/mc' ;;
# mosquitto drives its dynsec admin — and its run-once bootstrap seeds the store — through
# `mosquitto_ctrl`. It is not in `mosquitto-clients` on bookworm; the `mosquitto` package carries
# it (with its shared libraries), and installing from apt keeps them together — copying the binary
# out of the (musl) eclipse-mosquitto image into this (glibc) base would not load.
mosquitto) EXTRA='RUN apt-get update && apt-get install -y --no-install-recommends mosquitto && rm -rf /var/lib/apt/lists/*' ;;
# mongodb's client shells out to `mongosh`. Install it from MongoDB's own apt repo so its shared
# libraries come with it — copying just the binary out of the mongo image leaves it unable to load.
mongodb) EXTRA='RUN apt-get update && apt-get install -y --no-install-recommends gnupg curl ca-certificates && curl -fsSL https://pgp.mongodb.com/server-7.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb.gpg && echo "deb [signed-by=/usr/share/keyrings/mongodb.gpg] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/7.0 main" > /etc/apt/sources.list.d/mongodb.list && apt-get update && apt-get install -y --no-install-recommends mongodb-mongosh && rm -rf /var/lib/apt/lists/*' ;;
esac
cat > "$STAGE/Dockerfile" <<DOCKER
FROM $BASE
WORKDIR /app
$EXTRA
COPY package.json ./
COPY node_modules ./node_modules
COPY dist ./dist
COPY modules ./modules
ENV MESH_TOOL_MODULES=$ENTRIES
ENTRYPOINT ["node", "dist/main.js"]
DOCKER
docker build -t "$TAG" "$STAGE"
docker save -o "$OUT" "$TAG"
echo "built $TAG (entrypoints: $ENTRIES) -> $OUT"