# 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` + 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 (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 `.deb`s, 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 ("**n**ucleic **a**gent **r**untime 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_container`s, and (in a later phase) the
Linux VM guest. It ships **nash** ([NASH.md](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](NASH.md) (the shell narOS forces; narOS v1 is the vehicle for its
M3 milestone), [CONTAINER_ISOLATION.md](CONTAINER_ISOLATION.md),
[VSOCK_CONTROL_PLANE.md](VSOCK_CONTROL_PLANE.md), [LINUX_VM.md](LINUX_VM.md),
[COVALENCE_RUNNER.md](COVALENCE_RUNNER.md) / [CLOUD_RUNTIME.md](CLOUD_RUNTIME.md) (runner
image consumers), [OBSERVABILITY_AND_TESTING.md](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 `.deb`s; 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 + 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](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](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 `.deb`s → `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](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](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: 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

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 M0–M2 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 |
