Merge nucleic/eager-ancient-heron-p5em into dev
This commit is contained in:
@@ -26,6 +26,12 @@ packages/
|
||||
repo/
|
||||
publish.sh # dist/pool → apt tree (dists/<channel>/…), signs when a key is present
|
||||
r2-sync.sh # pushes the apt tree to Cloudflare R2 (CI; needs credentials)
|
||||
images/
|
||||
agent/ # naros-agent OCI layer FROM naros-base (Dockerfile + build.sh):
|
||||
# naros-tier-agent (apt), Node 22, rustup, Go, mise, warm
|
||||
# caches, agent user (uid 501), agent CLIs, Playwright
|
||||
tests/
|
||||
parity-sweep.sh # tool-inventory diff vs the previous default sandbox image
|
||||
dist/ # build output (gitignored): bin/, pool/, repo/, rootfs tars
|
||||
```
|
||||
|
||||
@@ -55,6 +61,15 @@ docker run --rm naros-base:test sh -c '. /etc/os-release && echo "$ID $VERSION_I
|
||||
Without `--pool`, the build produces a plain identity-only base (no Nucleic packages) —
|
||||
useful for validating the mmdebstrap/snapshot/identity plumbing in isolation.
|
||||
|
||||
Agent tier (needs docker; the agent tier is an OCI **layer** on naros-base, not a
|
||||
separate mmdebstrap run, so pulls dedupe on the shared base):
|
||||
|
||||
```sh
|
||||
docker import os/dist/naros-base-<ver>-arm64.tar naros-base:local
|
||||
os/images/agent/build.sh arm64 --base naros-base:local
|
||||
docker run --rm naros-agent:build-arm64 nash -lc 'naros info'
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
`VERSION` + `SNAPSHOT` define a release (NAROS.md §8). Channels: `edge` (weekly CI,
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
# naros-agent — the agent-experience tier (NAROS.md §4, §6; milestone N2).
|
||||
#
|
||||
# Built FROM the imported naros-base rootfs (mmdebstrap tar → docker import), so the base
|
||||
# layer — trixie snapshot, narOS identity, nash FORCED via dpkg-divert, naros-init, the
|
||||
# naros CLI — is exactly the naros-base image, and this file layers only the agent tier:
|
||||
# the apt half via the naros-tier-agent meta-package (build-essential, python3, the modern
|
||||
# CLI kit, nucleic-bridge), Node 22 (NodeSource), rustup + stable Rust, Go, mise, warm
|
||||
# shared caches under /opt/cache, the `agent` user, the agent CLIs, and Playwright with a
|
||||
# bundled Chromium. It replaces containers/nucleic-sandbox (last tag: v7) as the default
|
||||
# sandbox image — keep `ProjectSandbox.defaultImage` (Sources/NucleicCore/Project.swift)
|
||||
# pinned to a published tag of THIS image.
|
||||
#
|
||||
# Build via build.sh (assembles the context: this directory + the dist/pool apt pool).
|
||||
# NOTE: every RUN here executes under nash — /bin/sh is already diverted in naros-base.
|
||||
# That is deliberate dogfood (this build is part of nash's M3 validation surface); nash's
|
||||
# parse-failure fallback re-execs the preserved /usr/bin/bash.real, so a nash regression
|
||||
# degrades loudly in CI rather than silently corrupting the image.
|
||||
ARG BASE=naros-base:build
|
||||
FROM ${BASE}
|
||||
|
||||
# arm64 | amd64, set by buildx (or build.sh). Toolchain pins live here — bump them
|
||||
# together with os/VERSION so a release records one coherent toolchain set (§6.2); the
|
||||
# versions actually installed are recorded in /etc/naros/manifest.json either way.
|
||||
ARG TARGETARCH
|
||||
ARG NODE_MAJOR=22
|
||||
ARG GO_VERSION=1.26.5
|
||||
ARG GO_SHA256_AMD64=5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053
|
||||
ARG GO_SHA256_ARM64=fe4789e92b1f33358680864bbe8704289e7bb5fc207d80623c308935bd696d49
|
||||
ARG MISE_VERSION=v2026.7.7
|
||||
ARG MISE_SHA256_AMD64=0953810c2785eb4a75159f67f8b5721c4f3c80b8a6a812015d5af7d7fbd1b8a4
|
||||
ARG MISE_SHA256_ARM64=c4e542b53a15d2ec641e072f7b2d9da8a0554b92fd2c09a51febde32c6080ab8
|
||||
|
||||
# Warm shared caches at fixed world-readable paths (§6.3). Exported here so the BUILD's
|
||||
# own installs warm them; the runtime equivalent for agent shells is
|
||||
# /etc/profile.d/naros-env.sh (ContainerEngine ignores OCI env — nash -l sources
|
||||
# profile.d), and `naros info` records them in the manifest.
|
||||
ENV npm_config_cache=/opt/cache/npm \
|
||||
PIP_CACHE_DIR=/opt/cache/pip \
|
||||
UV_CACHE_DIR=/opt/cache/uv \
|
||||
CARGO_HOME=/opt/cache/cargo \
|
||||
RUSTUP_HOME=/opt/rustup \
|
||||
GOMODCACHE=/opt/cache/gomod \
|
||||
PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers \
|
||||
PATH=/opt/mise/shims:/opt/cache/cargo/bin:/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||||
|
||||
# The local apt pool (CI's just-built Nucleic packages) rides in only for this stage's
|
||||
# installs; the hosted signed repo (naros-keyring, already in the base) is the runtime
|
||||
# channel. The Debian half still resolves from the snapshot mirror already present in
|
||||
# /etc/apt/sources.list — same pin, same bytes, as the base build.
|
||||
COPY pool /tmp/naros-pool
|
||||
|
||||
# Node 22 first (NodeSource — narOS no longer inherits node from a base image): its
|
||||
# `nodejs` deb must be the one satisfying nucleic-bridge's Depends before the tier meta
|
||||
# resolves, or apt would pull trixie's older nodejs alongside it.
|
||||
RUN set -eu; \
|
||||
echo "deb [trusted=yes] copy:///tmp/naros-pool ./" > /etc/apt/sources.list.d/naros-pool.list; \
|
||||
mkdir -p -m 755 /etc/apt/keyrings; \
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
|
||||
-o /etc/apt/keyrings/nodesource.asc; \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" \
|
||||
> /etc/apt/sources.list.d/nodesource.list; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends nodejs; \
|
||||
apt-get install -y --no-install-recommends naros-tier-agent; \
|
||||
rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/naros-pool.list /tmp/naros-pool; \
|
||||
node --version | grep -q "^v${NODE_MAJOR}\."; \
|
||||
# Debian ships fd as fdfind; agents reach for `fd`.
|
||||
ln -sf /usr/bin/fdfind /usr/local/bin/fd
|
||||
|
||||
# Rust (rustup + stable, minimal profile) and Go (upstream tarball, pinned + checksummed).
|
||||
# CARGO_HOME doubles as the shared cargo cache/bin dir; RUSTUP_HOME holds toolchains.
|
||||
# Everything world-readable so the non-root agent uses them in place (§6.1: no /root
|
||||
# permission hacks — system paths or /opt by convention).
|
||||
RUN set -eu; \
|
||||
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal \
|
||||
--default-toolchain stable --no-modify-path; \
|
||||
"$CARGO_HOME/bin/rustc" --version; \
|
||||
case "$TARGETARCH" in \
|
||||
amd64) go_sha="$GO_SHA256_AMD64";; \
|
||||
arm64) go_sha="$GO_SHA256_ARM64";; \
|
||||
*) echo "unsupported TARGETARCH: $TARGETARCH" >&2; exit 1;; \
|
||||
esac; \
|
||||
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${TARGETARCH}.tar.gz" -o /tmp/go.tgz; \
|
||||
echo "$go_sha /tmp/go.tgz" | sha256sum -c -; \
|
||||
tar -C /usr/local -xzf /tmp/go.tgz; rm /tmp/go.tgz; \
|
||||
go version
|
||||
|
||||
# mise — the on-demand toolchain manager ("get me Java 21" without apt archaeology).
|
||||
# System-wide: binary in /usr/local/bin, data/shims under /opt/mise (world-writable like
|
||||
# the caches — per-session rootfs clones make that private copy-on-write state).
|
||||
RUN set -eu; \
|
||||
case "$TARGETARCH" in \
|
||||
amd64) mise_arch=x64; mise_sha="$MISE_SHA256_AMD64";; \
|
||||
arm64) mise_arch=arm64; mise_sha="$MISE_SHA256_ARM64";; \
|
||||
esac; \
|
||||
curl -fsSL "https://github.com/jdx/mise/releases/download/${MISE_VERSION}/mise-${MISE_VERSION}-linux-${mise_arch}.tar.gz" \
|
||||
-o /tmp/mise.tgz; \
|
||||
echo "$mise_sha /tmp/mise.tgz" | sha256sum -c -; \
|
||||
tar -C /tmp -xzf /tmp/mise.tgz; \
|
||||
install -m 0755 /tmp/mise/bin/mise /usr/local/bin/mise; \
|
||||
rm -rf /tmp/mise /tmp/mise.tgz; \
|
||||
mkdir -p /opt/mise/shims; \
|
||||
MISE_DATA_DIR=/opt/mise mise --version
|
||||
|
||||
# Runtime environment for agent shells: cache paths, toolchain PATH entries, mise
|
||||
# activation. nash -l sources /etc/profile.d, which is how every containerized exec
|
||||
# (ContainerEngine exec → nash -lc) sees this without OCI env.
|
||||
COPY files/etc/profile.d/naros-env.sh /etc/profile.d/naros-env.sh
|
||||
|
||||
# The `agent` user (§6.1): fixed uid 501 — the uid ContainerEngine execs as on a
|
||||
# default single-user Mac (getuid() of the first macOS user), so bind-mount ownership
|
||||
# and the baked passwd entry agree without seeding. Login shell nash; passwordless sudo
|
||||
# via a drop-in mirroring the Linux VM guest. (Asserted in CI smoke + a Swift test
|
||||
# against ContainerSpec.narosAgentUID — keep the three in lockstep.)
|
||||
COPY files/etc/sudoers.d/naros-agent /etc/sudoers.d/naros-agent
|
||||
RUN set -eu; \
|
||||
useradd --uid 501 --user-group --create-home --home-dir /home/agent \
|
||||
--shell /usr/local/bin/nash agent; \
|
||||
chmod 0440 /etc/sudoers.d/naros-agent; \
|
||||
visudo -cf /etc/sudoers.d/naros-agent; \
|
||||
chmod 0755 /home/agent
|
||||
|
||||
# Agent CLIs, pinned @latest at build so every rebuild ships the current releases (and
|
||||
# the models they unlock); they update in place via ContainerManager.updateAgentCLIs
|
||||
# between rebuilds. Deliberately npm/vendor installs, not debs (NAROS.md §3.1). The npm
|
||||
# installs run with the shared cache env above, warming /opt/cache/npm as a side effect.
|
||||
RUN npm install -g @anthropic-ai/claude-code@latest \
|
||||
&& npm install -g @openai/codex@latest \
|
||||
# yarn: the node:22 base image shipped it, so agents (and repos' packageManager
|
||||
# fields) expect it — parity-sweep guards it.
|
||||
&& npm install -g yarn \
|
||||
&& claude --version && codex --version && yarn --version
|
||||
|
||||
# xAI Grok Build via its official installer, relocated from the 0700 /root to a
|
||||
# world-traversable /opt and relinked onto PATH (same dance as nucleic-sandbox v4+ —
|
||||
# narOS keeps agent-reachable installs out of /root by convention).
|
||||
RUN curl -fsSL https://x.ai/cli/install.sh | bash \
|
||||
&& rm -f /usr/local/bin/grok /usr/local/bin/agent \
|
||||
&& mv /root/.grok /opt/grok \
|
||||
&& chmod -R a+rX /opt/grok \
|
||||
&& ln -s /opt/grok/bin/grok /usr/local/bin/grok \
|
||||
&& ln -s /opt/grok/bin/agent /usr/local/bin/agent \
|
||||
&& /opt/grok/bin/grok --version
|
||||
|
||||
# Headless browser tool — Playwright + bundled Chromium at the fixed world-readable
|
||||
# path, exactly as in nucleic-sandbox v7: any uid can launch it, and an agent's local
|
||||
# `npm install playwright` / `pip install playwright` reuses the browsers instead of
|
||||
# re-downloading. `--with-deps` apt-installs Chromium's shared libraries (root here).
|
||||
RUN npm install -g playwright@latest \
|
||||
&& mkdir -p /opt/playwright-browsers \
|
||||
&& playwright install --with-deps chromium \
|
||||
&& chmod -R a+rX /opt/playwright-browsers \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& playwright --version
|
||||
|
||||
# Warm the cargo sparse-index + a first crate into the shared cache (network-light
|
||||
# `cargo add serde` for agents), then open the caches to every uid: world-writable is
|
||||
# safe here because each session's container gets its own copy-on-write rootfs clone.
|
||||
RUN set -eu; \
|
||||
tmp="$(mktemp -d)"; cd "$tmp"; \
|
||||
cargo init -q --name warmcache .; \
|
||||
cargo add -q serde >/dev/null 2>&1; \
|
||||
cargo fetch -q; \
|
||||
cd /; rm -rf "$tmp"; \
|
||||
mkdir -p /opt/cache/npm /opt/cache/pip /opt/cache/uv /opt/cache/gomod; \
|
||||
chmod -R a+rwX /opt/cache /opt/mise; \
|
||||
chmod -R a+rX /opt/rustup /usr/local/go
|
||||
|
||||
# Stamp the tier: os-release VARIANT and the §6.3 capability manifest (tier, every baked
|
||||
# toolchain + version, cache paths) so `naros info` / the host probe read one file
|
||||
# instead of probing binary-by-binary.
|
||||
RUN set -eu; \
|
||||
sed -i -e 's/^VARIANT=.*/VARIANT="agent"/' -e 's/^VARIANT_ID=.*/VARIANT_ID=agent/' /etc/os-release; \
|
||||
node_v="$(node --version)"; \
|
||||
python_v="$(python3 -c 'import platform; print(platform.python_version())')"; \
|
||||
rust_v="$(rustc --version | awk '{print $2}')"; \
|
||||
go_v="$(go version | awk '{print $3}')"; \
|
||||
mise_v="$(mise --version 2>/dev/null | awk '{print $1}')"; \
|
||||
gcc_v="$(gcc -dumpfullversion)"; \
|
||||
pw_v="$(playwright --version | awk '{print $2}')"; \
|
||||
jq --arg node "$node_v" --arg python "$python_v" --arg rust "$rust_v" \
|
||||
--arg go "$go_v" --arg mise "$mise_v" --arg gcc "$gcc_v" --arg playwright "$pw_v" \
|
||||
'.tier = "agent"
|
||||
| .toolchains = {node: $node, python: $python, rust: $rust, go: $go, mise: $mise, gcc: $gcc}
|
||||
| .caches = {npm: "/opt/cache/npm", pip: "/opt/cache/pip", uv: "/opt/cache/uv",
|
||||
cargo: "/opt/cache/cargo", gomod: "/opt/cache/gomod"}
|
||||
| .playwright = {version: $playwright, browsers: "/opt/playwright-browsers", chromium: true}' \
|
||||
/etc/naros/manifest.json > /etc/naros/manifest.json.new; \
|
||||
mv /etc/naros/manifest.json.new /etc/naros/manifest.json; \
|
||||
naros info --json | jq -e '.tier == "agent" and .toolchains.node != null' > /dev/null
|
||||
|
||||
WORKDIR /workspace
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the naros-agent OCI image FROM an imported naros-base image (NAROS.md §4, N2).
|
||||
#
|
||||
# build.sh <arch> --base IMAGE[:TAG] [--tag IMAGE:TAG] [--pool DIR]
|
||||
#
|
||||
# Assembles a build context (this directory + the Nucleic apt pool) so the Docker build
|
||||
# context never drags in os/dist's rootfs tars, then runs a plain `docker build` for the
|
||||
# requested platform. CI calls this per arch in the rootfs matrix, right after importing
|
||||
# the base tar it just built; locally any imported naros-base works:
|
||||
#
|
||||
# docker import os/dist/naros-base-<ver>-arm64.tar naros-base:local
|
||||
# os/images/agent/build.sh arm64 --base naros-base:local
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
OS_DIR="$(cd "$HERE/../.." && pwd)"
|
||||
ARCH="${1:?usage: build.sh <arch> --base IMAGE [--tag IMAGE:TAG] [--pool DIR]}"
|
||||
shift
|
||||
|
||||
BASE="" TAG="naros-agent:build-$ARCH" POOL="$OS_DIR/dist/pool"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--base) BASE="$2"; shift 2 ;;
|
||||
--tag) TAG="$2"; shift 2 ;;
|
||||
--pool) POOL="$(cd "$2" && pwd)"; shift 2 ;;
|
||||
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$BASE" ] || { echo "--base IMAGE required (an imported naros-base)" >&2; exit 2; }
|
||||
[ -d "$POOL" ] || { echo "no apt pool at $POOL — run packages/build-all.sh first" >&2; exit 2; }
|
||||
|
||||
# The pool needs a flat-repo index for the in-build [trusted=yes] copy:// source; keep it
|
||||
# fresh the same way build-rootfs.sh does.
|
||||
if [ ! -f "$POOL/Packages" ] || [ -n "$(find "$POOL" -name '*.deb' -newer "$POOL/Packages" 2>/dev/null)" ]; then
|
||||
(cd "$POOL" && dpkg-scanpackages --multiversion . > Packages)
|
||||
fi
|
||||
|
||||
CTX="$(mktemp -d)"
|
||||
trap 'rm -rf "$CTX"' EXIT
|
||||
cp -a "$HERE/Dockerfile" "$HERE/files" "$CTX/"
|
||||
mkdir -p "$CTX/pool"
|
||||
cp -a "$POOL/." "$CTX/pool/"
|
||||
|
||||
DOCKER_BUILDKIT=1 docker build \
|
||||
--platform "linux/$ARCH" \
|
||||
--build-arg "BASE=$BASE" \
|
||||
--build-arg "TARGETARCH=$ARCH" \
|
||||
-t "$TAG" \
|
||||
"$CTX"
|
||||
|
||||
echo "built $TAG (linux/$ARCH FROM $BASE)"
|
||||
@@ -0,0 +1,26 @@
|
||||
# narOS agent-tier environment (NAROS.md §6.3), sourced by nash -l for every
|
||||
# containerized exec (ContainerEngine ignores OCI image env, so this file — not the
|
||||
# Dockerfile ENV — is what agent shells actually see).
|
||||
#
|
||||
# Warm shared caches at fixed world-readable paths. Pre-seeded at image build; the
|
||||
# per-session copy-on-write rootfs clone makes them private per container.
|
||||
export npm_config_cache=/opt/cache/npm
|
||||
export PIP_CACHE_DIR=/opt/cache/pip
|
||||
export UV_CACHE_DIR=/opt/cache/uv
|
||||
export CARGO_HOME=/opt/cache/cargo
|
||||
export RUSTUP_HOME=/opt/rustup
|
||||
export GOMODCACHE=/opt/cache/gomod
|
||||
export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers
|
||||
|
||||
# mise (§6.3): shims-on-PATH activation — works for non-interactive `nash -lc` scripts,
|
||||
# where the interactive `mise activate` hook would not run.
|
||||
export MISE_DATA_DIR=/opt/mise
|
||||
|
||||
# Baked toolchains (§6.2): cargo/rustup bins, Go, and the mise shims ahead of them all
|
||||
# so `mise use` versions win per-project. Idempotent (guarded) — profile.d can be
|
||||
# sourced more than once per session.
|
||||
case ":$PATH:" in
|
||||
*:/opt/mise/shims:*) ;;
|
||||
*) PATH="/opt/mise/shims:/opt/cache/cargo/bin:/usr/local/go/bin:$PATH" ;;
|
||||
esac
|
||||
export PATH
|
||||
@@ -0,0 +1,3 @@
|
||||
# narOS agent user (NAROS.md §6.1): frictionless escalation for `apt install` etc.,
|
||||
# mirroring the Linux VM guest's drop-in. The container is the isolation boundary.
|
||||
agent ALL=(ALL) NOPASSWD:ALL
|
||||
@@ -6,6 +6,12 @@
|
||||
set -eu
|
||||
R="$1"
|
||||
|
||||
# mmdebstrap writes every build source into the target's sources.list — including the
|
||||
# CI-local [trusted=yes] copy:// pool, which doesn't exist at runtime and would fail
|
||||
# every `apt update` in a running container. Keep only the real mirrors (the pinned
|
||||
# snapshot); the hosted Nucleic repo arrives via naros-keyring's sources.list.d entry.
|
||||
sed -i '\#copy://#d' "$R/etc/apt/sources.list"
|
||||
|
||||
rm -f "$R/etc/os-release"
|
||||
cat > "$R/etc/os-release" <<EOF
|
||||
NAME="narOS"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# naros-agent tier (NAROS.md §4, milestone N2 — placeholder until the agent tier lands).
|
||||
# Will carry: build-essential/pkg-config, python3 + pip/venv, the modern CLI kit, and the
|
||||
# hooks that add Node (NodeSource), rustup, Go, mise, warm caches, agent CLIs, Playwright.
|
||||
@@ -4,7 +4,7 @@ Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
Section: metapackages
|
||||
Priority: optional
|
||||
Depends: naros-tier-base, nucleic-bridge, build-essential, pkg-config, python3, python3-pip, python3-venv, ripgrep, fd-find, jq, sqlite3, htop, tree, zip, unzip, zstd, xz-utils, moreutils, rsync, less, procps, file, bsdextrautils
|
||||
Depends: naros-tier-base, nucleic-bridge, build-essential, pkg-config, python3, python3-pip, python3-venv, sudo, gh, ripgrep, fd-find, jq, yq, sqlite3, htop, tree, zip, unzip, zstd, xz-utils, moreutils, rsync, less, procps, file, bsdextrautils
|
||||
Description: narOS agent tier — apt-resolvable half (NAROS.md §4, §6)
|
||||
The dev toolchain and modern CLI kit that come from Debian, plus the control
|
||||
bridge. The non-apt half of the agent tier — Node (NodeSource), rustup, Go,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Parity-sweep allowlist (os/tests/parity-sweep.sh): names present in the reference
|
||||
# image (nucleic-sandbox:v7, FROM node:22-bookworm-slim) that naros-agent deliberately
|
||||
# does not carry. One name per line; every entry needs a "why".
|
||||
|
||||
# node:22 image plumbing, not a tool agents use.
|
||||
docker-entrypoint.sh
|
||||
|
||||
# The node:22 image ships node under /usr/local with npx/corepack symlinked there;
|
||||
# NodeSource's deb provides the same commands at /usr/bin — same names, so only
|
||||
# image-internal helper names should ever land here, not node/npm/npx/corepack.
|
||||
|
||||
# bookworm→trixie package renames/drops surface here as they are DISCOVERED and
|
||||
# understood in CI — do not pre-seed guesses.
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tool-inventory parity sweep (NAROS.md §10.3): nothing agents rely on may silently
|
||||
# vanish when the default sandbox image moves from nucleic-sandbox:v7 to naros-agent.
|
||||
#
|
||||
# parity-sweep.sh <candidate-image> <reference-image> [--allow FILE]
|
||||
#
|
||||
# Compares (a) the set of executables on PATH and (b) the npm global package set, in
|
||||
# each image. Anything present in the REFERENCE but missing from the CANDIDATE fails
|
||||
# the sweep unless listed in the allowlist (deliberate, understood removals — one name
|
||||
# per line, `#` comments). Additions are reported informationally. Python module parity
|
||||
# is not swept: neither image bakes site-packages beyond pip's own.
|
||||
#
|
||||
# Exit: 0 parity holds, 1 unexplained removals, 2 usage/docker errors.
|
||||
set -euo pipefail
|
||||
|
||||
CAND="${1:?usage: parity-sweep.sh <candidate-image> <reference-image> [--allow FILE]}"
|
||||
REF="${2:?missing reference image}"
|
||||
shift 2
|
||||
ALLOW="$(cd "$(dirname "$0")" && pwd)/parity-allowlist.txt"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--allow) ALLOW="$2"; shift 2 ;;
|
||||
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Every executable name reachable on the image's default PATH. --entrypoint /bin/sh is
|
||||
# nash in naros images and dash in the v7 base — the script is portable to both.
|
||||
path_inventory() {
|
||||
docker run --rm --entrypoint /bin/sh "$1" -c \
|
||||
'for d in $(echo "$PATH" | tr : " "); do ls -1 "$d" 2>/dev/null; done | sort -u'
|
||||
}
|
||||
|
||||
npm_inventory() {
|
||||
docker run --rm --entrypoint /bin/sh "$1" -c \
|
||||
'npm ls -g --depth=0 --parseable 2>/dev/null | tail -n +2' \
|
||||
| awk -F/ 'NF { if ($(NF-1) ~ /^@/) print $(NF-1)"/"$NF; else print $NF }' | sort -u
|
||||
}
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
echo "inventorying candidate $CAND …"
|
||||
path_inventory "$CAND" > "$tmp/cand.path"
|
||||
npm_inventory "$CAND" > "$tmp/cand.npm"
|
||||
echo "inventorying reference $REF …"
|
||||
path_inventory "$REF" > "$tmp/ref.path"
|
||||
npm_inventory "$REF" > "$tmp/ref.npm"
|
||||
|
||||
allow_filter() {
|
||||
if [ -f "$ALLOW" ]; then
|
||||
grep -vE '^\s*(#|$)' "$ALLOW" | grep -vxF -f /dev/stdin "$1" || true
|
||||
else
|
||||
cat "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
comm -23 "$tmp/ref.path" "$tmp/cand.path" > "$tmp/removed.path"
|
||||
comm -23 "$tmp/ref.npm" "$tmp/cand.npm" > "$tmp/removed.npm"
|
||||
added_path=$(comm -13 "$tmp/ref.path" "$tmp/cand.path" | wc -l)
|
||||
|
||||
cat "$tmp/removed.path" "$tmp/removed.npm" | sort -u > "$tmp/removed.all"
|
||||
allow_filter "$tmp/removed.all" > "$tmp/unexplained" || true
|
||||
|
||||
echo "PATH: $(wc -l < "$tmp/ref.path") reference, $(wc -l < "$tmp/cand.path") candidate," \
|
||||
"$(wc -l < "$tmp/removed.path") removed, $added_path added"
|
||||
echo "npm globals removed: $(wc -l < "$tmp/removed.npm")"
|
||||
if [ -s "$tmp/removed.all" ]; then
|
||||
echo "--- removed (before allowlist) ---"
|
||||
cat "$tmp/removed.all"
|
||||
fi
|
||||
|
||||
if [ -s "$tmp/unexplained" ]; then
|
||||
echo "PARITY FAILURE — present in $REF, missing from $CAND, not allowlisted:" >&2
|
||||
cat "$tmp/unexplained" >&2
|
||||
echo "(fix the image, or add to $ALLOW with a comment explaining the removal)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "parity OK"
|
||||
Reference in New Issue
Block a user