commit d919c693dd049ce2728f4b6512a0aabe05ef8ed2 Author: Nucleic Date: Sat Jul 18 15:14:54 2026 -0700 Merge nucleic/olive-ember-seal-q7vk into dev diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5e2ddc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ +repo/keys/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb51ef1 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# os/ — the narOS build tree + +This directory builds **narOS** (the Nucleic Agent Runtime OS) per +[docs/NAROS.md](../docs/NAROS.md): a Debian-trixie-derived rootfs assembled from scratch +with `mmdebstrap` against a pinned `snapshot.debian.org` date, plus a Nucleic apt package +layer (nash, naros-init, tier meta-packages, …). CI (`.github/workflows/naros.yml`) +publishes OCI images to GHCR (`naros-base`, later `naros-agent`/`naros-runner`) and the +signed apt repository to Cloudflare R2. + +## Layout + +``` +VERSION # the narOS release version (26.MM[.p]) — single source of truth +SNAPSHOT # pinned snapshot.debian.org timestamp for this release +mkimage/ + build-rootfs.sh # mmdebstrap driver: → rootfs tar (needs root/unshare) + hooks/00-identity.sh # writes /etc/os-release (ID=naros) + /etc/naros/{manifest.json,…} + profiles/.pkgs # Debian package list per tier + profiles/.naros-pkgs # Nucleic packages per tier (installed when a pool is supplied) +packages/ + build-all.sh # builds every package below into dist/pool/ with dpkg-deb + /control # control template (@VERSION@/@ARCH@ substituted) + /files/ # static payload, copied verbatim + /stage.sh # optional dynamic staging (e.g. install a prebuilt binary) + /postinst,prerm # optional maintainer scripts +repo/ + publish.sh # dist/pool → apt tree (dists//…), signs when a key is present + r2-sync.sh # pushes the apt tree to Cloudflare R2 (CI; needs credentials) +dist/ # build output (gitignored): bin/, pool/, repo/, rootfs tars +``` + +## Building + +Packages (any Debian-family host, no root needed): + +```sh +os/packages/build-all.sh --arch arm64,amd64 # expects prebuilt nash/naros-init in + # os/dist/bin/- (see below) +``` + +Prebuilt binaries: `nash` and `naros-init` are Rust (musl-static, built from `shell/`); +CI drops them at `os/dist/bin/nash-{arm64,amd64}` and `os/dist/bin/naros-init-{arm64,amd64}`. +Locally: `cargo build --release -p nash -p naros-init` (with the musl targets) and copy. +Packages whose binary is missing are skipped with a warning, so pure-metadata iteration +works without a Rust toolchain. + +Rootfs (needs mmdebstrap; root or unshare-capable user — CI, or a root container): + +```sh +os/mkimage/build-rootfs.sh base arm64 --pool os/dist/pool +docker import os/dist/naros-base--arm64.tar naros-base:test +docker run --rm naros-base:test sh -c '. /etc/os-release && echo "$ID $VERSION_ID"' +``` + +Without `--pool`, the build produces a plain identity-only base (no Nucleic packages) — +useful for validating the mmdebstrap/snapshot/identity plumbing in isolation. + +## Versioning + +`VERSION` + `SNAPSHOT` define a release (NAROS.md §8). Channels: `edge` (weekly CI, +fresh snapshot) and `stable` (promoted deliberately; what `ProjectSandbox.defaultImage` +pins). Nucleic packages carry their own versions in `packages//VERSION` (falling +back to 0.1.0), suffixed with the channel. diff --git a/SNAPSHOT b/SNAPSHOT new file mode 100644 index 0000000..45781a3 --- /dev/null +++ b/SNAPSHOT @@ -0,0 +1 @@ +20260701T000000Z diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..1d91af3 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +26.07 diff --git a/mkimage/build-rootfs.sh b/mkimage/build-rootfs.sh new file mode 100755 index 0000000..de26500 --- /dev/null +++ b/mkimage/build-rootfs.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash +# narOS rootfs builder (NAROS.md §2.2): mmdebstrap against the pinned Debian snapshot, +# plus (optionally) the local Nucleic package pool. Produces a rootfs tar that CI turns +# into an OCI image (naros-base/…) or the VM payload tarball. +# +# build-rootfs.sh [--pool DIR] [--out DIR] [--channel edge|stable] +# +# Needs mmdebstrap and either root or an unshare-capable user. The snapshot mirror serves +# stale Release files by design, hence Check-Valid-Until off (standard snapshot practice). +set -euo pipefail + +OS_DIR="$(cd "$(dirname "$0")/.." && pwd)" +TIER="${1:?usage: build-rootfs.sh [--pool DIR] [--out DIR] [--channel C]}" +ARCH="${2:?missing arch (arm64|amd64)}" +shift 2 + +POOL="" OUT="$OS_DIR/dist" CHANNEL="${NAROS_CHANNEL:-edge}" +while [ $# -gt 0 ]; do + case "$1" in + --pool) POOL="$(cd "$2" && pwd)"; shift 2 ;; + --out) OUT="$2"; shift 2 ;; + --channel) CHANNEL="$2"; shift 2 ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +VERSION="$(cat "$OS_DIR/VERSION")" +SNAPSHOT="$(cat "$OS_DIR/SNAPSHOT")" +SUITE="trixie" +MIRROR="https://snapshot.debian.org/archive/debian/${SNAPSHOT}/" +PROFILE="$OS_DIR/mkimage/profiles/$TIER.pkgs" +[ -f "$PROFILE" ] || { echo "no profile for tier '$TIER' ($PROFILE)" >&2; exit 2; } + +pkg_list() { grep -vE '^\s*(#|$)' "$1" | tr '\n' ',' | sed 's/,$//'; } +INCLUDE="$(pkg_list "$PROFILE")" + +SOURCES=("deb [check-valid-until=no] $MIRROR $SUITE main") +if [ -n "$POOL" ]; then + # Flat file:// repo over the pool; trusted because it is CI's own just-built artifact — + # end-user trust comes from the signed hosted repo (repo/publish.sh), not this path. + if [ ! -f "$POOL/Packages" ] || [ -n "$(find "$POOL" -name '*.deb' -newer "$POOL/Packages" 2>/dev/null)" ]; then + (cd "$POOL" && dpkg-scanpackages --multiversion . > Packages) + fi + # Late packages (profiles/.late-pkgs) install via a finish hook AFTER every + # Debian package is configured — mandatory for nash-default-shell: apt's configure + # order is not deterministic, so a mid-transaction divert would run the remaining + # Debian postinsts under nash (observed: ca-certificates' UTF-8 filenames mangled). + # The divert must be the image's final configure step. + LATE_DEBS=() + LPROFILE="$OS_DIR/mkimage/profiles/$TIER.late-pkgs" + if [ -f "$LPROFILE" ]; then + while IFS= read -r pkg; do + deb="$(ls "$POOL/${pkg}_"*.deb 2>/dev/null | head -1)" + if [ -n "$deb" ]; then + LATE_DEBS+=("$deb") + elif [ "${NAROS_STRICT:-0}" = "1" ]; then + echo "ERROR: late package $pkg absent from pool" >&2; exit 1 + else + echo "WARNING: late package $pkg absent from pool — building WITHOUT it" >&2 + fi + done < <(grep -vE '^\s*(#|$)' "$LPROFILE") + fi + + NPROFILE="$OS_DIR/mkimage/profiles/$TIER.naros-pkgs" + if [ -f "$NPROFILE" ]; then + # Only request packages the pool actually carries: local builds legitimately lack + # some (e.g. naros-keyring without the signing key). Loud per-package warning; + # NAROS_STRICT=1 (CI) turns any gap into a hard failure. + while IFS= read -r pkg; do + if grep -qx "Package: $pkg" "$POOL/Packages"; then + INCLUDE="$INCLUDE,$pkg" + elif [ "${NAROS_STRICT:-0}" = "1" ]; then + echo "ERROR: $pkg requested by $TIER tier but absent from pool" >&2; exit 1 + else + echo "WARNING: $pkg absent from pool — building WITHOUT it" >&2 + fi + done < <(grep -vE '^\s*(#|$)' "$NPROFILE") + fi + SOURCES+=("deb [trusted=yes] copy://$POOL ./") +fi + +mkdir -p "$OUT" +TAR="$OUT/naros-$TIER-$VERSION-$ARCH.tar" + +export NAROS_TIER="$TIER" NAROS_VERSION="$VERSION" NAROS_ARCH="$ARCH" \ + NAROS_CHANNEL="$CHANNEL" NAROS_SNAPSHOT="$SNAPSHOT" NAROS_SUITE="$SUITE" + +HOOKS=() +if [ "${#LATE_DEBS[@]}" -gt 0 ]; then + LATE_ARGS="" + for deb in "${LATE_DEBS[@]}"; do + b="$(basename "$deb")" + HOOKS+=(--customize-hook="copy-in $deb /tmp") + LATE_ARGS="$LATE_ARGS /tmp/$b" + done + HOOKS+=(--customize-hook="chroot \"\$1\" dpkg -i$LATE_ARGS") + HOOKS+=(--customize-hook="rm -f$(printf ' "$1"%s' $LATE_ARGS)") +fi +HOOKS+=(--customize-hook="$OS_DIR/mkimage/hooks/00-identity.sh"' "$1"') + +echo "narOS $VERSION/$CHANNEL: tier=$TIER arch=$ARCH snapshot=$SNAPSHOT pool=${POOL:-none}" +mmdebstrap \ + --architectures="$ARCH" \ + --variant=apt \ + --include="$INCLUDE" \ + --aptopt='Acquire::Check-Valid-Until "false"' \ + "${HOOKS[@]}" \ + "$SUITE" "$TAR" "${SOURCES[@]}" + +echo "built $TAR" diff --git a/mkimage/hooks/00-identity.sh b/mkimage/hooks/00-identity.sh new file mode 100755 index 0000000..4edde5d --- /dev/null +++ b/mkimage/hooks/00-identity.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# mmdebstrap customize-hook: stamp narOS identity into the rootfs (NAROS.md §2.3). +# $1 = rootfs dir; NAROS_* env exported by build-rootfs.sh. Debian's own +# /usr/lib/os-release stays intact (ID_LIKE tooling keeps working); we replace only the +# /etc/os-release symlink with the narOS file. +set -eu +R="$1" + +rm -f "$R/etc/os-release" +cat > "$R/etc/os-release" < "$R/etc/naros/channel" +echo "$NAROS_SNAPSHOT" > "$R/etc/naros/snapshot-date" +echo "container" > "$R/etc/naros/role" + +# Capability manifest (NAROS.md §6.3). Base fields here; toolchain entries are appended +# by the tiers that install them (agent-tier hook, N2). Versions of Nucleic packages are +# queryable via dpkg, listed here for one-stop reads. +nash_ver="$(chroot "$R" dpkg-query -W -f '${Version}' nash 2>/dev/null || echo null)" +[ "$nash_ver" = null ] || nash_ver="\"$nash_ver\"" +cat > "$R/etc/naros/manifest.json" < ), optional +# postinst/prerm/preinst/postrm. Arch-any packages build once per requested arch and +# typically stage a prebuilt binary from dist/bin/-; missing binaries skip +# the package with a warning so metadata-only iteration needs no Rust toolchain. +set -euo pipefail + +PKG_DIR="$(cd "$(dirname "$0")" && pwd)" +OS_DIR="$(cd "$PKG_DIR/.." && pwd)" +ARCHES="arm64,amd64" CHANNEL="${NAROS_CHANNEL:-edge}" ONLY="" +while [ $# -gt 0 ]; do + case "$1" in + --arch) ARCHES="$2"; shift 2 ;; + --channel) CHANNEL="$2"; shift 2 ;; + --only) ONLY="$2"; shift 2 ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +OS_VERSION="$(cat "$OS_DIR/VERSION")" +POOL="$OS_DIR/dist/pool" +mkdir -p "$POOL" + +pkg_version() { # package semver + channel tag, e.g. 0.1.0+edge26.07 + local d="$1" base + base="$( [ -f "$d/VERSION" ] && cat "$d/VERSION" || echo 0.1.0 )" + echo "${base}+${CHANNEL}${OS_VERSION}" +} + +build_one() { # + local d="$1" arch="$2" name ver stage out + name="$(basename "$d")" + ver="$(pkg_version "$d")" + stage="$(mktemp -d)" + trap 'rm -rf "$stage"' RETURN + + [ -d "$d/files" ] && cp -a "$d/files/." "$stage/" + if [ -f "$d/stage.sh" ]; then + # shellcheck source=/dev/null + ( set -euo pipefail; OS_DIR="$OS_DIR" . "$d/stage.sh"; stage "$stage" "$arch" ) || { + echo "SKIP $name/$arch: $(cat "$stage/.skip-reason" 2>/dev/null || echo staging failed)" >&2 + return 0 + } + fi + + mkdir -p "$stage/DEBIAN" + sed -e "s/@VERSION@/$ver/" -e "s/@ARCH@/$arch/" "$d/control" > "$stage/DEBIAN/control" + local s + for s in preinst postinst prerm postrm; do + [ -f "$d/$s" ] && install -m 0755 "$d/$s" "$stage/DEBIAN/$s" + done + + out="$POOL/${name}_${ver}_$(grep -Po '^Architecture: \K.*' "$stage/DEBIAN/control").deb" + dpkg-deb --root-owner-group -Zxz --build "$stage" "$out" > /dev/null + echo "built ${out#"$OS_DIR/"}" +} + +for d in "$PKG_DIR"/*/; do + name="$(basename "$d")" + [ -f "$d/control" ] || continue + if [ -n "$ONLY" ] && ! echo ",$ONLY," | grep -q ",$name,"; then continue; fi + if grep -q '^Architecture: @ARCH@' "$d/control"; then + IFS=, read -ra AA <<< "$ARCHES" + for a in "${AA[@]}"; do build_one "$d" "$a"; done + else + build_one "$d" all + fi +done diff --git a/packages/naros-init/control b/packages/naros-init/control new file mode 100644 index 0000000..2a0866b --- /dev/null +++ b/packages/naros-init/control @@ -0,0 +1,12 @@ +Package: naros-init +Version: @VERSION@ +Architecture: @ARCH@ +Maintainer: Nucleic +Section: admin +Priority: optional +Description: narOS PID-1 supervisor for container surfaces (NAROS.md §5) + Small static init: reaps zombies, forwards signals, optionally supervises the + in-container control bridge (NAROS_BRIDGE=1) and/or a primary command + (everything after --), and otherwise acts as the keepalive that replaces + ContainerEngine's sleep loop. Role-driven via /etc/naros/role or NAROS_ROLE. + In the VM desktop flavor systemd stays PID 1 and naros-init runs as a unit. diff --git a/packages/naros-init/stage.sh b/packages/naros-init/stage.sh new file mode 100644 index 0000000..7cb00d6 --- /dev/null +++ b/packages/naros-init/stage.sh @@ -0,0 +1,9 @@ +# Stage the prebuilt static naros-init binary (built by CI from shell/naros-init). +stage() { + local dest="$1" arch="$2" bin="$OS_DIR/dist/bin/naros-init-$arch" + if [ ! -x "$bin" ]; then + echo "prebuilt binary missing: $bin" > "$dest/.skip-reason" + return 1 + fi + install -D -m 0755 "$bin" "$dest/usr/sbin/naros-init" +} diff --git a/packages/naros-keyring/control b/packages/naros-keyring/control new file mode 100644 index 0000000..2f0f6d9 --- /dev/null +++ b/packages/naros-keyring/control @@ -0,0 +1,11 @@ +Package: naros-keyring +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: misc +Priority: optional +Description: narOS apt archive keyring and source entry (NAROS.md §3.2) + The narOS apt repository's signing public key + (/usr/share/keyrings/naros-archive-keyring.gpg) plus the deb822 source entry + for apt.naros.dev pinned to that key. Installing this on any Debian-family + system enables `apt install naros-tier-agent` conversion (NAROS.md §7.3). diff --git a/packages/naros-keyring/files/etc/apt/sources.list.d/naros.sources b/packages/naros-keyring/files/etc/apt/sources.list.d/naros.sources new file mode 100644 index 0000000..c264c0e --- /dev/null +++ b/packages/naros-keyring/files/etc/apt/sources.list.d/naros.sources @@ -0,0 +1,5 @@ +Types: deb +URIs: https://apt.naros.dev +Suites: stable +Components: main +Signed-By: /usr/share/keyrings/naros-archive-keyring.gpg diff --git a/packages/naros-keyring/stage.sh b/packages/naros-keyring/stage.sh new file mode 100644 index 0000000..63139a5 --- /dev/null +++ b/packages/naros-keyring/stage.sh @@ -0,0 +1,10 @@ +# The public key is materialized by CI from the NAROS_APT_PUBLIC_KEY secret (or by an +# operator into os/repo/keys/). No key in the tree, no keyring package — skip cleanly. +stage() { + local dest="$1" key="$OS_DIR/repo/keys/naros-archive-keyring.gpg" + if [ ! -f "$key" ]; then + echo "public key missing: $key (CI materializes it from secrets)" > "$dest/.skip-reason" + return 1 + fi + install -D -m 0644 "$key" "$dest/usr/share/keyrings/naros-archive-keyring.gpg" +} diff --git a/packages/naros-tier-agent/control b/packages/naros-tier-agent/control new file mode 100644 index 0000000..1d39cae --- /dev/null +++ b/packages/naros-tier-agent/control @@ -0,0 +1,13 @@ +Package: naros-tier-agent +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: metapackages +Priority: optional +Depends: naros-tier-base, nucleic-bridge, build-essential, pkg-config, python3, python3-pip, python3-venv, ripgrep, fd-find, jq, sqlite3, htop, tree, zip, unzip, zstd, xz-utils, moreutils, rsync, less, procps, file, bsdextrautils +Description: narOS agent tier — apt-resolvable half (NAROS.md §4, §6) + The dev toolchain and modern CLI kit that come from Debian, plus the control + bridge. The non-apt half of the agent tier — Node (NodeSource), rustup, Go, + mise, warm caches, agent CLIs, Playwright — is layered by the naros-agent + image build (milestone N2); this meta is what `apt install` can deliver into + any Debian-family container (NAROS.md §7.3 conversion path). diff --git a/packages/naros-tier-base/control b/packages/naros-tier-base/control new file mode 100644 index 0000000..d832a42 --- /dev/null +++ b/packages/naros-tier-base/control @@ -0,0 +1,12 @@ +Package: naros-tier-base +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: metapackages +Priority: optional +Depends: nash, nash-default-shell, naros-init, naros, ca-certificates, curl, git, openssh-client, iproute2 +Recommends: naros-keyring +Description: narOS base tier (NAROS.md §4) + The minimal narOS surface: nash forced as the default shell, naros-init, the + naros CLI, and the small always-wanted utility set. Installing this meta on a + stock Debian-family system converts it to a naros-base-equivalent environment. diff --git a/packages/naros-tier-runner/control b/packages/naros-tier-runner/control new file mode 100644 index 0000000..49c41d8 --- /dev/null +++ b/packages/naros-tier-runner/control @@ -0,0 +1,12 @@ +Package: naros-tier-runner +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: metapackages +Priority: optional +Depends: naros-tier-agent +Description: narOS runner tier (NAROS.md §4) + The Covalence runner surface: everything in the agent tier. nucleicd itself is + a direct image COPY (versioned with the app, not a deb — NAROS.md §3.1), so + this meta currently only anchors the tier for introspection and future + runner-only dependencies. diff --git a/packages/naros-tier-vm/control b/packages/naros-tier-vm/control new file mode 100644 index 0000000..84a8cf0 --- /dev/null +++ b/packages/naros-tier-vm/control @@ -0,0 +1,12 @@ +Package: naros-tier-vm +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: metapackages +Priority: optional +Depends: naros-tier-base, systemd, dbus, sudo +Description: narOS VM guest tier — headless scope (NAROS.md §4, §7.4, milestone N4) + The bootable-guest surface. Headless scope for now: systemd (the VM flavor + keeps it as PID 1), dbus, sudo. nucleic-linux-agent packaging, the firstboot + provisioning glue, and the GNOME 50 desktop stack (naros-desktop, N5) land in + later milestones and will extend this meta. diff --git a/packages/naros/control b/packages/naros/control new file mode 100644 index 0000000..e52a5dc --- /dev/null +++ b/packages/naros/control @@ -0,0 +1,12 @@ +Package: naros +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: utils +Priority: optional +Description: narOS introspection CLI (NAROS.md §6.3) + `naros info [--json]` prints the capability manifest (/etc/naros/manifest.json: + tier, version, channel, snapshot, toolchains, caches); `naros version` prints + the release. Lets agents and the host ask "what can this box do" instead of + probing binary-by-binary. v1 is a POSIX sh script; a compiled multi-call + binary shared with naros-init can replace it without interface change. diff --git a/packages/naros/files/usr/bin/naros b/packages/naros/files/usr/bin/naros new file mode 100755 index 0000000..e423c92 --- /dev/null +++ b/packages/naros/files/usr/bin/naros @@ -0,0 +1,24 @@ +#!/bin/sh +# narOS introspection CLI (NAROS.md §6.3). Reads /etc/naros + /etc/os-release only. +set -eu +MANIFEST=/etc/naros/manifest.json + +case "${1:-info}" in + version) + . /etc/os-release + echo "narOS ${VERSION_ID:-unknown} (${VARIANT:-?}/$(cat /etc/naros/channel 2>/dev/null || echo '?'))" + ;; + info) + if [ "${2:-}" = "--json" ]; then + cat "$MANIFEST" + elif command -v jq > /dev/null 2>&1; then + jq . "$MANIFEST" + else + cat "$MANIFEST" + fi + ;; + *) + echo "usage: naros [info [--json] | version]" >&2 + exit 2 + ;; +esac diff --git a/packages/nash-default-shell/control b/packages/nash-default-shell/control new file mode 100644 index 0000000..66a58df --- /dev/null +++ b/packages/nash-default-shell/control @@ -0,0 +1,14 @@ +Package: nash-default-shell +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: shells +Priority: optional +Depends: nash +Description: force nash as the system default shell (NASH.md §7.1) + The maintainer-script form of the locked divert block: /bin/bash and /bin/dash + are dpkg-diverted to /usr/bin/{bash,dash}.real and /bin/{bash,dash,sh} point at + nash, so shebangs and tools that hardcode sh/bash land in nash. Real shells + stay reachable at the .real paths — nash's parse-failure fallback depends on + them. Removing this package cleanly restores stock shells. dpkg-divert keeps + apt upgrades of bash/dash from clobbering the links. diff --git a/packages/nash-default-shell/files/etc/profile.d/naros-shell.sh b/packages/nash-default-shell/files/etc/profile.d/naros-shell.sh new file mode 100644 index 0000000..f622b7c --- /dev/null +++ b/packages/nash-default-shell/files/etc/profile.d/naros-shell.sh @@ -0,0 +1,4 @@ +# narOS shell environment (nash-default-shell). NUCLEIC_REAL_BASH is nash's +# parse-failure fallback + NUCLEIC_NASH_DISABLE target (NASH.md §4.1). +export NUCLEIC_REAL_BASH=/usr/bin/bash.real +[ -n "${SHELL:-}" ] || export SHELL=/usr/local/bin/nash diff --git a/packages/nash-default-shell/postinst b/packages/nash-default-shell/postinst new file mode 100644 index 0000000..409701d --- /dev/null +++ b/packages/nash-default-shell/postinst @@ -0,0 +1,10 @@ +#!/bin/sh +# NASH.md §7.1, verbatim semantics (merged-usr symlink handling verified in nash M0). +set -e +if [ "$1" = "configure" ]; then + dpkg-divert --package nash-default-shell --divert /usr/bin/bash.real --rename --add /bin/bash + dpkg-divert --package nash-default-shell --divert /usr/bin/dash.real --rename --add /bin/dash + ln -sf /usr/local/bin/nash /bin/bash + ln -sf /usr/local/bin/nash /bin/dash + ln -sf /usr/local/bin/nash /bin/sh +fi diff --git a/packages/nash-default-shell/prerm b/packages/nash-default-shell/prerm new file mode 100644 index 0000000..fc5dbfb --- /dev/null +++ b/packages/nash-default-shell/prerm @@ -0,0 +1,9 @@ +#!/bin/sh +# Clean revert: drop the nash links, un-divert the real shells, restore sh -> dash. +set -e +if [ "$1" = "remove" ]; then + rm -f /bin/bash /bin/dash + dpkg-divert --package nash-default-shell --rename --remove /bin/bash + dpkg-divert --package nash-default-shell --rename --remove /bin/dash + ln -sf dash /usr/bin/sh +fi diff --git a/packages/nash/control b/packages/nash/control new file mode 100644 index 0000000..35eaf81 --- /dev/null +++ b/packages/nash/control @@ -0,0 +1,12 @@ +Package: nash +Version: @VERSION@ +Architecture: @ARCH@ +Maintainer: Nucleic +Section: shells +Priority: optional +Description: Nucleic agent shell (brush fork) + Bourne/bash-compatible shell whose job is to make every shell action an agent + takes observable by construction (docs/NASH.md). Static musl binary; installs + as /usr/bin/nash with the locked /usr/local/bin/nash path provided as a + symlink. This package does NOT change the default shell — that is + nash-default-shell's job. diff --git a/packages/nash/postinst b/packages/nash/postinst new file mode 100644 index 0000000..e69a8ac --- /dev/null +++ b/packages/nash/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +# The locked contract (NASH.md §2, §7) addresses nash at /usr/local/bin/nash on every +# surface (probe, exec argv, $SHELL). The real file lives at /usr/bin/nash per policy; +# this symlink satisfies the contract path. +set -e +if [ "$1" = "configure" ]; then + mkdir -p /usr/local/bin + ln -sf /usr/bin/nash /usr/local/bin/nash +fi diff --git a/packages/nash/prerm b/packages/nash/prerm new file mode 100644 index 0000000..1da68c2 --- /dev/null +++ b/packages/nash/prerm @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +if [ "$1" = "remove" ]; then + [ -L /usr/local/bin/nash ] && rm -f /usr/local/bin/nash || true +fi diff --git a/packages/nash/stage.sh b/packages/nash/stage.sh new file mode 100644 index 0000000..59552cf --- /dev/null +++ b/packages/nash/stage.sh @@ -0,0 +1,9 @@ +# Stage the prebuilt static nash binary (built by CI from shell/, target musl). +stage() { + local dest="$1" arch="$2" bin="$OS_DIR/dist/bin/nash-$arch" + if [ ! -x "$bin" ]; then + echo "prebuilt binary missing: $bin" > "$dest/.skip-reason" + return 1 + fi + install -D -m 0755 "$bin" "$dest/usr/bin/nash" +} diff --git a/packages/nucleic-bridge/control b/packages/nucleic-bridge/control new file mode 100644 index 0000000..9142fed --- /dev/null +++ b/packages/nucleic-bridge/control @@ -0,0 +1,12 @@ +Package: nucleic-bridge +Version: @VERSION@ +Architecture: all +Maintainer: Nucleic +Section: net +Priority: optional +Depends: nodejs +Description: in-container control bridge (docs/VSOCK_CONTROL_PLANE.md) + Loopback TCP to the vsock-relayed host control socket, so the agent and the + interceptor shims reach the host approval server with no IP listener. + Launched by naros-init (or the container's root init) only when Nucleic + relays a control socket in. diff --git a/packages/nucleic-bridge/stage.sh b/packages/nucleic-bridge/stage.sh new file mode 100644 index 0000000..f2ec5e0 --- /dev/null +++ b/packages/nucleic-bridge/stage.sh @@ -0,0 +1,6 @@ +# The bridge source of truth stays beside the sandbox image context; the deb packages it. +stage() { + local dest="$1" + install -D -m 0644 "$OS_DIR/../containers/nucleic-sandbox/control-bridge.js" \ + "$dest/opt/nucleic/control-bridge.js" +} diff --git a/repo/publish.sh b/repo/publish.sh new file mode 100755 index 0000000..0a4cfda --- /dev/null +++ b/repo/publish.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Publish dist/pool as a static apt tree (NAROS.md §3.2): dist/repo/dists//main/ +# with per-arch Packages(.xz) and a signed InRelease when a key is available. +# +# publish.sh [--channel edge|stable] [--sign KEYID] +# +# Unsigned mode is for local/dev use only (consume with [trusted=yes]); CI always signs +# (key from the NAROS_APT_SIGNING_KEY secret). Sync to R2 is r2-sync.sh's job. +set -euo pipefail + +OS_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CHANNEL="${NAROS_CHANNEL:-edge}" KEYID="${NAROS_APT_KEYID:-}" +while [ $# -gt 0 ]; do + case "$1" in + --channel) CHANNEL="$2"; shift 2 ;; + --sign) KEYID="$2"; shift 2 ;; + *) echo "unknown arg: $1" >&2; exit 2 ;; + esac +done + +POOL="$OS_DIR/dist/pool" +REPO="$OS_DIR/dist/repo" +DISTS="$REPO/dists/$CHANNEL/main" +[ -d "$POOL" ] || { echo "no pool at $POOL — run packages/build-all.sh first" >&2; exit 2; } + +rm -rf "$REPO/dists/$CHANNEL" +mkdir -p "$REPO/pool/main" +cp -a "$POOL/." "$REPO/pool/main/" 2>/dev/null || true +rm -f "$REPO/pool/main/Packages" + +cd "$REPO" +for arch in arm64 amd64; do + bindir="dists/$CHANNEL/main/binary-$arch" + mkdir -p "$bindir" + # Arch-specific debs for this arch + arch:all debs, one Packages per binary-. + dpkg-scanpackages --multiversion --arch "$arch" pool > "$bindir/Packages" + xz -k -f "$bindir/Packages" +done + +apt-ftparchive \ + -o "APT::FTPArchive::Release::Origin=narOS" \ + -o "APT::FTPArchive::Release::Label=narOS" \ + -o "APT::FTPArchive::Release::Suite=$CHANNEL" \ + -o "APT::FTPArchive::Release::Codename=$CHANNEL" \ + -o "APT::FTPArchive::Release::Architectures=arm64 amd64" \ + -o "APT::FTPArchive::Release::Components=main" \ + release "dists/$CHANNEL" > "dists/$CHANNEL/Release" + +if [ -n "$KEYID" ]; then + gpg --batch --yes -u "$KEYID" --clearsign -o "dists/$CHANNEL/InRelease" "dists/$CHANNEL/Release" + gpg --batch --yes -u "$KEYID" --detach-sign --armor -o "dists/$CHANNEL/Release.gpg" "dists/$CHANNEL/Release" + echo "published SIGNED repo: $REPO (channel $CHANNEL, key $KEYID)" +else + echo "published UNSIGNED repo: $REPO (channel $CHANNEL) — dev only, consume with [trusted=yes]" +fi diff --git a/repo/r2-sync.sh b/repo/r2-sync.sh new file mode 100755 index 0000000..0031742 --- /dev/null +++ b/repo/r2-sync.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Sync the published apt tree to Cloudflare R2 (served as apt.naros.dev; NAROS.md §3.2). +# Uses rclone's S3 backend with env-provided credentials (CI secrets): +# R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET (default naros-apt) +set -euo pipefail + +OS_DIR="$(cd "$(dirname "$0")/.." && pwd)" +REPO="$OS_DIR/dist/repo" +: "${R2_ACCOUNT_ID:?}" "${R2_ACCESS_KEY_ID:?}" "${R2_SECRET_ACCESS_KEY:?}" +BUCKET="${R2_BUCKET:-naros-apt}" +[ -d "$REPO" ] || { echo "no repo at $REPO — run repo/publish.sh first" >&2; exit 2; } + +export RCLONE_CONFIG_R2_TYPE=s3 \ + RCLONE_CONFIG_R2_PROVIDER=Cloudflare \ + RCLONE_CONFIG_R2_ACCESS_KEY_ID="$R2_ACCESS_KEY_ID" \ + RCLONE_CONFIG_R2_SECRET_ACCESS_KEY="$R2_SECRET_ACCESS_KEY" \ + RCLONE_CONFIG_R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" + +# pool/ first, dists/ last: clients never see an index referencing a not-yet-uploaded deb. +rclone copy "$REPO/pool" "r2:$BUCKET/pool" --checksum +rclone sync "$REPO/dists" "r2:$BUCKET/dists" --checksum +echo "synced $REPO -> r2:$BUCKET"