Files
images/images/node-agent
2026-08-10 17:12:47 -04:00
..
2026-08-10 17:12:47 -04:00
2026-08-10 17:06:34 -04:00
2026-08-10 17:06:34 -04:00

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, kubeconform (+ vendored schemas)
Linting yamllint, kubeconform
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 and kubeconform come from upstream releases because Alpine does not package them; everything else is an Alpine package.

Schemas for kubeconform

Both schema sets are vendored under $KUBECONFORM_SCHEMAS, because the pod that uses them runs with no network egress:

Subdirectory Source Size
builtin/master-standalone-strict/ yannh/kubernetes-json-schema ~62 MB / 1505
crds/<group>/ datree CRDs-catalog, only the groups the 50W cluster uses ~18 MB / 229
kubeconform -strict -ignore-missing-schemas \
    -schema-location "$KUBECONFORM_SCHEMAS/builtin/master-standalone-strict/{{.ResourceKind}}{{.KindSuffix}}.json" \
    -schema-location "$KUBECONFORM_SCHEMAS/crds/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json" \
    manifest.yaml

Do not add -schema-location default. It is a remote raw.githubusercontent.com URL, so with no egress it fails for every kind — ConfigMap included — returning a DNS error rather than a verdict on the manifest. That failure is invisible on a networked machine, which is why test.sh runs the schema checks in their own --network none container.

Why bother: a CRD kubeconform has no schema for is reported Skipped, not Invalid, so a corrupted Flux HelmRelease would otherwise sail through. Only the groups in use are kept; the full catalog is ~211 MB / 4177 schemas. The list lives in the Dockerfile — add a group there and rebuild.

Groups with no published schema anywhere (toolhive.stacklok.dev, kagent.dev, openclaw.rocks, inference.llmkube.dev) are absent by necessity, so resources of those kinds report as Skipped. Read the summary line — a skip is not a pass.

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
KUBECONFORM_SCHEMAS /usr/local/share/kubeconform root of the vendored builtin/ and crds/ schema trees, so consumers do not hardcode the path

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