38 KiB
narOS — the Nucleic Agent Runtime OS
Status: N2 implemented (image layer + host-side swap), pending its first CI build +
the M3 dogfood gates; N3 (runner) implemented in-tree, pending the same CI sequencing.
N3 state: containers/nucleic-runner/Dockerfile is rebased FROM ghcr.io/abkslm/naros-agent:${NAROS_VERSION} (default = os/VERSION, amd64 slice) — it
keeps the nucleicd COPY/ENTRYPOINT + WITH_TAILSCALE arg (tailscale suite bumped
bookworm→trixie; the not-yet-live naros.sources hosted-repo entry is parked around its
apt-get update), and stamps the runner tier directly (VARIANT_ID=runner + manifest
.tier, since the naros-tier-runner meta needs the hosted repo). runner-image.yml now
preflights BOTH the NAROS_VERSION ↔ os/VERSION lockstep and the published-base
availability (fails fast with the N2 sequencing message when naros.yml hasn't pushed the
tag yet); a Swift test (narosVersionLockstepAcrossPins) holds os/VERSION,
Project.swift's pin, and the runner ARG in lockstep locally. NASH ride-along
(NASH.md §7.3): on a narOS host (/etc/os-release ID=naros —
CommandInterceptor.hostIsNarOS), host-run Claude sessions (RunSpec.container = nil,
exactly the runner's mode) now register the git/gh/command/shell report routes and wire
hookEnv at the loopback approval server, so nash — already /bin/sh via the base divert
— streams exec/data-flow events into the feed with no shim seeding; git/gh observation
returns via observeShellEvent's argv convergence. Codex/Grok host runs still start no
approval server (observation there is a follow-up). Remaining for N3 exit: naros.yml green
→ rerun runner-image.yml (preflight enforces the order) → Cloudflare push + Worker roll →
a Covalence session end-to-end on narOS with the amd64 event stream in the feed.
Prior status (N2): the agent tier now exists end-to-end in-tree: os/images/agent/
builds naros-agent as an OCI layer FROM the imported naros-base (naros-tier-agent
meta = the Debian half incl. sudo/gh/yq; NodeSource Node 22; rustup + stable; Go 1.26.5
pinned+checksummed; mise; warm caches under /opt/cache wired via
/etc/profile.d/naros-env.sh; agent uid 501 + NOPASSWD sudoers; claude/codex/grok/yarn
CLIs; Playwright+Chromium; manifest/VARIANT stamped) — deliberate dogfood: every RUN
executes under the diverted nash. CI (naros.yml) builds/smokes/pushes it per arch and
gates on os/tests/parity-sweep.sh vs nucleic-sandbox:v7 (allowlist:
os/tests/parity-allowlist.txt, expected to need iteration on the first runs). Host side
is swapped: ProjectSandbox.defaultImage → ghcr.io/abkslm/naros-agent:26.07
(legacyImage = v7 kept as the image rollback), PID 1 prefers /usr/sbin/naros-init
(NAROS_BRIDGE=1; sleep-loop fallback for non-narOS images), the bridge probe fast-paths
on ID=naros + agent/runner VARIANT_ID, seeded passwd entries get nash where present,
and the legacyShell rollback lever is live
(ContainerServiceSettings.legacyShell: execs prefer bash.real + every hook env
carries NUCLEIC_NASH_DISABLE=1). Also fixed: the base rootfs no longer keeps the
build-time copy:// pool in sources.list (runtime apt update was broken). Validated
locally: package layer builds, full Swift build green, ContainerSandboxTests 38/38 (new:
argv lever, hook-env lever, uid lockstep, pin). Sequencing caution: the app's default
image pin now points at naros-agent:26.07, which exists only after naros.yml runs
green on dev — land/CI-build this branch before cutting any app build from it.
Remaining for N2 exit: first agent-tier CI run green (parity allowlist iteration
included), then the nash M3 gates — ≥99% corpus parity in-image, <3% overhead, one
clean dogfood week — plus the still-open N0/N1 operator infra (apt signing key + R2
bucket + apt domain).
Prior status (N0–N1, built and validated end-to-end, arm64 local): The os/ build tree
works for real: nash 0.4.0 + naros-init compiled musl-static from shell/, all nine
buildable packages produced as .debs, the apt tree published
(dists/edge + per-arch indexes), and mmdebstrap assembled a 289 MB naros-base
rootfs from the pinned trixie snapshot + pool in 127 s — smoke-tested in chroot:
ID=naros, /bin/{sh,bash,dash} → nash with .real preserved, NUCLEIC_NASH=1 under
/bin/sh, naros version/info green, every package configured. Two findings baked
into the build: the divert must be the image's final configure step (apt's configure
order otherwise runs remaining Debian postinsts under nash — hence
profiles/<tier>.late-pkgs + the dpkg-i finish hook), and a real nash byte-mangling
divergence (D2, shell/corpus/DIVERGENCES.md: non-ASCII bytes re-encoded through
read — found when ca-certificates' postinst broke). D2 is fixed in the fork
(incremental UTF-8 decode in the read builtin; corpus now 97/97 incl. three new
utf8-* cases; compat-suite failure set identical to pristine baseline;
update-ca-certificates green under nash). Remaining for N0/N1 exit — operator
infra only: first CI run green (incl. amd64), apt signing key + repo secrets, R2
bucket + apt domain provisioned. narOS ("nucleic agent runtime OS") is a Debian-derived Linux
distribution purpose-built as the home for Nucleic's agents: the OS inside sandbox
containers, the cloud runner, agent-created linux_containers, and (in a later phase) the
Linux VM guest. It ships nash (NASH.md) as the forced default shell, a
first-class agent user, a curated dev toolchain, and a Nucleic-owned package layer — and
it replaces today's arrangement where each surface is a differently-decorated third-party
base image (node:22-bookworm-slim + Dockerfile layers, plus a separately-provisioned
Ubuntu VM guest).
Related docs: NASH.md (the shell narOS forces; narOS v1 is the vehicle for its M3 milestone), CONTAINER_ISOLATION.md, VSOCK_CONTROL_PLANE.md, LINUX_VM.md, COVALENCE_RUNNER.md / CLOUD_RUNTIME.md (runner image consumers), OBSERVABILITY_AND_TESTING.md.
Locked decisions
| Decision | Choice |
|---|---|
| Lineage | Debian-derived, from trixie (Debian 13) — apt-native, keeps NASH.md's dpkg-divert forcing design, fresh base with long runway |
| Scope | All Linux surfaces — sandbox image, runner image, linux_container default, and the Linux VM guest (VM is a later phase, §7.4) |
| VM desktop GNOME | GNOME 50 forced — the naros-vm desktop flavor ships GNOME 50/Mutter (the semantic agent's target), sourced from a pinned Debian forky/testing snapshot, with a self-built naros-desktop backport pocket as fallback (§7.4) |
| Mutability | Fully mutable — agents keep root-capable apt + language package managers; Nucleic-critical paths protected by diverts/holds, not immutability |
| Rootfs build | mmdebstrap against a pinned snapshot.debian.org date + Nucleic apt overlay — a real distro build, not a Dockerfile on someone else's base |
| Init | Custom naros-init (small Rust supervisor) on every container surface; VM desktop flavor keeps systemd with naros-init as a supervised role unit (§5) |
| Image family | Tiered: naros-base → naros-agent → naros-runner, plus naros-vm bootable artifacts (§4) |
| Own components | Signed Nucleic apt repository — nash, naros-init, bridge, agents, keyring, metas as real .debs; boot-time seed override remains the fast channel (§3) |
| nash timing | narOS v1 = nash M3 vehicle — first release forces nash (/bin/sh, /bin/bash, $SHELL diverted) per NASH.md §7.1; M3 gates become narOS v1 ship gates |
| Versioning | Date-based + channels — naros 26.MM from a pinned snapshot date; stable (app-pinned) and edge (weekly dogfood) channels (§8) |
| User model | agent user + passwordless sudo — fixed uid, home /home/agent, login shell nash, NOPASSWD sudoers drop-in (§6.1) |
| Agent UX layer | All of: modern CLI kit, warm shared caches, naros introspection surface, mise toolchain manager (§6) |
| Baked toolchains | Node + Python + Rust + Go (JDK and others on demand via mise/apt) (§6.2) |
| apt repo hosting | Cloudflare R2 + custom domain (static apt tree, CI-published, signed; public key ships as naros-keyring) (§3.2) |
| Code home | In-repo under os/ — mmdebstrap profiles, package sources, image recipes, publishing tooling beside shell/ and containers/ |
1. Goals and non-goals
Goals
- One OS identity across every Linux surface Nucleic controls.
/etc/os-releasesaysID=naroseverywhere an agent lands; same package set, same defaults, same release train. The sandbox image, runner image,linux_containerdefault, and (phase 2) the VM guest stop being four separately-maintained decorations of third-party bases. - nash by construction. narOS ships nash installed and forced (divert of
/bin/sh//bin/bash//bin/dash,$SHELL,.realpreservation) as part of the distro build — not as a patch applied to someone else's image. Installing narOS is completing NASH.md M3. - The best possible home for an agent. Zero-turn availability of the tools agents constantly reach for; warm caches so installs are fast; a first-class non-root user with frictionless escalation; machine-readable introspection of what the box can do; a toolchain manager for anything not baked in. An agent should almost never burn turns on environment yak-shaving.
- A real distro spine. Reproducible-ish rootfs builds pinned to a Debian snapshot date, our own signed apt repository for Nucleic components, tiered images, channels, and a single version constant replacing today's four-place lockstep tag bump.
- Drop-in adoption. Each surface adopts narOS by swapping an image reference (or, for the VM, a rootfs artifact) — no protocol changes to the control plane, approval server, shims, or seeding paths.
Non-goals
- The macOS VM and the user's real Mac. narOS is Linux-only; macOS surfaces follow NASH.md §7.5 independently.
- A general-purpose public distribution (in v1). narOS is built for Nucleic's agent surfaces; publishing it more broadly is a possible later story, not a requirement.
- Building our own kernel. Containers use the host-provided kernel
(
third_party/containerization); the VM continues to use the externally-sourced kernel/initrd flow of LINUX_VM.md. narOS owns the rootfs. - Replacing the container engine or isolation model. narOS is the contents of the rootfs, not the runtime around it (CONTAINER_ISOLATION.md unchanged).
- Immutability / image-based OS updates. Locked: fully mutable. Agents may
apt installanything; resets come from the container/VM lifecycle (fresh rootfs per session), not from an ostree-style base.
2. Base and build system
2.1 Debian trixie, snapshot-pinned
narOS derives from Debian 13 (trixie). Rationale: apt is what agents know; the entire
nash forcing design (NASH.md §7.1) is built on dpkg-divert; trixie is the current stable
with the longest runway (gcc 14, Python 3.13) and adopting it now folds the
bookworm→trixie migration into narOS validation (corpus replay + dogfood) instead of a
separate future flag day. (The one deliberate exception: the VM desktop flavor's GNOME
stack, which is forced to GNOME 50 and sourced newer than trixie — §7.4.)
Every narOS release builds against a pinned snapshot.debian.org date. The pin is the
release's identity: two builds of naros 26.07 see byte-identical Debian package lists.
Security updates arrive by cutting a new release with a newer snapshot date (cheap — CI
does it for edge weekly), never by silent base drift.
2.2 mmdebstrap rootfs build
The rootfs is built from scratch with mmdebstrap (runs unprivileged in CI, fast, reproducible-friendly):
mmdebstrap --variant=minbaseagainst the pinned snapshot mirror + the Nucleic apt repo (§3.2), installing the tier's package list (§4).- Post-build hooks apply narOS identity (§2.3), the nash divert block, the
agentuser, profile.d environment, and the capability manifest (§6.3). - The resulting rootfs is emitted two ways:
- OCI image (via
mmdebstrap's tar →skopeo/cranepush): per-arch images + multi-arch index to GHCR (ghcr.io/abkslm/naros-{base,agent,runner}), exactly whereContainerEngineand the Cloudflare push pipeline already pull from. - Rootfs tarball artifact for the VM two-phase build (§7.4), replacing the Ubuntu cloud tarball download.
- OCI image (via
Architectures: arm64 + amd64 for all container tiers (Apple-silicon sandboxes +
Cloudflare's amd64 runners — the same dual-arch requirement that forced sandbox v6);
arm64 only for naros-vm (Virtualization.framework guests).
2.3 Identity
/etc/os-release:ID=naros,ID_LIKE=debian,NAME="narOS",PRETTY_NAME="narOS 26.07 (Nucleic Agent Runtime OS)",VERSION_ID,VARIANT(base/agent/runner/vm),HOME_URL. Debian's own release files remain intact so tooling that sniffsID_LIKEkeeps working./etc/naros/— distro-owned config root:manifest.json(§6.3),channel,snapshot-date.- A short, useful MOTD/issue for interactive debugging (which images, versions, where the
manifest lives) — informational only; agents get the same via
naros info.
Kernel identity (naros-identity)
The files above are only found by a caller that knows to read them. Anything deriving an OS
string from uname(2) — including the OS Version: line agent harnesses put in their
environment block, which is os.type() + os.release() — sees the host kernel and no
narOS at all. On Cloudflare Containers that surfaces as Linux 6.x-cloudflare-firecracker,
which is what the runner reports to a Claude agent.
narOS builds no kernel to fix this at the source: containers share the host's, and the VM
tiers boot an externally-fetched vmlinux (scripts/fetch-kernel.sh), so there is no
CONFIG_LOCALVERSION to set. Instead naros-identity ships
/usr/lib/naros/libnaros-uname.so, registered in /etc/ld.so.preload by its postinst, and
appends a release tag the way a distro kernel package does — Debian's own kernels report
6.1.0-18-amd64:
$ uname -sr
Linux 6.18.15-naros26.07
The tag is recorded as kernel_tag in manifest.json so a reader can tell it from the
host kernel's own version and strip it.
Deliberately narrow, and the constraints are the design:
sysnamestaysLinux. It is the most-switched-onunamefield in build tooling —config.guess,CMAKE_SYSTEM_NAME, node-gyp, the Go and rustup installers all compare it againstLinux. Onlyreleaseis tagged;versionis untouched.NAROS_UNAME_PASSTHROUGH=1returns the kernel's answer verbatim. Required because/lib/modules/$(uname -r)is a real path that kmod, depmod and udev resolve through.naros-identity-modules.serviceuses it at boot to symlink the tagged name onto the real module directory, so autoloading works on the VM tiers without per-caller plumbing. Inert on container tiers, which run naros-init as PID 1 and load no modules.- Static binaries and Go programs that issue the raw syscall bypass it. The tag is an identity marker, never a security or correctness boundary.
Because a bad .so in /etc/ld.so.preload would make the loader warn on every exec, the
base and agent smoke tests (§10) assert the tag, the untouched sysname, and the
passthrough — and every other command in those tests runs under the preload already.
3. The Nucleic package layer
3.1 Packages
Nucleic components become real Debian packages, built in CI from in-repo sources
(os/packages/*/debian/):
| Package | Contents | Source |
|---|---|---|
nash |
/usr/local/bin/nash (static musl, per-arch) |
shell/nash (existing build) |
nash-default-shell |
The NASH.md §7.1 divert block as maintainer scripts: divert bash/dash/sh → .real, symlink to nash, set NUCLEIC_REAL_BASH; prerm cleanly reverts. Depends: nash |
new |
naros-init |
PID-1 supervisor (§5) | new crate, shell/naros-init |
naros (CLI) |
naros info/version introspection tool (§6.3); may ship as a multi-call binary with naros-init |
new |
naros-identity |
uname(2) release-tag interposer + /etc/ld.so.preload registration and the /lib/modules alias unit (§2.3) — the one artifact in os/ that is a dynamic glibc .so, since it must interpose glibc processes |
os/src/naros-identity |
nucleic-bridge |
/opt/nucleic/control-bridge.js (Depends: nodejs) |
containers/nucleic-sandbox/control-bridge.js |
nucleic-linux-agent |
the vsock exec agent (VM tier) | guest/nucleic-linux-agent |
naros-keyring |
apt signing public key + sources.list.d entry | new |
naros-tier-base / -agent / -runner / -vm |
meta-packages pinning each tier's package list (§4) — the tier definitions live in debian/control, not in image scripts |
new |
Deliberately not packaged: the agent CLIs (@anthropic-ai/claude-code,
@openai/codex, grok) stay npm/vendor installs in the naros-agent image layer, pinned
@latest at build — they update independently via image rebuilds and the existing
in-place updater (ContainerManager.updateAgentCLIs), and wrapping fast-moving
third-party installers in debs would add lag for no control. nucleicd also stays a
direct image COPY in the runner tier (it's our binary, versioned with the app, built by
scripts/build-nucleicd-linux.sh — a deb adds ceremony without benefit).
The git/gh shims remain host-seeded per session (they carry per-session tokens/config) — narOS guarantees their prerequisites (node on PATH, seed paths writable) but does not bake them. The NASH.md §8 boot-time seed override likewise continues unchanged as the fast channel for nash fixes; the deb is the deterministic baseline being overridden.
3.2 The apt repository
A static, signed apt tree published to Cloudflare R2 behind a custom domain
(apt.naros.nucleic.blakeslee.xyz), fronted by Cloudflare's CDN:
- CI builds
.debs →aptly/repreprogenerates the pool +InRelease(signed with a CI-held key; public half ships innaros-keyringand is embedded in the mmdebstrap profile for bootstrap trust) → synced to R2. - Suites mirror channels:
naros stableandnaros edge(§8). - Image builds install from it; running systems can
apt upgradeNucleic components in place — the third update channel alongside image releases and boot-time seeding. - Until the domain is live, CI falls back to a
file://local pool during image builds so N-milestones aren't blocked on DNS/hosting (§9, N1).
4. Image tiers
naros-base minimal trixie rootfs + naros identity
+ nash, nash-default-shell ← the forced shell (NASH.md §7.1 lives HERE)
+ naros-init, naros CLI, naros-keyring
+ ca-certificates, curl, git, openssh-client, iproute2
naros-agent (FROM naros-base; replaces containers/nucleic-sandbox)
+ build-essential, pkg-config, python3/pip/venv
+ Node 22 (NodeSource), rustup+cargo, Go (§6.2)
+ modern CLI kit, mise, warm caches (§6)
+ nucleic-bridge
+ claude-code / codex / grok CLIs (npm/vendor layers, as today)
+ Playwright + Chromium at /opt/playwright-browsers (as today)
+ `agent` user + sudoers drop-in (§6.1)
naros-runner (FROM naros-agent; replaces containers/nucleic-runner)
+ nucleicd (COPY), NUCLEIC_RUNNER_DATA_DIR, optional tailscale (WITH_TAILSCALE arg)
ENTRYPOINT unchanged: nucleicd
naros-vm (artifact set, not an OCI image — §7.4)
naros-base rootfs tarball + naros-tier-vm (nucleic-linux-agent, systemd,
firstboot provisioning, desktop stack in the desktop flavor)
naros-base exists so linux_container has a light default and so the divert + identity
layer is testable in isolation, but naros-agent is the default image everywhere an
agent works (sandbox sessions, linux_container default), exactly as nucleic-sandbox
is today. Tiers are OCI layers (agent FROM base), so pulls dedupe.
5. naros-init
A small Rust binary (new crate in the shell/ cargo workspace, sharing its musl
static-build CI) that becomes the uniform Nucleic boot story:
- Containers (PID 1):
ContainerEnginetoday keeps sandboxes alive with ash -csleep loop (ContainerEngine.swift:547) and launches the control bridge conditionally. naros-init replaces that: proper zombie reaping, signal forwarding, and supervision of the in-container services (control bridge when a control socket is relayed in; future spool drains). It reads the same env contract the engine already injects and requires no new host↔guest protocol — the engine just execs/usr/sbin/naros-initinstead of the sleep loop. The runner keepsnucleicdas its entrypoint (nucleicd is already a competent PID 1 for that role); adopting naros-init there is optional later. - VM: the desktop flavor realistically requires systemd (GNOME/gdm/logind/dbus are
hard-wired to it), so there systemd stays PID 1 and naros-init runs as a systemd
unit owning the Nucleic role duties: launch
nucleic-linux-agent, apply boot-time seed overrides, drain the nash spool (/var/spool/nucleic-nash, NASH.md §6.2), write readiness. A future headless VM flavor (exec-only, no GUI) can run naros-init as true PID 1. - Behavior is role-driven (
/etc/naros/roleor env), so one binary serves every surface; it is deliberately boring — supervision and plumbing only, no policy.
6. The agent-experience layer
What makes narOS a home rather than a base image. All of it lives in the naros-agent
tier.
6.1 The agent user
- Fixed identity baked into the distro: user
agent, a stable uid/gid chosen to match whatContainerEngine+Rootfs.swiftseeds today (one constant, asserted by a test so host seeding and the image can never disagree), home/home/agent, login shell/usr/local/bin/nash. /etc/sudoers.d/naros-agent:agent ALL=(ALL) NOPASSWD:ALL— mirroring the Linux VM's existing drop-in. Agents escalate forapt installwithout running everything as root, and the class of /root-permission hacks (see the Grok relocation dance in the current Dockerfile) is designed out: anything installed for the agent installs to system paths or/opt, world-readable, by build-time convention./etc/passwdseeding inContainerEngine+Rootfs.swift:184simplifies to matching what the image already contains (and per NASH.md §7.2 the seeded shell becomes nash).
6.2 Baked toolchains
Node 22 (NodeSource apt repo — narOS no longer inherits node from a base image), Python
3.13 (trixie) + pip/venv, rustup + stable Rust, Go (upstream tarball, pinned) —
plus build-essential, pkg-config. Versions are pinned in the tier meta/build recipe
and recorded in the manifest (§6.3). Everything else (JDK, .NET, Ruby, Zig, …) is
on-demand via mise or apt.
6.3 mise, CLI kit, caches, introspection
- mise preinstalled and preconfigured system-wide (
MISE_DATA_DIR=/opt/mise, world-readable, activated in profile.d for nash-l): "get me Java 21" ismise use java@21— one line, no apt archaeology, per-project versions. - Modern CLI kit (apt, from the snapshot):
ripgrep fd-find jq yq sqlite3 htop tree zip unzip zstd xz-utils moreutils rsync less procps file bsdextrautils(+fdsymlinked from Debian'sfdfind). The tools agents reach for every session, present on turn zero. - Warm shared caches at fixed world-readable paths, wired via
/etc/profile.d/naros-env.sh:npm_config_cache=/opt/cache/npm,PIP_CACHE_DIR=/opt/cache/pip,UV_CACHE_DIR=/opt/cache/uv,CARGO_HOME=/opt/cache/cargo(registry index pre-fetched),GOMODCACHE=/opt/cache/gomod,PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers(unchanged). Build-time pre-seeding keeps common installs network-light; per-session container rootfs cloning makes the caches copy-on-write private per session. - Introspection:
/etc/naros/manifest.json— generated at image build; lists tier, version/channel/snapshot-date, arch, every baked toolchain + version, cache paths, cap abilities (playwright: yes, nash: version, bridge: version).naros info [--json]prints it. Agent CLIs' system prompts (and the host, e.g. for the §7.6 custom-image probe) can consume one file instead of probing binary-by-binary.
7. Per-surface adoption
7.1 Sandbox containers
ghcr.io/abkslm/naros-agent:<stable> replaces nucleic-sandbox:v7 as
ProjectSandbox.defaultImage (Sources/NucleicCore/Project.swift:119). Because narOS v1
bakes the nash divert, this single image swap executes NASH.md §7.1 — the v8-lockstep
step of nash M3 becomes "bump to naros-agent". The Swift-side changes are exactly NASH.md
§7.2 (exec argv → nash, passwd seed → nash/agent, hookEnv already done in M1) plus the
PID-1 swap to naros-init (§5). containers/nucleic-sandbox/ is retired in favor of
os/images/agent/.
7.2 Runner
containers/nucleic-runner/Dockerfile rebases FROM naros-agent (amd64 slice), keeping
its nucleicd COPY/ENTRYPOINT and the WITH_TAILSCALE arg; the Cloudflare
managed-registry push pipeline (runner-image.yml) is unchanged. The runner inherits
nash, naros identity, and the agent-UX layer with zero further work.
Implemented (N3): the base pin is ARG NAROS_VERSION (default = os/VERSION; the §8
lockstep is asserted by a runner-image.yml preflight AND a Swift test), the image stamps
VARIANT_ID=runner + manifest .tier = "runner" directly (the naros-tier-runner meta
waits on the hosted repo), and the preflight also verifies the base tag is published with
an amd64 slice before the expensive build — encoding the "naros.yml first" sequencing.
Host-side, nucleicd detects its narOS home via CommandInterceptor.hostIsNarOS and wires
nash observation for host-run sessions (see NASH.md §7.3).
7.3 linux_container
Default image → naros-agent. The custom-image probe (NASH.md §7.6,
ContainerEngine+Rootfs.swift:200-207) gains a cheap first check: if
/etc/os-release says ID=naros, everything (nash, bridge, node) is known-present —
probe done. Non-narOS custom images keep the existing degrade path, and gain a new
upgrade path: apt install naros-keyring && apt install naros-tier-agent converts any
Debian-family container into a narOS-equivalent environment.
7.4 Linux VM (phase 2, flavored)
The two-phase in-guest build (LINUX_VM.md) keeps its mechanics — bootstrap
boot, payload disk, firstboot provisioning — but the payload rootfs tarball becomes
narOS and provision-linux-guest.sh shrinks toward apt install naros-tier-vm (the
meta-package encodes what the script hand-installs today: agent, sudoers, toolchain,
desktop stack, AT-SPI switches, Firefox-from-Mozilla-apt).
GNOME 50 is forced (locked). The semantic agent targets GNOME 50/Mutter (LINUX_VM_SEMANTIC_AGENT.md) — it's why the current guest pins Ubuntu 26.04 — and trixie only ships GNOME 48, so the narOS desktop flavor does not wait for Debian stable to catch up; it ships GNOME 50 from day one and the Ubuntu pin retires on the narOS schedule, not GNOME's migration schedule. Sourcing, in order of preference:
- Primary — pinned forky/testing snapshot for the desktop flavor. The naros-vm
desktop rootfs builds from a pinned
snapshot.debian.orgdate of Debian forky (testing) whose GNOME stack is at 50 (mutter, gnome-shell, gjs, gtk4, libadwaita, gdm3, at-spi). Same reproducibility discipline as trixie builds — a testing snapshot is frozen, so "testing churn" doesn't apply within a release; churn is absorbed deliberately at each new snapshot pin. The whole desktop rootfs comes from one coherent pocket (no trixie/forky ABI mixing — pulling GNOME alone into trixie would drag glibc and half the library stack anyway). - Fallback —
naros-desktopbackport pocket. If the pinned-date forky GNOME stack is not yet fully at 50 (or a forky freeze stalls it), CI rebuilds the GNOME 50 stack (from Debian experimental / Ubuntu 26.04 sources) into a dedicatednaros-desktopcomponent of our apt repo, layered onto the forky base. This is a maintenance cost we accept only for the delta packages, only until the primary path covers them.
Flavor split, restated:
- Headless flavor (exec-only guests): pure trixie, identical base to the container tiers — lands first (N4).
- Desktop flavor: forky-snapshot base + GNOME 50 as above (N5). The container tiers are never affected by the forky pocket — trixie stays their base. Divergence between the two flavors is limited to the base snapshot; the Nucleic layer (nash, naros-init, agent, tier metas, identity) is the same packages from the same repo.
Semantic-agent validation (AT-SPI tree parity, mutter-geometry-helper against forky's
Mutter build) moves from being a gate on shipping to being N5's acceptance test.
8. Versioning, channels, release engineering
- Version:
naros 26.MM[.p]— year.month of the release, patch for rebuilds inside a month. Each release records its snapshot date and the versions of every Nucleic package in the manifest. Nucleic packages carry their own semver independently. - Channels:
stable— whatProject.swiftpins; moves deliberately, at milestone quality gates, with the M3-style checklist.edge— CI-built weekly (fresh snapshot date, latest Nucleic packages, latest agent CLIs) for dogfood sessions; promotion to stable is a tag, not a rebuild. - The lockstep collapses: today's four-place
v7→v8bump (Dockerfile, workflowIMAGE_TAG,Project.swift, runner base pin) becomes one version constant inos/VERSIONpropagated by CI, with a CI assertion thatProject.swift's pin matches a published stable tag (extending the existing tag-lockstep check in NASH.md §12). - CI (
.github/workflows/naros.yml): package builds (reusingnash.ymlartifacts) → apt repo publish to R2 → mmdebstrap per tier × arch → OCI push to GHCR + VM tarball artifact → test suites (§10). Runner image CI continues inrunner-image.yml, now FROM naros-agent.
Repo layout
os/
VERSION # single source of truth for the release version (26.MM)
SNAPSHOT # pinned snapshot.debian.org timestamp for this release
README.md # build-tree guide
mkimage/
build-rootfs.sh # mmdebstrap driver: <tier> <arch> [--pool] → rootfs tar
hooks/00-identity.sh # /etc/os-release (ID=naros) + /etc/naros/{manifest,…}
profiles/<tier>.pkgs # Debian package list per tier
profiles/<tier>.naros-pkgs # Nucleic packages per tier (from the pool)
packages/ # one dir per package: control (+files/, stage.sh,
build-all.sh # maintainer scripts); built with dpkg-deb into dist/pool
src/naros-identity/ # uname(2) release-tag interposer (§2.3) + its boot unit;
# build.sh → dist/bin/libnaros-uname-<arch>.so
repo/
publish.sh # pool → apt tree (dists/<channel>), signs when keyed
r2-sync.sh # apt tree → Cloudflare R2 (apt.naros.nucleic.blakeslee.xyz)
images/ # (N2) per-tier extras: agent CLI layer, playwright layer,
# runner Dockerfile once rebased
shell/naros-init/ # init crate, in the existing cargo workspace
.github/workflows/naros.yml # binaries → debs → repo → rootfs → smoke → GHCR index
docs/NAROS.md # this doc
9. Milestones
| Phase | Deliverable | Exit criteria |
|---|---|---|
| N0 — spike | os/ scaffold; mmdebstrap builds a snapshot-pinned trixie naros-base (arm64+amd64) with os-release identity; pushed to GHCR; boots as a linux_container |
Container runs, ID=naros, rebuild from same snapshot → identical package manifest |
| N1 — packaging | nash / nash-default-shell / naros-init / naros / nucleic-bridge / keyring / tier metas as debs; CI package pipeline; local file:// pool wired into image builds; R2 apt repo live |
apt install naros-tier-agent works from the hosted repo in a stock trixie container |
| N2 — naros-agent ≙ nash M3 | Full agent tier (toolchains, CLI kit, mise, caches, agent user, agent CLIs, Playwright); nash forced via divert; naros-init as container PID 1; host-side swap (Project.swift pin, NASH.md §7.2 argv/passwd changes, §7.6 probe fast-path); legacyShell rollback lever live |
Tool-inventory parity vs nucleic-sandbox:v7; nash M3 gates: ≥99% corpus parity in-image, <3% overhead, one clean dogfood week, zero unresolved fallback regressions |
| N3 — runner | nucleic-runner rebased FROM naros-agent (amd64); Cloudflare push; a Covalence session end-to-end on narOS |
Runner enrolls, pairs, runs a session; amd64 event stream in feed |
| N4 — VM (headless scope) | naros-vm rootfs artifact + naros-tier-vm; two-phase build consumes it; provisioning script reduced to the meta-package + firstboot glue; nash forced in guest (NASH.md M4 alignment) |
linux_vm_exec on a narOS guest with feed events; provisioning script <25% of current size |
| N5 — VM desktop (GNOME 50) | Desktop flavor on the pinned forky snapshot with GNOME 50 (fallback: naros-desktop backport pocket, §7.4); Ubuntu pin retired |
GNOME 50/Mutter running in-guest; semantic agent + mutter-geometry-helper parity vs the Ubuntu guest; computer-use screenshot/input flows green |
| N6 — distro polish | edge channel cadence automated; in-place apt upgrade of Nucleic components exercised in dogfood; manifest consumed by agent prompts/host probe; SBOM emission |
Two consecutive stable releases shipped purely through the channel machinery |
Sequencing with nash: N0–N1 can proceed now (they don't depend on nash M2's data-flow
taps); N2 lands after nash M2, because N2's ship gate is the M3 gate set. narOS replaces
the hypothetical nucleic-sandbox:v8 — there is no separate v8.
10. Testing
- Distro conformance (CI-gating): boot each tier; assert os-release identity,
divert integrity (
/bin/sh→nash,bash.realpresent and functional), agent uid/sudoers, manifest completeness/accuracy (every listed tool runs--version), profile.d env (caches, mise) visible throughnash -lc. - nash corpus replay in-image (NASH.md §10.3): the transcript-replay corpus runs
inside
naros-agent— the same ≥99% parity bar, now also catching trixie-vs-bookworm toolchain drift, not just shell drift. - Parity sweep vs
nucleic-sandbox:v7: scripted inventory diff (binaries on PATH, npm globals, Python modules, Playwright launch) so nothing agents rely on silently vanishes in the base swap. - apt-layer tests: repo signature verification from a clean container;
apt install naros-tier-agenton stock trixie (the §7.3 conversion path);apt upgradeof nash from stable→edge in a running container;apt full-upgradedoes not clobber the divert (dpkg-divert holds). - Reproducibility check: two CI builds from the same snapshot date + package set → identical package manifests (full bit-reproducibility is aspirational, manifest-level is gated).
- Swift-side:
ContainerSandboxTestsextended for naros-init PID 1 (signal/reap behavior, bridge launch), the seeded-passwd/uid constant assertion (§6.1), the probe fast-path (§7.3); runner smoke via the existing qemu-x86_64 flow.
11. Risks and mitigations
| Risk | Mitigation |
|---|---|
| trixie base bump breaks agent workflows (tool versions, python 3.13, gcc 14) | Corpus replay + parity sweep run in the new image before any pin flip; edge dogfood week; rollback = repoint one image pin (old v7 image remains published) |
| Two risky changes ride one release (base bump + nash forcing) | Deliberate (one migration, locked); mitigated by nash's independent M0–M2 validation, the NUCLEIC_NASH_DISABLE / legacyShell levers isolating shell regressions from image regressions, and v7 rollback isolating the rest |
| Desktop flavor rides Debian testing (forky) to force GNOME 50 | Snapshot pinning freezes each release (testing churn only enters at a deliberate re-pin); the forky pocket is confined to the desktop VM flavor — containers/headless stay trixie; N5 acceptance = semantic-agent parity vs the Ubuntu guest; the pinned Ubuntu image remains a published rollback until N5 exits |
| GNOME 50 not fully migrated in forky at pin time (or a freeze stalls it) | Fallback pocket (§7.4): CI rebuilds the missing GNOME 50 packages from experimental/Ubuntu sources into naros-desktop; scope limited to the delta and retired once forky catches up |
| apt signing key compromise / repo hygiene | Key in CI secrets only, scoped publish credentials, key rotation drill in N6; images pin the key via naros-keyring so a repo hijack alone can't inject packages |
| Image size creep (rust + go + caches + chromium) | Size budget asserted in CI per tier with a diff report per release; caches pre-seeded selectively (registry indexes, not the world); tiering keeps naros-base small for linux_container cold pulls |
| naros-init PID-1 subtleties (reaping, signal forwarding, tty) | Tiny scope by design (supervise + reap, no policy); Rust; dedicated ContainerSandboxTests; fallback: engine can revert to the sleep-loop keepalive per session |
| snapshot.debian.org availability/slowness in CI | CI-side mirror cache of the pinned snapshot in R2 (fetched once per release), so builds don't hammer or depend on snapshot uptime |
| R2/domain outage blocks image builds | Repo is also mirrored into a CI artifact per release; mmdebstrap can build from the file:// pool fallback (kept from N1) |
| Node-from-NodeSource drift vs today's node:22 base | Pin the NodeSource major (22.x) in the tier meta; parity sweep covers node --version; agent CLIs re-verified per release |
| Four surfaces adopt at different speeds → mixed fleet | os-release + manifest make the running variant observable per session (SessionController logs it, as it will nash's version); probe fast-path/degrade paths already handle mixed images |