#!/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 # -> tags mesh-runtime-:development and saves it to set -euo pipefail MODULE="${1:?usage: build-module-runtime.sh }" OUT="${2:?usage: build-module-runtime.sh }" 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, migrate for model-usage's run-once schema step. tsc pulls # in their imports, so leaf files they use are compiled with them. A module may also carry an ambient # `.d.ts` typing a third-party dep it default-imports (model-usage's pg.d.ts) — listed here so the # ambient declaration is in the program even though the dep is only installed into the image below. 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 migrate/index.ts \ pg.d.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" # And the sdk, from the sibling this script just built, whatever form the installed tree holds it # in. It used to be relied on being a symlink into that sibling, which `-L` above materialised — # true only on a workstation where somebody had linked them, and false the moment the runtime's # dependencies are installed the ordinary way, which now fetches the sdk as sources with nothing # compiled in it. The image built then looked fine and every entry point inside it pointed at # nothing. rm -rf "$STAGE/node_modules/@novox/mesh-sdk" mkdir -p "$STAGE/node_modules/@novox" cp -rL "$MESH_SDK" "$STAGE/node_modules/@novox/mesh-sdk" rm -rf "$STAGE/node_modules/@novox/mesh-sdk/node_modules" # **The image runs compiled code and never compiles any**, so it does not need a compiler. The # tree copied above is the runtime's full install, development dependencies and all — and the # compiler alone is 23 of its 28 MB. Every module image carried one, on every machine, for nothing: # tsc runs on the workstation a few lines above, not in here. # # Removed by name rather than by `npm prune --omit=dev`, which would re-resolve dependencies — one # of them a git URL with no registry behind it — and could drop something the image needs. rm -rf "$STAGE/node_modules/typescript" "$STAGE/node_modules/@types" 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//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=quay.io/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" < $OUT"