Files
nucleic/docs/NAROS.md
T

40 KiB
Raw Blame History

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_VERSIONos/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=narosCommandInterceptor.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.defaultImageghcr.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 + the seeded /etc/nucleic/nash.conf carries disable=1 — NASH.md §4.3, which moved the lever off the agent-writable environment). 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 (N0N1, 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-basenaros-agentnaros-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 + channelsnaros 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 + Swift (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

  1. One OS identity across every Linux surface Nucleic controls. /etc/os-release says ID=naros everywhere an agent lands; same package set, same defaults, same release train. The sandbox image, runner image, linux_container default, and (phase 2) the VM guest stop being four separately-maintained decorations of third-party bases.
  2. nash by construction. narOS ships nash installed and forced (divert of /bin/sh / /bin/bash / /bin/dash, $SHELL, .real preservation) as part of the distro build — not as a patch applied to someone else's image. Installing narOS is completing NASH.md M3.
  3. 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.
  4. 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.
  5. 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 install anything; 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):

  1. mmdebstrap --variant=minbase against the pinned snapshot mirror + the Nucleic apt repo (§3.2), installing the tier's package list (§4).
  2. Post-build hooks apply narOS identity (§2.3), the nash divert block, the agent user, profile.d environment, and the capability manifest (§6.3).
  3. The resulting rootfs is emitted two ways:
    • OCI image (via mmdebstrap's tar → skopeo/crane push): per-arch images + multi-arch index to GHCR (ghcr.io/abkslm/naros-{base,agent,runner}), exactly where ContainerEngine and the Cloudflare push pipeline already pull from.
    • Rootfs tarball artifact for the VM two-phase build (§7.4), replacing the Ubuntu cloud tarball download.

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 sniffs ID_LIKE keeps 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:

  • sysname stays Linux. It is the most-switched-on uname field in build tooling — config.guess, CMAKE_SYSTEM_NAME, node-gyp, the Go and rustup installers all compare it against Linux. Only release is tagged; version is untouched.
  • NAROS_UNAME_PASSTHROUGH=1 returns 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.service uses 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; ships the root-owned /etc/nucleic/nash.conf operator policy (NASH.md §4.3) — no NUCLEIC_REAL_BASH export, since an env lever is one the agent can rewrite; 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/reprepro generates the pool + InRelease (signed with a CI-held key; public half ships in naros-keyring and is embedded in the mmdebstrap profile for bootstrap trust) → synced to R2.
  • Suites mirror channels: naros stable and naros edge (§8).
  • Image builds install from it; running systems can apt upgrade Nucleic 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, Swift   (§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): ContainerEngine today keeps sandboxes alive with a sh -c sleep 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-init instead of the sleep loop. The runner keeps nucleicd as 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/role or 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 what ContainerEngine+Rootfs.swift seeds 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 for apt install without 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/passwd seeding in ContainerEngine+Rootfs.swift:184 simplifies 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), Swift (swift.org release tarball, pinned + checksummed, at /opt/swift) — 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.

Swift is Linux Swift, not a Mac: swiftc/SwiftPM/swift-testing/sourcekit-lsp/swift-format build and test pure-Swift targets in-container, while anything Apple-SDK (SwiftUI, AppKit, UIKit, xcodebuild) still belongs on the host or in a macOS VM. Two Swift sources coexist by necessity — the container takes the swift.org build (Debian's trixie swiftlang is 6.0.3, too old for a swift-tools-version: 6.2 package), the VM desktop flavor takes forky's swiftlang (every swift.org slice links libxml2.so.2, which forky no longer ships; §7.4). The container layer strips upstream's LLDB — it links libpython3.11, absent on trixie, so it could never load.

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" is mise 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 (+ fd symlinked from Debian's fdfind). 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:

  1. Primary — pinned forky/testing snapshot for the desktop flavor. The naros-vm desktop rootfs builds from a pinned snapshot.debian.org date 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).
  2. Fallback — naros-desktop backport 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 dedicated naros-desktop component 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.

    The one-pocket rule also decides where this flavor's Swift comes from: forky's swiftlang (6.2.3 at the current pin), not the swift.org tarball the container bakes. Upstream's builds all link libxml2.so.2 and forky ships only libxml2-16 (soname .so.16, no versioned symbols to alias), so a swift.org toolchain dropped in here fails in the loader on the first swift build. Debian's package is built against the pocket's own libxml2-16/python3.14, which also gets the flavor a working LLDB.

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 — what Project.swift pins; 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→v8 bump (Dockerfile, workflow IMAGE_TAG, Project.swift, runner base pin) becomes one version constant in os/VERSION propagated by CI, with a CI assertion that Project.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 (reusing nash.yml artifacts) → apt repo publish to R2 → mmdebstrap per tier × arch → OCI push to GHCR + VM tarball artifact → test suites (§10). Runner image CI continues in runner-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: N0N1 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

  1. Distro conformance (CI-gating): boot each tier; assert os-release identity, divert integrity (/bin/sh→nash, bash.real present and functional), agent uid/sudoers, manifest completeness/accuracy (every listed tool runs --version), profile.d env (caches, mise) visible through nash -lc.
  2. 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.
  3. 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.
  4. apt-layer tests: repo signature verification from a clean container; apt install naros-tier-agent on stock trixie (the §7.3 conversion path); apt upgrade of nash from stable→edge in a running container; apt full-upgrade does not clobber the divert (dpkg-divert holds).
  5. Reproducibility check: two CI builds from the same snapshot date + package set → identical package manifests (full bit-reproducibility is aspirational, manifest-level is gated).
  6. Swift-side: ContainerSandboxTests extended 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 M0M2 validation, the policy-file kill switch / legacyShell levers (NASH.md §4.3) 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