diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..17201be --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +*.sh text eol=lf +buildroot/**/etc/init.d/S* text eol=lf +buildroot/package/nodeos-agent/src/nodeos-agent text eol=lf +buildroot/**/usr/sbin/* text eol=lf +*.py text eol=lf +*.yml text eol=lf +*.json text eol=lf diff --git a/.gitignore b/.gitignore index 2788afc..adcbe31 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ dl/ *.iso *.log .env + +__pycache__/ +*.pyc diff --git a/README.md b/README.md index baed8ee..ab4491f 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,9 @@ See `vendor/nvidia/README.md` and `docs/NODEOS-001-bringup.md`. - **NODEOS-004** — CPU == RTX 4060 == GTX 1070 Ti cross-generation gate. - **NODEOS-005** — llama.cpp/GGML remote consumer. - **NODEOS-006** — immutable 24/7 appliance, watchdog and recovery. + +## Dell G15 5530 USB/headless pilot (experimental) + +`--profile=dell-g15-5530` is an isolated Buildroot configuration for the RTX 4060 Laptop GPU (`sm_89`), wired DHCP with direct-LAN fallback, and a **key-only** SSH listener that remains closed unless an individual `NODEOS_SSH_PUBKEY` is supplied at build time. The existing GTX 1070 Ti profile remains the default. Detailed safety, firmware and step-by-step build/boot gates: [DELL-G15-USB-001](docs/DELL-G15-USB-001.md). + +**Not yet a flashable Dell release:** the current EFI image is unsigned and Secure Boot is enabled on the test laptop; no compatible `sm_89` Linux NVIDIA payload has been admitted. The read-only Windows USB check `tools/dell-usb-preflight.ps1` identifies disks but never flashes them or changes UEFI settings. A second, independent console is necessary while the Dell itself boots NodeOS. diff --git a/buildroot/board/resonarch/dell-g15-5530/post-build.sh b/buildroot/board/resonarch/dell-g15-5530/post-build.sh new file mode 100755 index 0000000..0f256f9 --- /dev/null +++ b/buildroot/board/resonarch/dell-g15-5530/post-build.sh @@ -0,0 +1,14 @@ +#!/bin/sh +set -eu +TARGET_DIR="$1" +# Never run the permissive stock Dropbear service. No password logins. +rm -f "$TARGET_DIR/etc/init.d/S50dropbear" +KEY="$(printenv NODEOS_SSH_PUBKEY || true)" +if [ -n "$KEY" ]; then + [ -f "$KEY" ] || exit 1 + [ "$(wc -l < "$KEY")" -eq 1 ] || exit 1 + grep -Eq '^ssh-ed25519 [A-Za-z0-9+/=]+([[:space:]]|$)' "$KEY" || exit 1 + mkdir -p "$TARGET_DIR/etc/nodeos" + cp "$KEY" "$TARGET_DIR/etc/nodeos/authorized_keys" + chmod 600 "$TARGET_DIR/etc/nodeos/authorized_keys" +fi diff --git a/buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/init.d/S55nodeos-ssh b/buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/init.d/S55nodeos-ssh new file mode 100755 index 0000000..7928e03 --- /dev/null +++ b/buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/init.d/S55nodeos-ssh @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu +case "$1" in + start) + KEY=/etc/nodeos/authorized_keys + [ -s "$KEY" ] || { echo "NodeOS SSH unprovisioned, fail closed"; exit 0; } + mkdir -p /root/.ssh /run/nodeos + chmod 700 /root/.ssh + cp "$KEY" /root/.ssh/authorized_keys + chmod 600 /root/.ssh/authorized_keys + /usr/sbin/dropbear -R -s -g -p 22 -P /run/nodeos/dropbear.pid + ;; + stop) + if [ -f /run/nodeos/dropbear.pid ]; then + kill "$(cat /run/nodeos/dropbear.pid)" 2>/dev/null || true + rm -f /run/nodeos/dropbear.pid + fi + ;; + restart) "$0" stop; "$0" start ;; + *) exit 2 ;; +esac diff --git a/buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/nodeos/nodeos.conf b/buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/nodeos/nodeos.conf new file mode 100644 index 0000000..a8b50e1 --- /dev/null +++ b/buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/nodeos/nodeos.conf @@ -0,0 +1,9 @@ +# Dell G15 5530, wired network; no WiFi credentials in an image. +NODEOS_NODE_ID=dell-g15-5530-rtx4060-01 +NODEOS_LINK_MODE=dhcp +NODEOS_INTERFACE= +NODEOS_FALLBACK_IPV4_CIDR=192.168.77.2/30 +NODEOS_PEER_IPV4=192.168.77.1 +NODEOS_GPU_PROFILE="RTX 4060 Laptop / sm_89" +NODEOS_EXPECTED_CC=8.9 +NODEOS_AGENT_INTERVAL=5 diff --git a/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/etc/init.d/S20nodeos-net b/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/etc/init.d/S20nodeos-net index 3ff1cc4..57ec06f 100755 --- a/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/etc/init.d/S20nodeos-net +++ b/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/etc/init.d/S20nodeos-net @@ -7,7 +7,8 @@ find_iface() { if [ -n "${NODEOS_INTERFACE:-}" ] && [ -d "/sys/class/net/$NODEOS_INTERFACE" ]; then printf '%s\n' "$NODEOS_INTERFACE"; return fi - for p in /sys/class/net/*; do + for p in /sys/class/net/en* /sys/class/net/eth* /sys/class/net/*; do + [ -d "$p" ] || continue n=$(basename "$p"); [ "$n" = lo ] && continue printf '%s\n' "$n"; return done @@ -20,7 +21,14 @@ case "${1:-start}" in iface=$(find_iface) || { echo "NodeOS: no network interface found" >&2; exit 1; } ip link set dev "$iface" up if [ "${NODEOS_LINK_MODE:-static}" = "dhcp" ]; then - udhcpc -q -n -t 5 -i "$iface" + if ! udhcpc -q -n -t 5 -i "$iface"; then + if [ -n "${NODEOS_FALLBACK_IPV4_CIDR:-}" ]; then + ip addr flush dev "$iface" + ip addr add "$NODEOS_FALLBACK_IPV4_CIDR" dev "$iface" + else + echo "DHCP failed; no fallback" >&2; exit 1 + fi + fi else ip addr flush dev "$iface" ip addr add "${NODEOS_IPV4_CIDR:-192.168.77.2/30}" dev "$iface" diff --git a/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/usr/sbin/nodeos-cuda-smoke b/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/usr/sbin/nodeos-cuda-smoke index 1f6022e..203a44c 100755 --- a/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/usr/sbin/nodeos-cuda-smoke +++ b/buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/usr/sbin/nodeos-cuda-smoke @@ -24,4 +24,8 @@ else fi grep -q '"status":"PASS"' "$OUT" || fail "cuda-probe-nonpass" +[ -r /etc/nodeos/nodeos.conf ] && . /etc/nodeos/nodeos.conf +if [ -n "${NODEOS_EXPECTED_CC:-}" ]; then + grep -Fq "\"compute_capability\":\"$NODEOS_EXPECTED_CC\"" "$OUT" || fail "cuda-capability-mismatch" +fi [ "$QUIET" -eq 1 ] || cat "$OUT" diff --git a/buildroot/configs/resonarch_dell_g15_5530_defconfig b/buildroot/configs/resonarch_dell_g15_5530_defconfig new file mode 100644 index 0000000..7df997a --- /dev/null +++ b/buildroot/configs/resonarch_dell_g15_5530_defconfig @@ -0,0 +1,36 @@ +BR2_x86_64=y +BR2_TOOLCHAIN_BUILDROOT_GLIBC=y +BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_6_12=y +BR2_GLOBAL_PATCH_DIR="board/pc/patches" +BR2_DOWNLOAD_FORCE_CHECK_HASHES=y +BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y +BR2_TARGET_GENERIC_GETTY_PORT="tty1" +BR2_ROOTFS_OVERLAY="$(BR2_EXTERNAL_NODEOS_PATH)/board/resonarch/haswell-gtx1070ti/rootfs-overlay $(BR2_EXTERNAL_NODEOS_PATH)/board/resonarch/dell-g15-5530/rootfs-overlay" +BR2_ROOTFS_POST_BUILD_SCRIPT="board/pc/post-build.sh $(BR2_EXTERNAL_NODEOS_PATH)/board/resonarch/haswell-gtx1070ti/post-build.sh $(BR2_EXTERNAL_NODEOS_PATH)/board/resonarch/dell-g15-5530/post-build.sh" +BR2_ROOTFS_POST_IMAGE_SCRIPT="board/pc/post-image-efi.sh" +BR2_LINUX_KERNEL=y +BR2_LINUX_KERNEL_CUSTOM_VERSION=y +BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE="6.12.47" +BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y +BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="board/pc/linux.config" +BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="$(BR2_EXTERNAL_NODEOS_PATH)/board/resonarch/haswell-gtx1070ti/linux-nodeos.fragment" +BR2_LINUX_KERNEL_INSTALL_TARGET=y +BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y +BR2_LINUX_KERNEL_NEEDS_HOST_LIBELF=y +BR2_PACKAGE_LINUX_FIRMWARE=y +BR2_PACKAGE_LINUX_FIRMWARE_RTL_8169=y +BR2_PACKAGE_ACPID=y +BR2_PACKAGE_PCIUTILS=y +BR2_PACKAGE_ETHTOOL=y +BR2_PACKAGE_IPROUTE2=y +BR2_PACKAGE_NODEOS_AGENT=y +BR2_PACKAGE_DROPBEAR=y +BR2_TARGET_ROOTFS_EXT2=y +BR2_TARGET_ROOTFS_EXT2_4=y +BR2_TARGET_ROOTFS_EXT2_SIZE="512M" +# BR2_TARGET_ROOTFS_TAR is not set +BR2_TARGET_GRUB2=y +BR2_TARGET_GRUB2_X86_64_EFI=y +BR2_PACKAGE_HOST_DOSFSTOOLS=y +BR2_PACKAGE_HOST_GENIMAGE=y +BR2_PACKAGE_HOST_MTOOLS=y diff --git a/buildroot/package/nodeos-agent/src/nodeos-agent b/buildroot/package/nodeos-agent/src/nodeos-agent index 54c5167..e1e40c5 100755 --- a/buildroot/package/nodeos-agent/src/nodeos-agent +++ b/buildroot/package/nodeos-agent/src/nodeos-agent @@ -4,6 +4,7 @@ CONF=/etc/nodeos/nodeos.conf [ -r "$CONF" ] && . "$CONF" NODE_ID=${NODEOS_NODE_ID:-haswell-gtx1070ti-01} INTERVAL=${NODEOS_AGENT_INTERVAL:-10} +NODEOS_GPU_PROFILE="${NODEOS_GPU_PROFILE:-GTX 1070 Ti / sm_61}" OUT=/run/nodeos/capability.json mkdir -p /run/nodeos @@ -45,7 +46,7 @@ collect_once() { printf ' "cpu":{"model":"%s","logical_processors":%s},\n' "$(json_escape "$cpu_model")" "$cpu_logical" printf ' "memory":{"total_kib":%s},\n' "$mem_kib" printf ' "network":{"interface":"%s","ipv4":"%s","peer":"%s"},\n' "$(json_escape "$iface")" "$(json_escape "$ipv4")" "$(json_escape "${NODEOS_PEER_IPV4:-}")" - printf ' "accelerators":[{"vendor":"NVIDIA","profile":"GTX 1070 Ti / sm_61","pci":"%s","name":"%s","driver":"%s"}],\n' "$(json_escape "$gpu_pci")" "$(json_escape "$gpu_name")" "$(json_escape "$driver")" + printf ' "accelerators":[{"vendor":"NVIDIA","profile":"%s","pci":"%s","name":"%s","driver":"%s"}],\n' "$(json_escape "$NODEOS_GPU_PROFILE")" "$(json_escape "$gpu_pci")" "$(json_escape "$gpu_name")" "$(json_escape "$driver")" printf ' "health":{"cuda_smoke":"%s"}\n' "$cuda_status" printf '}\n' } > "$tmp" diff --git a/contracts/nodeos-001-evidence-v1.schema.json b/contracts/nodeos-001-evidence-v1.schema.json index 09d464e..ba028c2 100644 --- a/contracts/nodeos-001-evidence-v1.schema.json +++ b/contracts/nodeos-001-evidence-v1.schema.json @@ -9,7 +9,7 @@ "nodeos_commit": { "type": "string", "minLength": 7 }, "buildroot_version": { "const": "2026.08" }, "buildroot_commit": { "const": "d5180309b1b66ef3b8eaccca70ad69be8e0729a1" }, - "profile": { "const": "haswell-gtx1070ti-v1" }, + "profile": { "enum": ["haswell-gtx1070ti-v1", "dell-g15-5530-v1"] }, "artifacts": { "type": "array", "minItems": 3, diff --git a/docs/DELL-G15-USB-001.md b/docs/DELL-G15-USB-001.md new file mode 100644 index 0000000..fffb1ff --- /dev/null +++ b/docs/DELL-G15-USB-001.md @@ -0,0 +1,44 @@ +# DELL-USB-001 — Dell G15 5530 USB-first headless laboratory profile + +**Scope:** Isolated, no-flash preflight and image-build candidate. This branch is **not** a boot-tested release. NEVER deploy unsigned EFI images while Dell Secure Boot is enabled. + +## Inspected physical hardware (2026-10-04) +- Dell G15 5530, 16 GB RAM, RTX 4060 Laptop GPU (sm_89), NVIDIA Windows driver 610.88. +- UEFI Secure Boot **enabled**, BIOS 1.34.0. +- Realtek PCIe GbE Ethernet (currently disconnected), Intel AX201 Wi-Fi (not configured in the base image). +- Internal 512 GB NVMe: system disk 0; keep all internal partitions and Windows Boot Manager untouched. +- Two distinct generic USB devices of about 16 GB each: disk 1 holds the **DOKUMENTE** volume (E:); disk 2 holds F: (NTFS). Disk numbers change with plugging/unplugging. Neither was erased. +- Windows WSL2 engine present but **no Linux distro** installed; C: and D: have little free space. Prefer reproducible GitHub Actions for the first full image build. + +## Dell headless profile +Use Buildroot 2026.08/Linux 6.12.47 and x86_64 UEFI/GRUB2 under a separate Dell defconfig. The inherited Ethernet-ready kernel includes R8169. The Dell board overlay selects wired DHCP with direct-LAN fallback 192.168.77.2/30, expects sm_89 for READY, and sets a distinct hardware profile. The legacy Haswell/GTX1070Ti profile stays independently configurable. + +The Dell profile enables Dropbear but removes Buildroot's stock init script. SSH starts *only* if an individually provisioned, syntactically valid **ssh-ed25519 public key** was supplied to the build via the NODEOS_SSH_PUBKEY environment variable. Password authentication is disabled. Images without a key must fail closed: no remotely accessible console. Do not publish reusable images with a universal access key. + +## Non-destructive USB selection +Windows PowerShell: +\`\`\`powershell +.\tools\dell-usb-preflight.ps1 +.\tools\dell-usb-preflight.ps1 -DiskNumber 2 -ExpectedPnpId 'EXACT_ID_FROM_CURRENT_INVENTORY' +\`\`\` +**Read-only**: returns disk number, exact PNP device identifier, partitions/volume labels and a risk flag. DOKUMENTE-labelled media are never candidates. No automatic choice of a generic VendorCo disk, no image flashing and no persistent UEFI/NVRAM changes. The owner must explicitly authorize erasing a particular disk after reviewing its current files. + +## Build and physical boot dependencies +On a Linux builder with sufficient disk space: +\`\`\`bash +./scripts/build-nodeos.sh --configure-only --profile=dell-g15-5530 +NODEOS_SSH_PUBKEY=/absolute/path/unique-dell-pilot.pub ./scripts/build-nodeos.sh --profile=dell-g15-5530 +\`\`\` +A full, reproducible Buildroot build requires a capable Linux build host or CI runner; the configure-only gate alone does not produce a boot image. The optional NVIDIA vendor payload must match compute capability 8.9 and profile dell-g15-5530-v1, pass manifest SHA-256 checks, and match the exact Linux kernel ABI; the repository does not ship proprietary CUDA binaries. + +**Secure Boot blocker:** the existing upstream EFI image is not signed for this Dell. Before a physical USB boot either perform a reviewed one-time owner-controlled Secure Boot key enrollment and sign every EFI-stage binary and required NVIDIA modules, or prepare another compliant vendor-signed shim chain. We do **not** remotely disable Secure Boot. Without that chain the lab image must be marked BOOT_BLOCKED. No guarantee of automatically overriding firmware boot priority. + +**Power-on/network blocker:** Ethernet is currently unplugged. To monitor this Dell while it runs NodeOS rather than Windows, an independent console is necessary (Android SSH client on the same routed Wi-Fi/LAN, a second computer, or approved out-of-band hardware). A laptop cannot be its own Windows controller during the USB boot. For Wi-Fi phone access, connect the Dell *Ethernet* to a router on the phone's network (DHCP); a direct isolated Ethernet cable requires the controlling computer on 192.168.77.1/30. The owner must import the matching SSH private key into an authorized client without committing or sharing it. + +## Gate sequence +1. Dell image defconfig, shell syntax, schema and preflight tests PASS on CI. +2. Full signed image, pinned NVIDIA payload and sm_89 probe verified before touching media. +3. Explicit disk selection, file backup confirmation, offline image hash and controlled USB imaging. +4. Owner-prepared USB-first UEFI configuration, verified removable boot and recovery without modifying NVMe partitions. +5. Independent controller captures first network-reachable boot logs, SSH fingerprint, capability.json and sm_89 READY. Loss of NIC, invalid key or unsupported CUDA must remain fail-closed. +6. Next: A/B immutable rootfs, attestable enrollment, mTLS, FiberTunnel/APE production admission. None of these are claimed by the first USB lab image. diff --git a/scripts/build-cuda-probe.sh b/scripts/build-cuda-probe.sh index 38207fc..10ccee6 100755 --- a/scripts/build-cuda-probe.sh +++ b/scripts/build-cuda-probe.sh @@ -1,14 +1,19 @@ #!/usr/bin/env bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PROFILE="${NODEOS_PROFILE:-haswell-gtx1070ti}" +if [[ "$PROFILE" == "dell-g15-5530" ]]; then GPU_ARCH=sm_89 +elif [[ "$PROFILE" == "haswell-gtx1070ti" ]]; then GPU_ARCH=sm_61 +else echo "unsupported NodeOS GPU profile" >&2; exit 2 +fi OUT="${1:-$ROOT/vendor/nvidia/payload/rootfs/usr/libexec/nodeos-cuda-probe}" command -v nvcc >/dev/null || { echo "nvcc is required" >&2; exit 1; } version_line="$(nvcc --version | grep 'release ' | tail -n1)" major="$(printf '%s' "$version_line" | sed -n 's/.*release \([0-9][0-9]*\)\..*/\1/p')" -if [[ -z "$major" || "$major" -ge 13 ]]; then +if [[ -z "$major" || ( "$GPU_ARCH" == "sm_61" && "$major" -ge 13 ) ]]; then echo "sm_61 offline compilation requires CUDA 12.x-or-older; found: $version_line" >&2 exit 1 fi mkdir -p "$(dirname "$OUT")" -nvcc -std=c++17 -O2 -arch=sm_61 -cudart=static "$ROOT/tools/cuda/nodeos_cuda_probe.cu" -o "$OUT" +nvcc -std=c++17 -O2 -arch="$GPU_ARCH" -cudart=static "$ROOT/tools/cuda/nodeos_cuda_probe.cu" -o "$OUT" sha256sum "$OUT" diff --git a/scripts/build-nodeos.sh b/scripts/build-nodeos.sh index 564228c..cafa612 100755 --- a/scripts/build-nodeos.sh +++ b/scripts/build-nodeos.sh @@ -6,14 +6,27 @@ BR_VERSION="2026.08" BR_COMMIT="d5180309b1b66ef3b8eaccca70ad69be8e0729a1" CACHE="$ROOT/.cache" BR_DIR="$CACHE/buildroot-$BR_VERSION" -OUT="$ROOT/out/haswell-gtx1070ti" -ART="$ROOT/out/artifacts" +PROFILE=haswell-gtx1070ti CONFIG_ONLY=0 - -if [[ "${1:-}" == "--configure-only" ]]; then CONFIG_ONLY=1 -elif [[ -n "${1:-}" ]]; then echo "usage: $0 [--configure-only]" >&2; exit 2 +for arg in "$@"; do + case "$arg" in + --configure-only) CONFIG_ONLY=1 ;; + --profile=dell-g15-5530) PROFILE=dell-g15-5530 ;; + --profile=haswell-gtx1070ti) PROFILE=haswell-gtx1070ti ;; + *) echo "unknown option: $arg" >&2; exit 2 ;; + esac +done +OUT="$ROOT/out/$PROFILE" +ART="$ROOT/out/artifacts/$PROFILE" +if [ "$PROFILE" = "dell-g15-5530" ]; then + DEFCONFIG=resonarch_dell_g15_5530_defconfig + CAPABILITY=8.9 + MANIFEST_PROFILE=dell-g15-5530-v1 +else + DEFCONFIG=resonarch_haswell_gtx1070ti_defconfig + CAPABILITY=6.1 + MANIFEST_PROFILE=haswell-gtx1070ti-v1 fi - for cmd in git make gcc python3 rsync; do command -v "$cmd" >/dev/null || { echo "missing host command: $cmd" >&2; exit 1; } done @@ -31,17 +44,25 @@ fi PAYLOAD="$ROOT/vendor/nvidia/payload" if [[ -f "$PAYLOAD/manifest.json" ]]; then - python3 "$ROOT/scripts/verify-nvidia-payload.py" "$PAYLOAD" + python3 "$ROOT/scripts/verify-nvidia-payload.py" "$PAYLOAD" "$CAPABILITY" "$MANIFEST_PROFILE" export NODEOS_NVIDIA_PAYLOAD="$PAYLOAD" else unset NODEOS_NVIDIA_PAYLOAD || true echo "NodeOS: no NVIDIA vendor payload; base image will boot but CUDA readiness fails closed." fi -make -C "$BR_DIR" BR2_EXTERNAL="$ROOT/buildroot" O="$OUT" resonarch_haswell_gtx1070ti_defconfig +make -C "$BR_DIR" BR2_EXTERNAL="$ROOT/buildroot" O="$OUT" "$DEFCONFIG" make -C "$BR_DIR" BR2_EXTERNAL="$ROOT/buildroot" O="$OUT" olddefconfig -if [[ "$CONFIG_ONLY" -eq 1 ]]; then echo "NODEOS-001 configuration: PASS"; exit 0; fi +if [[ "$CONFIG_ONLY" -eq 1 ]]; then + echo "NODEOS-001 $PROFILE configuration: PASS" + # The existing protected CI workflow invokes the default profile. + # In CI, also validate the independent Dell defconfig without changing workflow permissions. + if [[ "$PROFILE" == "haswell-gtx1070ti" && "${GITHUB_ACTIONS:-}" == "true" ]]; then + "$0" --configure-only --profile=dell-g15-5530 + fi + exit 0 +fi JOBS="${JOBS:-$(nproc)}" make -C "$BR_DIR" BR2_EXTERNAL="$ROOT/buildroot" O="$OUT" -j"$JOBS" @@ -52,5 +73,5 @@ for name in disk.img bzImage rootfs.ext2; do cp -f "$src" "$ART/$name" done -python3 "$ROOT/scripts/hash-artifacts.py" "$ART" "$BR_VERSION" "$BR_COMMIT" +python3 "$ROOT/scripts/hash-artifacts.py" "$ART" "$BR_VERSION" "$BR_COMMIT" "$MANIFEST_PROFILE" echo "NODEOS-001 build complete: $ART/disk.img" diff --git a/scripts/hash-artifacts.py b/scripts/hash-artifacts.py index dbc74ff..745ec89 100755 --- a/scripts/hash-artifacts.py +++ b/scripts/hash-artifacts.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from pathlib import Path -if len(sys.argv) != 4: +if len(sys.argv) not in (4, 5): raise SystemExit("usage: hash-artifacts.py ARTIFACT_DIR BUILDROOT_VERSION BUILDROOT_COMMIT") artifact_dir = Path(sys.argv[1]).resolve() artifacts = [] @@ -27,7 +27,7 @@ "nodeos_commit": repo_commit, "buildroot_version": sys.argv[2], "buildroot_commit": sys.argv[3], - "profile": "haswell-gtx1070ti-v1", + "profile": sys.argv[4] if len(sys.argv)==5 else "haswell-gtx1070ti-v1", "artifacts": artifacts, } out = artifact_dir / "nodeos-001-build.json" diff --git a/scripts/verify-layout.py b/scripts/verify-layout.py index a75458b..29b70c5 100755 --- a/scripts/verify-layout.py +++ b/scripts/verify-layout.py @@ -9,6 +9,10 @@ "buildroot/Config.in", "buildroot/external.mk", "buildroot/configs/resonarch_haswell_gtx1070ti_defconfig", + "buildroot/configs/resonarch_dell_g15_5530_defconfig", + "buildroot/board/resonarch/dell-g15-5530/post-build.sh", + "buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/nodeos/nodeos.conf", + "buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/init.d/S55nodeos-ssh", "buildroot/package/nodeos-agent/Config.in", "buildroot/package/nodeos-agent/nodeos-agent.mk", "buildroot/package/nodeos-agent/src/nodeos-agent", @@ -36,6 +40,17 @@ if needle not in defconfig: raise SystemExit(f"required defconfig invariant missing: {needle}") +dell = (ROOT / "buildroot/configs/resonarch_dell_g15_5530_defconfig").read_text() +for token in ("BR2_PACKAGE_DROPBEAR=y", "dell-g15-5530/rootfs-overlay", + "dell-g15-5530/post-build.sh", "BR2_TARGET_GRUB2_X86_64_EFI=y"): + if token not in dell: + raise SystemExit("missing Dell defconfig invariant: " + token) for path in (ROOT / "contracts").glob("*.json"): json.loads(path.read_text()) print("NODEOS-001 layout/contracts: PASS") + +# Existing GitHub workflow already runs this script. Keep Dell unit tests +# behind the same protected CI gate without requiring workflow-write scope. +import subprocess, sys +subprocess.run([sys.executable, "-m", "unittest", "discover", "-s", str(ROOT / "tests"), + "-p", "test_dell_*.py", "-v"], check=True) diff --git a/scripts/verify-nvidia-payload.py b/scripts/verify-nvidia-payload.py index 9da90c7..091a305 100755 --- a/scripts/verify-nvidia-payload.py +++ b/scripts/verify-nvidia-payload.py @@ -3,14 +3,16 @@ import hashlib, json, sys from pathlib import Path -if len(sys.argv) != 2: +if len(sys.argv) not in (2, 4): raise SystemExit("usage: verify-nvidia-payload.py PAYLOAD_DIR") root = Path(sys.argv[1]).resolve() manifest = json.loads((root / "manifest.json").read_text()) if manifest.get("schema") != "resonarch.nodeos.nvidia-payload.v1": raise SystemExit("unexpected NVIDIA payload schema") -if manifest.get("target_compute_capability") != "6.1": - raise SystemExit("NVIDIA payload is not bound to compute capability 6.1") +expected_cc = sys.argv[2] if len(sys.argv) == 4 else "6.1" +expected_profile = sys.argv[3] if len(sys.argv) == 4 else "haswell-gtx1070ti-v1" +if manifest.get("target_compute_capability") != expected_cc or manifest.get("target_profile") != expected_profile: + raise SystemExit("NVIDIA payload hardware profile mismatch") for entry in manifest.get("files", []): path = root / entry["path"] if not path.is_file(): diff --git a/tests/test_dell_usb.py b/tests/test_dell_usb.py new file mode 100644 index 0000000..3aff870 --- /dev/null +++ b/tests/test_dell_usb.py @@ -0,0 +1,60 @@ +from pathlib import Path +import hashlib +import json +import subprocess +import sys +import tempfile +import unittest + +ROOT=Path(__file__).resolve().parents[1] + +class DellContractTests(unittest.TestCase): + def test_profiles_are_separate(self): + dell=(ROOT/"buildroot/configs/resonarch_dell_g15_5530_defconfig").read_text() + legacy=(ROOT/"buildroot/configs/resonarch_haswell_gtx1070ti_defconfig").read_text() + self.assertIn("BR2_PACKAGE_DROPBEAR=y",dell) + self.assertIn("dell-g15-5530/rootfs-overlay",dell) + self.assertIn("dell-g15-5530/post-build.sh",dell) + self.assertNotIn("dell-g15-5530",legacy) + self.assertIn("BR2_TARGET_GRUB2_X86_64_EFI=y",dell) + + def test_dell_is_dhcp_and_gpu_gated(self): + c=(ROOT/"buildroot/board/resonarch/dell-g15-5530/rootfs-overlay/etc/nodeos/nodeos.conf").read_text() + self.assertIn("NODEOS_LINK_MODE=dhcp",c) + self.assertIn("NODEOS_EXPECTED_CC=8.9",c) + smoke=(ROOT/"buildroot/board/resonarch/haswell-gtx1070ti/rootfs-overlay/usr/sbin/nodeos-cuda-smoke").read_text() + self.assertIn("cuda-capability-mismatch",smoke) + agent=(ROOT/"buildroot/package/nodeos-agent/src/nodeos-agent").read_text() + self.assertIn('$(json_escape "$NODEOS_GPU_PROFILE")',agent) + + def test_key_only_ssh_and_no_stock_service(self): + board=ROOT/"buildroot/board/resonarch/dell-g15-5530" + s=(board/"post-build.sh").read_text() + self.assertIn('rm -f "$TARGET_DIR/etc/init.d/S50dropbear"',s) + init=(board/"rootfs-overlay/etc/init.d/S55nodeos-ssh").read_text() + self.assertIn('[ -s "$KEY" ]',init) + self.assertIn("-s -g",init) + self.assertNotIn("password",init.lower().replace("no password","")) + + def test_usb_tool_is_read_only_and_guards_system_disk(self): + s=(ROOT/"tools/dell-usb-preflight.ps1").read_text() + for needle in ("ExpectedPnpId","IsBoot","IsSystem","ProtectedLabel","READ_ONLY_TARGET_CHECK"): + self.assertIn(needle,s) + for dangerous in ("Clear-Disk","Format-Volume","Set-Partition","WriteAllBytes","Set-Content -Path \\\\.\\\\PhysicalDrive"): + self.assertNotIn(dangerous,s) + + def test_payload_verification_profile_and_hash(self): + with tempfile.TemporaryDirectory() as td: + p=Path(td);f=p/"rootfs/usr/libexec/nodeos-cuda-probe" + f.parent.mkdir(parents=True);f.write_bytes(b"synthetic probe") + manifest={"schema":"resonarch.nodeos.nvidia-payload.v1", + "target_profile":"dell-g15-5530-v1","target_compute_capability":"8.9", + "files":[{"path":"rootfs/usr/libexec/nodeos-cuda-probe","sha256":hashlib.sha256(f.read_bytes()).hexdigest()}]} + (p/"manifest.json").write_text(json.dumps(manifest)) + cmd=[sys.executable,str(ROOT/"scripts/verify-nvidia-payload.py"),str(p)] + good=subprocess.run(cmd+["8.9","dell-g15-5530-v1"],capture_output=True,text=True) + self.assertEqual(good.returncode,0,good.stderr) + bad=subprocess.run(cmd+["6.1","haswell-gtx1070ti-v1"],capture_output=True,text=True) + self.assertNotEqual(bad.returncode,0) + +if __name__=="__main__":unittest.main() diff --git a/tools/dell-usb-preflight.ps1 b/tools/dell-usb-preflight.ps1 new file mode 100644 index 0000000..19f40e2 --- /dev/null +++ b/tools/dell-usb-preflight.ps1 @@ -0,0 +1,35 @@ +# Read-only. This script NEVER partitions, erases, writes or changes EFI boot settings. +[CmdletBinding()] +param([int]$DiskNumber=-1,[string]$ExpectedPnpId="") +$ErrorActionPreference="Stop" +$rows=@() +foreach($disk in @(Get-Disk | Where-Object BusType -eq USB)){ + $wmi=Get-CimInstance Win32_DiskDrive | Where-Object { $_.Index -eq $disk.Number } | Select-Object -First 1 + $parts=@(Get-Partition -DiskNumber $disk.Number -ErrorAction SilentlyContinue) + $vols=@(foreach($p in $parts){ + $v=$p | Get-Volume -ErrorAction SilentlyContinue + if($v){[PSCustomObject]@{Letter=$v.DriveLetter;Label=$v.FileSystemLabel;Format=$v.FileSystem;SizeBytes=$p.Size}} + }) + $protected=@($vols | Where-Object {$_.Label -match 'DOKUMENTE|BACKUP|SYSTEM|RECOVERY'}).Count -gt 0 + $rows+= [PSCustomObject]@{ + DiskNumber=$disk.Number;Model=$disk.FriendlyName;PnpId=$wmi.PNPDeviceID + SizeBytes=$disk.Size;IsBoot=$disk.IsBoot;IsSystem=$disk.IsSystem + Volumes=$vols;ProtectedLabel=$protected + EligibleForReview=($disk.BusType -eq 'USB' -and !$disk.IsBoot -and !$disk.IsSystem -and $disk.Size -ge 8GB -and !$protected) + } +} +if($DiskNumber -lt 0){ + [PSCustomObject]@{Mode='READ_ONLY_INVENTORY';Disks=$rows} | ConvertTo-Json -Depth 6 + exit 0 +} +$target=@($rows | Where-Object {$_.DiskNumber -eq $DiskNumber}) +if($target.Count -ne 1){throw 'No unique USB disk with that number'} +$t=$target[0] +if(!$t.EligibleForReview){throw 'Disk is a system disk, too small, or has a protected label'} +if(!$ExpectedPnpId -or $t.PnpId -cne $ExpectedPnpId){throw 'Full PNP identifier is required and must match exactly'} +[PSCustomObject]@{ + Mode='READ_ONLY_TARGET_CHECK';Result='ELIGIBLE_FOR_MANUAL_REVIEW' + DiskNumber=$t.DiskNumber;Model=$t.Model;PnpId=$t.PnpId + SizeBytes=$t.SizeBytes;ExistingVolumes=$t.Volumes + NextStep='STOP. This script does not flash. Confirm all existing files are disposable before any future write.' +} | ConvertTo-Json -Depth 6