Merge nucleic/olive-ember-seal-q7vk into dev
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
name: Build narOS
|
||||
|
||||
# narOS build pipeline (docs/NAROS.md §8): Rust binaries (nash, naros-init) → Nucleic
|
||||
# .debs → signed apt repo (R2) → mmdebstrap rootfs per tier × arch → OCI images on GHCR.
|
||||
#
|
||||
# Channels: pushes to dev build `edge`; the weekly cron refreshes `edge` with a new
|
||||
# snapshot date implicitly only when SNAPSHOT is bumped (snapshot stays pinned in-tree —
|
||||
# reproducibility over freshness); `stable` is cut via workflow_dispatch. Images push
|
||||
# only from dev/main/dispatch, never from PRs.
|
||||
#
|
||||
# Secrets (all optional — jobs degrade to unsigned/unpublished artifacts without them):
|
||||
# NAROS_APT_SIGNING_KEY / NAROS_APT_KEYID ascii-armored private key + key id
|
||||
# R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
paths:
|
||||
- os/**
|
||||
- shell/**
|
||||
- .github/workflows/naros.yml
|
||||
schedule:
|
||||
- cron: "17 6 * * 1" # weekly edge rebuild
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: "Release channel"
|
||||
type: choice
|
||||
options: [edge, stable]
|
||||
default: edge
|
||||
|
||||
env:
|
||||
CHANNEL: ${{ inputs.channel || 'edge' }}
|
||||
|
||||
jobs:
|
||||
binaries:
|
||||
name: nash + naros-init (${{ matrix.arch }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
target: x86_64-unknown-linux-musl
|
||||
- arch: arm64
|
||||
target: aarch64-unknown-linux-musl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
# cargo-zigbuild (zig as the musl cross-linker) instead of `cross`: cross mounts
|
||||
# only the cargo workspace (shell/) into its build container, which would hide the
|
||||
# ../third_party/brush path dependencies.
|
||||
- name: Install cargo-zigbuild
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-zigbuild
|
||||
- name: Install zig
|
||||
run: pip3 install ziglang
|
||||
- name: Build (musl static)
|
||||
working-directory: shell
|
||||
run: cargo zigbuild --release --target ${{ matrix.target }} -p nash -p naros-init
|
||||
- name: Collect
|
||||
run: |
|
||||
mkdir -p os/dist/bin
|
||||
cp shell/target/${{ matrix.target }}/release/nash os/dist/bin/nash-${{ matrix.arch }}
|
||||
cp shell/target/${{ matrix.target }}/release/naros-init os/dist/bin/naros-init-${{ matrix.arch }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: naros-bin-${{ matrix.arch }}
|
||||
path: os/dist/bin/*
|
||||
|
||||
packages:
|
||||
name: debs + apt repo
|
||||
needs: binaries
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: naros-bin-*
|
||||
merge-multiple: true
|
||||
path: os/dist/bin
|
||||
- name: Tools
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends dpkg-dev apt-utils
|
||||
- name: Import signing key
|
||||
if: ${{ secrets.NAROS_APT_SIGNING_KEY != '' }}
|
||||
env:
|
||||
NAROS_APT_SIGNING_KEY: ${{ secrets.NAROS_APT_SIGNING_KEY }}
|
||||
NAROS_APT_KEYID: ${{ secrets.NAROS_APT_KEYID }}
|
||||
run: |
|
||||
echo "$NAROS_APT_SIGNING_KEY" | gpg --batch --import
|
||||
mkdir -p os/repo/keys
|
||||
gpg --batch --export "$NAROS_APT_KEYID" > os/repo/keys/naros-archive-keyring.gpg
|
||||
echo "NAROS_APT_KEYID=$NAROS_APT_KEYID" >> "$GITHUB_ENV"
|
||||
- name: Build packages
|
||||
run: |
|
||||
chmod +x os/dist/bin/* || true
|
||||
os/packages/build-all.sh --arch arm64,amd64 --channel "$CHANNEL"
|
||||
- name: Publish apt tree
|
||||
run: os/repo/publish.sh --channel "$CHANNEL" ${NAROS_APT_KEYID:+--sign "$NAROS_APT_KEYID"}
|
||||
- name: Sync to R2
|
||||
if: ${{ github.event_name != 'pull_request' && secrets.R2_ACCOUNT_ID != '' }}
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
sudo apt-get install -y rclone
|
||||
os/repo/r2-sync.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: naros-pool
|
||||
path: os/dist/pool/
|
||||
|
||||
rootfs:
|
||||
name: naros-base rootfs (${{ matrix.arch }})
|
||||
needs: packages
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: naros-pool
|
||||
path: os/dist/pool
|
||||
- name: Tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends mmdebstrap dpkg-dev \
|
||||
qemu-user-static binfmt-support skopeo
|
||||
- name: Build rootfs
|
||||
# Strict only when the keyring can exist (signing key provisioned); until then a
|
||||
# keyring-less pool is expected and must not fail the build.
|
||||
run: sudo -E env NAROS_STRICT=${{ secrets.NAROS_APT_SIGNING_KEY != '' && '1' || '0' }} \
|
||||
os/mkimage/build-rootfs.sh base ${{ matrix.arch }} --pool os/dist/pool --channel "$CHANNEL"
|
||||
- name: Smoke test
|
||||
run: |
|
||||
VER="$(cat os/VERSION)"
|
||||
docker import "os/dist/naros-base-$VER-${{ matrix.arch }}.tar" naros-test
|
||||
docker run --rm --platform linux/${{ matrix.arch }} naros-test /usr/bin/nash -lc '
|
||||
set -e
|
||||
. /etc/os-release; test "$ID" = naros
|
||||
readlink /bin/sh | grep -q nash
|
||||
test -x /usr/bin/bash.real
|
||||
naros version
|
||||
naros info --json > /dev/null'
|
||||
- name: Push to GHCR
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
VER="$(cat os/VERSION)"
|
||||
owner="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
|
||||
skopeo login ghcr.io -u '${{ github.actor }}' -p "$GH_TOKEN"
|
||||
skopeo copy "docker-archive:/dev/stdin" \
|
||||
"docker://ghcr.io/${owner}/naros-base:${VER}-${{ matrix.arch }}" \
|
||||
< <(docker save naros-test)
|
||||
|
||||
index:
|
||||
name: multi-arch index
|
||||
needs: rootfs
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create index
|
||||
run: |
|
||||
VER="$(cat os/VERSION)"
|
||||
owner="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
|
||||
img="ghcr.io/${owner}/naros-base"
|
||||
docker buildx imagetools create -t "$img:$VER" -t "$img:$CHANNEL" \
|
||||
"$img:$VER-amd64" "$img:$VER-arm64"
|
||||
+30
-11
@@ -1,6 +1,19 @@
|
||||
# narOS — the Nucleic Agent Runtime OS
|
||||
|
||||
**Status: plan.** narOS ("**n**ucleic **a**gent **r**untime OS") is a Debian-derived Linux
|
||||
**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`/`echo` — found when `ca-certificates`' postinst broke; blocks the M3 forcing
|
||||
gates until fixed). Remaining for N0/N1 exit: first CI run green (incl. amd64),
|
||||
R2/apt.naros.dev hosting + signing key provisioned, D2 fixed + corpus case added. 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
|
||||
@@ -376,17 +389,23 @@ Mutter build) moves from being a *gate on shipping* to being N5's **acceptance t
|
||||
|
||||
```
|
||||
os/
|
||||
VERSION # single source of truth for the release version
|
||||
mkimage/ # mmdebstrap profiles + hooks (identity, divert, user, manifest)
|
||||
naros-base.profile naros-agent.profile naros-runner.profile naros-vm.profile
|
||||
packages/ # debian/ source trees: nash, nash-default-shell, naros-init,
|
||||
# naros, nucleic-bridge, nucleic-linux-agent, naros-keyring,
|
||||
# naros-tier-*
|
||||
repo/ # aptly/reprepro config + R2 publish scripts
|
||||
images/ # per-tier extras (agent CLI install layer, playwright layer,
|
||||
# runner Dockerfile once rebased)
|
||||
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
|
||||
repo/
|
||||
publish.sh # pool → apt tree (dists/<channel>), signs when keyed
|
||||
r2-sync.sh # apt tree → Cloudflare R2 (apt.naros.dev)
|
||||
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
|
||||
.github/workflows/naros.yml # binaries → debs → repo → rootfs → smoke → GHCR index
|
||||
docs/NAROS.md # this doc
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
dist/
|
||||
repo/keys/
|
||||
@@ -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: <tier> <arch> → rootfs tar (needs root/unshare)
|
||||
hooks/00-identity.sh # writes /etc/os-release (ID=naros) + /etc/naros/{manifest.json,…}
|
||||
profiles/<tier>.pkgs # Debian package list per tier
|
||||
profiles/<tier>.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
|
||||
<name>/control # control template (@VERSION@/@ARCH@ substituted)
|
||||
<name>/files/ # static payload, copied verbatim
|
||||
<name>/stage.sh # optional dynamic staging (e.g. install a prebuilt binary)
|
||||
<name>/postinst,prerm # optional maintainer scripts
|
||||
repo/
|
||||
publish.sh # dist/pool → apt tree (dists/<channel>/…), 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/<name>-<arch> (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-<ver>-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/<name>/VERSION` (falling
|
||||
back to 0.1.0), suffixed with the channel.
|
||||
@@ -0,0 +1 @@
|
||||
20260701T000000Z
|
||||
@@ -0,0 +1 @@
|
||||
26.07
|
||||
Executable
+110
@@ -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 <tier> <arch> [--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 <tier> <arch> [--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/<tier>.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"
|
||||
Executable
+48
@@ -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" <<EOF
|
||||
NAME="narOS"
|
||||
PRETTY_NAME="narOS $NAROS_VERSION (Nucleic Agent Runtime OS)"
|
||||
ID=naros
|
||||
ID_LIKE=debian
|
||||
VERSION_ID="$NAROS_VERSION"
|
||||
VERSION="$NAROS_VERSION ($NAROS_CHANNEL)"
|
||||
VERSION_CODENAME=$NAROS_SUITE
|
||||
VARIANT="$NAROS_TIER"
|
||||
VARIANT_ID=$NAROS_TIER
|
||||
HOME_URL="https://github.com/abkslm/nucleic"
|
||||
DOCUMENTATION_URL="https://github.com/abkslm/nucleic/blob/main/docs/NAROS.md"
|
||||
EOF
|
||||
|
||||
mkdir -p "$R/etc/naros"
|
||||
echo "$NAROS_CHANNEL" > "$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" <<EOF
|
||||
{
|
||||
"os": "naros",
|
||||
"version": "$NAROS_VERSION",
|
||||
"channel": "$NAROS_CHANNEL",
|
||||
"tier": "$NAROS_TIER",
|
||||
"arch": "$NAROS_ARCH",
|
||||
"debian": { "suite": "$NAROS_SUITE", "snapshot": "$NAROS_SNAPSHOT" },
|
||||
"nash": $nash_ver,
|
||||
"toolchains": {},
|
||||
"caches": {}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "narOS identity: $(. "$R/etc/os-release" && echo "$PRETTY_NAME [$VARIANT/$NAROS_ARCH]")"
|
||||
@@ -0,0 +1,3 @@
|
||||
# naros-agent tier (NAROS.md §4, milestone N2 — placeholder until the agent tier lands).
|
||||
# Will carry: build-essential/pkg-config, python3 + pip/venv, the modern CLI kit, and the
|
||||
# hooks that add Node (NodeSource), rustup, Go, mise, warm caches, agent CLIs, Playwright.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Installed via dpkg -i in a finish hook, after every Debian package is configured.
|
||||
# nash-default-shell MUST be last-stage: its divert flips /bin/sh to nash, and any
|
||||
# Debian postinst that runs after the flip runs under nash (see build-rootfs.sh).
|
||||
nash-default-shell
|
||||
naros-tier-base
|
||||
@@ -0,0 +1,7 @@
|
||||
# Nucleic packages for naros-base, resolved from the local pool when --pool is given.
|
||||
# The divert package + tier meta are in base.late-pkgs (must configure last); this list
|
||||
# is what installs alongside the Debian set.
|
||||
nash
|
||||
naros-init
|
||||
naros
|
||||
naros-keyring
|
||||
@@ -0,0 +1,7 @@
|
||||
# naros-base Debian package list (NAROS.md §4). Kept deliberately small — the agent
|
||||
# toolchain lives in the agent tier. apt itself comes from --variant=apt.
|
||||
ca-certificates
|
||||
curl
|
||||
git
|
||||
openssh-client
|
||||
iproute2
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build every narOS package into dist/pool/ with plain dpkg-deb (NAROS.md §3.1).
|
||||
#
|
||||
# Deliberately not debhelper: these are binary payloads, scripts, and meta-packages —
|
||||
# a uniform stage-and-pack loop keeps the whole layer readable and buildable anywhere
|
||||
# dpkg-deb exists (this container, CI). Graduating a package to dpkg-buildpackage later
|
||||
# is a per-package decision, not a build-system change.
|
||||
#
|
||||
# build-all.sh [--arch arm64,amd64] [--channel edge|stable] [--only pkg1,pkg2]
|
||||
#
|
||||
# Per package dir: control (template: @VERSION@ @ARCH@), optional files/ (copied
|
||||
# verbatim), optional stage.sh (sourced; must define stage <destdir> <arch>), optional
|
||||
# postinst/prerm/preinst/postrm. Arch-any packages build once per requested arch and
|
||||
# typically stage a prebuilt binary from dist/bin/<name>-<arch>; 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() { # <pkgdir> <arch>
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: naros-init
|
||||
Version: @VERSION@
|
||||
Architecture: @ARCH@
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
Package: naros-keyring
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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).
|
||||
@@ -0,0 +1,5 @@
|
||||
Types: deb
|
||||
URIs: https://apt.naros.dev
|
||||
Suites: stable
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/naros-archive-keyring.gpg
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
Package: naros-tier-agent
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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).
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: naros-tier-base
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: naros-tier-runner
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: naros-tier-vm
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: naros
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
Executable
+24
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
Package: nash-default-shell
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: nash
|
||||
Version: @VERSION@
|
||||
Architecture: @ARCH@
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: nucleic-bridge
|
||||
Version: @VERSION@
|
||||
Architecture: all
|
||||
Maintainer: Nucleic <[email protected]>
|
||||
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.
|
||||
@@ -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"
|
||||
}
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish dist/pool as a static apt tree (NAROS.md §3.2): dist/repo/dists/<channel>/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-<arch>.
|
||||
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
|
||||
Executable
+22
@@ -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"
|
||||
Generated
+7
@@ -1366,6 +1366,13 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "naros-init"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nash"
|
||||
version = "0.1.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["nash", "nash-observe"]
|
||||
members = ["nash", "nash-observe", "naros-init"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
@@ -6,7 +6,27 @@ stays open until fixed in the fork (or upstream) and re-verified by the corpus.
|
||||
|
||||
## Open
|
||||
|
||||
(none)
|
||||
### D2 — non-ASCII bytes re-encoded through `read`/`echo` under C/empty locale
|
||||
|
||||
- **Found**: narOS N0 rootfs validation (first real-world install run under the
|
||||
divert): with `/bin/sh → nash`, `ca-certificates`' postinst
|
||||
(`update-ca-certificates`, a `while read`-over-conf loop) mangled the UTF-8
|
||||
filename `NetLock_Arany_=Class_Gold=_Főtanúsítvány.crt` and failed the
|
||||
whole image build. Worked around structurally in `os/mkimage/build-rootfs.sh`
|
||||
(the divert is now always the image's last configure step), but any
|
||||
*runtime* `apt install` inside narOS still runs postinsts under nash, so this
|
||||
blocks narOS/M3 forcing gates until fixed.
|
||||
- **Repro** (nash `0.4.0` musl arm64, empty locale):
|
||||
`echo 'Főtanúsítvány' | nash -c 'while read x; do echo "$x"; done'` —
|
||||
bash emits the input bytes unchanged (`F \305\221 t a n \303\272 …`); nash
|
||||
emits each byte Latin-1→UTF-8 double-encoded (`F \303\205 \302\221 …`).
|
||||
- **Suspected root cause**: brush decodes input bytes to `String` with a lossy/
|
||||
Latin-1 assumption on the `read` path (or at word-splitting) instead of
|
||||
keeping raw bytes; on output the char sequence is re-encoded as UTF-8.
|
||||
POSIX shells treat variable values as byte strings.
|
||||
- **Severity**: silent data corruption (not a parse failure, so the §4.1
|
||||
bash-fallback cannot catch it). Needs a corpus case (`utf8-bytes-passthru`)
|
||||
and a byte-preservation sweep of read/expansion/heredoc paths.
|
||||
|
||||
## Closed
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "naros-init"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "narOS PID-1 supervisor for container surfaces (docs/NAROS.md §5)"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,154 @@
|
||||
//! naros-init — narOS PID-1 supervisor for container surfaces (docs/NAROS.md §5).
|
||||
//!
|
||||
//! Supervision and plumbing only, no policy:
|
||||
//! - reaps zombies (the historical job of PID 1);
|
||||
//! - forwards SIGTERM/SIGINT to supervised children;
|
||||
//! - `naros-init -- CMD ARGS…` supervises a primary command and exits with its status
|
||||
//! (129+signum on signal death), replacing `sh -c` wrappers as the container entrypoint;
|
||||
//! - with no primary command it is the keepalive that replaces ContainerEngine's
|
||||
//! sleep loop, staying alive until signalled;
|
||||
//! - NAROS_BRIDGE=1 additionally supervises the control bridge
|
||||
//! (`node /opt/nucleic/control-bridge.js`), restarting it with backoff — the bridge is
|
||||
//! best-effort transport, so its failures never affect the primary command or init.
|
||||
//!
|
||||
//! In the VM desktop flavor systemd stays PID 1 and this binary runs as a role unit.
|
||||
|
||||
use std::env;
|
||||
use std::process::{exit, Command};
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
static PENDING_SIGNAL: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
extern "C" fn on_signal(sig: libc::c_int) {
|
||||
PENDING_SIGNAL.store(sig, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn install_handlers() {
|
||||
unsafe {
|
||||
let handler = on_signal as *const () as usize;
|
||||
for sig in [libc::SIGTERM, libc::SIGINT] {
|
||||
libc::signal(sig, handler);
|
||||
}
|
||||
// SIG_DFL for SIGCHLD keeps children reapable via waitpid below.
|
||||
}
|
||||
}
|
||||
|
||||
/// Reap every exited child; returns the primary's status when it is among them.
|
||||
fn reap(primary: Option<libc::pid_t>, bridge: Option<libc::pid_t>) -> (Option<i32>, bool) {
|
||||
let mut primary_status = None;
|
||||
let mut bridge_died = false;
|
||||
loop {
|
||||
let mut status: libc::c_int = 0;
|
||||
let pid = unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) };
|
||||
if pid <= 0 {
|
||||
break;
|
||||
}
|
||||
let code = if libc::WIFEXITED(status) {
|
||||
libc::WEXITSTATUS(status)
|
||||
} else if libc::WIFSIGNALED(status) {
|
||||
128 + libc::WTERMSIG(status)
|
||||
} else {
|
||||
1
|
||||
};
|
||||
if Some(pid) == primary {
|
||||
primary_status = Some(code);
|
||||
} else if Some(pid) == bridge {
|
||||
bridge_died = true;
|
||||
}
|
||||
}
|
||||
(primary_status, bridge_died)
|
||||
}
|
||||
|
||||
fn forward(sig: i32, pids: &[Option<libc::pid_t>]) {
|
||||
for pid in pids.iter().flatten() {
|
||||
unsafe {
|
||||
libc::kill(*pid, sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_bridge() -> Option<libc::pid_t> {
|
||||
const BRIDGE: &str = "/opt/nucleic/control-bridge.js";
|
||||
if !std::path::Path::new(BRIDGE).exists() {
|
||||
return None;
|
||||
}
|
||||
match Command::new("node").arg(BRIDGE).spawn() {
|
||||
Ok(child) => Some(child.id() as libc::pid_t),
|
||||
Err(err) => {
|
||||
eprintln!("naros-init: bridge spawn failed: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.get(1).map(String::as_str) == Some("--version") {
|
||||
println!("naros-init {}", env!("CARGO_PKG_VERSION"));
|
||||
return;
|
||||
}
|
||||
|
||||
install_handlers();
|
||||
|
||||
let cmd: Vec<&String> = match args.iter().position(|a| a == "--") {
|
||||
Some(i) => args[i + 1..].iter().collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
let mut primary: Option<libc::pid_t> = None;
|
||||
if let Some((prog, rest)) = cmd.split_first() {
|
||||
match Command::new(prog).args(rest).spawn() {
|
||||
Ok(child) => primary = Some(child.id() as libc::pid_t),
|
||||
Err(err) => {
|
||||
eprintln!("naros-init: exec {prog}: {err}");
|
||||
exit(127);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let want_bridge = env::var("NAROS_BRIDGE").ok().as_deref() == Some("1");
|
||||
let mut bridge = if want_bridge { spawn_bridge() } else { None };
|
||||
let mut bridge_backoff = Duration::from_millis(500);
|
||||
let mut bridge_retry_at: Option<Instant> = None;
|
||||
|
||||
loop {
|
||||
let sig = PENDING_SIGNAL.swap(0, Ordering::SeqCst);
|
||||
if sig != 0 {
|
||||
forward(sig, &[primary, bridge]);
|
||||
if primary.is_none() {
|
||||
// Keepalive role: the signal is our own shutdown request.
|
||||
exit(128 + sig);
|
||||
}
|
||||
}
|
||||
|
||||
let (primary_status, bridge_died) = reap(primary, bridge);
|
||||
if let Some(code) = primary_status {
|
||||
forward(libc::SIGTERM, &[bridge]);
|
||||
reap(None, bridge);
|
||||
exit(code);
|
||||
}
|
||||
if bridge_died {
|
||||
bridge = None;
|
||||
bridge_retry_at = Some(Instant::now() + bridge_backoff);
|
||||
bridge_backoff = (bridge_backoff * 2).min(Duration::from_secs(30));
|
||||
}
|
||||
if want_bridge && bridge.is_none() {
|
||||
if let Some(at) = bridge_retry_at {
|
||||
if Instant::now() >= at {
|
||||
bridge = spawn_bridge();
|
||||
bridge_retry_at = None;
|
||||
if bridge.is_some() {
|
||||
bridge_backoff = Duration::from_millis(500);
|
||||
} else {
|
||||
bridge_retry_at = Some(Instant::now() + bridge_backoff);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bridge_retry_at = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user