208 lines
12 KiB
Docker
208 lines
12 KiB
Docker
# 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
|
|
# `-c` 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. Scripts
|
|
# piped on stdin do not cross that fallback and must choose their interpreter explicitly.
|
|
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; \
|
|
# The base bakes naros-keyring's hosted-repo entry (naros.sources) — the runtime
|
|
# apt channel. Set it aside for the whole build (here, and under playwright
|
|
# --with-deps below) so THIS stage installs only from the local pool + Node source
|
|
# above: the freshly built naros-tier-agent must resolve from this run's pool, not a
|
|
# same-version deb already published to the hosted channel. It also keeps the image
|
|
# build independent of the hosted repo — the same reason the base rootfs smoke test
|
|
# scopes sources.list.d away. Restored in the final stage so the image ships it.
|
|
mv /etc/apt/sources.list.d/naros.sources /tmp/naros.sources.disabled 2>/dev/null || true; \
|
|
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; \
|
|
# nash's parse fallback applies to `-c`, not scripts supplied on stdin.
|
|
# Upstream installers are outside the dogfood surface, so run them with the
|
|
# preserved Debian bash instead of feeding them to the diverted sh/bash.
|
|
curl -fsSL https://sh.rustup.rs | /usr/bin/bash.real -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). As with rustup, the
|
|
# stdin-fed upstream script needs the preserved real bash rather than diverted `bash`.
|
|
RUN curl -fsSL https://x.ai/cli/install.sh | /usr/bin/bash.real \
|
|
&& 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; \
|
|
# Restore the hosted runtime apt channel set aside at the start of the build.
|
|
mv /tmp/naros.sources.disabled /etc/apt/sources.list.d/naros.sources 2>/dev/null || true; \
|
|
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
|