Files
images/images/node-agent/README.md
T
rmcguireandClaude Opus 5 1878df96ac
Lint / lint (push) Failing after 34s
Build Images / ${{ fromJSON(needs.select.outputs.images) }} (push) Canceled after 7m37s
Build Images / Select images (push) Successful in 17s
Initial commit: image registry structure and node-agent
Set up this repo as a base image registry. Each image is self-contained in
its own directory under images/ (Dockerfile + README.md + optional test.sh);
CI discovers them by glob, so adding an image needs no workflow changes.
template/ is the skeleton to copy.

The Makefile is the single entry point for both local work and CI, so a green
`make all` locally means a green pipeline.

Workflows:
  * lint.yaml  — layout check, hadolint, shellcheck
  * build.yaml — diffs against the base commit to build only the images that
    changed, smoke-tests each one before anything is published, then pushes.
    Releases are per-image tags (<image>/vX.Y.Z); main publishes :edge.

First image, node-agent: node:22-alpine plus a GNU userland (Alpine ships
BusyBox, whose applets take narrower flags than scripts and models expect),
helm, kubectl, jq, yq, bind-tools, curl, git, ripgrep, fd and friends.
Nothing is version-pinned — rebuilding is how upstream updates land, and the
published tag is what pins things for consumers.

Its smoke test asserts the deployment contract as well as tool presence: the
image must work non-root, with a read-only root filesystem and all
capabilities dropped, which is how ToolHive runs it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-09 11:40:29 -04:00

3.3 KiB

node-agent

A general-purpose command execution environment for AI agents: Node 22 on Alpine, with the shell, text, network and Kubernetes tooling an agent actually reaches for already installed.

gitea.libretechconsulting.com/rmcguire/node-agent

Alpine's stock userland is BusyBox, whose applets accept a narrower set of flags than the GNU tools most scripts and models assume. This image replaces the ones that matter with their GNU equivalents, so grep -P, sed -i, find -printf, sort -h and friends behave as expected.

Base: node:22-alpine · ~400 MB uncompressed (kubectl and helm are about 110 MB of that)

Contents

Group Tools
Shell bash, GNU coreutils, findutils, diffutils, gawk, grep, sed
Text & search jq, yq, ripgrep (rg), fd, less, file, patch, tree
Network curl, wget, dig/nslookup/host (bind-tools), nc, socat, rsync, ping, openssl
Kubernetes helm, kubectl
Source control git, git-lfs, ssh/scp/sftp
Archives tar, gzip, xz, zip, unzip
System ps/top/free (procps-ng), make, su-exec, tini, tzdata, ca-certificates
Runtime node, npm, npx (from the base image)

Versions track Alpine's repositories and upstream releases at build time — nothing is pinned. Run <tool> --version in the image for what a given tag actually shipped.

yq comes from upstream releases because Alpine does not package it; everything else is an Alpine package.

Environment

Variable Value Why
PAGER, GIT_PAGER cat git, kubectl and helm otherwise block on less when there is no TTY
GIT_TERMINAL_PROMPT 0 a remote needing credentials fails fast instead of hanging on a prompt
HELM_CACHE_HOME, HELM_CONFIG_HOME, HELM_DATA_HOME under /tmp lets helm run with a read-only root filesystem

ENTRYPOINT is tini -- docker-entrypoint.sh, so orphaned grandchildren of a shell command get reaped. CMD is bash.

Usage

docker run --rm -it gitea.libretechconsulting.com/rmcguire/node-agent:latest

The image is built to run locked down, and is tested that way — non-root, read-only root filesystem, all capabilities dropped:

docker run --rm -it \
  --user 1000:1000 --read-only --tmpfs /tmp --cap-drop ALL \
  -e HOME=/tmp \
  gitea.libretechconsulting.com/rmcguire/node-agent:latest

In Kubernetes, /tmp must be a writable volume; uid 1000 is the base image's node user. See toolhive/mcpserver-shell.yaml in the 50W/kube-manifests repo for the deployed example.

Notes

  • ping needs CAP_NET_RAW. With capabilities: drop: [ALL] it fails; dig, curl and nc are the alternatives that work without it.
  • No kubeconfig is baked in. kubectl and helm are present as tools; they only reach a cluster if credentials are mounted.
  • No shell completions. They are dead weight in a non-interactive environment.

Extending

The image does not set USER, so it builds as root just like its base — add what you need and let the deployment enforce non-root at runtime.

FROM gitea.libretechconsulting.com/rmcguire/node-agent:latest

# hadolint ignore=DL3018
RUN apk add --no-cache sops age