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
pingneedsCAP_NET_RAW. Withcapabilities: drop: [ALL]it fails;dig,curlandncare the alternatives that work without it.- No
kubeconfigis baked in.kubectlandhelmare 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