From 5b0bdb3ca02bb0d31b224ce66ee70862b5f3b8eb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:31:17 +0200 Subject: [PATCH 001/719] exp: add Proxmox real-system KVM RDTE --- scripts/gha_kvm_proxmox_rdte.sh | 226 ++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 scripts/gha_kvm_proxmox_rdte.sh diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh new file mode 100644 index 000000000..279cabfd5 --- /dev/null +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash +set -euo pipefail + +ISO_NAME="proxmox-ve_9.2-1.iso" +ISO_URL="https://enterprise.proxmox.com/iso/$ISO_NAME" +ISO_SHA256="4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c" +RAM_MIB=4096 +VCPUS=2 +DISK_SIZE="40G" +MIN_HOST_MEM_KIB=$((6 * 1024 * 1024)) +MIN_HOST_FREE_KIB=$((24 * 1024 * 1024)) + +usage() { + echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR]" +} + +OUT="" +STATE_DIR="" +while [[ $# -gt 0 ]]; do + case "$1" in + --out) OUT="$2"; shift 2 ;; + --state-dir) STATE_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; + esac +done +[[ -n "$OUT" ]] || { usage >&2; exit 2; } + +if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-proxmox.XXXXXX)"; fi +mkdir -p "$STATE_DIR" "$(dirname "$OUT")" +STATE_DIR="$(realpath "$STATE_DIR")" +OUT="$(realpath -m "$OUT")" +[[ "$OUT" != "$STATE_DIR/"* ]] || { echo "receipt must be outside disposable state" >&2; exit 2; } + +QEMU_PID="" +cleanup() { + set +e + if [[ -n "$QEMU_PID" ]]; then + sudo -n kill "$QEMU_PID" >/dev/null 2>&1 || true + sleep 1 + sudo -n kill -9 "$QEMU_PID" >/dev/null 2>&1 || true + fi + rm -rf -- "$STATE_DIR" +} +trap cleanup EXIT INT TERM + +write_receipt() { + local classification="$1" oracle="$2" phase="$3" detail="$4" + local serial_tail="" + if [[ -f "$STATE_DIR/serial.log" ]]; then + serial_tail="$(tail -n 80 "$STATE_DIR/serial.log" | tr -d '\000' | sed -E 's/[^[:print:]\t]//g' | tail -c 12000)" + fi + export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" + export R_SERIAL="$serial_tail" R_ISO_SHA="\${OBSERVED_ISO_SHA:-}" R_API="\${API_VERSION_JSON:-}" R_NESTED="\${NESTED_KVM:-unknown}" + python3 - <<'PY' +import json, os, pathlib +payload = { + "contract": "gha-kvm-system-lab/v1", + "target": {"product": "proxmox-ve", "version": "9.2-1"}, + "classification": os.environ["R_CLASS"], + "oracleSatisfied": os.environ["R_ORACLE"].lower() == "true", + "phase": os.environ["R_PHASE"], + "detail": os.environ["R_DETAIL"], + "requested_shape": {"vcpus": 2, "ram_mib": 4096, "disk": "40G"}, + "source": { + "iso_name": "proxmox-ve_9.2-1.iso", + "iso_url": "https://enterprise.proxmox.com/iso/proxmox-ve_9.2-1.iso", + "expected_sha256": "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", + "observed_sha256": os.environ.get("R_ISO_SHA") or None, + }, + "oracles": { + "vendor_iso_digest": bool(os.environ.get("R_ISO_SHA")), + "unattended_install_completed": os.environ["R_PHASE"] in {"installed-api", "complete"}, + "installed_https_api": bool(os.environ.get("R_API")), + "nested_kvm_observed_via_ssh": os.environ.get("R_NESTED") == "yes", + }, + "api_version": json.loads(os.environ["R_API"]) if os.environ.get("R_API") else None, + "nested_kvm": os.environ.get("R_NESTED"), + "serial_tail": os.environ.get("R_SERIAL", ""), + "limitations": [ + "Disposable virtual-hardware target profile; not physical-HBA/SMART/IPMI/HA qualification.", + "Nested KVM is a separate oracle from Proxmox management-plane support.", + ], +} +pathlib.Path(os.environ["R_OUT"]).write_text(json.dumps(payload, indent=2, sort_keys=True)+"\n", encoding="utf-8") +PY +} + +fail_evidence() { + local class="$1" phase="$2" detail="$3" + write_receipt "$class" false "$phase" "$detail" + echo "$class: $phase: $detail" >&2 + exit 0 +} + +for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do + command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" +done +[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || fail_evidence ENVIRONMENT_FAILURE preflight "requires Linux x86_64" +[[ -e /dev/kvm ]] || fail_evidence ENVIRONMENT_FAILURE preflight "/dev/kvm absent" +sudo -n test -r /dev/kvm && sudo -n test -w /dev/kvm || fail_evidence ENVIRONMENT_FAILURE preflight "passwordless sudo KVM boundary unavailable" + +MEM_AVAIL_KIB="$(awk '/MemAvailable:/ {print $2}' /proc/meminfo)" +FREE_KIB="$(df -Pk "$STATE_DIR" | awk 'NR==2 {print $4}')" +(( MEM_AVAIL_KIB >= MIN_HOST_MEM_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host memory headroom below 6 GiB" +(( FREE_KIB >= MIN_HOST_FREE_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host disk headroom below 24 GiB" + +ISO="$STATE_DIR/$ISO_NAME" +curl --fail --location --retry 3 --silent --show-error "$ISO_URL" -o "$ISO" || fail_evidence ENVIRONMENT_FAILURE acquire "vendor ISO download failed" +OBSERVED_ISO_SHA="$(sha256sum "$ISO" | awk '{print $1}')" +[[ "$OBSERVED_ISO_SHA" == "$ISO_SHA256" ]] || fail_evidence ORACLE_FAILURE acquire "vendor ISO digest mismatch" + +cat >"$STATE_DIR/answer.toml" <<'EOF' +[global] +keyboard = "en-us" +country = "us" +fqdn = "pve-rdte.example.invalid" +mailto = "rdte@example.invalid" +timezone = "Etc/UTC" +root-password = "rdte-proxmox-ephemeral-9264" + +[network] +source = "from-dhcp" + +[disk-setup] +filesystem = "ext4" +disk-list = ["sda"] +EOF +printf 'mode = "iso"\n' >"$STATE_DIR/auto-installer-mode.toml" + +xorriso -osirrox on -indev "$ISO" -extract /boot/grub/grub.cfg "$STATE_DIR/grub.cfg" >/dev/null 2>&1 || + fail_evidence HARNESS_FAILURE prepare "could not extract Proxmox GRUB config" +python3 - "$STATE_DIR/grub.cfg" <<'PY' +import pathlib, sys +p=pathlib.Path(sys.argv[1]) +s=p.read_text(encoding="utf-8") +lines=[] +changed=False +for line in s.splitlines(): + if "proxmox-start-auto-installer" in line and line.lstrip().startswith("linux") and "console=ttyS0" not in line: + line += " console=ttyS0,115200" + changed=True + lines.append(line) +if not changed: + raise SystemExit("automated installer kernel line not found") +p.write_text("\n".join(lines)+"\n", encoding="utf-8") +PY + +AUTO_ISO="$STATE_DIR/proxmox-auto.iso" +xorriso -indev "$ISO" -outdev "$AUTO_ISO" -boot_image any replay \ + -map "$STATE_DIR/auto-installer-mode.toml" /auto-installer-mode.toml \ + -map "$STATE_DIR/answer.toml" /answer.toml \ + -map "$STATE_DIR/grub.cfg" /boot/grub/grub.cfg \ + -commit >/dev/null 2>"$STATE_DIR/xorriso.log" || + fail_evidence HARNESS_FAILURE prepare "failed to construct unattended Proxmox ISO" + +qemu-img create -q -f qcow2 "$STATE_DIR/system.qcow2" "$DISK_SIZE" +SSH_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" +WEB_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" + +start_qemu() { + local with_iso="$1" + : >"$STATE_DIR/serial.log" + local args=( + -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" + -device virtio-scsi-pci,id=scsi0 + -drive "file=$STATE_DIR/system.qcow2,if=none,format=qcow2,id=system" + -device scsi-hd,drive=system + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$SSH_PORT-:22,hostfwd=tcp:127.0.0.1:$WEB_PORT-:8006" + -device e1000,netdev=net0 + -display none -monitor none + -serial "file:$STATE_DIR/serial.log" + -daemonize -pidfile "$STATE_DIR/qemu.pid" + ) + if [[ "$with_iso" == yes ]]; then args+=( -cdrom "$AUTO_ISO" -boot order=d ); else args+=( -boot order=c ); fi + sudo -n qemu-system-x86_64 "\${args[@]}" + QEMU_PID="$(sudo -n cat "$STATE_DIR/qemu.pid")" +} +stop_qemu() { + if [[ -n "$QEMU_PID" ]]; then + sudo -n kill "$QEMU_PID" >/dev/null 2>&1 || true + for _ in $(seq 1 30); do sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1 || break; sleep 0.25; done + sudo -n kill -9 "$QEMU_PID" >/dev/null 2>&1 || true + QEMU_PID="" + fi +} + +start_qemu yes +INSTALL_OK=false +for _ in $(seq 1 360); do + if grep -Eqi 'Installation done|installation finished|powering off|rebooting' "$STATE_DIR/serial.log" 2>/dev/null; then INSTALL_OK=true; break; fi + if ! sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then break; fi + sleep 5 +done +[[ "$INSTALL_OK" == true ]] || fail_evidence ORACLE_FAILURE install "unattended installer did not reach completion marker within bounded window" +stop_qemu + +start_qemu no +API_VERSION_JSON="" +for _ in $(seq 1 120); do + API_VERSION_JSON="$(curl -sk --max-time 3 "https://127.0.0.1:$WEB_PORT/api2/json/version" 2>/dev/null || true)" + if [[ "$API_VERSION_JSON" == *'"data"'* ]]; then break; fi + sleep 3 +done +[[ "$API_VERSION_JSON" == *'"data"'* ]] || fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer" + +NESTED_KVM="unknown" +if command -v sshpass >/dev/null 2>&1; then + if sshpass -p 'rdte-proxmox-ephemeral-9264' ssh -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 'test -e /dev/kvm && grep -Eq "(vmx|svm)" /proc/cpuinfo' >/dev/null 2>&1; then + NESTED_KVM="yes" + else + NESTED_KVM="no" + fi +fi + +write_receipt SUPPORTED true complete "real Proxmox VE unattended install completed and installed HTTPS API answered" From 79d895b4a32d778c9d0a4a32789abb4d77ac505b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:32:19 +0200 Subject: [PATCH 002/719] exp: add TrueNAS installer KVM RDTE --- scripts/gha_kvm_truenas_rdte.sh | 182 ++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 scripts/gha_kvm_truenas_rdte.sh diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh new file mode 100644 index 000000000..3bb728d91 --- /dev/null +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +set -euo pipefail + +VERSION="26.0.0-BETA.3" +ISO_NAME="TrueNAS-26.0.0-BETA.3.iso" +BASE_URL="https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3" +ISO_URL="$BASE_URL/$ISO_NAME" +SHA_URL="$ISO_URL.sha256" +RAM_MIB=8192 +VCPUS=2 +DISK_SIZE="24G" +MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) +MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) + +usage() { + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR]" +} + +OUT="" +STATE_DIR="" +while [[ $# -gt 0 ]]; do + case "$1" in + --out) OUT="$2"; shift 2 ;; + --state-dir) STATE_DIR="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; + esac +done +[[ -n "$OUT" ]] || { usage >&2; exit 2; } + +if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi +mkdir -p "$STATE_DIR" "$(dirname "$OUT")" +STATE_DIR="$(realpath "$STATE_DIR")" +OUT="$(realpath -m "$OUT")" +[[ "$OUT" != "$STATE_DIR/"* ]] || { echo "receipt must be outside disposable state" >&2; exit 2; } + +QEMU_PID="" +cleanup() { + set +e + if [[ -n "$QEMU_PID" ]]; then + sudo -n kill "$QEMU_PID" >/dev/null 2>&1 || true + sleep 1 + sudo -n kill -9 "$QEMU_PID" >/dev/null 2>&1 || true + fi + rm -rf -- "$STATE_DIR" +} +trap cleanup EXIT INT TERM + +write_receipt() { + local classification="$1" oracle="$2" phase="$3" detail="$4" + local serial_tail="" + if [[ -f "$STATE_DIR/serial.log" ]]; then + serial_tail="$(tail -n 120 "$STATE_DIR/serial.log" | tr -d '\000' | sed -E 's/[^[:print:]\t]//g' | tail -c 16000)" + fi + export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" + export R_SERIAL="$serial_tail" R_ISO_SHA="\${OBSERVED_ISO_SHA:-}" R_EXPECTED="\${EXPECTED_ISO_SHA:-}" R_GRUB="\${GRUB_PATH:-}" + python3 - <<'PY' +import json, os, pathlib +payload = { + "contract": "gha-kvm-system-lab/v1", + "target": {"product": "truenas", "version": "26.0.0-BETA.3", "rung": "T0"}, + "classification": os.environ["R_CLASS"], + "oracleSatisfied": os.environ["R_ORACLE"].lower() == "true", + "phase": os.environ["R_PHASE"], + "detail": os.environ["R_DETAIL"], + "requested_shape": {"vcpus": 2, "ram_mib": 8192, "boot_disk": "24G"}, + "source": { + "iso_name": "TrueNAS-26.0.0-BETA.3.iso", + "iso_url": "https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3/TrueNAS-26.0.0-BETA.3.iso", + "vendor_sha256_url": "https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3/TrueNAS-26.0.0-BETA.3.iso.sha256", + "expected_sha256": os.environ.get("R_EXPECTED") or None, + "observed_sha256": os.environ.get("R_ISO_SHA") or None, + }, + "installer_grub_path": os.environ.get("R_GRUB") or None, + "serial_tail": os.environ.get("R_SERIAL", ""), + "limitations": [ + "T0 proves only pinned vendor media integrity and installer-environment boot under the disposable virtual target profile.", + "ZFS, middleware, Apps, and application lifecycle remain separate T1-T6 gates.", + "This does not qualify physical storage controllers, SMART, GPU, IPMI, or HA behavior.", + ], +} +pathlib.Path(os.environ["R_OUT"]).write_text(json.dumps(payload, indent=2, sort_keys=True)+"\n", encoding="utf-8") +PY +} + +fail_evidence() { + local class="$1" phase="$2" detail="$3" + write_receipt "$class" false "$phase" "$detail" + echo "$class: $phase: $detail" >&2 + exit 0 +} + +for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do + command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" +done +[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || fail_evidence ENVIRONMENT_FAILURE preflight "requires Linux x86_64" +[[ -e /dev/kvm ]] || fail_evidence ENVIRONMENT_FAILURE preflight "/dev/kvm absent" +sudo -n test -r /dev/kvm && sudo -n test -w /dev/kvm || fail_evidence ENVIRONMENT_FAILURE preflight "passwordless sudo KVM boundary unavailable" + +MEM_AVAIL_KIB="$(awk '/MemAvailable:/ {print $2}' /proc/meminfo)" +FREE_KIB="$(df -Pk "$STATE_DIR" | awk 'NR==2 {print $4}')" +(( MEM_AVAIL_KIB >= MIN_HOST_MEM_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host memory headroom below 11 GiB required before allocating 8 GiB guest" +(( FREE_KIB >= MIN_HOST_FREE_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host disk headroom below 28 GiB" + +ISO="$STATE_DIR/$ISO_NAME" +curl --fail --location --retry 3 --silent --show-error "$SHA_URL" -o "$STATE_DIR/vendor.sha256" || + fail_evidence ENVIRONMENT_FAILURE acquire "vendor SHA256 sidecar download failed" +EXPECTED_ISO_SHA="$(grep -Eo '[0-9a-fA-F]{64}' "$STATE_DIR/vendor.sha256" | head -n1 | tr 'A-F' 'a-f')" +[[ "$EXPECTED_ISO_SHA" =~ ^[0-9a-f]{64}$ ]] || fail_evidence HARNESS_FAILURE acquire "vendor SHA256 sidecar did not contain a digest" +curl --fail --location --retry 3 --silent --show-error "$ISO_URL" -o "$ISO" || + fail_evidence ENVIRONMENT_FAILURE acquire "vendor ISO download failed" +OBSERVED_ISO_SHA="$(sha256sum "$ISO" | awk '{print $1}')" +[[ "$OBSERVED_ISO_SHA" == "$EXPECTED_ISO_SHA" ]] || fail_evidence ORACLE_FAILURE acquire "vendor ISO digest mismatch" + +GRUB_PATH="" +for candidate in /boot/grub/grub.cfg /EFI/BOOT/grub.cfg /efi/boot/grub.cfg; do + if xorriso -osirrox on -indev "$ISO" -extract "$candidate" "$STATE_DIR/grub.cfg" >/dev/null 2>&1; then + GRUB_PATH="$candidate" + break + fi +done +[[ -n "$GRUB_PATH" ]] || fail_evidence HARNESS_FAILURE prepare "could not locate installer GRUB config in pinned ISO" + +python3 - "$STATE_DIR/grub.cfg" <<'PY' +import pathlib, sys +p=pathlib.Path(sys.argv[1]) +s=p.read_text(encoding="utf-8", errors="replace") +prefix="""insmod serial +serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1 +terminal_input serial +terminal_output serial +set timeout=0 +""" +lines=[] +changed=False +for line in s.splitlines(): + stripped=line.lstrip() + if (stripped.startswith("linux ") or stripped.startswith("linuxefi ")) and "console=ttyS0" not in line: + line += " console=ttyS0,115200" + changed=True + lines.append(line) +if not changed: + raise SystemExit("no Linux kernel command line found in GRUB config") +p.write_text(prefix + "\n".join(lines)+"\n", encoding="utf-8") +PY + +SERIAL_ISO="$STATE_DIR/truenas-serial.iso" +xorriso -indev "$ISO" -outdev "$SERIAL_ISO" -boot_image any replay \ + -map "$STATE_DIR/grub.cfg" "$GRUB_PATH" -commit >/dev/null 2>"$STATE_DIR/xorriso.log" || + fail_evidence HARNESS_FAILURE prepare "failed to construct serial-observable TrueNAS ISO" + +qemu-img create -q -f qcow2 "$STATE_DIR/boot.qcow2" "$DISK_SIZE" +: >"$STATE_DIR/serial.log" +sudo -n qemu-system-x86_64 \ + -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ + -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ + -cdrom "$SERIAL_ISO" -boot order=d \ + -netdev user,id=net0 -device virtio-net-pci,netdev=net0 \ + -display none -monitor none \ + -serial "file:$STATE_DIR/serial.log" \ + -daemonize -pidfile "$STATE_DIR/qemu.pid" || + fail_evidence ENVIRONMENT_FAILURE boot "QEMU could not start TrueNAS guest" +QEMU_PID="$(sudo -n cat "$STATE_DIR/qemu.pid")" + +BOOT_OK=false +for _ in $(seq 1 120); do + if grep -Eqi 'TrueNAS|truenas-installer|TrueNAS Installer|Install/Upgrade' "$STATE_DIR/serial.log" 2>/dev/null; then + BOOT_OK=true + break + fi + if ! sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then break; fi + sleep 3 +done + +if [[ "$BOOT_OK" == true ]]; then + write_receipt SUPPORTED true installer-boot "pinned TrueNAS vendor ISO reached a TrueNAS-labelled installer environment over serial" +else + if grep -Eqi 'Linux version|systemd|Starting' "$STATE_DIR/serial.log" 2>/dev/null; then + fail_evidence INCONCLUSIVE installer-boot "Linux boot was observed but a TrueNAS-specific installer marker was not reached" + fi + fail_evidence ORACLE_FAILURE installer-boot "no bounded installer-environment boot oracle was observed" +fi From bb88b0a74e1570d4b490bc1c730ee8716dc9f84d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:32:52 +0200 Subject: [PATCH 003/719] fix: correct system RDTE shell expansions --- scripts/gha_kvm_proxmox_rdte.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 279cabfd5..9879622d3 100644 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -33,6 +33,9 @@ OUT="$(realpath -m "$OUT")" [[ "$OUT" != "$STATE_DIR/"* ]] || { echo "receipt must be outside disposable state" >&2; exit 2; } QEMU_PID="" +OBSERVED_ISO_SHA="" +API_VERSION_JSON="" +NESTED_KVM="unknown" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then From 98e5aa3df1962ac061c2a0dda2a954c8e85b3fae Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:32:56 +0200 Subject: [PATCH 004/719] fix: correct system RDTE shell expansions --- scripts/gha_kvm_truenas_rdte.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 3bb728d91..4b77200d9 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -35,6 +35,9 @@ OUT="$(realpath -m "$OUT")" [[ "$OUT" != "$STATE_DIR/"* ]] || { echo "receipt must be outside disposable state" >&2; exit 2; } QEMU_PID="" +OBSERVED_ISO_SHA="" +EXPECTED_ISO_SHA="" +GRUB_PATH="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then From 0340da1a8d8a65748b2b35d7645a6a66608876bb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:33:14 +0200 Subject: [PATCH 005/719] fix: restore runtime shell interpolation --- scripts/gha_kvm_proxmox_rdte.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 9879622d3..259eac4dc 100644 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -54,7 +54,7 @@ write_receipt() { serial_tail="$(tail -n 80 "$STATE_DIR/serial.log" | tr -d '\000' | sed -E 's/[^[:print:]\t]//g' | tail -c 12000)" fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" - export R_SERIAL="$serial_tail" R_ISO_SHA="\${OBSERVED_ISO_SHA:-}" R_API="\${API_VERSION_JSON:-}" R_NESTED="\${NESTED_KVM:-unknown}" + export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" python3 - <<'PY' import json, os, pathlib payload = { @@ -184,7 +184,7 @@ start_qemu() { -daemonize -pidfile "$STATE_DIR/qemu.pid" ) if [[ "$with_iso" == yes ]]; then args+=( -cdrom "$AUTO_ISO" -boot order=d ); else args+=( -boot order=c ); fi - sudo -n qemu-system-x86_64 "\${args[@]}" + sudo -n qemu-system-x86_64 "${args[@]}" QEMU_PID="$(sudo -n cat "$STATE_DIR/qemu.pid")" } stop_qemu() { From cdb0af759ccb43c8c9b7064124bbd29e6f946638 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:33:16 +0200 Subject: [PATCH 006/719] fix: restore runtime shell interpolation --- scripts/gha_kvm_truenas_rdte.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 4b77200d9..eca538ce7 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -56,7 +56,7 @@ write_receipt() { serial_tail="$(tail -n 120 "$STATE_DIR/serial.log" | tr -d '\000' | sed -E 's/[^[:print:]\t]//g' | tail -c 16000)" fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" - export R_SERIAL="$serial_tail" R_ISO_SHA="\${OBSERVED_ISO_SHA:-}" R_EXPECTED="\${EXPECTED_ISO_SHA:-}" R_GRUB="\${GRUB_PATH:-}" + export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" python3 - <<'PY' import json, os, pathlib payload = { From 7ef949e05069a7b1210457006a88b7ace65e63fd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:34:02 +0200 Subject: [PATCH 007/719] test: add system RDTE contract guardrails --- tests/test_gha_kvm_system_rdte.py | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_gha_kvm_system_rdte.py diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py new file mode 100644 index 000000000..cfa38b136 --- /dev/null +++ b/tests/test_gha_kvm_system_rdte.py @@ -0,0 +1,70 @@ +import pathlib +import subprocess +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +PVE = ROOT / "scripts" / "gha_kvm_proxmox_rdte.sh" +TRUENAS = ROOT / "scripts" / "gha_kvm_truenas_rdte.sh" + + +class SystemRdteContractTests(unittest.TestCase): + def test_shell_syntax(self): + for path in (PVE, TRUENAS): + cp = subprocess.run(["bash", "-n", str(path)], text=True, capture_output=True) + self.assertEqual(cp.returncode, 0, f"{path}: {cp.stderr}") + + def test_common_evidence_contract(self): + for path in (PVE, TRUENAS): + text = path.read_text(encoding="utf-8") + self.assertIn("gha-kvm-system-lab/v1", text) + self.assertIn("oracleSatisfied", text) + self.assertIn("classification", text) + self.assertIn("passwordless sudo KVM boundary", text) + self.assertIn("SKIPPED_GUARDRAIL", text) + self.assertIn('rm -rf -- "$STATE_DIR"', text) + + def test_no_private_runner_or_secret_surface(self): + forbidden = [ + "self-hosted", + "garm-provider-truenas-private", + "agent-dispatch-private.git", + "ghp_", + "github_pat_", + "ACTIONS_RUNNER_INPUT_TOKEN", + ] + for path in (PVE, TRUENAS): + text = path.read_text(encoding="utf-8") + for needle in forbidden: + self.assertNotIn(needle, text) + + def test_proxmox_is_pinned_and_uses_vendor_auto_install_contract(self): + text = PVE.read_text(encoding="utf-8") + self.assertIn("proxmox-ve_9.2-1.iso", text) + self.assertIn( + "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", + text, + ) + self.assertIn('mode = "iso"', text) + self.assertIn('source = "from-dhcp"', text) + self.assertIn('filesystem = "ext4"', text) + self.assertIn('disk-list = ["sda"]', text) + self.assertIn("/api2/json/version", text) + self.assertNotIn("xdotool", text) + + def test_truenas_is_pinned_and_digest_sidecar_is_required(self): + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn("TrueNAS-26.0.0-BETA.3.iso", text) + self.assertIn("TrueNAS-26.0.0-BETA.3.iso.sha256", text) + self.assertIn("RAM_MIB=8192", text) + self.assertIn("installer-boot", text) + self.assertNotIn("nightly", text.lower()) + self.assertNotIn("xdotool", text) + + def test_no_literal_escaped_shell_parameter_expansions(self): + needle = chr(92) + "$" + "{" + for path in (PVE, TRUENAS): + self.assertNotIn(needle, path.read_text(encoding="utf-8")) + + +if __name__ == "__main__": + unittest.main() From f4e93a7c8bfee0f996c7a67b862cdd1fbc65e092 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:34:42 +0200 Subject: [PATCH 008/719] docs: define real-system KVM RDTE contract --- docs/gha-kvm-system-rdte.md | 118 ++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 docs/gha-kvm-system-rdte.md diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md new file mode 100644 index 000000000..3d7b080c3 --- /dev/null +++ b/docs/gha-kvm-system-rdte.md @@ -0,0 +1,118 @@ +# Real-system KVM RDTE on public GitHub Actions + +Private experiment authority: `SemperSupra/agent-dispatch-private#396`. + +This public experiment qualifies whether the already-proven x64 GitHub-hosted KVM +surface can run disposable **real TrueNAS and Proxmox systems** for development and +compatibility testing. It extends the accepted runner census from +`SemperSupra/agent-dispatch-private#223`; it does not reopen or reinterpret that census. + +## Responsibility boundary + +```text +Agent Dispatch / private experiment authority + | + v +public-safe system RDTE adapters + SemperSupra/agent-dispatch + | | + v v + TrueNAS ISO Proxmox ISO + | | + +---- QEMU/KVM -+ + | + v + sanitized qualification receipt + +TrueNAS App Foundry consumes a proven TrueNAS target profile later. +It does not own or duplicate the VM substrate. +``` + +The common contract is deliberately small: host/resource preflight, exact source +identity, KVM entry gate, bounded target shape, target oracle, evidence classification, +diagnostics, and teardown. Installer behavior stays target-local. + +## Initial target rungs + +### TrueNAS + +Initial target: pinned `26.0.0-BETA.3`. + +- **T0 (implemented here):** vendor SHA256 sidecar -> exact ISO verification -> serial-observable installer boot. +- **T1:** invoke the vendor's own installer machinery deterministically. +- **T2:** boot installed TrueNAS and prove middleware/API health. +- **T3:** attach disposable sparse data disks and create a real ZFS pool. +- **T4:** initialize Apps and run one synthetic public-safe custom app. +- **T5:** app lifecycle/configuration/restart/delete. +- **T6:** consume an exact public TrueNAS App Foundry materialization control. +- **T7:** only after T0-T6, add stable and moving prerelease/nightly profiles. + +The T0 ISO modification changes only GRUB console observability so the experiment has +a machine-readable serial oracle. It does not substitute a different installer. + +### Proxmox VE + +Initial target: pinned `9.2-1`. + +The adapter embeds a minimal answer file using the vendor automated-installer contract, +constructs an auto-installable ISO, patches only the automated boot entry to expose its +console on `ttyS0`, installs to a disposable sparse SCSI disk, boots the installed +system, and probes its HTTPS API. + +Current rungs: + +- **P0:** exact ISO + published digest. +- **P1:** official unattended installation. +- **P2:** installed management-plane/API response. +- **P4 observation:** if SSH is available, record whether nested KVM is exposed. +- **P3/P5:** LXC lifecycle and an actual L3 KVM VCPU nonce are follow-up gates; presence + of `/dev/kvm` is not sufficient evidence for P5. + +## Resource and safety guardrails + +- public/free GitHub-hosted x64 runners only; +- one heavyweight system VM at a time; +- no private repository payloads or credentials; +- no paid/larger runner assumptions; +- TrueNAS guest capped at 8 GiB for the first rung; +- sparse virtual disks only; +- host RAM/disk headroom checked before a multi-gigabyte download/VM launch; +- localhost port forwards instead of persistent host bridges/TAP configuration; +- no stress/load testing; +- experiment-owned state removed on exit; +- failures are receipts, not automatically failed CI jobs. + +A public GHA pass is RDTE evidence for this virtual target profile. It is not physical +hardware qualification and is not production hosting. + +## Evidence semantics + +Retain the existing ladder: + +```text +advertised -> observed -> installed -> callable -> exercised -> oracleSatisfied +``` + +and classifications including `SUPPORTED`, `INCONCLUSIVE`, +`HARNESS_FAILURE`, `ENVIRONMENT_FAILURE`, `ORACLE_FAILURE`, and +`SKIPPED_GUARDRAIL`. + +Do not promote placement or target support merely because the workflow is green. + +## Active-campaign workflow posture + +During the bounded experiment campaign the workflow is allowed to run on pushes to the +single experiment branch so harness corrections receive real hosted-runner evidence. +After the active qualification rungs complete, freeze it back to explicit +`workflow_dispatch` as done by the earlier runner-census campaign. + +The workflow installs only host-side packages required to supply the already-qualified +QEMU/KVM test mechanism (`qemu-system-x86`, `qemu-utils`, `xorriso`, and +`sshpass`). Target operating-system packages are supplied by their pinned vendor ISOs. + +## Promotion boundary + +Only after TrueNAS T4/T5 pass should TrueNAS App Foundry receive a runtime target-profile +adapter. Only public materializations are eligible for this public RDTE. Application +development remains in product repositories and private promotion/evaluator authority +remains outside this public repository. From 0e9fdb8512142d346aaef0d76748842dde627339 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:35:36 +0200 Subject: [PATCH 009/719] ci: run real TrueNAS and Proxmox KVM RDTE --- .github/workflows/gha-kvm-system-rdte.yml | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 .github/workflows/gha-kvm-system-rdte.yml diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml new file mode 100644 index 000000000..0661386a8 --- /dev/null +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -0,0 +1,87 @@ +name: Real-system KVM RDTE + +on: + push: + branches: + - experiment/gha-kvm-system-rdte + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: gha-kvm-system-rdte + cancel-in-progress: false + +jobs: + contract: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Validate public-safe contract + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py + + proxmox-9-2: + needs: contract + runs-on: ubuntu-26.04 + timeout-minutes: 35 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Ensure bounded QEMU/KVM prerequisites + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-system-x86 qemu-utils xorriso sshpass + - name: Exercise pinned Proxmox real-system target + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" + bash scripts/gha_kvm_proxmox_rdte.sh \ + --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-state" \ + --out "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" + python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" + - name: Upload Proxmox evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: gha-kvm-proxmox-9-2-${{ github.run_attempt }} + path: ${{ runner.temp }}/gha-kvm-system-rdte/proxmox-receipt.json + if-no-files-found: error + retention-days: 7 + + truenas-26-beta3: + needs: proxmox-9-2 + runs-on: ubuntu-26.04 + timeout-minutes: 18 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Ensure bounded QEMU/KVM prerequisites + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-system-x86 qemu-utils xorriso + - name: Exercise pinned TrueNAS installer target + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" + bash scripts/gha_kvm_truenas_rdte.sh \ + --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ + --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + - name: Upload TrueNAS evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: gha-kvm-truenas-26-beta3-${{ github.run_attempt }} + path: ${{ runner.temp }}/gha-kvm-system-rdte/truenas-receipt.json + if-no-files-found: error + retention-days: 7 From 24e0ebbf181c47a91b3745bc555c0db4da2e4980 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:38:38 +0200 Subject: [PATCH 010/719] fix: make extracted GRUB config writable From 1960281b79260a2ad6f9e95ab35d5e0a58255ea5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:38:43 +0200 Subject: [PATCH 011/719] fix: make extracted GRUB config writable From 7c4077196883f13d78e7ae29a00368ac677dbb66 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:39:30 +0200 Subject: [PATCH 012/719] fix: make extracted GRUB config writable --- scripts/gha_kvm_proxmox_rdte.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 259eac4dc..1648cfd23 100644 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -133,6 +133,7 @@ printf 'mode = "iso"\n' >"$STATE_DIR/auto-installer-mode.toml" xorriso -osirrox on -indev "$ISO" -extract /boot/grub/grub.cfg "$STATE_DIR/grub.cfg" >/dev/null 2>&1 || fail_evidence HARNESS_FAILURE prepare "could not extract Proxmox GRUB config" +chmod u+w "$STATE_DIR/grub.cfg" python3 - "$STATE_DIR/grub.cfg" <<'PY' import pathlib, sys p=pathlib.Path(sys.argv[1]) From 53c6e064167f828773a013185e9632d4e2910881 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:39:34 +0200 Subject: [PATCH 013/719] fix: make extracted GRUB config writable --- scripts/gha_kvm_truenas_rdte.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index eca538ce7..fa9ca1454 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -123,6 +123,7 @@ for candidate in /boot/grub/grub.cfg /EFI/BOOT/grub.cfg /efi/boot/grub.cfg; do fi done [[ -n "$GRUB_PATH" ]] || fail_evidence HARNESS_FAILURE prepare "could not locate installer GRUB config in pinned ISO" +chmod u+w "$STATE_DIR/grub.cfg" python3 - "$STATE_DIR/grub.cfg" <<'PY' import pathlib, sys From 1abd4b467febf28d2c4e3c073d9a3fa2d5db022d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:40:21 +0200 Subject: [PATCH 014/719] ci: preserve failure evidence and cancel stale reps --- .github/workflows/gha-kvm-system-rdte.yml | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 0661386a8..61c200996 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -11,7 +11,7 @@ permissions: concurrency: group: gha-kvm-system-rdte - cancel-in-progress: false + cancel-in-progress: true jobs: contract: @@ -46,6 +46,24 @@ jobs: --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-state" \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" + - name: Preserve Proxmox harness failure evidence + if: always() + run: | + set -euo pipefail + receipt="$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" + if [ ! -f "$receipt" ]; then + python3 - "$receipt" <<'PY' + import json, pathlib, sys + pathlib.Path(sys.argv[1]).write_text(json.dumps({ + "contract":"gha-kvm-system-lab/v1", + "target":{"product":"proxmox-ve","version":"9.2-1"}, + "classification":"HARNESS_FAILURE", + "oracleSatisfied":False, + "phase":"workflow-fallback", + "detail":"adapter exited before emitting its normal receipt" + }, indent=2, sort_keys=True)+"\n") + PY + fi - name: Upload Proxmox evidence if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 @@ -56,7 +74,8 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: proxmox-9-2 + needs: [contract, proxmox-9-2] + if: needs.contract.result == 'success' runs-on: ubuntu-26.04 timeout-minutes: 18 steps: @@ -77,6 +96,24 @@ jobs: --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + - name: Preserve TrueNAS harness failure evidence + if: always() + run: | + set -euo pipefail + receipt="$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + if [ ! -f "$receipt" ]; then + python3 - "$receipt" <<'PY' + import json, pathlib, sys + pathlib.Path(sys.argv[1]).write_text(json.dumps({ + "contract":"gha-kvm-system-lab/v1", + "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T0"}, + "classification":"HARNESS_FAILURE", + "oracleSatisfied":False, + "phase":"workflow-fallback", + "detail":"adapter exited before emitting its normal receipt" + }, indent=2, sort_keys=True)+"\n") + PY + fi - name: Upload TrueNAS evidence if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 From 131da8aaca61c4341dbc790a8cd447c3019a42f0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:44:38 +0200 Subject: [PATCH 015/719] fix: mirror Proxmox in-place ISO injection --- scripts/gha_kvm_proxmox_rdte.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 1648cfd23..11536de60 100644 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -151,7 +151,9 @@ p.write_text("\n".join(lines)+"\n", encoding="utf-8") PY AUTO_ISO="$STATE_DIR/proxmox-auto.iso" -xorriso -indev "$ISO" -outdev "$AUTO_ISO" -boot_image any replay \ +cp --reflink=auto "$ISO" "$AUTO_ISO" +chmod u+w "$AUTO_ISO" +xorriso -boot_image any keep -dev "$AUTO_ISO" \ -map "$STATE_DIR/auto-installer-mode.toml" /auto-installer-mode.toml \ -map "$STATE_DIR/answer.toml" /answer.toml \ -map "$STATE_DIR/grub.cfg" /boot/grub/grub.cfg \ From 324f498392dc0f50683ef4eef723c885d817f2de Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:44:48 +0200 Subject: [PATCH 016/719] fix: use in-place ISO injection for TrueNAS observability --- scripts/gha_kvm_truenas_rdte.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index fa9ca1454..202df2bf6 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -149,7 +149,9 @@ p.write_text(prefix + "\n".join(lines)+"\n", encoding="utf-8") PY SERIAL_ISO="$STATE_DIR/truenas-serial.iso" -xorriso -indev "$ISO" -outdev "$SERIAL_ISO" -boot_image any replay \ +cp --reflink=auto "$ISO" "$SERIAL_ISO" +chmod u+w "$SERIAL_ISO" +xorriso -boot_image any keep -dev "$SERIAL_ISO" \ -map "$STATE_DIR/grub.cfg" "$GRUB_PATH" -commit >/dev/null 2>"$STATE_DIR/xorriso.log" || fail_evidence HARNESS_FAILURE prepare "failed to construct serial-observable TrueNAS ISO" From 81fd621e63012564636da83eeb3d3eae169e9697 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:09:59 +0200 Subject: [PATCH 017/719] diag: distinguish Proxmox boot network and pveproxy failures --- scripts/gha_kvm_proxmox_rdte.sh | 77 +++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 11536de60..09c163ffb 100644 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -36,6 +36,10 @@ QEMU_PID="" OBSERVED_ISO_SHA="" API_VERSION_JSON="" NESTED_KVM="unknown" +SSH_REACHABLE="false" +API_PORT_REACHABLE="false" +QEMU_ALIVE_AT_API_GATE="unknown" +GUEST_DIAGNOSTICS="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -55,6 +59,8 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" + export R_SSH_REACHABLE="$SSH_REACHABLE" R_API_PORT_REACHABLE="$API_PORT_REACHABLE" + export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" python3 - <<'PY' import json, os, pathlib payload = { @@ -79,6 +85,12 @@ payload = { }, "api_version": json.loads(os.environ["R_API"]) if os.environ.get("R_API") else None, "nested_kvm": os.environ.get("R_NESTED"), + "diagnostics": { + "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), + "ssh_port_reachable": os.environ.get("R_SSH_REACHABLE") == "true", + "api_port_reachable": os.environ.get("R_API_PORT_REACHABLE") == "true", + "guest": os.environ.get("R_GUEST_DIAGNOSTICS") or None, + }, "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ "Disposable virtual-hardware target profile; not physical-HBA/SMART/IPMI/HA qualification.", @@ -172,6 +184,47 @@ s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() PY )" + +port_open() { + local port="$1" + python3 - "$port" <<'PY' +import socket, sys +s=socket.socket() +s.settimeout(1.0) +try: + s.connect(("127.0.0.1", int(sys.argv[1]))) +except OSError: + raise SystemExit(1) +finally: + s.close() +PY +} + +guest_diagnostics() { + command -v sshpass >/dev/null 2>&1 || return 0 + sshpass -p 'rdte-proxmox-ephemeral-9264' ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 root@127.0.0.1 ' + echo "===PVEVERSION==="; + pveversion -v 2>&1 || true; + echo "===SYSTEMD_FAILED==="; + systemctl --failed --no-pager 2>&1 || true; + echo "===PVE_SERVICES==="; + for s in pveproxy pvedaemon pvestatd pve-cluster networking systemd-networkd; do + echo "--- $s ---"; + systemctl status "$s" --no-pager -l 2>&1 | tail -n 40 || true; + done; + echo "===LISTENERS==="; + ss -lntup 2>&1 || true; + echo "===IP_ADDR==="; + ip -brief addr 2>&1 || true; + echo "===IP_ROUTE==="; + ip route 2>&1 || true; + echo "===RESOLV==="; + cat /etc/resolv.conf 2>&1 || true; + echo "===PVE_PROXY_JOURNAL==="; + journalctl -u pveproxy -b --no-pager -n 100 2>&1 || true; + ' 2>&1 | tail -c 24000 +} + start_qemu() { local with_iso="$1" : >"$STATE_DIR/serial.log" @@ -211,12 +264,30 @@ stop_qemu start_qemu no API_VERSION_JSON="" +SSH_REACHABLE="false" +API_PORT_REACHABLE="false" +QEMU_ALIVE_AT_API_GATE="unknown" for _ in $(seq 1 120); do - API_VERSION_JSON="$(curl -sk --max-time 3 "https://127.0.0.1:$WEB_PORT/api2/json/version" 2>/dev/null || true)" - if [[ "$API_VERSION_JSON" == *'"data"'* ]]; then break; fi + if sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then + QEMU_ALIVE_AT_API_GATE="true" + else + QEMU_ALIVE_AT_API_GATE="false" + break + fi + if port_open "$SSH_PORT"; then SSH_REACHABLE="true"; fi + if port_open "$WEB_PORT"; then API_PORT_REACHABLE="true"; fi + if [[ "$API_PORT_REACHABLE" == "true" ]]; then + API_VERSION_JSON="$(curl -sk --max-time 3 "https://127.0.0.1:$WEB_PORT/api2/json/version" 2>/dev/null || true)" + if [[ "$API_VERSION_JSON" == *'"data"'* ]]; then break; fi + fi sleep 3 done -[[ "$API_VERSION_JSON" == *'"data"'* ]] || fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer" +if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then + if [[ "$SSH_REACHABLE" == "true" ]]; then + GUEST_DIAGNOSTICS="$(guest_diagnostics || true)" + fi + fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer (qemu_alive=$QEMU_ALIVE_AT_API_GATE ssh=$SSH_REACHABLE tcp8006=$API_PORT_REACHABLE)" +fi NESTED_KVM="unknown" if command -v sshpass >/dev/null 2>&1; then From 68c8c35afd2dea0556505ad2b6283d617d2c5ef4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:11:54 +0200 Subject: [PATCH 018/719] exp: add dependency-free TrueNAS installer RPC discovery probe --- scripts/truenas_installer_rpc_probe.py | 161 +++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 scripts/truenas_installer_rpc_probe.py diff --git a/scripts/truenas_installer_rpc_probe.py b/scripts/truenas_installer_rpc_probe.py new file mode 100644 index 000000000..9487ebfb3 --- /dev/null +++ b/scripts/truenas_installer_rpc_probe.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Minimal dependency-free JSON-RPC 2.0 client for the TrueNAS installer WebSocket API.""" +from __future__ import annotations +import argparse, base64, hashlib, json, os, pathlib, socket, struct, time + + +class WebSocket: + def __init__(self, host: str, port: int, path: str = "/ws", timeout: float = 5.0): + self.sock = socket.create_connection((host, port), timeout=timeout) + self.sock.settimeout(timeout) + key = base64.b64encode(os.urandom(16)).decode() + request = ( + f"GET {path} HTTP/1.1\r\n" + f"Host: {host}:{port}\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {key}\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n" + ).encode() + self.sock.sendall(request) + head = self._read_until(b"\r\n\r\n", 16384) + status = head.split(b"\r\n", 1)[0] + if b" 101 " not in status: + raise RuntimeError(f"websocket handshake failed: {status.decode(errors='replace')}") + expected = base64.b64encode( + hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest() + ).decode() + headers = {} + for line in head.decode(errors="replace").split("\r\n")[1:]: + if ":" in line: + k, v = line.split(":", 1) + headers[k.lower().strip()] = v.strip() + if headers.get("sec-websocket-accept") != expected: + raise RuntimeError("websocket accept digest mismatch") + + def _read_exact(self, n: int) -> bytes: + chunks = [] + remaining = n + while remaining: + part = self.sock.recv(remaining) + if not part: + raise EOFError("websocket closed") + chunks.append(part) + remaining -= len(part) + return b"".join(chunks) + + def _read_until(self, marker: bytes, limit: int) -> bytes: + data = bytearray() + while marker not in data: + part = self.sock.recv(4096) + if not part: + raise EOFError("socket closed during handshake") + data.extend(part) + if len(data) > limit: + raise RuntimeError("handshake exceeded limit") + return bytes(data) + + def _send_frame(self, opcode: int, payload: bytes): + first = 0x80 | opcode + mask = os.urandom(4) + n = len(payload) + if n < 126: + header = bytes([first, 0x80 | n]) + elif n <= 0xFFFF: + header = bytes([first, 0x80 | 126]) + struct.pack("!H", n) + else: + header = bytes([first, 0x80 | 127]) + struct.pack("!Q", n) + masked = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) + self.sock.sendall(header + mask + masked) + + def send_json(self, obj): + self._send_frame(0x1, json.dumps(obj, separators=(",", ":")).encode()) + + def recv_text(self) -> str: + fragments = [] + while True: + b1, b2 = self._read_exact(2) + fin = bool(b1 & 0x80) + opcode = b1 & 0x0F + masked = bool(b2 & 0x80) + n = b2 & 0x7F + if n == 126: + n = struct.unpack("!H", self._read_exact(2))[0] + elif n == 127: + n = struct.unpack("!Q", self._read_exact(8))[0] + mask = self._read_exact(4) if masked else b"" + payload = self._read_exact(n) + if masked: + payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) + if opcode == 0x8: + raise EOFError("websocket close frame") + if opcode == 0x9: + self._send_frame(0xA, payload) + continue + if opcode in (0x1, 0x0): + fragments.append(payload) + if fin: + return b"".join(fragments).decode() + elif opcode == 0xA: + continue + else: + raise RuntimeError(f"unexpected websocket opcode {opcode}") + + def call(self, method: str, request_id: int): + self.send_json({"jsonrpc": "2.0", "id": request_id, "method": method}) + while True: + response = json.loads(self.recv_text()) + if response.get("id") != request_id: + continue + if response.get("error") is not None: + raise RuntimeError(f"{method}: {json.dumps(response['error'], sort_keys=True)}") + return response.get("result") + + def close(self): + try: + self._send_frame(0x8, b"") + except Exception: + pass + self.sock.close() + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--out", required=True) + p.add_argument("--timeout", type=float, default=6.0) + a = p.parse_args() + started = time.time() + payload = { + "schema": "truenas-installer-rpc-discovery/v1", + "endpoint": {"host": a.host, "port": a.port, "path": "/ws"}, + "methods": {}, + "oracleSatisfied": False, + } + ws = WebSocket(a.host, a.port, timeout=a.timeout) + try: + adopted = ws.call("is_adopted", 1) + payload["methods"]["is_adopted"] = adopted + if adopted: + raise RuntimeError("fresh installer unexpectedly reports adopted=true") + for i, method in enumerate( + ("system_info", "list_disks", "list_network_interfaces"), start=2 + ): + payload["methods"][method] = ws.call(method, i) + payload["oracleSatisfied"] = True + payload["classification"] = "SUPPORTED" + payload["detail"] = "read-only TrueNAS installer JSON-RPC discovery methods answered" + except Exception as exc: + payload["classification"] = "ORACLE_FAILURE" + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + ws.close() + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 1e990d709750cc1ca378213d16f660780f9b3228 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:12:47 +0200 Subject: [PATCH 019/719] exp: advance TrueNAS to read-only installer RPC discovery --- scripts/gha_kvm_truenas_rdte.sh | 103 +++++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 202df2bf6..2a909a035 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + VERSION="26.0.0-BETA.3" ISO_NAME="TrueNAS-26.0.0-BETA.3.iso" BASE_URL="https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3" @@ -13,20 +15,23 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1]" } OUT="" STATE_DIR="" +RUNG="t0" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; --state-dir) STATE_DIR="$2"; shift 2 ;; + --rung) RUNG="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } +[[ "$RUNG" == "t0" || "$RUNG" == "t1" ]] || { echo "rung must be t0 or t1" >&2; exit 2; } if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -38,6 +43,10 @@ QEMU_PID="" OBSERVED_ISO_SHA="" EXPECTED_ISO_SHA="" GRUB_PATH="" +T0_OBSERVED="false" +RPC_REACHABLE="false" +RPC_DISCOVERY_JSON="" +QEMU_ALIVE_AT_GATE="unknown" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -57,11 +66,13 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" + export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_REACHABLE="$RPC_REACHABLE" + export R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" python3 - <<'PY' import json, os, pathlib payload = { "contract": "gha-kvm-system-lab/v1", - "target": {"product": "truenas", "version": "26.0.0-BETA.3", "rung": "T0"}, + "target": {"product": "truenas", "version": "26.0.0-BETA.3", "rung": os.environ.get("R_RUNG", "t0").upper()}, "classification": os.environ["R_CLASS"], "oracleSatisfied": os.environ["R_ORACLE"].lower() == "true", "phase": os.environ["R_PHASE"], @@ -75,10 +86,18 @@ payload = { "observed_sha256": os.environ.get("R_ISO_SHA") or None, }, "installer_grub_path": os.environ.get("R_GRUB") or None, + "oracles": { + "vendor_iso_digest": bool(os.environ.get("R_ISO_SHA")) and os.environ.get("R_ISO_SHA") == os.environ.get("R_EXPECTED"), + "installer_environment_observed": os.environ.get("R_T0") == "true", + "installer_rpc_port_reachable": os.environ.get("R_RPC_REACHABLE") == "true", + "installer_rpc_readonly": bool(os.environ.get("R_RPC_DISCOVERY")), + }, + "rpc_discovery": json.loads(os.environ["R_RPC_DISCOVERY"]) if os.environ.get("R_RPC_DISCOVERY") else None, + "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ - "T0 proves only pinned vendor media integrity and installer-environment boot under the disposable virtual target profile.", - "ZFS, middleware, Apps, and application lifecycle remain separate T1-T6 gates.", + "T0 proves pinned vendor media integrity and installer-environment boot under the disposable virtual target profile.", + "T1 is read-only installer RPC discovery; installation, ZFS, middleware, Containers, Apps, and application lifecycle remain later gates.", "This does not qualify physical storage controllers, SMART, GPU, IPMI, or HA behavior.", ], } @@ -155,34 +174,90 @@ xorriso -boot_image any keep -dev "$SERIAL_ISO" \ -map "$STATE_DIR/grub.cfg" "$GRUB_PATH" -commit >/dev/null 2>"$STATE_DIR/xorriso.log" || fail_evidence HARNESS_FAILURE prepare "failed to construct serial-observable TrueNAS ISO" + +port_open() { + local port="$1" + python3 - "$port" <<'PY' +import socket, sys +s=socket.socket() +s.settimeout(1.0) +try: + s.connect(("127.0.0.1", int(sys.argv[1]))) +except OSError: + raise SystemExit(1) +finally: + s.close() +PY +} + qemu-img create -q -f qcow2 "$STATE_DIR/boot.qcow2" "$DISK_SIZE" +RPC_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" : >"$STATE_DIR/serial.log" sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ -cdrom "$SERIAL_ISO" -boot order=d \ - -netdev user,id=net0 -device virtio-net-pci,netdev=net0 \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080" -device virtio-net-pci,netdev=net0 \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ -daemonize -pidfile "$STATE_DIR/qemu.pid" || fail_evidence ENVIRONMENT_FAILURE boot "QEMU could not start TrueNAS guest" QEMU_PID="$(sudo -n cat "$STATE_DIR/qemu.pid")" -BOOT_OK=false -for _ in $(seq 1 120); do +T0_OBSERVED="false" +RPC_REACHABLE="false" +for _ in $(seq 1 220); do if grep -Eqi 'TrueNAS|truenas-installer|TrueNAS Installer|Install/Upgrade' "$STATE_DIR/serial.log" 2>/dev/null; then - BOOT_OK=true + T0_OBSERVED="true" + fi + if sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then + QEMU_ALIVE_AT_GATE="true" + else + QEMU_ALIVE_AT_GATE="false" + break + fi + if [[ "$RUNG" == "t0" && "$T0_OBSERVED" == "true" ]]; then + break + fi + if [[ "$RUNG" == "t1" ]] && port_open "$RPC_PORT"; then + RPC_REACHABLE="true" break fi - if ! sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then break; fi sleep 3 done -if [[ "$BOOT_OK" == true ]]; then - write_receipt SUPPORTED true installer-boot "pinned TrueNAS vendor ISO reached a TrueNAS-labelled installer environment over serial" -else - if grep -Eqi 'Linux version|systemd|Starting' "$STATE_DIR/serial.log" 2>/dev/null; then +if [[ "$RUNG" == "t0" ]]; then + if [[ "$T0_OBSERVED" == "true" ]]; then + write_receipt SUPPORTED true installer-boot "pinned TrueNAS vendor ISO reached a TrueNAS-labelled installer environment over serial" + elif grep -Eqi 'Linux version|systemd|Starting' "$STATE_DIR/serial.log" 2>/dev/null; then fail_evidence INCONCLUSIVE installer-boot "Linux boot was observed but a TrueNAS-specific installer marker was not reached" + else + fail_evidence ORACLE_FAILURE installer-boot "no bounded installer-environment boot oracle was observed" fi - fail_evidence ORACLE_FAILURE installer-boot "no bounded installer-environment boot oracle was observed" + exit 0 fi + +[[ "$T0_OBSERVED" == "true" ]] || fail_evidence ORACLE_FAILURE installer-rpc "T1 did not preserve the T0 installer-environment oracle" +[[ "$RPC_REACHABLE" == "true" ]] || fail_evidence ORACLE_FAILURE installer-rpc "TrueNAS installer RPC port 8080 did not become reachable" + +RPC_OUT="$STATE_DIR/rpc-discovery.json" +python3 "$SCRIPT_DIR/truenas_installer_rpc_probe.py" --host 127.0.0.1 --port "$RPC_PORT" --out "$RPC_OUT" --timeout 8 || true +if [[ -f "$RPC_OUT" ]]; then + RPC_DISCOVERY_JSON="$(cat "$RPC_OUT")" +fi +RPC_OK="$(python3 - "$RPC_OUT" <<'PY' +import json, pathlib, sys +try: + data=json.loads(pathlib.Path(sys.argv[1]).read_text()) +except Exception: + print("false") +else: + print("true" if data.get("oracleSatisfied") is True else "false") +PY +)" +[[ "$RPC_OK" == "true" ]] || fail_evidence ORACLE_FAILURE installer-rpc "TrueNAS installer WebSocket JSON-RPC answered but read-only discovery oracle failed" +write_receipt SUPPORTED true installer-rpc "pinned TrueNAS installer answered read-only JSON-RPC discovery methods" From d5bba531e425b6a92c1724ca226e429dd95d7a0b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:13:12 +0200 Subject: [PATCH 020/719] ci: run TrueNAS read-only installer RPC rung --- .github/workflows/gha-kvm-system-rdte.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 61c200996..675936a66 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -77,7 +77,7 @@ jobs: needs: [contract, proxmox-9-2] if: needs.contract.result == 'success' runs-on: ubuntu-26.04 - timeout-minutes: 18 + timeout-minutes: 32 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -94,6 +94,7 @@ jobs: mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" bash scripts/gha_kvm_truenas_rdte.sh \ --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ + --rung t1 \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence @@ -106,7 +107,7 @@ jobs: import json, pathlib, sys pathlib.Path(sys.argv[1]).write_text(json.dumps({ "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T0"}, + "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T1"}, "classification":"HARNESS_FAILURE", "oracleSatisfied":False, "phase":"workflow-fallback", From dd4173d5cf2f88b0d011b8116c73cc8a82a701d8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:13:14 +0200 Subject: [PATCH 021/719] test: lock TrueNAS read-only RPC discovery boundary --- tests/test_gha_kvm_system_rdte.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index cfa38b136..0c300d881 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -5,6 +5,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] PVE = ROOT / "scripts" / "gha_kvm_proxmox_rdte.sh" TRUENAS = ROOT / "scripts" / "gha_kvm_truenas_rdte.sh" +TRUENAS_RPC = ROOT / "scripts" / "truenas_installer_rpc_probe.py" class SystemRdteContractTests(unittest.TestCase): @@ -57,6 +58,8 @@ def test_truenas_is_pinned_and_digest_sidecar_is_required(self): self.assertIn("TrueNAS-26.0.0-BETA.3.iso.sha256", text) self.assertIn("RAM_MIB=8192", text) self.assertIn("installer-boot", text) + self.assertIn("installer-rpc", text) + self.assertIn("truenas_installer_rpc_probe.py", text) self.assertNotIn("nightly", text.lower()) self.assertNotIn("xdotool", text) @@ -66,5 +69,19 @@ def test_no_literal_escaped_shell_parameter_expansions(self): self.assertNotIn(needle, path.read_text(encoding="utf-8")) + def test_truenas_rpc_probe_is_dependency_free_and_read_only(self): + text = TRUENAS_RPC.read_text(encoding="utf-8") + for method in ("is_adopted", "system_info", "list_disks", "list_network_interfaces"): + self.assertIn(method, text) + self.assertNotIn('"install"', text) + self.assertNotIn("pip install", text) + cp = subprocess.run( + ["python3", "-m", "py_compile", str(TRUENAS_RPC)], + text=True, + capture_output=True, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + + if __name__ == "__main__": unittest.main() From 2706fd33a8a1411aacfe632fe8bfdb3c115d9491 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:26:16 +0200 Subject: [PATCH 022/719] ci: scope system RDTE and qualify TrueNAS RPC transport --- .github/workflows/gha-kvm-system-rdte.yml | 68 +++++++++- docs/gha-kvm-system-rdte.md | 13 ++ scripts/gha_kvm_truenas_rdte.sh | 2 + tests/test_gha_kvm_system_rdte.py | 149 ++++++++++++++++++++++ 4 files changed, 229 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 675936a66..35ec3bfaf 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -5,6 +5,16 @@ on: branches: - experiment/gha-kvm-system-rdte workflow_dispatch: + inputs: + target: + description: "Heavy target to exercise" + required: true + default: both + type: choice + options: + - both + - proxmox + - truenas permissions: contents: read @@ -14,6 +24,53 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-24.04 + timeout-minutes: 2 + outputs: + proxmox: ${{ steps.scope.outputs.proxmox }} + truenas: ${{ steps.scope.outputs.truenas }} + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + fetch-depth: 0 + - id: scope + env: + EVENT_NAME: ${{ github.event_name }} + DISPATCH_TARGET: ${{ inputs.target }} + BEFORE_SHA: ${{ github.event.before }} + AFTER_SHA: ${{ github.sha }} + run: | + set -euo pipefail + proxmox=false + truenas=false + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + case "$DISPATCH_TARGET" in + proxmox) proxmox=true ;; + truenas) truenas=true ;; + both) proxmox=true; truenas=true ;; + *) echo "unsupported dispatch target: $DISPATCH_TARGET" >&2; exit 2 ;; + esac + elif [[ -n "$BEFORE_SHA" && -n "$AFTER_SHA" && ! "$BEFORE_SHA" =~ ^0+$ ]]; then + while IFS= read -r path; do + case "$path" in + scripts/gha_kvm_proxmox_rdte.sh) + proxmox=true + ;; + scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py) + truenas=true + ;; + esac + done < <(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA") + else + proxmox=true + truenas=true + fi + echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" + echo "truenas=$truenas" >>"$GITHUB_OUTPUT" + printf 'target scope: proxmox=%s truenas=%s\n' "$proxmox" "$truenas" + contract: runs-on: ubuntu-24.04 timeout-minutes: 5 @@ -25,7 +82,8 @@ jobs: run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py proxmox-9-2: - needs: contract + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.proxmox == 'true' runs-on: ubuntu-26.04 timeout-minutes: 35 steps: @@ -74,8 +132,12 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [contract, proxmox-9-2] - if: needs.contract.result == 'success' + needs: [changes, contract, proxmox-9-2] + if: >- + always() && + needs.contract.result == 'success' && + needs.changes.outputs.truenas == 'true' && + (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') runs-on: ubuntu-26.04 timeout-minutes: 32 steps: diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 3d7b080c3..14e9f0ef0 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -116,3 +116,16 @@ Only after TrueNAS T4/T5 pass should TrueNAS App Foundry receive a runtime targe adapter. Only public materializations are eligible for this public RDTE. Application development remains in product repositories and private promotion/evaluator authority remains outside this public repository. + + +## Active-campaign target scoping + +Heavy system jobs are demand-scoped during the experiment campaign. A push that +changes only the Proxmox adapter runs the Proxmox lane; a push that changes the +TrueNAS adapter or installer RPC probe runs the TrueNAS lane. Contract, test, or +documentation-only changes do not launch a system VM. Manual dispatch can request +`proxmox`, `truenas`, or `both`. + +When both heavy targets are requested they remain serialized. This keeps the public +free-tier RDTE useful without replaying multi-gigabyte installs that cannot change the +evidence under test. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 2a909a035..b1d6bda02 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -112,6 +112,8 @@ fail_evidence() { exit 0 } +[[ -f "$SCRIPT_DIR/truenas_installer_rpc_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer RPC probe" for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" done diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 0c300d881..e9f92f354 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -1,3 +1,11 @@ +import base64 +import hashlib +import json +import socket +import struct +import sys +import tempfile +import threading import pathlib import subprocess import unittest @@ -6,6 +14,7 @@ PVE = ROOT / "scripts" / "gha_kvm_proxmox_rdte.sh" TRUENAS = ROOT / "scripts" / "gha_kvm_truenas_rdte.sh" TRUENAS_RPC = ROOT / "scripts" / "truenas_installer_rpc_probe.py" +WORKFLOW = ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml" class SystemRdteContractTests(unittest.TestCase): @@ -82,6 +91,146 @@ def test_truenas_rpc_probe_is_dependency_free_and_read_only(self): ) self.assertEqual(cp.returncode, 0, cp.stderr) + def test_workflow_scopes_heavy_targets(self): + text = WORKFLOW.read_text(encoding="utf-8") + self.assertIn("fetch-depth: 0", text) + self.assertIn("needs.changes.outputs.proxmox", text) + self.assertIn("needs.changes.outputs.truenas", text) + self.assertIn("DISPATCH_TARGET", text) + self.assertIn("BEFORE_SHA:", text) + self.assertIn('git diff --name-only "$BEFORE_SHA" "$AFTER_SHA"', text) + self.assertIn("scripts/truenas_installer_rpc_probe.py", text) + + def test_truenas_t1_requires_local_rpc_probe(self): + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn("missing TrueNAS installer RPC probe", text) + + def test_truenas_rpc_probe_protocol_round_trip(self): + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + listener.listen(1) + port = listener.getsockname()[1] + errors = [] + + def read_exact(conn, count): + parts = [] + remaining = count + while remaining: + part = conn.recv(remaining) + if not part: + raise EOFError("synthetic websocket peer closed") + parts.append(part) + remaining -= len(part) + return b"".join(parts) + + def recv_json(conn): + b1, b2 = read_exact(conn, 2) + opcode = b1 & 0x0F + if opcode == 0x8: + return None + if opcode != 0x1: + raise AssertionError(f"unexpected client opcode {opcode}") + masked = bool(b2 & 0x80) + if not masked: + raise AssertionError("client websocket frame must be masked") + size = b2 & 0x7F + if size == 126: + size = struct.unpack("!H", read_exact(conn, 2))[0] + elif size == 127: + size = struct.unpack("!Q", read_exact(conn, 8))[0] + mask = read_exact(conn, 4) + payload = read_exact(conn, size) + payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) + return json.loads(payload.decode()) + + def send_json(conn, value): + payload = json.dumps(value, separators=(",", ":")).encode() + if len(payload) < 126: + header = bytes([0x81, len(payload)]) + else: + header = bytes([0x81, 126]) + struct.pack("!H", len(payload)) + conn.sendall(header + payload) + + def server(): + try: + conn, _ = listener.accept() + with conn: + request = bytearray() + while b"\r\n\r\n" not in request: + request.extend(conn.recv(4096)) + headers = {} + for line in request.decode().split("\r\n")[1:]: + if ":" in line: + key, value = line.split(":", 1) + headers[key.lower().strip()] = value.strip() + key = headers["sec-websocket-key"] + accept = base64.b64encode( + hashlib.sha1( + (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode() + ).digest() + ).decode() + conn.sendall( + ( + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {accept}\r\n\r\n" + ).encode() + ) + answers = [ + ("is_adopted", False), + ("system_info", {"version": "synthetic", "installation_running": False}), + ("list_disks", [{"name": "sda", "size": 1024}]), + ("list_network_interfaces", [{"name": "eth0"}]), + ] + for method, result in answers: + request_obj = recv_json(conn) + if request_obj["method"] != method: + raise AssertionError( + f"expected {method}, got {request_obj['method']}" + ) + send_json( + conn, + { + "jsonrpc": "2.0", + "id": request_obj["id"], + "result": result, + }, + ) + except Exception as exc: + errors.append(exc) + finally: + listener.close() + + thread = threading.Thread(target=server, daemon=True) + thread.start() + with tempfile.TemporaryDirectory() as td: + out = pathlib.Path(td) / "rpc.json" + cp = subprocess.run( + [ + sys.executable, + str(TRUENAS_RPC), + "--port", + str(port), + "--out", + str(out), + "--timeout", + "2", + ], + text=True, + capture_output=True, + timeout=10, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + payload = json.loads(out.read_text(encoding="utf-8")) + self.assertTrue(payload["oracleSatisfied"]) + self.assertEqual(payload["classification"], "SUPPORTED") + self.assertFalse(payload["methods"]["is_adopted"]) + self.assertEqual(payload["methods"]["system_info"]["version"], "synthetic") + thread.join(timeout=5) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + if __name__ == "__main__": unittest.main() From 9246dcda00bc4b22ce85d6f6eb43b0b7a3a1449d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:31:12 +0200 Subject: [PATCH 023/719] fix: require real TrueNAS installer RPC oracle --- docs/gha-kvm-system-rdte.md | 15 +++++++ scripts/gha_kvm_truenas_rdte.sh | 75 ++++++++++++++++++------------- tests/test_gha_kvm_system_rdte.py | 8 ++++ 3 files changed, 67 insertions(+), 31 deletions(-) diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 14e9f0ef0..3a73d578a 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -129,3 +129,18 @@ documentation-only changes do not launch a system VM. Manual dispatch can reques When both heavy targets are requested they remain serialized. This keeps the public free-tier RDTE useful without replaying multi-gigabyte installs that cannot change the evidence under test. + + +### TrueNAS T1 service-oracle semantics + +QEMU user-mode `hostfwd` accepting a localhost TCP connection is not a TrueNAS +installer-service oracle. T1 records that condition only as +`installer_rpc_hostfwd_accepted` diagnostic evidence. T1 is satisfied only by a +completed WebSocket/JSON-RPC exchange with the pinned installer that successfully +returns the read-only discovery methods. + +The T0 serial marker remains the T0-only oracle. On T1, a successful response from +the vendor installer RPC is stronger evidence that the installer environment is +running, so lack of an earlier serial-label observation does not negate a completed +RPC oracle. This avoids both a premature TCP false positive and a weaker-oracle +dependency blocking a stronger observed interface. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index b1d6bda02..1ea11334f 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -44,7 +44,8 @@ OBSERVED_ISO_SHA="" EXPECTED_ISO_SHA="" GRUB_PATH="" T0_OBSERVED="false" -RPC_REACHABLE="false" +RPC_HOSTFWD_ACCEPTED="false" +RPC_DISCOVERY_OK="false" RPC_DISCOVERY_JSON="" QEMU_ALIVE_AT_GATE="unknown" cleanup() { @@ -66,8 +67,8 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" - export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_REACHABLE="$RPC_REACHABLE" - export R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" + export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" + export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" python3 - <<'PY' import json, os, pathlib payload = { @@ -88,9 +89,10 @@ payload = { "installer_grub_path": os.environ.get("R_GRUB") or None, "oracles": { "vendor_iso_digest": bool(os.environ.get("R_ISO_SHA")) and os.environ.get("R_ISO_SHA") == os.environ.get("R_EXPECTED"), - "installer_environment_observed": os.environ.get("R_T0") == "true", - "installer_rpc_port_reachable": os.environ.get("R_RPC_REACHABLE") == "true", - "installer_rpc_readonly": bool(os.environ.get("R_RPC_DISCOVERY")), + "installer_environment_observed": os.environ.get("R_T0") == "true" or os.environ.get("R_RPC_OK") == "true", + "installer_serial_marker_observed": os.environ.get("R_T0") == "true", + "installer_rpc_hostfwd_accepted": os.environ.get("R_RPC_HOSTFWD") == "true", + "installer_rpc_readonly": os.environ.get("R_RPC_OK") == "true", }, "rpc_discovery": json.loads(os.environ["R_RPC_DISCOVERY"]) if os.environ.get("R_RPC_DISCOVERY") else None, "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), @@ -192,6 +194,27 @@ finally: PY } +try_rpc_discovery() { + local rpc_out="$STATE_DIR/rpc-discovery.json" + rm -f "$rpc_out" + python3 "$SCRIPT_DIR/truenas_installer_rpc_probe.py" --host 127.0.0.1 --port "$RPC_PORT" --out "$rpc_out" --timeout 3 >/dev/null 2>&1 || true + [[ -f "$rpc_out" ]] || return 1 + RPC_DISCOVERY_JSON="$(cat "$rpc_out")" + if python3 - "$rpc_out" <<'PY' +import json, pathlib, sys +try: + data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +except Exception: + raise SystemExit(1) +raise SystemExit(0 if data.get("oracleSatisfied") is True else 1) +PY + then + RPC_DISCOVERY_OK="true" + return 0 + fi + return 1 +} + qemu-img create -q -f qcow2 "$STATE_DIR/boot.qcow2" "$DISK_SIZE" RPC_PORT="$(python3 - <<'PY' import socket @@ -211,8 +234,9 @@ sudo -n qemu-system-x86_64 \ QEMU_PID="$(sudo -n cat "$STATE_DIR/qemu.pid")" T0_OBSERVED="false" -RPC_REACHABLE="false" -for _ in $(seq 1 220); do +RPC_HOSTFWD_ACCEPTED="false" +RPC_DISCOVERY_OK="false" +for attempt in $(seq 1 220); do if grep -Eqi 'TrueNAS|truenas-installer|TrueNAS Installer|Install/Upgrade' "$STATE_DIR/serial.log" 2>/dev/null; then T0_OBSERVED="true" fi @@ -225,9 +249,15 @@ for _ in $(seq 1 220); do if [[ "$RUNG" == "t0" && "$T0_OBSERVED" == "true" ]]; then break fi - if [[ "$RUNG" == "t1" ]] && port_open "$RPC_PORT"; then - RPC_REACHABLE="true" - break + if [[ "$RUNG" == "t1" ]]; then + if port_open "$RPC_PORT"; then + RPC_HOSTFWD_ACCEPTED="true" + fi + # QEMU hostfwd accepting TCP is only a scheduling hint. The oracle is a + # completed vendor WebSocket/JSON-RPC discovery exchange. + if (( attempt % 2 == 0 )) && try_rpc_discovery; then + break + fi fi sleep 3 done @@ -243,23 +273,6 @@ if [[ "$RUNG" == "t0" ]]; then exit 0 fi -[[ "$T0_OBSERVED" == "true" ]] || fail_evidence ORACLE_FAILURE installer-rpc "T1 did not preserve the T0 installer-environment oracle" -[[ "$RPC_REACHABLE" == "true" ]] || fail_evidence ORACLE_FAILURE installer-rpc "TrueNAS installer RPC port 8080 did not become reachable" - -RPC_OUT="$STATE_DIR/rpc-discovery.json" -python3 "$SCRIPT_DIR/truenas_installer_rpc_probe.py" --host 127.0.0.1 --port "$RPC_PORT" --out "$RPC_OUT" --timeout 8 || true -if [[ -f "$RPC_OUT" ]]; then - RPC_DISCOVERY_JSON="$(cat "$RPC_OUT")" -fi -RPC_OK="$(python3 - "$RPC_OUT" <<'PY' -import json, pathlib, sys -try: - data=json.loads(pathlib.Path(sys.argv[1]).read_text()) -except Exception: - print("false") -else: - print("true" if data.get("oracleSatisfied") is True else "false") -PY -)" -[[ "$RPC_OK" == "true" ]] || fail_evidence ORACLE_FAILURE installer-rpc "TrueNAS installer WebSocket JSON-RPC answered but read-only discovery oracle failed" -write_receipt SUPPORTED true installer-rpc "pinned TrueNAS installer answered read-only JSON-RPC discovery methods" +[[ "$RPC_DISCOVERY_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE installer-rpc "TrueNAS installer did not complete the read-only WebSocket JSON-RPC discovery oracle" +write_receipt SUPPORTED true installer-rpc "pinned TrueNAS installer answered read-only JSON-RPC discovery methods; stronger RPC evidence also establishes the installer environment for this rung" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index e9f92f354..5f0128a57 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -231,6 +231,14 @@ def server(): self.assertFalse(thread.is_alive()) self.assertEqual(errors, []) + def test_truenas_t1_requires_real_rpc_not_hostfwd_tcp(self): + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn("try_rpc_discovery", text) + self.assertIn("RPC_DISCOVERY_OK", text) + self.assertIn("installer_rpc_hostfwd_accepted", text) + self.assertNotIn("RPC_REACHABLE", text) + self.assertIn("completed vendor WebSocket/JSON-RPC discovery exchange", text) + if __name__ == "__main__": unittest.main() From 2800991b01f2b976db1a996e7fe4276f880caeb7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:36:44 +0200 Subject: [PATCH 024/719] prep: add gated TrueNAS install and middleware clients --- scripts/truenas_installer_rpc_install.py | 123 ++++++++++++++ scripts/truenas_middleware_ddp_probe.py | 199 +++++++++++++++++++++++ tests/test_gha_kvm_system_rdte.py | 20 +++ 3 files changed, 342 insertions(+) create mode 100755 scripts/truenas_installer_rpc_install.py create mode 100755 scripts/truenas_middleware_ddp_probe.py diff --git a/scripts/truenas_installer_rpc_install.py b/scripts/truenas_installer_rpc_install.py new file mode 100755 index 000000000..c3af0c9df --- /dev/null +++ b/scripts/truenas_installer_rpc_install.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Bounded mutating client for the TrueNAS installer JSON-RPC API.""" +from __future__ import annotations + +import argparse +import json +import pathlib +import time + +from truenas_installer_rpc_probe import WebSocket + + +def rpc_call(ws, method, request_id, params=None, progress=None): + request = {"jsonrpc": "2.0", "id": request_id, "method": method} + if params is not None: + request["params"] = params + ws.send_json(request) + while True: + message = json.loads(ws.recv_text()) + if message.get("method") == "installation_progress": + values = message.get("params") or [] + if progress is not None and values and isinstance(values[0], dict): + progress.append(values[0]) + continue + if message.get("id") != request_id: + continue + if message.get("error") is not None: + raise RuntimeError(f"{method}: {json.dumps(message['error'], sort_keys=True)}") + return message.get("result") + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--out", required=True) + p.add_argument("--timeout", type=float, default=15.0) + a = p.parse_args() + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + if len(password) < 12: + raise SystemExit("ephemeral install password must be at least 12 characters") + + payload = { + "schema": "truenas-installer-rpc-install/v1", + "oracleSatisfied": False, + "classification": "ORACLE_FAILURE", + "selected_disk": None, + "selected_interface": None, + "progress": [], + } + started = time.time() + ws = None + try: + ws = WebSocket(a.host, a.port, timeout=a.timeout) + adopted = rpc_call(ws, "is_adopted", 1) + payload["is_adopted"] = adopted + if adopted: + raise RuntimeError("fresh disposable installer unexpectedly reports adopted=true") + + disks = rpc_call(ws, "list_disks", 2) + payload["discovered_disks"] = disks + candidates = [disk for disk in disks if not disk.get("removable", False)] + if len(candidates) != 1: + raise RuntimeError( + f"refusing install: expected exactly one non-removable disk, found {len(candidates)}" + ) + disk = candidates[0] + payload["selected_disk"] = disk["name"] + + interfaces = rpc_call(ws, "list_network_interfaces", 3) + payload["discovered_interfaces"] = interfaces + if len(interfaces) != 1: + raise RuntimeError( + f"refusing install: expected exactly one non-loopback interface, found {len(interfaces)}" + ) + interface = interfaces[0] + payload["selected_interface"] = interface["name"] + + install_params = { + "disks": [disk["name"]], + "set_pmbr": True, + "authentication": { + "username": "truenas_admin", + "password": password, + }, + "post_install": { + "network_interfaces": [ + { + "name": interface["name"], + "ipv4_dhcp": True, + "ipv6_auto": True, + } + ] + }, + } + rpc_call(ws, "install", 4, install_params, progress=payload["progress"]) + payload["oracleSatisfied"] = True + payload["classification"] = "SUPPORTED" + payload["detail"] = "vendor installer completed on the sole disposable disk" + payload["install_contract"] = { + "disk_count": 1, + "set_pmbr": True, + "authentication_username": "truenas_admin", + "password_configured": True, + "ipv4_dhcp": True, + "ipv6_auto": True, + } + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/truenas_middleware_ddp_probe.py b/scripts/truenas_middleware_ddp_probe.py new file mode 100755 index 000000000..4266563ce --- /dev/null +++ b/scripts/truenas_middleware_ddp_probe.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""Dependency-free installed TrueNAS middleware DDP health probe.""" +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import pathlib +import socket +import ssl +import struct +import time + + +class WebSocket: + def __init__(self, host, port, path="/websocket", timeout=6.0, tls=False): + raw = socket.create_connection((host, port), timeout=timeout) + if tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + raw = context.wrap_socket(raw, server_hostname=host) + self.sock = raw + self.sock.settimeout(timeout) + key = base64.b64encode(os.urandom(16)).decode() + request = ( + f"GET {path} HTTP/1.1\r\n" + f"Host: {host}:{port}\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {key}\r\n" + "Sec-WebSocket-Version: 13\r\n\r\n" + ).encode() + self.sock.sendall(request) + head = self._read_until(b"\r\n\r\n", 16384) + status = head.split(b"\r\n", 1)[0] + if b" 101 " not in status: + raise RuntimeError(f"websocket handshake failed: {status.decode(errors='replace')}") + expected = base64.b64encode( + hashlib.sha1((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest() + ).decode() + headers = {} + for line in head.decode(errors="replace").split("\r\n")[1:]: + if ":" in line: + k, v = line.split(":", 1) + headers[k.lower().strip()] = v.strip() + if headers.get("sec-websocket-accept") != expected: + raise RuntimeError("websocket accept digest mismatch") + + def _read_exact(self, n): + parts = [] + while n: + part = self.sock.recv(n) + if not part: + raise EOFError("websocket closed") + parts.append(part) + n -= len(part) + return b"".join(parts) + + def _read_until(self, marker, limit): + data = bytearray() + while marker not in data: + part = self.sock.recv(4096) + if not part: + raise EOFError("socket closed during handshake") + data.extend(part) + if len(data) > limit: + raise RuntimeError("handshake exceeded limit") + return bytes(data) + + def _send_frame(self, opcode, payload): + mask = os.urandom(4) + first = 0x80 | opcode + n = len(payload) + if n < 126: + header = bytes([first, 0x80 | n]) + elif n <= 0xFFFF: + header = bytes([first, 0x80 | 126]) + struct.pack("!H", n) + else: + header = bytes([first, 0x80 | 127]) + struct.pack("!Q", n) + masked = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) + self.sock.sendall(header + mask + masked) + + def send_json(self, obj): + self._send_frame(0x1, json.dumps(obj, separators=(",", ":")).encode()) + + def recv_json(self): + while True: + b1, b2 = self._read_exact(2) + opcode = b1 & 0x0F + n = b2 & 0x7F + if n == 126: + n = struct.unpack("!H", self._read_exact(2))[0] + elif n == 127: + n = struct.unpack("!Q", self._read_exact(8))[0] + masked = bool(b2 & 0x80) + mask = self._read_exact(4) if masked else b"" + payload = self._read_exact(n) + if masked: + payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) + if opcode == 0x8: + raise EOFError("websocket close frame") + if opcode == 0x9: + self._send_frame(0xA, payload) + continue + if opcode != 0x1: + continue + return json.loads(payload.decode()) + + def close(self): + try: + self._send_frame(0x8, b"") + except Exception: + pass + self.sock.close() + + +def wait_for(ws, predicate): + while True: + message = ws.recv_json() + if message.get("msg") == "ping": + response = {"msg": "pong"} + if "id" in message: + response["id"] = message["id"] + ws.send_json(response) + continue + if predicate(message): + return message + + +def ddp_call(ws, request_id, method, params): + ws.send_json({"id": request_id, "msg": "method", "method": method, "params": params}) + message = wait_for(ws, lambda m: m.get("msg") == "result" and m.get("id") == request_id) + if message.get("error") is not None: + raise RuntimeError(f"{method}: {json.dumps(message['error'], sort_keys=True)}") + return message.get("result") + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--out", required=True) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=6.0) + a = p.parse_args() + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + payload = { + "schema": "truenas-middleware-ddp-health/v1", + "oracleSatisfied": False, + "classification": "ORACLE_FAILURE", + "transport": "wss" if a.tls else "ws", + } + started = time.time() + ws = None + try: + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError(f"DDP connection failed: {connected!r}") + + auth = ddp_call( + ws, + "1", + "auth.login_ex", + [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }], + ) + payload["auth_response_type"] = auth.get("response_type") if isinstance(auth, dict) else None + if payload["auth_response_type"] != "SUCCESS": + raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") + + payload["system_version"] = ddp_call(ws, "2", "system.version", []) + payload["system_info"] = ddp_call(ws, "3", "system.info", []) + payload["oracleSatisfied"] = True + payload["classification"] = "SUPPORTED" + payload["detail"] = "installed TrueNAS middleware authenticated and answered system.version/system.info" + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 5f0128a57..bdf4c8104 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -15,6 +15,8 @@ TRUENAS = ROOT / "scripts" / "gha_kvm_truenas_rdte.sh" TRUENAS_RPC = ROOT / "scripts" / "truenas_installer_rpc_probe.py" WORKFLOW = ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml" +TRUENAS_INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" +TRUENAS_MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" class SystemRdteContractTests(unittest.TestCase): @@ -239,6 +241,24 @@ def test_truenas_t1_requires_real_rpc_not_hostfwd_tcp(self): self.assertNotIn("RPC_REACHABLE", text) self.assertIn("completed vendor WebSocket/JSON-RPC discovery exchange", text) + def test_future_truenas_mutating_clients_compile_and_refuse_ambiguity(self): + install = TRUENAS_INSTALL.read_text(encoding="utf-8") + middleware = TRUENAS_MIDDLEWARE.read_text(encoding="utf-8") + self.assertIn("expected exactly one non-removable disk", install) + self.assertIn("expected exactly one non-loopback interface", install) + self.assertIn('"truenas_admin"', install) + self.assertIn('"auth.login_ex"', middleware) + self.assertIn('"PASSWORD_PLAIN"', middleware) + self.assertIn('"system.version"', middleware) + self.assertIn('"system.info"', middleware) + for path in (TRUENAS_INSTALL, TRUENAS_MIDDLEWARE): + cp = subprocess.run( + ["python3", "-m", "py_compile", str(path)], + text=True, + capture_output=True, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + if __name__ == "__main__": unittest.main() From 66b35e567dd84257817c02179a0272dfe1f6ac64 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:37:44 +0200 Subject: [PATCH 025/719] test: qualify future TrueNAS install and middleware protocols --- .github/workflows/gha-kvm-system-rdte.yml | 2 +- tests/test_truenas_future_protocols.py | 254 ++++++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 tests/test_truenas_future_protocols.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 35ec3bfaf..ec5a387eb 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -79,7 +79,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py proxmox-9-2: needs: [changes, contract] diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py new file mode 100644 index 000000000..4759dd47c --- /dev/null +++ b/tests/test_truenas_future_protocols.py @@ -0,0 +1,254 @@ +import base64 +import hashlib +import json +import pathlib +import socket +import struct +import subprocess +import sys +import tempfile +import threading +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" +MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" + + +class SyntheticWebSocketPeer: + def __init__(self): + self.listener = socket.socket() + self.listener.bind(("127.0.0.1", 0)) + self.listener.listen(1) + self.port = self.listener.getsockname()[1] + + @staticmethod + def read_exact(conn, count): + chunks = [] + while count: + part = conn.recv(count) + if not part: + raise EOFError("peer closed") + chunks.append(part) + count -= len(part) + return b"".join(chunks) + + @classmethod + def recv_json(cls, conn): + b1, b2 = cls.read_exact(conn, 2) + opcode = b1 & 0x0F + if opcode == 0x8: + return None + if opcode != 0x1: + raise AssertionError(f"unexpected opcode {opcode}") + if not (b2 & 0x80): + raise AssertionError("client frame must be masked") + size = b2 & 0x7F + if size == 126: + size = struct.unpack("!H", cls.read_exact(conn, 2))[0] + elif size == 127: + size = struct.unpack("!Q", cls.read_exact(conn, 8))[0] + mask = cls.read_exact(conn, 4) + payload = cls.read_exact(conn, size) + payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) + return json.loads(payload.decode()) + + @staticmethod + def send_json(conn, value): + payload = json.dumps(value, separators=(",", ":")).encode() + if len(payload) < 126: + header = bytes([0x81, len(payload)]) + else: + header = bytes([0x81, 126]) + struct.pack("!H", len(payload)) + conn.sendall(header + payload) + + def accept_websocket(self): + conn, _ = self.listener.accept() + request = bytearray() + while b"\r\n\r\n" not in request: + request.extend(conn.recv(4096)) + headers = {} + for line in request.decode().split("\r\n")[1:]: + if ":" in line: + key, value = line.split(":", 1) + headers[key.lower().strip()] = value.strip() + key = headers["sec-websocket-key"] + accept = base64.b64encode( + hashlib.sha1( + (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode() + ).digest() + ).decode() + conn.sendall( + ( + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {accept}\r\n\r\n" + ).encode() + ) + return conn + + +class FutureTrueNASProtocolTests(unittest.TestCase): + def test_installer_mutation_is_exact_and_progress_is_retained(self): + peer = SyntheticWebSocketPeer() + errors = [] + password = "synthetic-ephemeral-9264" + + def server(): + try: + with peer.accept_websocket() as conn: + expected = [ + ("is_adopted", False), + ("list_disks", [{ + "name": "vda", + "size": 24_000_000_000, + "model": "QEMU", + "label": "", + "removable": False, + }]), + ("list_network_interfaces", [{"name": "ens3"}]), + ] + for method, result in expected: + req = peer.recv_json(conn) + self.assertEqual(req["method"], method) + peer.send_json(conn, { + "jsonrpc": "2.0", + "id": req["id"], + "result": result, + }) + + req = peer.recv_json(conn) + self.assertEqual(req["method"], "install") + params = req["params"] + self.assertEqual(params["disks"], ["vda"]) + self.assertTrue(params["set_pmbr"]) + self.assertEqual(params["authentication"]["username"], "truenas_admin") + self.assertEqual(params["authentication"]["password"], password) + self.assertEqual( + params["post_install"]["network_interfaces"], + [{"name": "ens3", "ipv4_dhcp": True, "ipv6_auto": True}], + ) + peer.send_json(conn, { + "jsonrpc": "2.0", + "method": "installation_progress", + "params": [{"progress": 0.5, "message": "Installing"}], + }) + peer.send_json(conn, { + "jsonrpc": "2.0", + "id": req["id"], + "result": None, + }) + except Exception as exc: + errors.append(exc) + finally: + peer.listener.close() + + thread = threading.Thread(target=server, daemon=True) + thread.start() + with tempfile.TemporaryDirectory() as td: + td = pathlib.Path(td) + password_file = td / "password" + password_file.write_text(password, encoding="utf-8") + out = td / "install.json" + cp = subprocess.run( + [ + sys.executable, + str(INSTALL), + "--port", str(peer.port), + "--password-file", str(password_file), + "--out", str(out), + "--timeout", "2", + ], + text=True, + capture_output=True, + timeout=10, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + receipt = json.loads(out.read_text(encoding="utf-8")) + self.assertTrue(receipt["oracleSatisfied"]) + self.assertEqual(receipt["selected_disk"], "vda") + self.assertEqual(receipt["selected_interface"], "ens3") + self.assertEqual(receipt["progress"][0]["message"], "Installing") + self.assertNotIn(password, out.read_text(encoding="utf-8")) + thread.join(timeout=5) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + + def test_installed_middleware_ddp_auth_and_health(self): + peer = SyntheticWebSocketPeer() + errors = [] + password = "synthetic-ephemeral-9264" + + def server(): + try: + with peer.accept_websocket() as conn: + msg = peer.recv_json(conn) + self.assertEqual(msg, {"msg": "connect", "version": "1", "support": ["1"]}) + peer.send_json(conn, {"msg": "connected", "session": "synthetic"}) + + msg = peer.recv_json(conn) + self.assertEqual(msg["method"], "auth.login_ex") + auth = msg["params"][0] + self.assertEqual(auth["mechanism"], "PASSWORD_PLAIN") + self.assertEqual(auth["username"], "truenas_admin") + self.assertEqual(auth["password"], password) + peer.send_json(conn, { + "id": msg["id"], + "msg": "result", + "result": {"response_type": "SUCCESS"}, + }) + + msg = peer.recv_json(conn) + self.assertEqual(msg["method"], "system.version") + peer.send_json(conn, { + "id": msg["id"], + "msg": "result", + "result": "TrueNAS-26.0.0-BETA.3", + }) + + msg = peer.recv_json(conn) + self.assertEqual(msg["method"], "system.info") + peer.send_json(conn, { + "id": msg["id"], + "msg": "result", + "result": {"version": "TrueNAS-26.0.0-BETA.3"}, + }) + except Exception as exc: + errors.append(exc) + finally: + peer.listener.close() + + thread = threading.Thread(target=server, daemon=True) + thread.start() + with tempfile.TemporaryDirectory() as td: + td = pathlib.Path(td) + password_file = td / "password" + password_file.write_text(password, encoding="utf-8") + out = td / "middleware.json" + cp = subprocess.run( + [ + sys.executable, + str(MIDDLEWARE), + "--port", str(peer.port), + "--password-file", str(password_file), + "--out", str(out), + "--timeout", "2", + ], + text=True, + capture_output=True, + timeout=10, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + receipt = json.loads(out.read_text(encoding="utf-8")) + self.assertTrue(receipt["oracleSatisfied"]) + self.assertEqual(receipt["auth_response_type"], "SUCCESS") + self.assertEqual(receipt["system_version"], "TrueNAS-26.0.0-BETA.3") + self.assertNotIn(password, out.read_text(encoding="utf-8")) + thread.join(timeout=5) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + + +if __name__ == "__main__": + unittest.main() From ddd3ef5eadead1258189752fb909a1124a0110d0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:40:29 +0200 Subject: [PATCH 026/719] prep: wire earned TrueNAS install and middleware T2 rung --- .github/workflows/gha-kvm-system-rdte.yml | 8 +- docs/gha-kvm-system-rdte.md | 21 ++++ scripts/gha_kvm_truenas_rdte.sh | 141 +++++++++++++++++++++- tests/test_gha_kvm_system_rdte.py | 9 ++ 4 files changed, 169 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index ec5a387eb..8d4cc200b 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -58,7 +58,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py) + scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py) truenas=true ;; esac @@ -139,7 +139,7 @@ jobs: needs.changes.outputs.truenas == 'true' && (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') runs-on: ubuntu-26.04 - timeout-minutes: 32 + timeout-minutes: 50 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -156,7 +156,7 @@ jobs: mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" bash scripts/gha_kvm_truenas_rdte.sh \ --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ - --rung t1 \ + --rung t2 \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence @@ -169,7 +169,7 @@ jobs: import json, pathlib, sys pathlib.Path(sys.argv[1]).write_text(json.dumps({ "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T1"}, + "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T2"}, "classification":"HARNESS_FAILURE", "oracleSatisfied":False, "phase":"workflow-fallback", diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 3a73d578a..08f6913f0 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -144,3 +144,24 @@ the vendor installer RPC is stronger evidence that the installer environment is running, so lack of an earlier serial-label observation does not negate a completed RPC oracle. This avoids both a premature TCP false positive and a weaker-oracle dependency blocking a stronger observed interface. + + +### TrueNAS T2 install + installed middleware + +T2 is a single disposable-system execution with separately retained sub-oracles: + +1. T1 read-only installer JSON-RPC discovery must pass. +2. The mutating installer client refuses adopted state, requires exactly one + non-removable destination disk and one non-loopback NIC, configures + `truenas_admin` with an ephemeral non-repository password, enables DHCP, + and requires the vendor `install` RPC to return successfully. +3. Only after that response (the upstream installer has exported `boot-pool`) + is the installer VM terminated. +4. The same qcow2 is booted with a stable virtual NIC identity. +5. Installed middleware is qualified independently over its source-defined DDP + `/websocket` interface using `auth.login_ex` with `PASSWORD_PLAIN`, + followed by `system.version` and `system.info`. + +An installed-middleware failure therefore does not erase a successful installer +sub-oracle. Fresh TrueNAS installation installs both i386-pc GRUB and EFI GRUB, +so the disposable legacy-BIOS QEMU target is a valid post-install boot oracle. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 1ea11334f..a7ef6293f 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -11,11 +11,12 @@ SHA_URL="$ISO_URL.sha256" RAM_MIB=8192 VCPUS=2 DISK_SIZE="24G" +NIC_MAC="52:54:00:54:4e:26" MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2]" } OUT="" @@ -31,7 +32,7 @@ while [[ $# -gt 0 ]]; do esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } -[[ "$RUNG" == "t0" || "$RUNG" == "t1" ]] || { echo "rung must be t0 or t1" >&2; exit 2; } +[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" ]] || { echo "rung must be t0, t1, or t2" >&2; exit 2; } if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -48,6 +49,8 @@ RPC_HOSTFWD_ACCEPTED="false" RPC_DISCOVERY_OK="false" RPC_DISCOVERY_JSON="" QEMU_ALIVE_AT_GATE="unknown" +INSTALL_RESULT_JSON="" +MIDDLEWARE_RESULT_JSON="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -69,6 +72,7 @@ write_receipt() { export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" + export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" python3 - <<'PY' import json, os, pathlib payload = { @@ -93,13 +97,18 @@ payload = { "installer_serial_marker_observed": os.environ.get("R_T0") == "true", "installer_rpc_hostfwd_accepted": os.environ.get("R_RPC_HOSTFWD") == "true", "installer_rpc_readonly": os.environ.get("R_RPC_OK") == "true", + "installer_install_completed": bool(os.environ.get("R_INSTALL_RESULT")) and json.loads(os.environ["R_INSTALL_RESULT"]).get("oracleSatisfied") is True, + "installed_middleware_authenticated": bool(os.environ.get("R_MIDDLEWARE_RESULT")) and json.loads(os.environ["R_MIDDLEWARE_RESULT"]).get("oracleSatisfied") is True, }, "rpc_discovery": json.loads(os.environ["R_RPC_DISCOVERY"]) if os.environ.get("R_RPC_DISCOVERY") else None, + "install_result": json.loads(os.environ["R_INSTALL_RESULT"]) if os.environ.get("R_INSTALL_RESULT") else None, + "installed_middleware": json.loads(os.environ["R_MIDDLEWARE_RESULT"]) if os.environ.get("R_MIDDLEWARE_RESULT") else None, "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ "T0 proves pinned vendor media integrity and installer-environment boot under the disposable virtual target profile.", - "T1 is read-only installer RPC discovery; installation, ZFS, middleware, Containers, Apps, and application lifecycle remain later gates.", + "T1 is read-only installer RPC discovery.", + "T2 adds vendor installation plus installed middleware authentication/health; ZFS data-pool, Containers, Apps, and application lifecycle remain later gates.", "This does not qualify physical storage controllers, SMART, GPU, IPMI, or HA behavior.", ], } @@ -116,6 +125,12 @@ fail_evidence() { [[ -f "$SCRIPT_DIR/truenas_installer_rpc_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer RPC probe" +if [[ "$RUNG" == "t2" ]]; then + [[ -f "$SCRIPT_DIR/truenas_installer_rpc_install.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer install client" + [[ -f "$SCRIPT_DIR/truenas_middleware_ddp_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS middleware health client" +fi for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" done @@ -226,7 +241,7 @@ sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ -cdrom "$SERIAL_ISO" -boot order=d \ - -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080" -device virtio-net-pci,netdev=net0 \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080" -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ -daemonize -pidfile "$STATE_DIR/qemu.pid" || @@ -249,7 +264,7 @@ for attempt in $(seq 1 220); do if [[ "$RUNG" == "t0" && "$T0_OBSERVED" == "true" ]]; then break fi - if [[ "$RUNG" == "t1" ]]; then + if [[ "$RUNG" == "t1" || "$RUNG" == "t2" ]]; then if port_open "$RPC_PORT"; then RPC_HOSTFWD_ACCEPTED="true" fi @@ -275,4 +290,118 @@ fi [[ "$RPC_DISCOVERY_OK" == "true" ]] || fail_evidence ORACLE_FAILURE installer-rpc "TrueNAS installer did not complete the read-only WebSocket JSON-RPC discovery oracle" -write_receipt SUPPORTED true installer-rpc "pinned TrueNAS installer answered read-only JSON-RPC discovery methods; stronger RPC evidence also establishes the installer environment for this rung" + +if [[ "$RUNG" == "t1" ]]; then + write_receipt SUPPORTED true installer-rpc "pinned TrueNAS installer answered read-only JSON-RPC discovery methods; stronger RPC evidence also establishes the installer environment for this rung" + exit 0 +fi + +PASSWORD_FILE="$STATE_DIR/install-password" +python3 - "$PASSWORD_FILE" <<'PY' +import pathlib, secrets, sys +path = pathlib.Path(sys.argv[1]) +path.write_text(secrets.token_urlsafe(24), encoding="utf-8") +path.chmod(0o600) +PY + +INSTALL_OUT="$STATE_DIR/install-result.json" +python3 "$SCRIPT_DIR/truenas_installer_rpc_install.py" \ + --host 127.0.0.1 --port "$RPC_PORT" \ + --password-file "$PASSWORD_FILE" \ + --out "$INSTALL_OUT" --timeout 120 >/dev/null 2>&1 || true +[[ -f "$INSTALL_OUT" ]] || fail_evidence HARNESS_FAILURE installer-install "installer mutation client did not emit a receipt" +INSTALL_RESULT_JSON="$(cat "$INSTALL_OUT")" +INSTALL_OK="$(python3 - "$INSTALL_OUT" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +print("true" if data.get("oracleSatisfied") is True else "false") +PY +)" +[[ "$INSTALL_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE installer-install "vendor installer RPC did not complete installation on the sole disposable disk" + +# The installer exports boot-pool before returning from install(). Terminate the +# disposable installer VM only after that completed response, then boot the same disk. +if [[ -n "$QEMU_PID" ]]; then + sudo -n kill "$QEMU_PID" >/dev/null 2>&1 || true + for _ in $(seq 1 40); do + sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1 || break + sleep 0.25 + done + sudo -n kill -9 "$QEMU_PID" >/dev/null 2>&1 || true + QEMU_PID="" +fi + +HTTP_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" +HTTPS_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" +: >"$STATE_DIR/serial.log" +sudo -n qemu-system-x86_64 \ + -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ + -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ + -boot order=c \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443" \ + -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC" \ + -display none -monitor none \ + -serial "file:$STATE_DIR/serial.log" \ + -daemonize -pidfile "$STATE_DIR/qemu.pid" || + fail_evidence ENVIRONMENT_FAILURE installed-boot "QEMU could not start installed TrueNAS guest" +QEMU_PID="$(sudo -n cat "$STATE_DIR/qemu.pid")" + +MIDDLEWARE_OUT="$STATE_DIR/middleware-health.json" +MIDDLEWARE_OK="false" +for attempt in $(seq 1 180); do + if ! sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then + QEMU_ALIVE_AT_GATE="false" + break + fi + QEMU_ALIVE_AT_GATE="true" + + if (( attempt % 2 == 0 )); then + rm -f "$MIDDLEWARE_OUT" + python3 "$SCRIPT_DIR/truenas_middleware_ddp_probe.py" \ + --host 127.0.0.1 --port "$HTTP_PORT" \ + --password-file "$PASSWORD_FILE" --out "$MIDDLEWARE_OUT" --timeout 4 \ + >/dev/null 2>&1 || true + if [[ -f "$MIDDLEWARE_OUT" ]] && python3 - "$MIDDLEWARE_OUT" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +raise SystemExit(0 if data.get("oracleSatisfied") is True else 1) +PY + then + MIDDLEWARE_OK="true" + break + fi + + rm -f "$MIDDLEWARE_OUT" + python3 "$SCRIPT_DIR/truenas_middleware_ddp_probe.py" \ + --host 127.0.0.1 --port "$HTTPS_PORT" --tls \ + --password-file "$PASSWORD_FILE" --out "$MIDDLEWARE_OUT" --timeout 4 \ + >/dev/null 2>&1 || true + if [[ -f "$MIDDLEWARE_OUT" ]] && python3 - "$MIDDLEWARE_OUT" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +raise SystemExit(0 if data.get("oracleSatisfied") is True else 1) +PY + then + MIDDLEWARE_OK="true" + break + fi + fi + sleep 3 +done + +if [[ -f "$MIDDLEWARE_OUT" ]]; then + MIDDLEWARE_RESULT_JSON="$(cat "$MIDDLEWARE_OUT")" +fi +[[ "$MIDDLEWARE_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE installed-middleware "installed TrueNAS did not authenticate and answer system.version/system.info within the bounded boot window" + +write_receipt SUPPORTED true installed-middleware "vendor installer completed and installed TrueNAS middleware authenticated and answered health methods" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index bdf4c8104..65b6a6ef4 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -259,6 +259,15 @@ def test_future_truenas_mutating_clients_compile_and_refuse_ambiguity(self): ) self.assertEqual(cp.returncode, 0, cp.stderr) + def test_truenas_t2_keeps_install_and_middleware_oracles_separate(self): + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn("t0|t1|t2", text) + self.assertIn("installer_install_completed", text) + self.assertIn("installed_middleware_authenticated", text) + self.assertIn("truenas_installer_rpc_install.py", text) + self.assertIn("truenas_middleware_ddp_probe.py", text) + self.assertIn('NIC_MAC="52:54:00:54:4e:26"', text) + if __name__ == "__main__": unittest.main() From 547be06d3c0c159ce54fd8af0566b095faaa2ddb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:57:10 +0200 Subject: [PATCH 027/719] fix: send TrueNAS install payload as one RPC argument --- scripts/truenas_installer_rpc_install.py | 2 +- tests/test_gha_kvm_system_rdte.py | 4 ++++ tests/test_truenas_future_protocols.py | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_installer_rpc_install.py b/scripts/truenas_installer_rpc_install.py index c3af0c9df..220851c2f 100755 --- a/scripts/truenas_installer_rpc_install.py +++ b/scripts/truenas_installer_rpc_install.py @@ -95,7 +95,7 @@ def main(): ] }, } - rpc_call(ws, "install", 4, install_params, progress=payload["progress"]) + rpc_call(ws, "install", 4, [install_params], progress=payload["progress"]) payload["oracleSatisfied"] = True payload["classification"] = "SUPPORTED" payload["detail"] = "vendor installer completed on the sole disposable disk" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 65b6a6ef4..dfa9a0774 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -268,6 +268,10 @@ def test_truenas_t2_keeps_install_and_middleware_oracles_separate(self): self.assertIn("truenas_middleware_ddp_probe.py", text) self.assertIn('NIC_MAC="52:54:00:54:4e:26"', text) + def test_truenas_install_rpc_uses_one_positional_object(self): + text = TRUENAS_INSTALL.read_text(encoding="utf-8") + self.assertIn('rpc_call(ws, "install", 4, [install_params]', text) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index 4759dd47c..1400d2c82 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -120,7 +120,9 @@ def server(): req = peer.recv_json(conn) self.assertEqual(req["method"], "install") - params = req["params"] + self.assertIsInstance(req["params"], list) + self.assertEqual(len(req["params"]), 1) + params = req["params"][0] self.assertEqual(params["disks"], ["vda"]) self.assertTrue(params["set_pmbr"]) self.assertEqual(params["authentication"]["username"], "truenas_admin") From 77a18599f8555b36f298a5243bd8337576cb1557 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:42:34 +0200 Subject: [PATCH 028/719] test: qualify TrueNAS T3 pool protocol --- scripts/truenas_middleware_pool_probe.py | 168 +++++++++++++++++++++++ tests/test_truenas_future_protocols.py | 130 ++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100755 scripts/truenas_middleware_pool_probe.py diff --git a/scripts/truenas_middleware_pool_probe.py b/scripts/truenas_middleware_pool_probe.py new file mode 100755 index 000000000..d3bedc458 --- /dev/null +++ b/scripts/truenas_middleware_pool_probe.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Bounded TrueNAS middleware client for the T3 disposable ZFS-pool oracle.""" +from __future__ import annotations + +import argparse +import json +import pathlib +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +def disk_name(disk): + return disk.get("devname") or disk.get("name") + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--out", required=True) + p.add_argument("--pool-name", default="rdtepool") + p.add_argument("--expected-data-disks", type=int, default=2) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=180.0) + a = p.parse_args() + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + payload = { + "schema": "truenas-middleware-pool-t3/v1", + "oracleSatisfied": False, + "classification": "ORACLE_FAILURE", + "pool_name": a.pool_name, + "expected_data_disks": a.expected_data_disks, + "transport": "wss" if a.tls else "ws", + } + started = time.time() + ws = None + try: + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError(f"DDP connection failed: {connected!r}") + + auth = ddp_call( + ws, "1", "auth.login_ex", + [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }], + ) + payload["auth_response_type"] = auth.get("response_type") if isinstance(auth, dict) else None + if payload["auth_response_type"] != "SUCCESS": + raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") + + payload["system_version"] = ddp_call(ws, "2", "system.version", []) + boot_disks = ddp_call(ws, "3", "boot.get_disks", []) + payload["boot_disks"] = sorted(boot_disks) + unused = ddp_call(ws, "4", "disk.get_unused", []) + payload["unused_disks"] = unused + + if len(unused) != a.expected_data_disks: + raise RuntimeError( + f"refusing pool mutation: expected exactly {a.expected_data_disks} unused data disks, found {len(unused)}" + ) + selected = [disk_name(disk) for disk in unused] + if any(not name for name in selected): + raise RuntimeError("refusing pool mutation: unused disk missing name/devname") + if len(set(selected)) != len(selected): + raise RuntimeError("refusing pool mutation: duplicate data-disk names") + if set(selected) & set(boot_disks): + raise RuntimeError("refusing pool mutation: a selected data disk overlaps the boot pool") + payload["selected_data_disks"] = sorted(selected) + + existing = ddp_call(ws, "5", "pool.query", [[["name", "=", a.pool_name]]]) + if existing: + raise RuntimeError(f"refusing pool mutation: pool {a.pool_name!r} already exists") + + create = { + "name": a.pool_name, + "encryption": False, + "allow_duplicate_serials": True, + "topology": { + "data": [{ + "type": "MIRROR", + "disks": selected, + }], + }, + } + payload["create_contract"] = { + "name": a.pool_name, + "encryption": False, + "allow_duplicate_serials": True, + "topology": {"data": [{"type": "MIRROR", "disks": sorted(selected)}]}, + } + job_id = ddp_call(ws, "6", "pool.create", [create]) + if not isinstance(job_id, int): + raise RuntimeError(f"pool.create did not return a legacy DDP job id: {job_id!r}") + payload["job_id"] = job_id + + deadline = time.monotonic() + a.job_timeout + job = None + poll = 7 + while time.monotonic() < deadline: + result = ddp_call( + ws, str(poll), "core.get_jobs", + [[["id", "=", job_id]], {"get": True}], + ) + poll += 1 + if result: + job = result + state = job.get("state") + payload["job_state"] = state + payload["job_progress"] = job.get("progress") + if state == "SUCCESS": + break + if state in {"FAILED", "ABORTED"}: + raise RuntimeError( + f"pool.create job {state}: {job.get('error') or job.get('exception')}" + ) + time.sleep(1) + else: + raise RuntimeError("pool.create job did not reach a terminal state within the bounded timeout") + + if not job or job.get("state") != "SUCCESS": + raise RuntimeError(f"pool.create did not reach SUCCESS: {job!r}") + + pool = ddp_call( + ws, str(poll), "pool.query", + [[["name", "=", a.pool_name]], {"get": True}], + ) + poll += 1 + payload["pool"] = pool + if pool.get("name") != a.pool_name: + raise RuntimeError(f"pool query returned wrong name: {pool.get('name')!r}") + if pool.get("status") != "ONLINE": + raise RuntimeError(f"created pool is not ONLINE: {pool.get('status')!r}") + if pool.get("healthy") is not True: + raise RuntimeError(f"created pool is not healthy: {pool.get('healthy')!r}") + + pool_disks = ddp_call(ws, str(poll), "pool.get_disks", [pool["id"]]) + payload["pool_disks"] = sorted(pool_disks) + if set(pool_disks) != set(selected): + raise RuntimeError( + f"pool disk membership mismatch: selected={sorted(selected)!r} pool={sorted(pool_disks)!r}" + ) + + payload["oracleSatisfied"] = True + payload["classification"] = "SUPPORTED" + payload["detail"] = "real two-disk mirror pool is ONLINE, healthy, and owns exactly the selected disposable disks" + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index 1400d2c82..8b666644d 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -13,6 +13,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" +POOL = ROOT / "scripts" / "truenas_middleware_pool_probe.py" class SyntheticWebSocketPeer: @@ -251,6 +252,135 @@ def server(): self.assertFalse(thread.is_alive()) self.assertEqual(errors, []) + def test_t3_pool_client_waits_for_job_and_proves_membership(self): + peer = SyntheticWebSocketPeer() + errors = [] + password = "synthetic-ephemeral-9264" + + def expect_call(conn, method): + msg = peer.recv_json(conn) + self.assertEqual(msg["msg"], "method") + self.assertEqual(msg["method"], method) + return msg + + def result(conn, msg, value): + peer.send_json(conn, {"id": msg["id"], "msg": "result", "result": value}) + + def server(): + try: + with peer.accept_websocket() as conn: + msg = peer.recv_json(conn) + self.assertEqual(msg, {"msg": "connect", "version": "1", "support": ["1"]}) + peer.send_json(conn, {"msg": "connected", "session": "synthetic"}) + + msg = expect_call(conn, "auth.login_ex") + self.assertEqual(msg["params"][0]["password"], password) + result(conn, msg, {"response_type": "SUCCESS"}) + + msg = expect_call(conn, "system.version") + result(conn, msg, "TrueNAS-26.0.0-BETA.3") + + msg = expect_call(conn, "boot.get_disks") + result(conn, msg, ["vda"]) + + msg = expect_call(conn, "disk.get_unused") + result(conn, msg, [ + {"name": "vdb", "devname": "vdb", "size": 8_589_934_592}, + {"name": "vdc", "devname": "vdc", "size": 8_589_934_592}, + ]) + + msg = expect_call(conn, "pool.query") + self.assertEqual(msg["params"], [[["name", "=", "rdtepool"]]]) + result(conn, msg, []) + + msg = expect_call(conn, "pool.create") + self.assertEqual(msg["params"], [{ + "name": "rdtepool", + "encryption": False, + "allow_duplicate_serials": True, + "topology": { + "data": [{ + "type": "MIRROR", + "disks": ["vdb", "vdc"], + }], + }, + }]) + result(conn, msg, 42) + + msg = expect_call(conn, "core.get_jobs") + self.assertEqual(msg["params"][0], [["id", "=", 42]]) + result(conn, msg, { + "id": 42, + "state": "RUNNING", + "progress": {"percent": 50, "description": "Creating pool"}, + }) + + msg = expect_call(conn, "core.get_jobs") + result(conn, msg, { + "id": 42, + "state": "SUCCESS", + "progress": {"percent": 100, "description": "Pool created"}, + "result": {"id": 7, "name": "rdtepool"}, + }) + + msg = expect_call(conn, "pool.query") + self.assertEqual(msg["params"], [ + [["name", "=", "rdtepool"]], + {"get": True}, + ]) + result(conn, msg, { + "id": 7, + "name": "rdtepool", + "guid": "synthetic", + "status": "ONLINE", + "healthy": True, + "path": "/mnt/rdtepool", + }) + + msg = expect_call(conn, "pool.get_disks") + self.assertEqual(msg["params"], [7]) + result(conn, msg, ["vdb", "vdc"]) + except Exception as exc: + errors.append(exc) + finally: + peer.listener.close() + + thread = threading.Thread(target=server, daemon=True) + thread.start() + with tempfile.TemporaryDirectory() as td: + td = pathlib.Path(td) + password_file = td / "password" + password_file.write_text(password, encoding="utf-8") + out = td / "pool.json" + cp = subprocess.run( + [ + sys.executable, + str(POOL), + "--port", str(peer.port), + "--password-file", str(password_file), + "--out", str(out), + "--timeout", "2", + "--job-timeout", "8", + ], + text=True, + capture_output=True, + timeout=15, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + receipt = json.loads(out.read_text(encoding="utf-8")) + self.assertTrue(receipt["oracleSatisfied"], receipt) + self.assertEqual(receipt["classification"], "SUPPORTED") + self.assertEqual(receipt["selected_data_disks"], ["vdb", "vdc"]) + self.assertEqual(receipt["pool_disks"], ["vdb", "vdc"]) + self.assertEqual(receipt["job_state"], "SUCCESS") + self.assertEqual(receipt["pool"]["status"], "ONLINE") + self.assertTrue(receipt["pool"]["healthy"]) + self.assertNotIn(password, out.read_text(encoding="utf-8")) + thread.join(timeout=5) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + if __name__ == "__main__": unittest.main() + unittest.main() From 9c967dda0c347522572e0e1476d4c372bca9812f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:48:03 +0200 Subject: [PATCH 029/719] exp: exercise TrueNAS T3 real ZFS data pool --- .github/workflows/gha-kvm-system-rdte.yml | 6 +- docs/gha-kvm-system-rdte.md | 20 ++++++ scripts/gha_kvm_truenas_rdte.sh | 75 ++++++++++++++++++++--- tests/test_gha_kvm_system_rdte.py | 19 ++++++ 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 8d4cc200b..0cd9e4b67 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -58,7 +58,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py) + scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py) truenas=true ;; esac @@ -156,7 +156,7 @@ jobs: mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" bash scripts/gha_kvm_truenas_rdte.sh \ --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ - --rung t2 \ + --rung t3 \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence @@ -169,7 +169,7 @@ jobs: import json, pathlib, sys pathlib.Path(sys.argv[1]).write_text(json.dumps({ "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T2"}, + "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T3"}, "classification":"HARNESS_FAILURE", "oracleSatisfied":False, "phase":"workflow-fallback", diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 08f6913f0..c6bd94a9a 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -165,3 +165,23 @@ T2 is a single disposable-system execution with separately retained sub-oracles: An installed-middleware failure therefore does not erase a successful installer sub-oracle. Fresh TrueNAS installation installs both i386-pc GRUB and EFI GRUB, so the disposable legacy-BIOS QEMU target is a valid post-install boot oracle. + + +### TrueNAS T3 disposable ZFS data pool + +T3 extends the accepted T2 system without changing the installer disk shape. The +installer still sees only the 24-GiB boot target. After vendor installation completes, +the installed-system boot adds exactly two experiment-owned 8-GiB sparse virtio disks. + +The T3 middleware client then: +- authenticates through the accepted installed-system DDP surface; +- records `boot.get_disks` and refuses any overlap with data candidates; +- requires exactly two disks from `disk.get_unused`; +- creates `rdtepool` as one two-disk `MIRROR`; +- waits for the returned `pool.create` job ID to reach `SUCCESS`; +- requires an independent `pool.query` oracle reporting `ONLINE` and healthy; +- requires `pool.get_disks` membership to match exactly the two selected data disks. + +QEMU disk attachment, middleware unused-disk classification, job acceptance, and pool +health are separate observations. T3 does not imply Apps support; T4 remains a distinct +gate. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index a7ef6293f..f4ba619f2 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -11,12 +11,15 @@ SHA_URL="$ISO_URL.sha256" RAM_MIB=8192 VCPUS=2 DISK_SIZE="24G" +DATA_DISK_SIZE="8G" +DATA_DISK_COUNT=2 +DATA_POOL_NAME="rdtepool" NIC_MAC="52:54:00:54:4e:26" MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3]" } OUT="" @@ -32,7 +35,7 @@ while [[ $# -gt 0 ]]; do esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } -[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" ]] || { echo "rung must be t0, t1, or t2" >&2; exit 2; } +[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]] || { echo "rung must be t0, t1, t2, or t3" >&2; exit 2; } if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -51,6 +54,7 @@ RPC_DISCOVERY_JSON="" QEMU_ALIVE_AT_GATE="unknown" INSTALL_RESULT_JSON="" MIDDLEWARE_RESULT_JSON="" +POOL_RESULT_JSON="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -72,7 +76,7 @@ write_receipt() { export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" - export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" + export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" python3 - <<'PY' import json, os, pathlib payload = { @@ -82,7 +86,13 @@ payload = { "oracleSatisfied": os.environ["R_ORACLE"].lower() == "true", "phase": os.environ["R_PHASE"], "detail": os.environ["R_DETAIL"], - "requested_shape": {"vcpus": 2, "ram_mib": 8192, "boot_disk": "24G"}, + "requested_shape": { + "vcpus": 2, + "ram_mib": 8192, + "boot_disk": "24G", + "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") == "t3" else [], + "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") == "t3" else None, + }, "source": { "iso_name": "TrueNAS-26.0.0-BETA.3.iso", "iso_url": "https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3/TrueNAS-26.0.0-BETA.3.iso", @@ -99,16 +109,20 @@ payload = { "installer_rpc_readonly": os.environ.get("R_RPC_OK") == "true", "installer_install_completed": bool(os.environ.get("R_INSTALL_RESULT")) and json.loads(os.environ["R_INSTALL_RESULT"]).get("oracleSatisfied") is True, "installed_middleware_authenticated": bool(os.environ.get("R_MIDDLEWARE_RESULT")) and json.loads(os.environ["R_MIDDLEWARE_RESULT"]).get("oracleSatisfied") is True, + "data_pool_created": bool(os.environ.get("R_POOL_RESULT")) and json.loads(os.environ["R_POOL_RESULT"]).get("oracleSatisfied") is True, }, "rpc_discovery": json.loads(os.environ["R_RPC_DISCOVERY"]) if os.environ.get("R_RPC_DISCOVERY") else None, "install_result": json.loads(os.environ["R_INSTALL_RESULT"]) if os.environ.get("R_INSTALL_RESULT") else None, "installed_middleware": json.loads(os.environ["R_MIDDLEWARE_RESULT"]) if os.environ.get("R_MIDDLEWARE_RESULT") else None, + "data_pool": json.loads(os.environ["R_POOL_RESULT"]) if os.environ.get("R_POOL_RESULT") else None, "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ "T0 proves pinned vendor media integrity and installer-environment boot under the disposable virtual target profile.", "T1 is read-only installer RPC discovery.", - "T2 adds vendor installation plus installed middleware authentication/health; ZFS data-pool, Containers, Apps, and application lifecycle remain later gates.", + "T2 adds vendor installation plus installed middleware authentication/health.", + "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", + "Containers, Apps, and application lifecycle remain later gates.", "This does not qualify physical storage controllers, SMART, GPU, IPMI, or HA behavior.", ], } @@ -125,11 +139,15 @@ fail_evidence() { [[ -f "$SCRIPT_DIR/truenas_installer_rpc_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer RPC probe" -if [[ "$RUNG" == "t2" ]]; then +if [[ "$RUNG" == "t2" || "$RUNG" == "t3" ]]; then [[ -f "$SCRIPT_DIR/truenas_installer_rpc_install.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer install client" [[ -f "$SCRIPT_DIR/truenas_middleware_ddp_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS middleware health client" + if [[ "$RUNG" == "t3" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_pool_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T3 pool client" + fi fi for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" @@ -264,7 +282,7 @@ for attempt in $(seq 1 220); do if [[ "$RUNG" == "t0" && "$T0_OBSERVED" == "true" ]]; then break fi - if [[ "$RUNG" == "t1" || "$RUNG" == "t2" ]]; then + if [[ "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]]; then if port_open "$RPC_PORT"; then RPC_HOSTFWD_ACCEPTED="true" fi @@ -332,6 +350,16 @@ if [[ -n "$QEMU_PID" ]]; then QEMU_PID="" fi +DATA_DRIVE_ARGS=() +if [[ "$RUNG" == "t3" ]]; then + for index in $(seq 0 $((DATA_DISK_COUNT - 1))); do + data_disk="$STATE_DIR/data${index}.qcow2" + qemu-img create -q -f qcow2 "$data_disk" "$DATA_DISK_SIZE" || + fail_evidence HARNESS_FAILURE data-disk-prepare "failed to create sparse data disk $index" + DATA_DRIVE_ARGS+=( -drive "file=$data_disk,if=virtio,format=qcow2" ) + done +fi + HTTP_PORT="$(python3 - <<'PY' import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() @@ -346,6 +374,7 @@ PY sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ + "${DATA_DRIVE_ARGS[@]}" \ -boot order=c \ -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443" \ -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC" \ @@ -357,6 +386,8 @@ QEMU_PID="$(sudo -n cat "$STATE_DIR/qemu.pid")" MIDDLEWARE_OUT="$STATE_DIR/middleware-health.json" MIDDLEWARE_OK="false" +MIDDLEWARE_PORT="" +MIDDLEWARE_TLS_ARG=() for attempt in $(seq 1 180); do if ! sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then QEMU_ALIVE_AT_GATE="false" @@ -377,6 +408,8 @@ raise SystemExit(0 if data.get("oracleSatisfied") is True else 1) PY then MIDDLEWARE_OK="true" + MIDDLEWARE_PORT="$HTTP_PORT" + MIDDLEWARE_TLS_ARG=() break fi @@ -392,6 +425,8 @@ raise SystemExit(0 if data.get("oracleSatisfied") is True else 1) PY then MIDDLEWARE_OK="true" + MIDDLEWARE_PORT="$HTTPS_PORT" + MIDDLEWARE_TLS_ARG=(--tls) break fi fi @@ -404,4 +439,28 @@ fi [[ "$MIDDLEWARE_OK" == "true" ]] || fail_evidence ORACLE_FAILURE installed-middleware "installed TrueNAS did not authenticate and answer system.version/system.info within the bounded boot window" -write_receipt SUPPORTED true installed-middleware "vendor installer completed and installed TrueNAS middleware authenticated and answered health methods" +if [[ "$RUNG" == "t2" ]]; then + write_receipt SUPPORTED true installed-middleware "vendor installer completed and installed TrueNAS middleware authenticated and answered health methods" + exit 0 +fi + +POOL_OUT="$STATE_DIR/data-pool.json" +python3 "$SCRIPT_DIR/truenas_middleware_pool_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --out "$POOL_OUT" --pool-name "$DATA_POOL_NAME" \ + --expected-data-disks "$DATA_DISK_COUNT" \ + --timeout 6 --job-timeout 180 >/dev/null 2>&1 || true +[[ -f "$POOL_OUT" ]] || fail_evidence HARNESS_FAILURE data-pool "T3 pool client did not emit a receipt" +POOL_RESULT_JSON="$(cat "$POOL_OUT")" +POOL_OK="$(python3 - "$POOL_OUT" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +print("true" if data.get("oracleSatisfied") is True else "false") +PY +)" +[[ "$POOL_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE data-pool "installed TrueNAS did not create and independently verify the disposable ZFS mirror pool" + +write_receipt SUPPORTED true data-pool "installed TrueNAS created an ONLINE healthy two-disk mirror containing exactly the selected disposable data disks" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index dfa9a0774..31977bcf5 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -17,6 +17,7 @@ WORKFLOW = ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml" TRUENAS_INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" TRUENAS_MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" +TRUENAS_POOL = ROOT / "scripts" / "truenas_middleware_pool_probe.py" class SystemRdteContractTests(unittest.TestCase): @@ -272,6 +273,24 @@ def test_truenas_install_rpc_uses_one_positional_object(self): text = TRUENAS_INSTALL.read_text(encoding="utf-8") self.assertIn('rpc_call(ws, "install", 4, [install_params]', text) + def test_truenas_t3_real_harness_contract(self): + text = TRUENAS.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn("t0|t1|t2|t3", text) + self.assertIn('DATA_DISK_SIZE="8G"', text) + self.assertIn("DATA_DISK_COUNT=2", text) + self.assertIn('DATA_POOL_NAME="rdtepool"', text) + self.assertIn("data${index}.qcow2", text) + self.assertIn('"${DATA_DRIVE_ARGS[@]}"', text) + self.assertIn("truenas_middleware_pool_probe.py", text) + self.assertIn('"data_pool_created"', text) + self.assertIn('"data_pool"', text) + self.assertIn("--rung t3", workflow) + self.assertIn("scripts/truenas_middleware_pool_probe.py", workflow) + shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) + self.assertEqual(shell_check.returncode, 0, shell_check.stderr) + py_check = subprocess.run(["python3", "-m", "py_compile", str(TRUENAS_POOL)], text=True, capture_output=True) + self.assertEqual(py_check.returncode, 0, py_check.stderr) if __name__ == "__main__": unittest.main() From e47ebcf86f8d7be277335e03cd83a5f1b0d8fbae Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:05:47 +0200 Subject: [PATCH 030/719] fix: use public disk.details for TrueNAS T3 --- scripts/truenas_middleware_pool_probe.py | 6 +++++- tests/test_truenas_future_protocols.py | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/scripts/truenas_middleware_pool_probe.py b/scripts/truenas_middleware_pool_probe.py index d3bedc458..7fbad5e4a 100755 --- a/scripts/truenas_middleware_pool_probe.py +++ b/scripts/truenas_middleware_pool_probe.py @@ -60,7 +60,11 @@ def main(): payload["system_version"] = ddp_call(ws, "2", "system.version", []) boot_disks = ddp_call(ws, "3", "boot.get_disks", []) payload["boot_disks"] = sorted(boot_disks) - unused = ddp_call(ws, "4", "disk.get_unused", []) + disk_details = ddp_call(ws, "4", "disk.details", []) + payload["disk_details"] = disk_details + if not isinstance(disk_details, dict) or not isinstance(disk_details.get("unused"), list): + raise RuntimeError(f"disk.details did not return an unused-disk list: {disk_details!r}") + unused = disk_details["unused"] payload["unused_disks"] = unused if len(unused) != a.expected_data_disks: diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index 8b666644d..ca879580d 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -283,11 +283,15 @@ def server(): msg = expect_call(conn, "boot.get_disks") result(conn, msg, ["vda"]) - msg = expect_call(conn, "disk.get_unused") - result(conn, msg, [ - {"name": "vdb", "devname": "vdb", "size": 8_589_934_592}, - {"name": "vdc", "devname": "vdc", "size": 8_589_934_592}, - ]) + msg = expect_call(conn, "disk.details") + self.assertEqual(msg["params"], []) + result(conn, msg, { + "used": [{"name": "vda", "devname": "vda"}], + "unused": [ + {"name": "vdb", "devname": "vdb", "size": 8_589_934_592}, + {"name": "vdc", "devname": "vdc", "size": 8_589_934_592}, + ], + }) msg = expect_call(conn, "pool.query") self.assertEqual(msg["params"], [[["name", "=", "rdtepool"]]]) @@ -380,6 +384,12 @@ def server(): self.assertFalse(thread.is_alive()) self.assertEqual(errors, []) + def test_t3_uses_public_disk_details_not_private_get_unused(self): + text = POOL.read_text(encoding="utf-8") + self.assertIn('"disk.details"', text) + self.assertNotIn('"disk.get_unused"', text) + self.assertIn('disk_details["unused"]', text) + if __name__ == "__main__": unittest.main() From f966400837cab12c2fae1b9ccdc578bcd6ee2b47 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:12:14 +0200 Subject: [PATCH 031/719] test: select only owned TrueNAS T3 data disks --- .github/workflows/gha-kvm-system-rdte.yml | 2 +- scripts/truenas_middleware_pool_probe.py | 26 +++++++++++++++++------ tests/test_truenas_future_protocols.py | 9 +++++--- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 0cd9e4b67..69eb9d30f 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -58,7 +58,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py) + scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py) truenas=true ;; esac diff --git a/scripts/truenas_middleware_pool_probe.py b/scripts/truenas_middleware_pool_probe.py index 7fbad5e4a..22ea878a9 100755 --- a/scripts/truenas_middleware_pool_probe.py +++ b/scripts/truenas_middleware_pool_probe.py @@ -22,6 +22,7 @@ def main(): p.add_argument("--out", required=True) p.add_argument("--pool-name", default="rdtepool") p.add_argument("--expected-data-disks", type=int, default=2) + p.add_argument("--data-serial-prefix", default="RDTE_DATA_") p.add_argument("--tls", action="store_true") p.add_argument("--timeout", type=float, default=8.0) p.add_argument("--job-timeout", type=float, default=180.0) @@ -34,6 +35,7 @@ def main(): "classification": "ORACLE_FAILURE", "pool_name": a.pool_name, "expected_data_disks": a.expected_data_disks, + "data_serial_prefix": a.data_serial_prefix, "transport": "wss" if a.tls else "ws", } started = time.time() @@ -67,17 +69,27 @@ def main(): unused = disk_details["unused"] payload["unused_disks"] = unused - if len(unused) != a.expected_data_disks: + expected_serials = { + f"{a.data_serial_prefix}{index}" for index in range(a.expected_data_disks) + } + owned = [ + disk for disk in unused + if disk.get("serial") in expected_serials + ] + payload["owned_data_disks"] = owned + observed_serials = {disk.get("serial") for disk in owned} + if observed_serials != expected_serials or len(owned) != a.expected_data_disks: raise RuntimeError( - f"refusing pool mutation: expected exactly {a.expected_data_disks} unused data disks, found {len(unused)}" + "refusing pool mutation: expected exactly owned serials " + f"{sorted(expected_serials)!r}, found {sorted(s for s in observed_serials if s)!r}" ) - selected = [disk_name(disk) for disk in unused] + selected = [disk_name(disk) for disk in owned] if any(not name for name in selected): - raise RuntimeError("refusing pool mutation: unused disk missing name/devname") + raise RuntimeError("refusing pool mutation: owned data disk missing name/devname") if len(set(selected)) != len(selected): - raise RuntimeError("refusing pool mutation: duplicate data-disk names") + raise RuntimeError("refusing pool mutation: duplicate owned data-disk names") if set(selected) & set(boot_disks): - raise RuntimeError("refusing pool mutation: a selected data disk overlaps the boot pool") + raise RuntimeError("refusing pool mutation: an owned data disk overlaps the boot pool") payload["selected_data_disks"] = sorted(selected) existing = ddp_call(ws, "5", "pool.query", [[["name", "=", a.pool_name]]]) @@ -87,7 +99,7 @@ def main(): create = { "name": a.pool_name, "encryption": False, - "allow_duplicate_serials": True, + "allow_duplicate_serials": False, "topology": { "data": [{ "type": "MIRROR", diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index ca879580d..57600ef9a 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -288,8 +288,9 @@ def server(): result(conn, msg, { "used": [{"name": "vda", "devname": "vda"}], "unused": [ - {"name": "vdb", "devname": "vdb", "size": 8_589_934_592}, - {"name": "vdc", "devname": "vdc", "size": 8_589_934_592}, + {"name": "vdb", "devname": "vdb", "serial": "RDTE_DATA_0", "size": 8_589_934_592}, + {"name": "vdc", "devname": "vdc", "serial": "RDTE_DATA_1", "size": 8_589_934_592}, + {"name": "fd0", "devname": "fd0", "serial": "", "size": 4096}, ], }) @@ -301,7 +302,7 @@ def server(): self.assertEqual(msg["params"], [{ "name": "rdtepool", "encryption": False, - "allow_duplicate_serials": True, + "allow_duplicate_serials": False, "topology": { "data": [{ "type": "MIRROR", @@ -375,6 +376,8 @@ def server(): self.assertTrue(receipt["oracleSatisfied"], receipt) self.assertEqual(receipt["classification"], "SUPPORTED") self.assertEqual(receipt["selected_data_disks"], ["vdb", "vdc"]) + self.assertEqual([d["serial"] for d in receipt["owned_data_disks"]], ["RDTE_DATA_0", "RDTE_DATA_1"]) + self.assertEqual(len(receipt["unused_disks"]), 3) self.assertEqual(receipt["pool_disks"], ["vdb", "vdc"]) self.assertEqual(receipt["job_state"], "SUCCESS") self.assertEqual(receipt["pool"]["status"], "ONLINE") From 71ed77aff3d95eab39a4c7a54e42db12119bc852 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:14:03 +0200 Subject: [PATCH 032/719] test: keep T3 protocol probe off heavy trigger --- tests/test_gha_kvm_system_rdte.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 31977bcf5..2d84e1e21 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -286,7 +286,7 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn('"data_pool_created"', text) self.assertIn('"data_pool"', text) self.assertIn("--rung t3", workflow) - self.assertIn("scripts/truenas_middleware_pool_probe.py", workflow) + self.assertNotIn("scripts/truenas_middleware_pool_probe.py", workflow) shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) py_check = subprocess.run(["python3", "-m", "py_compile", str(TRUENAS_POOL)], text=True, capture_output=True) From 5d0f67b94e866175b8a0f4a790b8a57f049b99cb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:15:17 +0200 Subject: [PATCH 033/719] fix: identify TrueNAS T3 owned data devices --- docs/gha-kvm-system-rdte.md | 2 +- scripts/gha_kvm_truenas_rdte.sh | 10 ++++++++-- tests/test_gha_kvm_system_rdte.py | 4 ++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index c6bd94a9a..a37f7213b 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -171,7 +171,7 @@ so the disposable legacy-BIOS QEMU target is a valid post-install boot oracle. T3 extends the accepted T2 system without changing the installer disk shape. The installer still sees only the 24-GiB boot target. After vendor installation completes, -the installed-system boot adds exactly two experiment-owned 8-GiB sparse virtio disks. +the installed-system boot adds exactly two experiment-owned 8-GiB sparse virtio disks. They are attached as explicit virtio-blk devices with deterministic serial identities `RDTE_DATA_0` and `RDTE_DATA_1`. The T3 middleware client then: - authenticates through the accepted installed-system DDP surface; diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index f4ba619f2..53c29ae08 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -14,6 +14,7 @@ DISK_SIZE="24G" DATA_DISK_SIZE="8G" DATA_DISK_COUNT=2 DATA_POOL_NAME="rdtepool" +DATA_SERIAL_PREFIX="RDTE_DATA_" NIC_MAC="52:54:00:54:4e:26" MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) @@ -92,6 +93,7 @@ payload = { "boot_disk": "24G", "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") == "t3" else [], "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") == "t3" else None, + "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") == "t3" else [], }, "source": { "iso_name": "TrueNAS-26.0.0-BETA.3.iso", @@ -356,7 +358,11 @@ if [[ "$RUNG" == "t3" ]]; then data_disk="$STATE_DIR/data${index}.qcow2" qemu-img create -q -f qcow2 "$data_disk" "$DATA_DISK_SIZE" || fail_evidence HARNESS_FAILURE data-disk-prepare "failed to create sparse data disk $index" - DATA_DRIVE_ARGS+=( -drive "file=$data_disk,if=virtio,format=qcow2" ) + drive_id="rdtedata${index}" + DATA_DRIVE_ARGS+=( + -drive "file=$data_disk,if=none,id=$drive_id,format=qcow2" + -device "virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index}" + ) done fi @@ -450,7 +456,7 @@ python3 "$SCRIPT_DIR/truenas_middleware_pool_probe.py" \ "${MIDDLEWARE_TLS_ARG[@]}" \ --password-file "$PASSWORD_FILE" \ --out "$POOL_OUT" --pool-name "$DATA_POOL_NAME" \ - --expected-data-disks "$DATA_DISK_COUNT" \ + --expected-data-disks "$DATA_DISK_COUNT" --data-serial-prefix "$DATA_SERIAL_PREFIX" \ --timeout 6 --job-timeout 180 >/dev/null 2>&1 || true [[ -f "$POOL_OUT" ]] || fail_evidence HARNESS_FAILURE data-pool "T3 pool client did not emit a receipt" POOL_RESULT_JSON="$(cat "$POOL_OUT")" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 2d84e1e21..69224f53f 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -280,6 +280,10 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn('DATA_DISK_SIZE="8G"', text) self.assertIn("DATA_DISK_COUNT=2", text) self.assertIn('DATA_POOL_NAME="rdtepool"', text) + self.assertIn('DATA_SERIAL_PREFIX="RDTE_DATA_"', text) + self.assertIn("if=none,id=$drive_id", text) + self.assertIn("virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index}", text) + self.assertIn('--data-serial-prefix "$DATA_SERIAL_PREFIX"', text) self.assertIn("data${index}.qcow2", text) self.assertIn('"${DATA_DRIVE_ARGS[@]}"', text) self.assertIn("truenas_middleware_pool_probe.py", text) From 1be7f7a0aa3affbaa0ab8487c16e167d40e1ee8b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:27:14 +0200 Subject: [PATCH 034/719] fix: preserve TrueNAS NIC PCI identity --- docs/gha-kvm-system-rdte.md | 2 +- scripts/gha_kvm_truenas_rdte.sh | 4 ++-- tests/test_gha_kvm_system_rdte.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index a37f7213b..4de388e5d 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -171,7 +171,7 @@ so the disposable legacy-BIOS QEMU target is a valid post-install boot oracle. T3 extends the accepted T2 system without changing the installer disk shape. The installer still sees only the 24-GiB boot target. After vendor installation completes, -the installed-system boot adds exactly two experiment-owned 8-GiB sparse virtio disks. They are attached as explicit virtio-blk devices with deterministic serial identities `RDTE_DATA_0` and `RDTE_DATA_1`. +the installed-system boot adds exactly two experiment-owned 8-GiB sparse virtio disks. They are attached as explicit virtio-blk devices with deterministic serial identities `RDTE_DATA_0` and `RDTE_DATA_1`. The virtio NIC is pinned to PCI address `0x3` in both installer and installed-system boots so adding data devices cannot rename the installer-persisted `ens3` interface. The T3 middleware client then: - authenticates through the accepted installed-system DDP surface; diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 53c29ae08..4cf67b29c 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -261,7 +261,7 @@ sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ -cdrom "$SERIAL_ISO" -boot order=d \ - -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080" -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC" \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080" -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ -daemonize -pidfile "$STATE_DIR/qemu.pid" || @@ -383,7 +383,7 @@ sudo -n qemu-system-x86_64 \ "${DATA_DRIVE_ARGS[@]}" \ -boot order=c \ -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443" \ - -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC" \ + -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ -daemonize -pidfile "$STATE_DIR/qemu.pid" || diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 69224f53f..4b9bed138 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -281,6 +281,7 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn("DATA_DISK_COUNT=2", text) self.assertIn('DATA_POOL_NAME="rdtepool"', text) self.assertIn('DATA_SERIAL_PREFIX="RDTE_DATA_"', text) + self.assertEqual(text.count("mac=$NIC_MAC,addr=0x3"), 2) self.assertIn("if=none,id=$drive_id", text) self.assertIn("virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index}", text) self.assertIn('--data-serial-prefix "$DATA_SERIAL_PREFIX"', text) From fa39ac7754a465c77b1f1ca7f40e076d6993633a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:37:18 +0200 Subject: [PATCH 035/719] fix: align T3 receipt with pool mutation --- scripts/truenas_middleware_pool_probe.py | 2 +- tests/test_truenas_future_protocols.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_pool_probe.py b/scripts/truenas_middleware_pool_probe.py index 22ea878a9..ad69c4c2f 100755 --- a/scripts/truenas_middleware_pool_probe.py +++ b/scripts/truenas_middleware_pool_probe.py @@ -110,7 +110,7 @@ def main(): payload["create_contract"] = { "name": a.pool_name, "encryption": False, - "allow_duplicate_serials": True, + "allow_duplicate_serials": False, "topology": {"data": [{"type": "MIRROR", "disks": sorted(selected)}]}, } job_id = ddp_call(ws, "6", "pool.create", [create]) diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index 57600ef9a..24ea50f35 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -380,6 +380,7 @@ def server(): self.assertEqual(len(receipt["unused_disks"]), 3) self.assertEqual(receipt["pool_disks"], ["vdb", "vdc"]) self.assertEqual(receipt["job_state"], "SUCCESS") + self.assertFalse(receipt["create_contract"]["allow_duplicate_serials"]) self.assertEqual(receipt["pool"]["status"], "ONLINE") self.assertTrue(receipt["pool"]["healthy"]) self.assertNotIn(password, out.read_text(encoding="utf-8")) From 60c05a0e8c9b4653006adfa92bd8cf27603b92a9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:56:03 +0200 Subject: [PATCH 036/719] fix: pin TrueNAS T3 owned data PCI slots --- scripts/gha_kvm_truenas_rdte.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 4cf67b29c..c0ee5a035 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -359,9 +359,10 @@ if [[ "$RUNG" == "t3" ]]; then qemu-img create -q -f qcow2 "$data_disk" "$DATA_DISK_SIZE" || fail_evidence HARNESS_FAILURE data-disk-prepare "failed to create sparse data disk $index" drive_id="rdtedata${index}" + pci_slot=$((4 + index)) DATA_DRIVE_ARGS+=( -drive "file=$data_disk,if=none,id=$drive_id,format=qcow2" - -device "virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index}" + -device "virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index},addr=0x${pci_slot}" ) done fi From b93c057dfb0c4090942885fac803ab4aba2ab2ec Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 03:56:14 +0200 Subject: [PATCH 037/719] test: lock TrueNAS T3 PCI topology --- tests/test_gha_kvm_system_rdte.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 4b9bed138..f4cbaea81 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -283,7 +283,11 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn('DATA_SERIAL_PREFIX="RDTE_DATA_"', text) self.assertEqual(text.count("mac=$NIC_MAC,addr=0x3"), 2) self.assertIn("if=none,id=$drive_id", text) - self.assertIn("virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index}", text) + self.assertIn("pci_slot=$((4 + index))", text) + self.assertIn( + "virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index},addr=0x${pci_slot}", + text, + ) self.assertIn('--data-serial-prefix "$DATA_SERIAL_PREFIX"', text) self.assertIn("data${index}.qcow2", text) self.assertIn('"${DATA_DRIVE_ARGS[@]}"', text) From a1fcc146ab9bf5c8136a93ea3d60011075cc3ca8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:33:02 +0200 Subject: [PATCH 038/719] test: qualify TrueNAS T3 QEMU boot topology --- .github/workflows/gha-kvm-system-rdte.yml | 41 ++++- docs/gha-kvm-system-rdte.md | 17 ++ scripts/gha_qemu_t3_topology_probe.py | 209 ++++++++++++++++++++++ tests/test_gha_kvm_system_rdte.py | 24 +++ 4 files changed, 289 insertions(+), 2 deletions(-) create mode 100755 scripts/gha_qemu_t3_topology_probe.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 69eb9d30f..9cf90688c 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -30,6 +30,7 @@ jobs: outputs: proxmox: ${{ steps.scope.outputs.proxmox }} truenas: ${{ steps.scope.outputs.truenas }} + qemu_topology: ${{ steps.scope.outputs.qemu_topology }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -45,11 +46,12 @@ jobs: set -euo pipefail proxmox=false truenas=false + qemu_topology=false if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then case "$DISPATCH_TARGET" in proxmox) proxmox=true ;; truenas) truenas=true ;; - both) proxmox=true; truenas=true ;; + both) proxmox=true; truenas=true; qemu_topology=true ;; *) echo "unsupported dispatch target: $DISPATCH_TARGET" >&2; exit 2 ;; esac elif [[ -n "$BEFORE_SHA" && -n "$AFTER_SHA" && ! "$BEFORE_SHA" =~ ^0+$ ]]; then @@ -61,15 +63,20 @@ jobs: scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py) truenas=true ;; + scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) + qemu_topology=true + ;; esac done < <(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA") else proxmox=true truenas=true + qemu_topology=true fi echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" echo "truenas=$truenas" >>"$GITHUB_OUTPUT" - printf 'target scope: proxmox=%s truenas=%s\n' "$proxmox" "$truenas" + echo "qemu_topology=$qemu_topology" >>"$GITHUB_OUTPUT" + printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s\n' "$proxmox" "$truenas" "$qemu_topology" contract: runs-on: ubuntu-24.04 @@ -81,6 +88,36 @@ jobs: - name: Validate public-safe contract run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py + qemu-t3-topology: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' + runs-on: ubuntu-26.04 + timeout-minutes: 8 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Install QEMU topology prerequisites + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-system-x86 qemu-utils + - name: Exercise actual hosted-runner QEMU topology + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" + python3 scripts/gha_qemu_t3_topology_probe.py \ + --out "$RUNNER_TEMP/gha-kvm-system-rdte/qemu-t3-topology.json" + - name: Upload QEMU topology evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: gha-qemu-t3-topology-${{ github.run_attempt }} + path: ${{ runner.temp }}/gha-kvm-system-rdte/qemu-t3-topology.json + if-no-files-found: error + retention-days: 7 + proxmox-9-2: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.proxmox == 'true' diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 4de388e5d..2966d4673 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -185,3 +185,20 @@ The T3 middleware client then: QEMU disk attachment, middleware unused-disk classification, job acceptance, and pool health are separate observations. T3 does not imply Apps support; T4 remains a distinct gate. + + +### QEMU T3 topology oracle + +Before another full TrueNAS T3 installation is spent, a cheap Ubuntu 26.04 job +instantiates the proposed installed-guest hardware shape under the hosted runner's +actual QEMU package without booting an operating system. QMP must report: + +- virtio NIC at PCI slot 0x3; +- explicit TrueNAS boot virtio-blk device at slot 0x4 with `bootindex=1`; +- experiment data disks at slots 0x5 and 0x6; +- data-disk serials `RDTE_DATA_0` and `RDTE_DATA_1`. + +This oracle exists because QEMU auto-assigns PCI devices to the first available +non-reserved slot and virtio-blk exposes both `serial` and `bootindex` as device +properties. It validates QEMU realization separately from the expensive TrueNAS +installer/boot/pool oracle. diff --git a/scripts/gha_qemu_t3_topology_probe.py b/scripts/gha_qemu_t3_topology_probe.py new file mode 100755 index 000000000..c33dedb4b --- /dev/null +++ b/scripts/gha_qemu_t3_topology_probe.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +"""Cheap QEMU 10.x topology oracle for the TrueNAS T3 installed-boot shape.""" +from __future__ import annotations + +import argparse +import json +import pathlib +import socket +import subprocess +import tempfile +import time + + +NIC_SLOT = 3 +BOOT_SLOT = 4 +DATA_SLOTS = [5, 6] +DATA_SERIALS = ["RDTE_DATA_0", "RDTE_DATA_1"] + + +class QMP: + def __init__(self, path: pathlib.Path, timeout: float = 5.0): + deadline = time.monotonic() + timeout + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + while True: + try: + self.sock.connect(str(path)) + break + except (FileNotFoundError, ConnectionRefusedError): + if time.monotonic() >= deadline: + raise + time.sleep(0.05) + self.file = self.sock.makefile("rwb", buffering=0) + greeting = self._recv() + if "QMP" not in greeting: + raise RuntimeError(f"missing QMP greeting: {greeting!r}") + self.execute("qmp_capabilities") + + def _recv(self): + line = self.file.readline() + if not line: + raise EOFError("QMP closed") + return json.loads(line) + + def execute(self, command, arguments=None): + request = {"execute": command} + if arguments is not None: + request["arguments"] = arguments + self.file.write((json.dumps(request, separators=(",", ":")) + "\n").encode()) + while True: + reply = self._recv() + if "event" in reply: + continue + if "error" in reply: + raise RuntimeError(f"{command}: {reply['error']!r}") + return reply.get("return") + + def close(self): + try: + self.execute("quit") + except Exception: + pass + try: + self.file.close() + finally: + self.sock.close() + + +def flatten_pci(buses): + rows = [] + def walk(devices): + for dev in devices: + rows.append({ + "bus": dev["bus"], + "slot": dev["slot"], + "function": dev["function"], + "qdev_id": dev.get("qdev_id", ""), + "vendor": dev.get("id", {}).get("vendor"), + "device": dev.get("id", {}).get("device"), + }) + bridge = dev.get("pci_bridge") + if bridge: + walk(bridge.get("devices", [])) + for bus in buses: + walk(bus.get("devices", [])) + return rows + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--out", required=True) + p.add_argument("--qemu", default="qemu-system-x86_64") + a = p.parse_args() + + out = pathlib.Path(a.out) + payload = { + "schema": "qemu-truenas-t3-topology/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected": { + "nic_slot": NIC_SLOT, + "boot_slot": BOOT_SLOT, + "data_slots": DATA_SLOTS, + "bootindex": 1, + "data_serials": DATA_SERIALS, + }, + } + + qemu = None + try: + version = subprocess.check_output([a.qemu, "--version"], text=True).splitlines()[0] + payload["qemu_version"] = version + + with tempfile.TemporaryDirectory(prefix="rdte-qemu-topology-") as td: + root = pathlib.Path(td) + for name in ("boot", "data0", "data1"): + subprocess.run( + ["qemu-img", "create", "-q", "-f", "qcow2", str(root / f"{name}.qcow2"), "64M"], + check=True, + ) + qmp_path = root / "qmp.sock" + pidfile = root / "qemu.pid" + args = [ + a.qemu, + "-S", + "-display", "none", + "-serial", "none", + "-monitor", "none", + "-m", "256", + "-smp", "1", + "-drive", f"file={root / 'boot.qcow2'},if=none,id=rdteboot,format=qcow2", + "-device", "virtio-blk-pci,drive=rdteboot,id=rdte-boot,addr=0x4,bootindex=1", + "-drive", f"file={root / 'data0.qcow2'},if=none,id=rdtedata0,format=qcow2", + "-device", "virtio-blk-pci,drive=rdtedata0,id=rdte-data0,serial=RDTE_DATA_0,addr=0x5", + "-drive", f"file={root / 'data1.qcow2'},if=none,id=rdtedata1,format=qcow2", + "-device", "virtio-blk-pci,drive=rdtedata1,id=rdte-data1,serial=RDTE_DATA_1,addr=0x6", + "-netdev", "user,id=net0", + "-device", "virtio-net-pci,netdev=net0,id=rdte-nic,mac=52:54:00:54:4e:26,addr=0x3", + "-boot", "strict=on", + "-qmp", f"unix:{qmp_path},server=on,wait=off", + "-daemonize", + "-pidfile", str(pidfile), + ] + payload["command_shape"] = [ + arg.replace(str(root), "$TMP") for arg in args[1:] + ] + subprocess.run(args, check=True) + qemu = int(pidfile.read_text().strip()) + + qmp = QMP(qmp_path) + try: + pci = flatten_pci(qmp.execute("query-pci")) + payload["pci"] = pci + by_id = {row["qdev_id"]: row for row in pci if row["qdev_id"]} + payload["bootindex"] = qmp.execute( + "qom-get", + {"path": "/machine/peripheral/rdte-boot", "property": "bootindex"}, + ) + payload["data_serials"] = [ + qmp.execute( + "qom-get", + {"path": f"/machine/peripheral/rdte-data{i}", "property": "serial"}, + ) + for i in range(2) + ] + payload["qtree"] = qmp.execute( + "human-monitor-command", + {"command-line": "info qtree"}, + ) + + checks = { + "nic_slot": by_id.get("rdte-nic", {}).get("slot") == NIC_SLOT, + "boot_slot": by_id.get("rdte-boot", {}).get("slot") == BOOT_SLOT, + "data0_slot": by_id.get("rdte-data0", {}).get("slot") == DATA_SLOTS[0], + "data1_slot": by_id.get("rdte-data1", {}).get("slot") == DATA_SLOTS[1], + "all_functions_zero": all( + by_id.get(name, {}).get("function") == 0 + for name in ("rdte-nic", "rdte-boot", "rdte-data0", "rdte-data1") + ), + "bootindex": payload["bootindex"] == 1, + "data_serials": payload["data_serials"] == DATA_SERIALS, + } + payload["checks"] = checks + if not all(checks.values()): + raise RuntimeError(f"topology checks failed: {checks!r}") + finally: + qmp.close() + + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "actual hosted-runner QEMU realized NIC@0x3, boot@0x4 bootindex=1, " + "and uniquely serialized data disks @0x5/0x6" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + if qemu: + try: + subprocess.run(["kill", str(qemu)], check=False) + except Exception: + pass + out.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(payload, indent=2, sort_keys=True)) + + return 0 if payload["oracleSatisfied"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index f4cbaea81..b12c58ede 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -15,6 +15,7 @@ TRUENAS = ROOT / "scripts" / "gha_kvm_truenas_rdte.sh" TRUENAS_RPC = ROOT / "scripts" / "truenas_installer_rpc_probe.py" WORKFLOW = ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml" +QEMU_TOPOLOGY = ROOT / "scripts" / "gha_qemu_t3_topology_probe.py" TRUENAS_INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" TRUENAS_MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" TRUENAS_POOL = ROOT / "scripts" / "truenas_middleware_pool_probe.py" @@ -301,5 +302,28 @@ def test_truenas_t3_real_harness_contract(self): py_check = subprocess.run(["python3", "-m", "py_compile", str(TRUENAS_POOL)], text=True, capture_output=True) self.assertEqual(py_check.returncode, 0, py_check.stderr) + def test_qemu_t3_topology_is_a_cheap_separate_oracle(self): + probe = QEMU_TOPOLOGY.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn('"rdte-nic"', probe) + self.assertIn('"rdte-boot"', probe) + self.assertIn('"bootindex": 1', probe) + self.assertIn('"RDTE_DATA_0"', probe) + self.assertIn('"RDTE_DATA_1"', probe) + self.assertIn('"query-pci"', probe) + self.assertIn('"qom-get"', probe) + self.assertIn("qemu-t3-topology:", workflow) + self.assertIn("needs.changes.outputs.qemu_topology", workflow) + self.assertNotIn( + "scripts/gha_qemu_t3_topology_probe.py)", + workflow.split("truenas=true", 1)[0], + ) + py = subprocess.run( + ["python3", "-m", "py_compile", str(QEMU_TOPOLOGY)], + text=True, + capture_output=True, + ) + self.assertEqual(py.returncode, 0, py.stderr) + if __name__ == "__main__": unittest.main() From 5ecdbaf03997df9465991cf1bbc7d8fded8840bc Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:34:42 +0200 Subject: [PATCH 039/719] fix: boot explicit TrueNAS disk before T3 data devices --- docs/gha-kvm-system-rdte.md | 2 +- scripts/gha_kvm_truenas_rdte.sh | 7 ++++--- tests/test_gha_kvm_system_rdte.py | 6 +++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 2966d4673..1b60063b3 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -171,7 +171,7 @@ so the disposable legacy-BIOS QEMU target is a valid post-install boot oracle. T3 extends the accepted T2 system without changing the installer disk shape. The installer still sees only the 24-GiB boot target. After vendor installation completes, -the installed-system boot adds exactly two experiment-owned 8-GiB sparse virtio disks. They are attached as explicit virtio-blk devices with deterministic serial identities `RDTE_DATA_0` and `RDTE_DATA_1`. The virtio NIC is pinned to PCI address `0x3` in both installer and installed-system boots so adding data devices cannot rename the installer-persisted `ens3` interface. +the installed-system boot adds exactly two experiment-owned 8-GiB sparse virtio disks. They are attached as explicit virtio-blk devices with deterministic serial identities `RDTE_DATA_0` and `RDTE_DATA_1`. The installed-system topology is explicit: the virtio NIC is PCI `0x3`, the TrueNAS boot virtio-blk device is PCI `0x4` with `bootindex=1`, and the data disks are PCI `0x5` and `0x6`. QEMU `-boot order=c` is not mixed with `bootindex`; the installed boot uses `-boot strict=on` so firmware follows the per-device boot priority. The T3 middleware client then: - authenticates through the accepted installed-system DDP surface; diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index c0ee5a035..a1f457413 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -359,7 +359,7 @@ if [[ "$RUNG" == "t3" ]]; then qemu-img create -q -f qcow2 "$data_disk" "$DATA_DISK_SIZE" || fail_evidence HARNESS_FAILURE data-disk-prepare "failed to create sparse data disk $index" drive_id="rdtedata${index}" - pci_slot=$((4 + index)) + pci_slot=$((5 + index)) DATA_DRIVE_ARGS+=( -drive "file=$data_disk,if=none,id=$drive_id,format=qcow2" -device "virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index},addr=0x${pci_slot}" @@ -380,9 +380,10 @@ PY : >"$STATE_DIR/serial.log" sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ - -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ + -drive "file=$STATE_DIR/boot.qcow2,if=none,id=rdteboot,format=qcow2" \ + -device "virtio-blk-pci,drive=rdteboot,id=rdte-boot,addr=0x4,bootindex=1" \ "${DATA_DRIVE_ARGS[@]}" \ - -boot order=c \ + -boot strict=on \ -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443" \ -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index b12c58ede..d3019c4db 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -283,8 +283,12 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn('DATA_POOL_NAME="rdtepool"', text) self.assertIn('DATA_SERIAL_PREFIX="RDTE_DATA_"', text) self.assertEqual(text.count("mac=$NIC_MAC,addr=0x3"), 2) + self.assertIn("if=none,id=rdteboot", text) + self.assertIn("id=rdte-boot,addr=0x4,bootindex=1", text) + self.assertIn("-boot strict=on", text) + self.assertNotIn("-boot order=c", text) self.assertIn("if=none,id=$drive_id", text) - self.assertIn("pci_slot=$((4 + index))", text) + self.assertIn("pci_slot=$((5 + index))", text) self.assertIn( "virtio-blk-pci,drive=$drive_id,serial=${DATA_SERIAL_PREFIX}${index},addr=0x${pci_slot}", text, From 967da7eee025cdb162001a7349a7997b215df049 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:06:56 +0200 Subject: [PATCH 040/719] exp: exercise TrueNAS T4 Apps runtime --- .github/workflows/gha-kvm-system-rdte.yml | 6 +- docs/gha-kvm-system-rdte.md | 20 +++ scripts/gha_kvm_truenas_rdte.sh | 58 +++++-- scripts/truenas_middleware_app_probe.py | 201 ++++++++++++++++++++++ tests/test_gha_kvm_system_rdte.py | 28 ++- 5 files changed, 295 insertions(+), 18 deletions(-) mode change 100644 => 100755 scripts/gha_kvm_truenas_rdte.sh create mode 100755 scripts/truenas_middleware_app_probe.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 9cf90688c..60b76ea21 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -60,7 +60,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py) + scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -193,7 +193,7 @@ jobs: mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" bash scripts/gha_kvm_truenas_rdte.sh \ --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ - --rung t3 \ + --rung t4 \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence @@ -206,7 +206,7 @@ jobs: import json, pathlib, sys pathlib.Path(sys.argv[1]).write_text(json.dumps({ "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T3"}, + "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T4"}, "classification":"HARNESS_FAILURE", "oracleSatisfied":False, "phase":"workflow-fallback", diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 1b60063b3..d835e3726 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -202,3 +202,23 @@ This oracle exists because QEMU auto-assigns PCI devices to the first available non-reserved slot and virtio-blk exposes both `serial` and `bootindex` as device properties. It validates QEMU realization separately from the expensive TrueNAS installer/boot/pool oracle. + + +### TrueNAS T4 Apps runtime + +T4 builds directly on the accepted T3 disposable mirror. The BETA.3 source-defined +control path is used end-to-end: + +1. require the `APPS` entitlement reported by `truenas.entitlements.check`; +2. run the real `docker.update({"pool":"rdtepool"})` job and independently require + `docker.config.pool == "rdtepool"` plus `docker.status == RUNNING`; +3. create the upstream-test-shaped custom app `rdte-t4-probe` with a minimal + `nginx:1.27-alpine` Compose service; +4. wait for the `app.create` job to succeed, then independently poll `app.query`; +5. require app state `RUNNING`, `custom_app == true`, exactly one active container, + service name `web`, image `nginx:1.27-alpine`, and container state `running`. + +The TrueNAS 26.0 entitlement matrix grants Apps to an unlicensed generic/VM Community +Edition system, so this QEMU target is expected to be eligible without adding a license. +T4 still does not promote broader lifecycle semantics or App Foundry materialization; +those remain T5 and T6. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh old mode 100644 new mode 100755 index a1f457413..a4fb14b4c --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -20,7 +20,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3|t4]" } OUT="" @@ -36,7 +36,7 @@ while [[ $# -gt 0 ]]; do esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } -[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]] || { echo "rung must be t0, t1, t2, or t3" >&2; exit 2; } +[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" ]] || { echo "rung must be t0, t1, t2, t3, or t4" >&2; exit 2; } if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -56,6 +56,7 @@ QEMU_ALIVE_AT_GATE="unknown" INSTALL_RESULT_JSON="" MIDDLEWARE_RESULT_JSON="" POOL_RESULT_JSON="" +APP_RESULT_JSON="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -77,7 +78,7 @@ write_receipt() { export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" - export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" + export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" python3 - <<'PY' import json, os, pathlib payload = { @@ -91,9 +92,10 @@ payload = { "vcpus": 2, "ram_mib": 8192, "boot_disk": "24G", - "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") == "t3" else [], - "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") == "t3" else None, - "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") == "t3" else [], + "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") in {"t3", "t4"} else [], + "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4"} else None, + "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4"} else [], + "app": {"name": "rdte-t4-probe", "image": "nginx:1.27-alpine"} if os.environ.get("R_RUNG") == "t4" else None, }, "source": { "iso_name": "TrueNAS-26.0.0-BETA.3.iso", @@ -112,11 +114,13 @@ payload = { "installer_install_completed": bool(os.environ.get("R_INSTALL_RESULT")) and json.loads(os.environ["R_INSTALL_RESULT"]).get("oracleSatisfied") is True, "installed_middleware_authenticated": bool(os.environ.get("R_MIDDLEWARE_RESULT")) and json.loads(os.environ["R_MIDDLEWARE_RESULT"]).get("oracleSatisfied") is True, "data_pool_created": bool(os.environ.get("R_POOL_RESULT")) and json.loads(os.environ["R_POOL_RESULT"]).get("oracleSatisfied") is True, + "apps_runtime_exercised": bool(os.environ.get("R_APP_RESULT")) and json.loads(os.environ["R_APP_RESULT"]).get("oracleSatisfied") is True, }, "rpc_discovery": json.loads(os.environ["R_RPC_DISCOVERY"]) if os.environ.get("R_RPC_DISCOVERY") else None, "install_result": json.loads(os.environ["R_INSTALL_RESULT"]) if os.environ.get("R_INSTALL_RESULT") else None, "installed_middleware": json.loads(os.environ["R_MIDDLEWARE_RESULT"]) if os.environ.get("R_MIDDLEWARE_RESULT") else None, "data_pool": json.loads(os.environ["R_POOL_RESULT"]) if os.environ.get("R_POOL_RESULT") else None, + "apps_runtime": json.loads(os.environ["R_APP_RESULT"]) if os.environ.get("R_APP_RESULT") else None, "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ @@ -124,7 +128,8 @@ payload = { "T1 is read-only installer RPC discovery.", "T2 adds vendor installation plus installed middleware authentication/health.", "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", - "Containers, Apps, and application lifecycle remain later gates.", + "T4 initializes Apps on that pool and runs one synthetic public-safe custom Compose app.", + "Broader application lifecycle and Foundry materialization remain later gates.", "This does not qualify physical storage controllers, SMART, GPU, IPMI, or HA behavior.", ], } @@ -141,14 +146,18 @@ fail_evidence() { [[ -f "$SCRIPT_DIR/truenas_installer_rpc_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer RPC probe" -if [[ "$RUNG" == "t2" || "$RUNG" == "t3" ]]; then +if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" ]]; then [[ -f "$SCRIPT_DIR/truenas_installer_rpc_install.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer install client" [[ -f "$SCRIPT_DIR/truenas_middleware_ddp_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS middleware health client" - if [[ "$RUNG" == "t3" ]]; then + if [[ "$RUNG" == "t3" || "$RUNG" == "t4" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_pool_probe.py" ]] || - fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T3 pool client" + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T3/T4 pool client" + fi + if [[ "$RUNG" == "t4" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_app_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T4 Apps client" fi fi for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do @@ -353,7 +362,7 @@ if [[ -n "$QEMU_PID" ]]; then fi DATA_DRIVE_ARGS=() -if [[ "$RUNG" == "t3" ]]; then +if [[ "$RUNG" == "t3" || "$RUNG" == "t4" ]]; then for index in $(seq 0 $((DATA_DISK_COUNT - 1))); do data_disk="$STATE_DIR/data${index}.qcow2" qemu-img create -q -f qcow2 "$data_disk" "$DATA_DISK_SIZE" || @@ -460,7 +469,7 @@ python3 "$SCRIPT_DIR/truenas_middleware_pool_probe.py" \ --out "$POOL_OUT" --pool-name "$DATA_POOL_NAME" \ --expected-data-disks "$DATA_DISK_COUNT" --data-serial-prefix "$DATA_SERIAL_PREFIX" \ --timeout 6 --job-timeout 180 >/dev/null 2>&1 || true -[[ -f "$POOL_OUT" ]] || fail_evidence HARNESS_FAILURE data-pool "T3 pool client did not emit a receipt" +[[ -f "$POOL_OUT" ]] || fail_evidence HARNESS_FAILURE data-pool "T3/T4 pool client did not emit a receipt" POOL_RESULT_JSON="$(cat "$POOL_OUT")" POOL_OK="$(python3 - "$POOL_OUT" <<'PY' import json, pathlib, sys @@ -471,4 +480,27 @@ PY [[ "$POOL_OK" == "true" ]] || fail_evidence ORACLE_FAILURE data-pool "installed TrueNAS did not create and independently verify the disposable ZFS mirror pool" -write_receipt SUPPORTED true data-pool "installed TrueNAS created an ONLINE healthy two-disk mirror containing exactly the selected disposable data disks" +if [[ "$RUNG" == "t3" ]]; then + write_receipt SUPPORTED true data-pool "installed TrueNAS created an ONLINE healthy two-disk mirror containing exactly the selected disposable data disks" + exit 0 +fi + +APP_OUT="$STATE_DIR/apps-runtime.json" +python3 "$SCRIPT_DIR/truenas_middleware_app_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --out "$APP_OUT" --pool-name "$DATA_POOL_NAME" \ + --timeout 8 --job-timeout 300 --state-timeout 180 >/dev/null 2>&1 || true +[[ -f "$APP_OUT" ]] || fail_evidence HARNESS_FAILURE apps-runtime "T4 Apps client did not emit a receipt" +APP_RESULT_JSON="$(cat "$APP_OUT")" +APP_OK="$(python3 - "$APP_OUT" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +print("true" if data.get("oracleSatisfied") is True else "false") +PY +)" +[[ "$APP_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE apps-runtime "TrueNAS Apps did not initialize and run the bounded custom app oracle" + +write_receipt SUPPORTED true apps-runtime "installed TrueNAS initialized Apps on rdtepool and ran the bounded custom nginx Compose app" diff --git a/scripts/truenas_middleware_app_probe.py b/scripts/truenas_middleware_app_probe.py new file mode 100755 index 000000000..98c043554 --- /dev/null +++ b/scripts/truenas_middleware_app_probe.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Bounded TrueNAS BETA.3 Apps T4 oracle using source-defined middleware APIs.""" +from __future__ import annotations + +import argparse +import json +import pathlib +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +APP_NAME = "rdte-t4-probe" +APP_IMAGE = "nginx:1.27-alpine" + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--out", required=True) + p.add_argument("--pool-name", default="rdtepool") + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + p.add_argument("--state-timeout", type=float, default=180.0) + a = p.parse_args() + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + payload = { + "schema": "truenas-middleware-app-t4/v1", + "oracleSatisfied": False, + "classification": "ORACLE_FAILURE", + "pool_name": a.pool_name, + "app_name": APP_NAME, + "app_image": APP_IMAGE, + "transport": "wss" if a.tls else "ws", + } + started = time.time() + ws = None + request_id = 1 + + try: + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError(f"DDP connection failed: {connected!r}") + + def call(method, params): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + def wait_job(job_id, label): + deadline = time.monotonic() + a.job_timeout + last = None + while time.monotonic() < deadline: + last = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last: + state = last.get("state") + if state == "SUCCESS": + return last + if state in {"FAILED", "ABORTED"}: + raise RuntimeError( + f"{label} job {state}: {last.get('error') or last.get('exception')}" + ) + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS within timeout: {last!r}") + + auth = call( + "auth.login_ex", + [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }], + ) + payload["auth_response_type"] = auth.get("response_type") if isinstance(auth, dict) else None + if payload["auth_response_type"] != "SUCCESS": + raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") + + payload["system_version"] = call("system.version", []) + entitlement = call("truenas.entitlements.check", ["APPS"]) + payload["apps_entitlement"] = entitlement + if not isinstance(entitlement, dict) or entitlement.get("entitled") is not True: + raise RuntimeError(f"Apps entitlement denied: {entitlement!r}") + + payload["docker_before"] = call("docker.config", []) + existing = call("app.query", [[[ "id", "=", APP_NAME ]]]) + if existing: + raise RuntimeError(f"refusing mutation: app {APP_NAME!r} already exists") + + docker_job_id = call("docker.update", [{"pool": a.pool_name}]) + if not isinstance(docker_job_id, int): + raise RuntimeError(f"docker.update did not return a job id: {docker_job_id!r}") + payload["docker_update_job_id"] = docker_job_id + docker_job = wait_job(docker_job_id, "docker.update") + payload["docker_update_job"] = { + "id": docker_job.get("id"), + "state": docker_job.get("state"), + "progress": docker_job.get("progress"), + } + + deadline = time.monotonic() + a.state_timeout + docker_status = None + while time.monotonic() < deadline: + docker_status = call("docker.status", []) + if isinstance(docker_status, dict) and docker_status.get("status") == "RUNNING": + break + if isinstance(docker_status, dict) and docker_status.get("status") in {"FAILED", "MIGRATION_FAILED"}: + raise RuntimeError(f"Docker entered failure state: {docker_status!r}") + time.sleep(1) + else: + raise RuntimeError(f"Docker did not reach RUNNING: {docker_status!r}") + payload["docker_status"] = docker_status + + docker_config = call("docker.config", []) + payload["docker_config"] = docker_config + if docker_config.get("pool") != a.pool_name: + raise RuntimeError( + f"Docker pool mismatch: expected {a.pool_name!r}, got {docker_config.get('pool')!r}" + ) + + compose = { + "services": { + "web": { + "image": APP_IMAGE, + "restart": "unless-stopped", + } + } + } + payload["create_contract"] = { + "app_name": APP_NAME, + "custom_app": True, + "custom_compose_config": compose, + } + app_job_id = call("app.create", [{ + "app_name": APP_NAME, + "custom_app": True, + "custom_compose_config": compose, + }]) + if not isinstance(app_job_id, int): + raise RuntimeError(f"app.create did not return a job id: {app_job_id!r}") + payload["app_create_job_id"] = app_job_id + app_job = wait_job(app_job_id, "app.create") + payload["app_create_job"] = { + "id": app_job.get("id"), + "state": app_job.get("state"), + "progress": app_job.get("progress"), + } + + deadline = time.monotonic() + a.state_timeout + app = None + while time.monotonic() < deadline: + app = call("app.query", [[[ "id", "=", APP_NAME ]], {"get": True}]) + if app: + workloads = app.get("active_workloads") or {} + details = workloads.get("container_details") or [] + matching = [ + c for c in details + if c.get("service_name") == "web" + and c.get("image") == APP_IMAGE + and c.get("state") == "running" + ] + if ( + app.get("state") == "RUNNING" + and app.get("custom_app") is True + and workloads.get("containers") == 1 + and len(matching) == 1 + ): + break + if app.get("state") == "CRASHED": + raise RuntimeError(f"custom app entered CRASHED state: {app!r}") + time.sleep(1) + else: + raise RuntimeError(f"custom app did not reach the running oracle: {app!r}") + + payload["app"] = app + payload["oracleSatisfied"] = True + payload["classification"] = "SUPPORTED" + payload["detail"] = ( + "Apps initialized on the real disposable ZFS pool and a source-shaped custom Compose app " + "reached RUNNING with exactly one expected nginx container" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index d3019c4db..4992190b4 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -19,6 +19,7 @@ TRUENAS_INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" TRUENAS_MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" TRUENAS_POOL = ROOT / "scripts" / "truenas_middleware_pool_probe.py" +TRUENAS_APP = ROOT / "scripts" / "truenas_middleware_app_probe.py" class SystemRdteContractTests(unittest.TestCase): @@ -277,7 +278,7 @@ def test_truenas_install_rpc_uses_one_positional_object(self): def test_truenas_t3_real_harness_contract(self): text = TRUENAS.read_text(encoding="utf-8") workflow = WORKFLOW.read_text(encoding="utf-8") - self.assertIn("t0|t1|t2|t3", text) + self.assertIn("t0|t1|t2|t3|t4", text) self.assertIn('DATA_DISK_SIZE="8G"', text) self.assertIn("DATA_DISK_COUNT=2", text) self.assertIn('DATA_POOL_NAME="rdtepool"', text) @@ -299,13 +300,36 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn("truenas_middleware_pool_probe.py", text) self.assertIn('"data_pool_created"', text) self.assertIn('"data_pool"', text) - self.assertIn("--rung t3", workflow) + self.assertIn("--rung t4", workflow) self.assertNotIn("scripts/truenas_middleware_pool_probe.py", workflow) shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) py_check = subprocess.run(["python3", "-m", "py_compile", str(TRUENAS_POOL)], text=True, capture_output=True) self.assertEqual(py_check.returncode, 0, py_check.stderr) + def test_truenas_t4_apps_contract(self): + text = TRUENAS.read_text(encoding="utf-8") + app = TRUENAS_APP.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn('APP_RESULT_JSON=""', text) + self.assertIn("apps_runtime_exercised", text) + self.assertIn("truenas_middleware_app_probe.py", text) + self.assertIn("docker.update", app) + self.assertIn("docker.status", app) + self.assertIn("truenas.entitlements.check", app) + self.assertIn('"APPS"', app) + self.assertIn("app.create", app) + self.assertIn("app.query", app) + self.assertIn('"nginx:1.27-alpine"', app) + self.assertIn('"state") == "RUNNING"', app) + self.assertIn('--rung t4', workflow) + py = subprocess.run( + ["python3", "-m", "py_compile", str(TRUENAS_APP)], + text=True, + capture_output=True, + ) + self.assertEqual(py.returncode, 0, py.stderr) + def test_qemu_t3_topology_is_a_cheap_separate_oracle(self): probe = QEMU_TOPOLOGY.read_text(encoding="utf-8") workflow = WORKFLOW.read_text(encoding="utf-8") From 572f9f5a9b10a409e94ba1a1a9b06dbb708e131c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:09:55 +0200 Subject: [PATCH 041/719] fix: align T4 scope contract and retain job results --- scripts/truenas_middleware_app_probe.py | 2 ++ tests/test_gha_kvm_system_rdte.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_app_probe.py b/scripts/truenas_middleware_app_probe.py index 98c043554..683533525 100755 --- a/scripts/truenas_middleware_app_probe.py +++ b/scripts/truenas_middleware_app_probe.py @@ -103,6 +103,7 @@ def wait_job(job_id, label): "state": docker_job.get("state"), "progress": docker_job.get("progress"), } + payload["docker_update_result"] = docker_job.get("result") deadline = time.monotonic() + a.state_timeout docker_status = None @@ -151,6 +152,7 @@ def wait_job(job_id, label): "state": app_job.get("state"), "progress": app_job.get("progress"), } + payload["app_create_result"] = app_job.get("result") deadline = time.monotonic() + a.state_timeout app = None diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 4992190b4..2aa312033 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -301,7 +301,7 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn('"data_pool_created"', text) self.assertIn('"data_pool"', text) self.assertIn("--rung t4", workflow) - self.assertNotIn("scripts/truenas_middleware_pool_probe.py", workflow) + self.assertIn("scripts/truenas_middleware_pool_probe.py", workflow) shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) py_check = subprocess.run(["python3", "-m", "py_compile", str(TRUENAS_POOL)], text=True, capture_output=True) @@ -320,6 +320,8 @@ def test_truenas_t4_apps_contract(self): self.assertIn('"APPS"', app) self.assertIn("app.create", app) self.assertIn("app.query", app) + self.assertIn('payload["docker_update_result"]', app) + self.assertIn('payload["app_create_result"]', app) self.assertIn('"nginx:1.27-alpine"', app) self.assertIn('"state") == "RUNNING"', app) self.assertIn('--rung t4', workflow) From 0b29db738909fe8f7f211a1bbdbd85b6d9dca3c1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:23:35 +0200 Subject: [PATCH 042/719] fix: share installer RPC gate across TrueNAS rungs --- scripts/gha_kvm_truenas_rdte.sh | 2 +- tests/test_gha_kvm_system_rdte.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) mode change 100755 => 100644 scripts/gha_kvm_truenas_rdte.sh diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh old mode 100755 new mode 100644 index a4fb14b4c..4faa8218d --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -293,7 +293,7 @@ for attempt in $(seq 1 220); do if [[ "$RUNG" == "t0" && "$T0_OBSERVED" == "true" ]]; then break fi - if [[ "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]]; then + if [[ "$RUNG" != "t0" ]]; then if port_open "$RPC_PORT"; then RPC_HOSTFWD_ACCEPTED="true" fi diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 2aa312033..3ad0d0dc6 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -355,5 +355,11 @@ def test_qemu_t3_topology_is_a_cheap_separate_oracle(self): ) self.assertEqual(py.returncode, 0, py.stderr) + def test_all_post_t0_rungs_share_installer_rpc_gate(self): + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn('if [[ "$RUNG" != "t0" ]]; then', text) + self.assertNotIn('if [[ "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]]\', text) + self.assertIn("try_rpc_discovery", text) + if __name__ == "__main__": unittest.main() From 0c6dbaa3710fdfa29e694b2d7ac9b6108adc93aa Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:24:44 +0200 Subject: [PATCH 043/719] test: fix post-T0 RPC gate assertion --- tests/test_gha_kvm_system_rdte.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 3ad0d0dc6..f71c9e4ec 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -358,7 +358,7 @@ def test_qemu_t3_topology_is_a_cheap_separate_oracle(self): def test_all_post_t0_rungs_share_installer_rpc_gate(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn('if [[ "$RUNG" != "t0" ]]; then', text) - self.assertNotIn('if [[ "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]]\', text) + self.assertNotIn('if [[ "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]]; then', text) self.assertIn("try_rpc_discovery", text) if __name__ == "__main__": From 939e0e6dc8f3889e5f8742570fd1f3d0a9f4206e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:26:04 +0200 Subject: [PATCH 044/719] docs: lock shared TrueNAS installer RPC invariant --- scripts/gha_kvm_truenas_rdte.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 4faa8218d..5ef6f421d 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -293,7 +293,7 @@ for attempt in $(seq 1 220); do if [[ "$RUNG" == "t0" && "$T0_OBSERVED" == "true" ]]; then break fi - if [[ "$RUNG" != "t0" ]]; then + # All post-T0 rungs share the installer RPC prerequisite; do not enumerate higher rungs here.\n if [[ "$RUNG" != "t0" ]]; then if port_open "$RPC_PORT"; then RPC_HOSTFWD_ACCEPTED="true" fi From 160b4547010113a7d97e52f771bd0809cf959c6b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:27:22 +0200 Subject: [PATCH 045/719] fix: repair shared RPC gate shell syntax --- scripts/gha_kvm_truenas_rdte.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 5ef6f421d..f85904bbf 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -293,7 +293,8 @@ for attempt in $(seq 1 220); do if [[ "$RUNG" == "t0" && "$T0_OBSERVED" == "true" ]]; then break fi - # All post-T0 rungs share the installer RPC prerequisite; do not enumerate higher rungs here.\n if [[ "$RUNG" != "t0" ]]; then + # All post-T0 rungs share the installer RPC prerequisite; do not enumerate higher rungs here. + if [[ "$RUNG" != "t0" ]]; then if port_open "$RPC_PORT"; then RPC_HOSTFWD_ACCEPTED="true" fi From 4a82d317b9e6cc64261525377dfaaace705223c2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:43:49 +0200 Subject: [PATCH 046/719] fix: bind T4 Apps gate to tagged BETA.3 source --- docs/gha-kvm-system-rdte.md | 13 +++++++++++++ scripts/truenas_middleware_app_probe.py | 10 +++++----- tests/test_gha_kvm_system_rdte.py | 8 ++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index d835e3726..e4fd59b40 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -222,3 +222,16 @@ The TrueNAS 26.0 entitlement matrix grants Apps to an unlicensed generic/VM Comm Edition system, so this QEMU target is expected to be eligible without adding a license. T4 still does not promote broader lifecycle semantics or App Foundry materialization; those remain T5 and T6. + + +### TrueNAS BETA.3 source binding + +Runtime API contracts for the 26.0.0-BETA.3 lane are bound to the published +`TS-26.0.0-BETA.3` middleware tag, commit +`81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3`. + +That tagged tree predates the later `truenas.entitlements.*` service. Its Apps gate +is `docker.license_active`: non-HA systems are permitted directly, while HA-capable +systems consult `system.feature_enabled("APPS")`. The T4 client therefore does not +call the later entitlement API; `docker.update` and Docker startup exercise the +actual BETA.3 product gate. diff --git a/scripts/truenas_middleware_app_probe.py b/scripts/truenas_middleware_app_probe.py index 683533525..6d2006b40 100755 --- a/scripts/truenas_middleware_app_probe.py +++ b/scripts/truenas_middleware_app_probe.py @@ -30,6 +30,11 @@ def main(): password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() payload = { "schema": "truenas-middleware-app-t4/v1", + "source_contract": { + "middleware_tag": "TS-26.0.0-BETA.3", + "middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", + "apps_gate": "docker.license_active", + }, "oracleSatisfied": False, "classification": "ORACLE_FAILURE", "pool_name": a.pool_name, @@ -83,11 +88,6 @@ def wait_job(job_id, label): raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") payload["system_version"] = call("system.version", []) - entitlement = call("truenas.entitlements.check", ["APPS"]) - payload["apps_entitlement"] = entitlement - if not isinstance(entitlement, dict) or entitlement.get("entitled") is not True: - raise RuntimeError(f"Apps entitlement denied: {entitlement!r}") - payload["docker_before"] = call("docker.config", []) existing = call("app.query", [[[ "id", "=", APP_NAME ]]]) if existing: diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index f71c9e4ec..7afbf0083 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -361,5 +361,13 @@ def test_all_post_t0_rungs_share_installer_rpc_gate(self): self.assertNotIn('if [[ "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]]; then', text) self.assertIn("try_rpc_discovery", text) + def test_t4_uses_tagged_beta3_apps_gate(self): + text = (ROOT / "scripts" / "truenas_middleware_app_probe.py").read_text(encoding="utf-8") + self.assertNotIn("truenas.entitlements.check", text) + self.assertIn('"middleware_tag": "TS-26.0.0-BETA.3"', text) + self.assertIn('"middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3"', text) + self.assertIn('"apps_gate": "docker.license_active"', text) + + if __name__ == "__main__": unittest.main() From 8f56c5121bd695e4ceb57e35701d3afc446004f9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:46:15 +0200 Subject: [PATCH 047/719] test: enforce tagged BETA.3 Apps contract --- scripts/truenas_middleware_app_probe.py | 1 + tests/test_gha_kvm_system_rdte.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_app_probe.py b/scripts/truenas_middleware_app_probe.py index 6d2006b40..390c07f6d 100755 --- a/scripts/truenas_middleware_app_probe.py +++ b/scripts/truenas_middleware_app_probe.py @@ -88,6 +88,7 @@ def wait_job(job_id, label): raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") payload["system_version"] = call("system.version", []) + # BETA.3 uses docker.license_active internally; the later truenas.entitlements API is absent. payload["docker_before"] = call("docker.config", []) existing = call("app.query", [[[ "id", "=", APP_NAME ]]]) if existing: diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 7afbf0083..83f5dce39 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -316,7 +316,9 @@ def test_truenas_t4_apps_contract(self): self.assertIn("truenas_middleware_app_probe.py", text) self.assertIn("docker.update", app) self.assertIn("docker.status", app) - self.assertIn("truenas.entitlements.check", app) + self.assertNotIn("truenas.entitlements.check", app) + self.assertIn('"apps_gate": "docker.license_active"', app) + self.assertIn('"middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3"', app) self.assertIn('"APPS"', app) self.assertIn("app.create", app) self.assertIn("app.query", app) From c30d5892142f85c9855e27c3cb8a32622e215105 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:48:17 +0200 Subject: [PATCH 048/719] test: align T4 contract with tagged BETA.3 gate --- scripts/truenas_middleware_app_probe.py | 1 + tests/test_gha_kvm_system_rdte.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_app_probe.py b/scripts/truenas_middleware_app_probe.py index 390c07f6d..96c9231fb 100755 --- a/scripts/truenas_middleware_app_probe.py +++ b/scripts/truenas_middleware_app_probe.py @@ -34,6 +34,7 @@ def main(): "middleware_tag": "TS-26.0.0-BETA.3", "middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", "apps_gate": "docker.license_active", + "apps_gate_behavior": "non-HA targets permitted directly; HA consults system.feature_enabled(APPS)", }, "oracleSatisfied": False, "classification": "ORACLE_FAILURE", diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 83f5dce39..beb97660b 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -318,8 +318,8 @@ def test_truenas_t4_apps_contract(self): self.assertIn("docker.status", app) self.assertNotIn("truenas.entitlements.check", app) self.assertIn('"apps_gate": "docker.license_active"', app) + self.assertIn('"apps_gate_behavior"', app) self.assertIn('"middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3"', app) - self.assertIn('"APPS"', app) self.assertIn("app.create", app) self.assertIn("app.query", app) self.assertIn('payload["docker_update_result"]', app) From 8968f0426e9aaa8f5ddea9c8cb1b49b23f57855f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:49:35 +0200 Subject: [PATCH 049/719] exp: pin T4 app image and correct BETA.3 oracle --- docs/gha-kvm-system-rdte.md | 22 +++++++++++++--------- scripts/truenas_middleware_app_probe.py | 2 +- tests/test_gha_kvm_system_rdte.py | 3 ++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index e4fd59b40..45d987567 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -209,19 +209,23 @@ installer/boot/pool oracle. T4 builds directly on the accepted T3 disposable mirror. The BETA.3 source-defined control path is used end-to-end: -1. require the `APPS` entitlement reported by `truenas.entitlements.check`; +1. rely on the exact BETA.3 product gate exercised by `docker.update`: + private `docker.license_active` permits non-HA targets directly and consults + `system.feature_enabled("APPS")` for HA-capable systems; 2. run the real `docker.update({"pool":"rdtepool"})` job and independently require `docker.config.pool == "rdtepool"` plus `docker.status == RUNNING`; -3. create the upstream-test-shaped custom app `rdte-t4-probe` with a minimal - `nginx:1.27-alpine` Compose service; +3. create the upstream-test-shaped custom app `rdte-t4-probe` using immutable image + `nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10`; 4. wait for the `app.create` job to succeed, then independently poll `app.query`; 5. require app state `RUNNING`, `custom_app == true`, exactly one active container, - service name `web`, image `nginx:1.27-alpine`, and container state `running`. - -The TrueNAS 26.0 entitlement matrix grants Apps to an unlicensed generic/VM Community -Edition system, so this QEMU target is expected to be eligible without adding a license. -T4 still does not promote broader lifecycle semantics or App Foundry materialization; -those remain T5 and T6. + service name `web`, the exact digest-pinned image, and container state `running`. + +The failed run `36536731116` is an oracle/harness failure, not an Apps capability +negative: authentication and the T3 pool oracle succeeded, but the T4 client called a +later `truenas.entitlements.check` service that is absent from the tagged BETA.3 +middleware before reaching `docker.update`. The corrected rep must exercise the +tagged source path above. T4 still does not promote broader lifecycle semantics or App +Foundry materialization; those remain T5 and T6. ### TrueNAS BETA.3 source binding diff --git a/scripts/truenas_middleware_app_probe.py b/scripts/truenas_middleware_app_probe.py index 96c9231fb..8dce26b8d 100755 --- a/scripts/truenas_middleware_app_probe.py +++ b/scripts/truenas_middleware_app_probe.py @@ -11,7 +11,7 @@ APP_NAME = "rdte-t4-probe" -APP_IMAGE = "nginx:1.27-alpine" +APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10" def main(): diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index beb97660b..b00971cc3 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -324,7 +324,8 @@ def test_truenas_t4_apps_contract(self): self.assertIn("app.query", app) self.assertIn('payload["docker_update_result"]', app) self.assertIn('payload["app_create_result"]', app) - self.assertIn('"nginx:1.27-alpine"', app) + self.assertIn('"nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', app) + self.assertNotIn('"nginx:1.27-alpine"', app) self.assertIn('"state") == "RUNNING"', app) self.assertIn('--rung t4', workflow) py = subprocess.run( From d9faf05c49795eb734aaef612fdecdf91f86649a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:09:03 +0200 Subject: [PATCH 050/719] prep: qualify digest-pinned TrueNAS T5 lifecycle --- .../truenas_middleware_app_lifecycle_probe.py | 244 ++++++++++++++++++ tests/test_truenas_future_protocols.py | 200 ++++++++++++++ 2 files changed, 444 insertions(+) create mode 100755 scripts/truenas_middleware_app_lifecycle_probe.py diff --git a/scripts/truenas_middleware_app_lifecycle_probe.py b/scripts/truenas_middleware_app_lifecycle_probe.py new file mode 100755 index 000000000..c51eba64e --- /dev/null +++ b/scripts/truenas_middleware_app_lifecycle_probe.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""Bounded TrueNAS 26.0.0-BETA.3 T5 custom-app lifecycle oracle.""" +from __future__ import annotations + +import argparse +import json +import pathlib +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +APP_NAME = "rdte-t4-probe" +APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10" + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--out", required=True) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + p.add_argument("--state-timeout", type=float, default=180.0) + a = p.parse_args() + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + payload = { + "schema": "truenas-app-lifecycle-t5/v2", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected_version": EXPECTED_VERSION, + "app_name": APP_NAME, + "app_image": APP_IMAGE, + "transport": "wss" if a.tls else "ws", + "jobs": {}, + "states": [], + } + ws = None + request_id = 1 + started = time.time() + + try: + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError(f"DDP connection failed: {connected!r}") + + def call(method, params): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + def wait_job(job_id, label): + deadline = time.monotonic() + a.job_timeout + last = None + while time.monotonic() < deadline: + last = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last: + state = last.get("state") + if state == "SUCCESS": + payload["jobs"][label] = { + "id": job_id, + "state": state, + "progress": last.get("progress"), + "result": last.get("result"), + } + return last + if state in {"FAILED", "ABORTED"}: + raise RuntimeError( + f"{label} job {state}: {last.get('error') or last.get('exception')}" + ) + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS: {last!r}") + + def app_query(get=True): + params = [[["id", "=", APP_NAME]]] + if get: + params.append({"get": True}) + return call("app.query", params) + + def running(app): + workloads = app.get("active_workloads") or {} + details = workloads.get("container_details") or [] + return ( + app.get("state") == "RUNNING" + and app.get("custom_app") is True + and workloads.get("containers") == 1 + and len(details) == 1 + and details[0].get("service_name") == "web" + and details[0].get("image") == APP_IMAGE + and details[0].get("state") == "running" + ) + + def wait_state(expected, require_running=False): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = app_query() + if require_running: + if running(last): + payload["states"].append(last.get("state")) + return last + elif last.get("state") == expected: + payload["states"].append(last.get("state")) + return last + if last.get("state") in {"CRASHED", "ERROR"}: + raise RuntimeError( + f"app entered failure state while waiting for {expected}: {last!r}" + ) + time.sleep(1) + raise RuntimeError(f"app did not reach {expected}: {last!r}") + + auth = call( + "auth.login_ex", + [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }], + ) + payload["auth_response_type"] = ( + auth.get("response_type") if isinstance(auth, dict) else None + ) + if payload["auth_response_type"] != "SUCCESS": + raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") + + version = call("system.version", []) + payload["system_version"] = version + if version != EXPECTED_VERSION: + raise RuntimeError( + f"target version changed: expected {EXPECTED_VERSION!r}, got {version!r}" + ) + + initial = app_query() + if not running(initial): + raise RuntimeError(f"T5 requires the exact T4 app already RUNNING: {initial!r}") + payload["states"].append(initial["state"]) + + job_id = call("app.stop", [APP_NAME]) + if not isinstance(job_id, int): + raise RuntimeError(f"app.stop did not return a job id: {job_id!r}") + wait_job(job_id, "stop_1") + wait_state("STOPPED") + + job_id = call("app.start", [APP_NAME]) + if not isinstance(job_id, int): + raise RuntimeError(f"app.start did not return a job id: {job_id!r}") + wait_job(job_id, "start") + wait_state("RUNNING", require_running=True) + + updated_compose = { + "services": { + "web": { + "image": APP_IMAGE, + "restart": "unless-stopped", + "environment": {"RDTE_GENERATION": "2"}, + } + } + } + job_id = call( + "app.update", + [APP_NAME, {"custom_compose_config": updated_compose}], + ) + if not isinstance(job_id, int): + raise RuntimeError(f"app.update did not return a job id: {job_id!r}") + wait_job(job_id, "update") + + config = call("app.config", [APP_NAME]) + payload["config_after_update"] = config + generation = ( + config.get("services", {}) + .get("web", {}) + .get("environment", {}) + .get("RDTE_GENERATION") + ) + if str(generation) != "2": + raise RuntimeError( + f"app.config did not read back RDTE_GENERATION=2: {config!r}" + ) + config_image = config.get("services", {}).get("web", {}).get("image") + if config_image != APP_IMAGE: + raise RuntimeError( + f"app.config image drifted from immutable T4 digest: {config_image!r}" + ) + wait_state("RUNNING", require_running=True) + + job_id = call("app.redeploy", [APP_NAME]) + if not isinstance(job_id, int): + raise RuntimeError(f"app.redeploy did not return a job id: {job_id!r}") + wait_job(job_id, "redeploy") + wait_state("RUNNING", require_running=True) + + job_id = call("app.stop", [APP_NAME]) + if not isinstance(job_id, int): + raise RuntimeError(f"second app.stop did not return a job id: {job_id!r}") + wait_job(job_id, "stop_2") + wait_state("STOPPED") + + job_id = call( + "app.delete", + [APP_NAME, { + "remove_images": True, + "remove_ix_volumes": True, + "force_remove_custom_app": False, + }], + ) + if not isinstance(job_id, int): + raise RuntimeError(f"app.delete did not return a job id: {job_id!r}") + wait_job(job_id, "delete") + + remaining = app_query(get=False) + payload["post_delete_query"] = remaining + if remaining: + raise RuntimeError(f"app still exists after delete: {remaining!r}") + + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "digest-pinned custom app completed stop/start, config mutation with " + "public read-back, redeploy, stop, and delete" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index 24ea50f35..2f37f14dc 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -14,6 +14,7 @@ INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" POOL = ROOT / "scripts" / "truenas_middleware_pool_probe.py" +LIFECYCLE = ROOT / "scripts" / "truenas_middleware_app_lifecycle_probe.py" class SyntheticWebSocketPeer: @@ -395,6 +396,205 @@ def test_t3_uses_public_disk_details_not_private_get_unused(self): self.assertIn('disk_details["unused"]', text) + def test_t5_lifecycle_contract_is_version_and_digest_pinned(self): + text = LIFECYCLE.read_text(encoding="utf-8") + self.assertIn('EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3"', text) + self.assertIn('APP_NAME = "rdte-t4-probe"', text) + self.assertIn('APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', text) + self.assertNotIn('APP_IMAGE = "nginx:1.27-alpine"', text) + for method in ( + "app.stop", + "app.start", + "app.update", + "app.config", + "app.redeploy", + "app.delete", + ): + self.assertIn(f'"{method}"', text) + self.assertIn('"RDTE_GENERATION": "2"', text) + self.assertIn('"remove_images": True', text) + self.assertIn('"remove_ix_volumes": True', text) + self.assertIn('"post_delete_query"', text) + cp = subprocess.run( + [sys.executable, "-m", "py_compile", str(LIFECYCLE)], + text=True, + capture_output=True, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + + def test_t5_lifecycle_round_trip(self): + peer = SyntheticWebSocketPeer() + errors = [] + password = "synthetic-ephemeral-9264" + + def expect_call(conn, method): + msg = peer.recv_json(conn) + self.assertEqual(msg["msg"], "method") + self.assertEqual(msg["method"], method) + return msg + + def result(conn, msg, value): + peer.send_json(conn, {"id": msg["id"], "msg": "result", "result": value}) + + def app(state): + running = state == "RUNNING" + return { + "id": "rdte-t4-probe", + "name": "rdte-t4-probe", + "state": state, + "custom_app": True, + "active_workloads": { + "containers": 1 if running else 0, + "container_details": [{ + "id": "synthetic-nginx", + "service_name": "web", + "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10", + "state": "running", + }] if running else [], + }, + } + + def job_success(conn, job_id, result_value=None): + msg = expect_call(conn, "core.get_jobs") + self.assertEqual(msg["params"][0], [["id", "=", job_id]]) + result(conn, msg, { + "id": job_id, + "state": "SUCCESS", + "progress": {"percent": 100, "description": "done"}, + "result": result_value, + }) + + def server(): + try: + with peer.accept_websocket() as conn: + msg = peer.recv_json(conn) + self.assertEqual( + msg, {"msg": "connect", "version": "1", "support": ["1"]} + ) + peer.send_json(conn, {"msg": "connected", "session": "synthetic"}) + + msg = expect_call(conn, "auth.login_ex") + self.assertEqual(msg["params"][0]["password"], password) + result(conn, msg, {"response_type": "SUCCESS"}) + + msg = expect_call(conn, "system.version") + result(conn, msg, "TrueNAS-26.0.0-BETA.3") + + msg = expect_call(conn, "app.query") + result(conn, msg, app("RUNNING")) + + msg = expect_call(conn, "app.stop") + self.assertEqual(msg["params"], ["rdte-t4-probe"]) + result(conn, msg, 31) + job_success(conn, 31) + msg = expect_call(conn, "app.query") + result(conn, msg, app("STOPPED")) + + msg = expect_call(conn, "app.start") + self.assertEqual(msg["params"], ["rdte-t4-probe"]) + result(conn, msg, 32) + job_success(conn, 32) + msg = expect_call(conn, "app.query") + result(conn, msg, app("RUNNING")) + + msg = expect_call(conn, "app.update") + self.assertEqual(msg["params"][0], "rdte-t4-probe") + update = msg["params"][1] + self.assertEqual( + update["custom_compose_config"]["services"]["web"]["image"], + "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10", + ) + self.assertEqual( + update["custom_compose_config"]["services"]["web"]["environment"]["RDTE_GENERATION"], + "2", + ) + result(conn, msg, 33) + job_success(conn, 33) + + msg = expect_call(conn, "app.config") + result(conn, msg, update["custom_compose_config"]) + + msg = expect_call(conn, "app.query") + result(conn, msg, app("RUNNING")) + + msg = expect_call(conn, "app.redeploy") + self.assertEqual(msg["params"], ["rdte-t4-probe"]) + result(conn, msg, 34) + job_success(conn, 34) + msg = expect_call(conn, "app.query") + result(conn, msg, app("RUNNING")) + + msg = expect_call(conn, "app.stop") + result(conn, msg, 35) + job_success(conn, 35) + msg = expect_call(conn, "app.query") + result(conn, msg, app("STOPPED")) + + msg = expect_call(conn, "app.delete") + self.assertEqual(msg["params"][0], "rdte-t4-probe") + self.assertEqual(msg["params"][1], { + "remove_images": True, + "remove_ix_volumes": True, + "force_remove_custom_app": False, + }) + result(conn, msg, 36) + job_success(conn, 36, True) + + msg = expect_call(conn, "app.query") + self.assertEqual( + msg["params"], [[["id", "=", "rdte-t4-probe"]]] + ) + result(conn, msg, []) + except Exception as exc: + errors.append(exc) + finally: + peer.listener.close() + + thread = threading.Thread(target=server, daemon=True) + thread.start() + with tempfile.TemporaryDirectory() as td: + td = pathlib.Path(td) + password_file = td / "password" + password_file.write_text(password, encoding="utf-8") + out = td / "lifecycle.json" + cp = subprocess.run( + [ + sys.executable, + str(LIFECYCLE), + "--port", str(peer.port), + "--password-file", str(password_file), + "--out", str(out), + "--timeout", "2", + "--job-timeout", "8", + "--state-timeout", "8", + ], + text=True, + capture_output=True, + timeout=20, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + receipt = json.loads(out.read_text(encoding="utf-8")) + self.assertTrue(receipt["oracleSatisfied"], receipt) + self.assertEqual(receipt["classification"], "SUPPORTED") + self.assertEqual(receipt["system_version"], "TrueNAS-26.0.0-BETA.3") + self.assertEqual(receipt["app_image"], "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10") + self.assertEqual( + receipt["states"], + ["RUNNING", "STOPPED", "RUNNING", "RUNNING", "RUNNING", "STOPPED"], + ) + self.assertEqual( + receipt["config_after_update"]["services"]["web"]["environment"]["RDTE_GENERATION"], + "2", + ) + self.assertEqual(receipt["post_delete_query"], []) + self.assertNotIn(password, out.read_text(encoding="utf-8")) + thread.join(timeout=5) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + + + + if __name__ == "__main__": unittest.main() unittest.main() From c88013f0c46ce172ec9cd6ade7638925df2a0da8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:31:15 +0200 Subject: [PATCH 051/719] exp: promote TrueNAS T5 lifecycle real-system rung --- .github/workflows/gha-kvm-system-rdte.yml | 6 +-- docs/gha-kvm-system-rdte.md | 18 ++++++++ scripts/gha_kvm_truenas_rdte.sh | 56 +++++++++++++++++------ tests/test_gha_kvm_system_rdte.py | 27 +++++++++-- 4 files changed, 88 insertions(+), 19 deletions(-) mode change 100644 => 100755 scripts/gha_kvm_truenas_rdte.sh diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 60b76ea21..74751670e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -60,7 +60,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py) + scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -193,7 +193,7 @@ jobs: mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" bash scripts/gha_kvm_truenas_rdte.sh \ --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ - --rung t4 \ + --rung t5 \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence @@ -206,7 +206,7 @@ jobs: import json, pathlib, sys pathlib.Path(sys.argv[1]).write_text(json.dumps({ "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T4"}, + "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T5"}, "classification":"HARNESS_FAILURE", "oracleSatisfied":False, "phase":"workflow-fallback", diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 45d987567..05ac20d12 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -239,3 +239,21 @@ is `docker.license_active`: non-HA systems are permitted directly, while HA-capa systems consult `system.feature_enabled("APPS")`. The T4 client therefore does not call the later entitlement API; `docker.update` and Docker startup exercise the actual BETA.3 product gate. + + +### TrueNAS T5 custom-App lifecycle + +T5 is eligible only after accepted T4 evidence. It reproduces the T4 pool and Apps +runtime first, including the exact immutable image +`nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10`, then exercises the tagged BETA.3 public lifecycle: + +- `app.stop` -> independently queried `STOPPED`; +- `app.start` -> independently queried `RUNNING` with the expected digest-pinned workload; +- `app.update` sets `RDTE_GENERATION=2`; +- public `app.config` must read back both `RDTE_GENERATION=2` and the same immutable image; +- `app.redeploy` -> independently queried `RUNNING`; +- final stop -> `STOPPED`; +- `app.delete` -> final exact `app.query` is absent. + +Job success alone never satisfies T5. T5 does not establish Foundry deployment-artifact +realization; that remains the distinct T6 gate. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh old mode 100644 new mode 100755 index f85904bbf..e19250011 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -20,7 +20,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3|t4]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3|t4|t5]" } OUT="" @@ -36,7 +36,7 @@ while [[ $# -gt 0 ]]; do esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } -[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" ]] || { echo "rung must be t0, t1, t2, t3, or t4" >&2; exit 2; } +[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]] || { echo "rung must be t0, t1, t2, t3, t4, or t5" >&2; exit 2; } if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -57,6 +57,7 @@ INSTALL_RESULT_JSON="" MIDDLEWARE_RESULT_JSON="" POOL_RESULT_JSON="" APP_RESULT_JSON="" +LIFECYCLE_RESULT_JSON="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -78,7 +79,7 @@ write_receipt() { export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" - export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" + export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" python3 - <<'PY' import json, os, pathlib payload = { @@ -92,10 +93,10 @@ payload = { "vcpus": 2, "ram_mib": 8192, "boot_disk": "24G", - "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") in {"t3", "t4"} else [], - "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4"} else None, - "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4"} else [], - "app": {"name": "rdte-t4-probe", "image": "nginx:1.27-alpine"} if os.environ.get("R_RUNG") == "t4" else None, + "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5"} else [], + "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4", "t5"} else None, + "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5"} else [], + "app": {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} if os.environ.get("R_RUNG") in {"t4", "t5"} else None, }, "source": { "iso_name": "TrueNAS-26.0.0-BETA.3.iso", @@ -115,12 +116,14 @@ payload = { "installed_middleware_authenticated": bool(os.environ.get("R_MIDDLEWARE_RESULT")) and json.loads(os.environ["R_MIDDLEWARE_RESULT"]).get("oracleSatisfied") is True, "data_pool_created": bool(os.environ.get("R_POOL_RESULT")) and json.loads(os.environ["R_POOL_RESULT"]).get("oracleSatisfied") is True, "apps_runtime_exercised": bool(os.environ.get("R_APP_RESULT")) and json.loads(os.environ["R_APP_RESULT"]).get("oracleSatisfied") is True, + "app_lifecycle_exercised": bool(os.environ.get("R_LIFECYCLE_RESULT")) and json.loads(os.environ["R_LIFECYCLE_RESULT"]).get("oracleSatisfied") is True, }, "rpc_discovery": json.loads(os.environ["R_RPC_DISCOVERY"]) if os.environ.get("R_RPC_DISCOVERY") else None, "install_result": json.loads(os.environ["R_INSTALL_RESULT"]) if os.environ.get("R_INSTALL_RESULT") else None, "installed_middleware": json.loads(os.environ["R_MIDDLEWARE_RESULT"]) if os.environ.get("R_MIDDLEWARE_RESULT") else None, "data_pool": json.loads(os.environ["R_POOL_RESULT"]) if os.environ.get("R_POOL_RESULT") else None, "apps_runtime": json.loads(os.environ["R_APP_RESULT"]) if os.environ.get("R_APP_RESULT") else None, + "app_lifecycle": json.loads(os.environ["R_LIFECYCLE_RESULT"]) if os.environ.get("R_LIFECYCLE_RESULT") else None, "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ @@ -129,6 +132,7 @@ payload = { "T2 adds vendor installation plus installed middleware authentication/health.", "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", "T4 initializes Apps on that pool and runs one synthetic public-safe custom Compose app.", + "T5 exercises stop/start, config mutation/read-back, redeploy, stop, and delete for that digest-pinned custom app.", "Broader application lifecycle and Foundry materialization remain later gates.", "This does not qualify physical storage controllers, SMART, GPU, IPMI, or HA behavior.", ], @@ -146,18 +150,22 @@ fail_evidence() { [[ -f "$SCRIPT_DIR/truenas_installer_rpc_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer RPC probe" -if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" ]]; then +if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then [[ -f "$SCRIPT_DIR/truenas_installer_rpc_install.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer install client" [[ -f "$SCRIPT_DIR/truenas_middleware_ddp_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS middleware health client" - if [[ "$RUNG" == "t3" || "$RUNG" == "t4" ]]; then + if [[ "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_pool_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T3/T4 pool client" fi - if [[ "$RUNG" == "t4" ]]; then + if [[ "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_app_probe.py" ]] || - fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T4 Apps client" + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS Apps client" + fi + if [[ "$RUNG" == "t5" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_app_lifecycle_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T5 lifecycle client" fi fi for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do @@ -363,7 +371,7 @@ if [[ -n "$QEMU_PID" ]]; then fi DATA_DRIVE_ARGS=() -if [[ "$RUNG" == "t3" || "$RUNG" == "t4" ]]; then +if [[ "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then for index in $(seq 0 $((DATA_DISK_COUNT - 1))); do data_disk="$STATE_DIR/data${index}.qcow2" qemu-img create -q -f qcow2 "$data_disk" "$DATA_DISK_SIZE" || @@ -504,4 +512,26 @@ PY [[ "$APP_OK" == "true" ]] || fail_evidence ORACLE_FAILURE apps-runtime "TrueNAS Apps did not initialize and run the bounded custom app oracle" -write_receipt SUPPORTED true apps-runtime "installed TrueNAS initialized Apps on rdtepool and ran the bounded custom nginx Compose app" +if [[ "$RUNG" == "t4" ]]; then + write_receipt SUPPORTED true apps-runtime "installed TrueNAS initialized Apps on rdtepool and ran the bounded digest-pinned custom nginx Compose app" + exit 0 +fi + +LIFECYCLE_OUT="$STATE_DIR/app-lifecycle.json" +python3 "$SCRIPT_DIR/truenas_middleware_app_lifecycle_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --out "$LIFECYCLE_OUT" --timeout 8 --job-timeout 300 --state-timeout 180 >/dev/null 2>&1 || true +[[ -f "$LIFECYCLE_OUT" ]] || fail_evidence HARNESS_FAILURE app-lifecycle "T5 lifecycle client did not emit a receipt" +LIFECYCLE_RESULT_JSON="$(cat "$LIFECYCLE_OUT")" +LIFECYCLE_OK="$(python3 - "$LIFECYCLE_OUT" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +print("true" if data.get("oracleSatisfied") is True else "false") +PY +)" +[[ "$LIFECYCLE_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE app-lifecycle "custom app did not complete the bounded T5 lifecycle" + +write_receipt SUPPORTED true app-lifecycle "digest-pinned custom app completed stop/start, config mutation with public read-back, redeploy, stop, and delete" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index b00971cc3..3ff47392d 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -20,6 +20,7 @@ TRUENAS_MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" TRUENAS_POOL = ROOT / "scripts" / "truenas_middleware_pool_probe.py" TRUENAS_APP = ROOT / "scripts" / "truenas_middleware_app_probe.py" +TRUENAS_LIFECYCLE = ROOT / "scripts" / "truenas_middleware_app_lifecycle_probe.py" class SystemRdteContractTests(unittest.TestCase): @@ -278,7 +279,7 @@ def test_truenas_install_rpc_uses_one_positional_object(self): def test_truenas_t3_real_harness_contract(self): text = TRUENAS.read_text(encoding="utf-8") workflow = WORKFLOW.read_text(encoding="utf-8") - self.assertIn("t0|t1|t2|t3|t4", text) + self.assertIn("t0|t1|t2|t3|t4|t5", text) self.assertIn('DATA_DISK_SIZE="8G"', text) self.assertIn("DATA_DISK_COUNT=2", text) self.assertIn('DATA_POOL_NAME="rdtepool"', text) @@ -300,7 +301,7 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn("truenas_middleware_pool_probe.py", text) self.assertIn('"data_pool_created"', text) self.assertIn('"data_pool"', text) - self.assertIn("--rung t4", workflow) + self.assertIn("--rung t5", workflow) self.assertIn("scripts/truenas_middleware_pool_probe.py", workflow) shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) @@ -327,7 +328,7 @@ def test_truenas_t4_apps_contract(self): self.assertIn('"nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', app) self.assertNotIn('"nginx:1.27-alpine"', app) self.assertIn('"state") == "RUNNING"', app) - self.assertIn('--rung t4', workflow) + self.assertIn('--rung t5', workflow) py = subprocess.run( ["python3", "-m", "py_compile", str(TRUENAS_APP)], text=True, @@ -335,6 +336,26 @@ def test_truenas_t4_apps_contract(self): ) self.assertEqual(py.returncode, 0, py.stderr) + + def test_truenas_t5_lifecycle_harness_contract(self): + text = TRUENAS.read_text(encoding="utf-8") + lifecycle = TRUENAS_LIFECYCLE.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn("t0|t1|t2|t3|t4|t5", text) + self.assertIn("truenas_middleware_app_lifecycle_probe.py", text) + self.assertIn('"app_lifecycle_exercised"', text) + self.assertIn('"app_lifecycle"', text) + self.assertIn("nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10", text) + self.assertIn('EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3"', lifecycle) + self.assertIn('APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', lifecycle) + self.assertIn("scripts/truenas_middleware_app_lifecycle_probe.py", workflow) + self.assertIn("--rung t5", workflow) + shell_check = subprocess.run( + ["bash", "-n", str(TRUENAS)], text=True, capture_output=True + ) + self.assertEqual(shell_check.returncode, 0, shell_check.stderr) + + def test_qemu_t3_topology_is_a_cheap_separate_oracle(self): probe = QEMU_TOPOLOGY.read_text(encoding="utf-8") workflow = WORKFLOW.read_text(encoding="utf-8") From e6665e62f83ff8242b59b98c1b0e4b9a70638a28 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:49:57 +0200 Subject: [PATCH 052/719] prep: rebase Proxmox P2 guest witness on accepted T5 head --- .github/workflows/prep-rdte-static.yml | 59 ++++++++++++++++++++ docs/gha-kvm-system-rdte.md | 15 +++++ scripts/gha_kvm_proxmox_rdte.sh | 77 +++++++++++++++++++++----- tests/test_gha_kvm_system_rdte.py | 17 ++++++ 4 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/prep-rdte-static.yml mode change 100644 => 100755 scripts/gha_kvm_proxmox_rdte.sh diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml new file mode 100644 index 000000000..fabbfabf4 --- /dev/null +++ b/.github/workflows/prep-rdte-static.yml @@ -0,0 +1,59 @@ +name: Prep RDTE static gate + +on: + push: + branches: + - 'prep/**' + paths: + - '.github/workflows/prep-rdte-static.yml' + - '.github/workflows/gha-kvm-system-rdte.yml' + - 'config/truenas-rdte-targets.json' + - 'scripts/gha_kvm_truenas_rdte.sh' + - 'scripts/gha_kvm_proxmox_rdte.sh' + - 'scripts/truenas_middleware_*.py' + - 'tests/test_gha_kvm_system_rdte.py' + - 'tests/test_truenas_future_protocols.py' + - 'tests/test_truenas_t6_foundry_control.py' + pull_request: + paths: + - '.github/workflows/prep-rdte-static.yml' + - '.github/workflows/gha-kvm-system-rdte.yml' + - 'config/truenas-rdte-targets.json' + - 'scripts/gha_kvm_truenas_rdte.sh' + - 'scripts/gha_kvm_proxmox_rdte.sh' + - 'scripts/truenas_middleware_*.py' + - 'tests/test_gha_kvm_system_rdte.py' + - 'tests/test_truenas_future_protocols.py' + - 'tests/test_truenas_t6_foundry_control.py' + +permissions: + contents: read + +jobs: + static: + runs-on: ubuntu-24.04 + timeout-minutes: 8 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Shell syntax + run: | + set -euo pipefail + bash -n scripts/gha_kvm_truenas_rdte.sh + bash -n scripts/gha_kvm_proxmox_rdte.sh + - name: Python compile + run: | + set -euo pipefail + python3 -m compileall -q scripts tests + - name: RDTE contract and protocol tests + run: | + set -euo pipefail + tests=( + tests/test_gha_kvm_system_rdte.py + tests/test_truenas_future_protocols.py + ) + if [[ -f tests/test_truenas_t6_foundry_control.py ]]; then + tests+=(tests/test_truenas_t6_foundry_control.py) + fi + python3 -m unittest -v "${tests[@]}" diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 05ac20d12..1740599fb 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -131,6 +131,21 @@ free-tier RDTE useful without replaying multi-gigabyte installs that cannot chan evidence under test. +### Proxmox P2 installed-guest witness + +P2 must distinguish the QEMU host-forward listener from the installed guest. The +diagnostic adapter therefore uses the vendor automated installer's supported +`[first-boot]` hook with `source = "from-iso"` and `ordering = "fully-up"`. +The hook emits a bounded serial witness from inside the installed guest containing +hostname/kernel identity, interface and route state, selected Proxmox service activity, +listening TCP sockets, and observational KVM/CPU-virtualization presence. + +The witness is diagnostic evidence only. P2 is satisfied only by a real HTTPS +`/api2/json/version` response. A host-side TCP accept is recorded only as +`*_hostfwd_accepted`; it is not guest-service reachability. Failure to complete SSH +leaves nested-KVM state `unknown`. P5 still requires an actual L3 KVM VCPU execution +nonce and cannot be inferred from `/dev/kvm` or CPU flags alone. + ### TrueNAS T1 service-oracle semantics QEMU user-mode `hostfwd` accepting a localhost TCP connection is not a TrueNAS diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh old mode 100644 new mode 100755 index 09c163ffb..fdc454cc5 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -36,10 +36,12 @@ QEMU_PID="" OBSERVED_ISO_SHA="" API_VERSION_JSON="" NESTED_KVM="unknown" -SSH_REACHABLE="false" -API_PORT_REACHABLE="false" +SSH_HOSTFWD_ACCEPTED="false" +API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" GUEST_DIAGNOSTICS="" +FIRST_BOOT_WITNESS="" +FIRST_BOOT_WITNESS_OBSERVED="false" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -59,8 +61,9 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" - export R_SSH_REACHABLE="$SSH_REACHABLE" R_API_PORT_REACHABLE="$API_PORT_REACHABLE" + export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" + export R_FIRST_BOOT_WITNESS="$FIRST_BOOT_WITNESS" R_FIRST_BOOT_WITNESS_OBSERVED="$FIRST_BOOT_WITNESS_OBSERVED" python3 - <<'PY' import json, os, pathlib payload = { @@ -78,7 +81,7 @@ payload = { "observed_sha256": os.environ.get("R_ISO_SHA") or None, }, "oracles": { - "vendor_iso_digest": bool(os.environ.get("R_ISO_SHA")), + "vendor_iso_digest": os.environ.get("R_ISO_SHA") == "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", "unattended_install_completed": os.environ["R_PHASE"] in {"installed-api", "complete"}, "installed_https_api": bool(os.environ.get("R_API")), "nested_kvm_observed_via_ssh": os.environ.get("R_NESTED") == "yes", @@ -87,9 +90,11 @@ payload = { "nested_kvm": os.environ.get("R_NESTED"), "diagnostics": { "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), - "ssh_port_reachable": os.environ.get("R_SSH_REACHABLE") == "true", - "api_port_reachable": os.environ.get("R_API_PORT_REACHABLE") == "true", + "ssh_hostfwd_accepted": os.environ.get("R_SSH_HOSTFWD_ACCEPTED") == "true", + "api_hostfwd_accepted": os.environ.get("R_API_HOSTFWD_ACCEPTED") == "true", "guest": os.environ.get("R_GUEST_DIAGNOSTICS") or None, + "first_boot_witness_observed": os.environ.get("R_FIRST_BOOT_WITNESS_OBSERVED") == "true", + "first_boot_witness": os.environ.get("R_FIRST_BOOT_WITNESS") or None, }, "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ @@ -134,6 +139,10 @@ mailto = "rdte@example.invalid" timezone = "Etc/UTC" root-password = "rdte-proxmox-ephemeral-9264" +[first-boot] +source = "from-iso" +ordering = "fully-up" + [network] source = "from-dhcp" @@ -143,6 +152,36 @@ disk-list = ["sda"] EOF printf 'mode = "iso"\n' >"$STATE_DIR/auto-installer-mode.toml" +cat >"$STATE_DIR/proxmox-first-boot" <<'EOF' +#!/bin/sh +set +e +if [ -c /dev/ttyS0 ]; then + exec >/dev/ttyS0 2>&1 +fi +echo "PVE_RDTE_WITNESS_BEGIN" +printf 'HOSTNAME='; hostname -f 2>/dev/null || hostname 2>/dev/null || true +printf 'KERNEL='; uname -a 2>/dev/null || true +echo "IP_ADDR_BEGIN" +ip -brief address 2>&1 || true +echo "IP_ADDR_END" +echo "IP_ROUTE_BEGIN" +ip route 2>&1 || true +echo "IP_ROUTE_END" +echo "PVE_SERVICES_BEGIN" +for service in pve-cluster pvedaemon pvestatd pveproxy ssh networking systemd-networkd; do + printf '%s=' "$service" + systemctl is-active "$service" 2>/dev/null || true +done +echo "PVE_SERVICES_END" +echo "LISTENERS_BEGIN" +ss -lntp 2>&1 || true +echo "LISTENERS_END" +if [ -e /dev/kvm ]; then echo "KVM_DEVICE=present"; else echo "KVM_DEVICE=absent"; fi +if grep -Eq '(vmx|svm)' /proc/cpuinfo 2>/dev/null; then echo "CPU_VIRT_FLAG=present"; else echo "CPU_VIRT_FLAG=absent"; fi +echo "PVE_RDTE_WITNESS_END" +EOF +chmod 0755 "$STATE_DIR/proxmox-first-boot" + xorriso -osirrox on -indev "$ISO" -extract /boot/grub/grub.cfg "$STATE_DIR/grub.cfg" >/dev/null 2>&1 || fail_evidence HARNESS_FAILURE prepare "could not extract Proxmox GRUB config" chmod u+w "$STATE_DIR/grub.cfg" @@ -168,6 +207,7 @@ chmod u+w "$AUTO_ISO" xorriso -boot_image any keep -dev "$AUTO_ISO" \ -map "$STATE_DIR/auto-installer-mode.toml" /auto-installer-mode.toml \ -map "$STATE_DIR/answer.toml" /answer.toml \ + -map "$STATE_DIR/proxmox-first-boot" /proxmox-first-boot \ -map "$STATE_DIR/grub.cfg" /boot/grub/grub.cfg \ -commit >/dev/null 2>"$STATE_DIR/xorriso.log" || fail_evidence HARNESS_FAILURE prepare "failed to construct unattended Proxmox ISO" @@ -264,8 +304,8 @@ stop_qemu start_qemu no API_VERSION_JSON="" -SSH_REACHABLE="false" -API_PORT_REACHABLE="false" +SSH_HOSTFWD_ACCEPTED="false" +API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" for _ in $(seq 1 120); do if sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then @@ -274,19 +314,28 @@ for _ in $(seq 1 120); do QEMU_ALIVE_AT_API_GATE="false" break fi - if port_open "$SSH_PORT"; then SSH_REACHABLE="true"; fi - if port_open "$WEB_PORT"; then API_PORT_REACHABLE="true"; fi - if [[ "$API_PORT_REACHABLE" == "true" ]]; then + if grep -q 'PVE_RDTE_WITNESS_END' "$STATE_DIR/serial.log" 2>/dev/null; then + FIRST_BOOT_WITNESS_OBSERVED="true" + FIRST_BOOT_WITNESS="$(awk '/PVE_RDTE_WITNESS_BEGIN/{capture=1} capture{print} /PVE_RDTE_WITNESS_END/{exit}' "$STATE_DIR/serial.log" | tr -d '\000' | sed -E 's/[^[:print:]\t]//g' | tail -c 16000)" + fi + if port_open "$SSH_PORT"; then SSH_HOSTFWD_ACCEPTED="true"; fi + if port_open "$WEB_PORT"; then API_HOSTFWD_ACCEPTED="true"; fi + if [[ "$API_HOSTFWD_ACCEPTED" == "true" ]]; then API_VERSION_JSON="$(curl -sk --max-time 3 "https://127.0.0.1:$WEB_PORT/api2/json/version" 2>/dev/null || true)" if [[ "$API_VERSION_JSON" == *'"data"'* ]]; then break; fi fi + if [[ "$FIRST_BOOT_WITNESS_OBSERVED" == "true" && -z "$API_VERSION_JSON" ]]; then + break + fi sleep 3 done if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then - if [[ "$SSH_REACHABLE" == "true" ]]; then + if command -v sshpass >/dev/null 2>&1 && sshpass -p 'rdte-proxmox-ephemeral-9264' ssh -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 'true' >/dev/null 2>&1; then GUEST_DIAGNOSTICS="$(guest_diagnostics || true)" fi - fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer (qemu_alive=$QEMU_ALIVE_AT_API_GATE ssh=$SSH_REACHABLE tcp8006=$API_PORT_REACHABLE)" + fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer (qemu_alive=$QEMU_ALIVE_AT_API_GATE first_boot_witness=$FIRST_BOOT_WITNESS_OBSERVED)" fi NESTED_KVM="unknown" @@ -296,7 +345,7 @@ if command -v sshpass >/dev/null 2>&1; then root@127.0.0.1 'test -e /dev/kvm && grep -Eq "(vmx|svm)" /proc/cpuinfo' >/dev/null 2>&1; then NESTED_KVM="yes" else - NESTED_KVM="no" + NESTED_KVM="unknown" fi fi diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 3ff47392d..22363def1 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -67,6 +67,23 @@ def test_proxmox_is_pinned_and_uses_vendor_auto_install_contract(self): self.assertIn("/api2/json/version", text) self.assertNotIn("xdotool", text) + def test_proxmox_first_boot_witness_uses_vendor_hook(self): + text = PVE.read_text(encoding="utf-8") + self.assertIn("[first-boot]", text) + self.assertIn('source = "from-iso"', text) + self.assertIn('ordering = "fully-up"', text) + self.assertIn("/proxmox-first-boot", text) + self.assertIn("PVE_RDTE_WITNESS_BEGIN", text) + self.assertIn("PVE_RDTE_WITNESS_END", text) + self.assertIn("first_boot_witness_observed", text) + + def test_proxmox_hostfwd_is_diagnostic_not_guest_or_nested_oracle(self): + text = PVE.read_text(encoding="utf-8") + self.assertNotIn('NESTED_KVM="no"', text) + self.assertIn('"ssh_hostfwd_accepted"', text) + self.assertIn('"api_hostfwd_accepted"', text) + self.assertIn("first_boot_witness_observed", text) + def test_truenas_is_pinned_and_digest_sidecar_is_required(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn("TrueNAS-26.0.0-BETA.3.iso", text) From e7b66ca2d2162ef8180b5c3dddc0b0ce4ae6d2fe Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:29:18 +0200 Subject: [PATCH 053/719] diag: decouple Proxmox P2 witness from pveproxy --- docs/gha-kvm-system-rdte.md | 20 +++++++++---- scripts/gha_kvm_proxmox_rdte.sh | 48 +++++++++++++++++++++++++++++-- tests/test_gha_kvm_system_rdte.py | 7 ++++- 3 files changed, 66 insertions(+), 9 deletions(-) mode change 100755 => 100644 scripts/gha_kvm_proxmox_rdte.sh diff --git a/docs/gha-kvm-system-rdte.md b/docs/gha-kvm-system-rdte.md index 1740599fb..f6e446dd4 100644 --- a/docs/gha-kvm-system-rdte.md +++ b/docs/gha-kvm-system-rdte.md @@ -134,11 +134,21 @@ evidence under test. ### Proxmox P2 installed-guest witness P2 must distinguish the QEMU host-forward listener from the installed guest. The -diagnostic adapter therefore uses the vendor automated installer's supported -`[first-boot]` hook with `source = "from-iso"` and `ordering = "fully-up"`. -The hook emits a bounded serial witness from inside the installed guest containing -hostname/kernel identity, interface and route state, selected Proxmox service activity, -listening TCP sockets, and observational KVM/CPU-virtualization presence. +diagnostic adapter uses the vendor automated installer's supported `[first-boot]` +hook with `source = "from-iso"`, but intentionally selects +`ordering = "network-online"`. + +The earlier `fully-up` witness was circular: upstream Proxmox first-boot packaging +orders the fully-up service after and wants the product-specific API proxy +(`pveproxy.service` on PVE). If that proxy is the failing P2 dependency, the witness +cannot run to explain it. The upstream `network-online` ordering instead runs after +networking and before that product-proxy dependency. + +The hook therefore emits a bounded serial witness from inside the installed guest, +records hostname/hosts/network state immediately, then waits at most 90 seconds for +`pveproxy`/TCP 8006 and records final service state, listeners, failed units, and +bounded `pveproxy`/`pve-cluster` journals. The prepared ISO is also read back before +launch and the exact `/proxmox-first-boot` content SHA-256 must match the source hook. The witness is diagnostic evidence only. P2 is satisfied only by a real HTTPS `/api2/json/version` response. A host-side TCP accept is recorded only as diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh old mode 100755 new mode 100644 index fdc454cc5..223a0dedd --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -141,7 +141,7 @@ root-password = "rdte-proxmox-ephemeral-9264" [first-boot] source = "from-iso" -ordering = "fully-up" +ordering = "network-online" [network] source = "from-dhcp" @@ -159,23 +159,56 @@ if [ -c /dev/ttyS0 ]; then exec >/dev/ttyS0 2>&1 fi echo "PVE_RDTE_WITNESS_BEGIN" +printf 'ORDERING_ARG=%s\n' "${1:-unset}" printf 'HOSTNAME='; hostname -f 2>/dev/null || hostname 2>/dev/null || true printf 'KERNEL='; uname -a 2>/dev/null || true +echo "HOSTS_BEGIN" +cat /etc/hosts 2>&1 || true +echo "HOSTS_END" echo "IP_ADDR_BEGIN" ip -brief address 2>&1 || true echo "IP_ADDR_END" echo "IP_ROUTE_BEGIN" ip route 2>&1 || true echo "IP_ROUTE_END" -echo "PVE_SERVICES_BEGIN" +echo "NETWORK_INTERFACES_BEGIN" +cat /etc/network/interfaces 2>&1 || true +echo "NETWORK_INTERFACES_END" +echo "PVE_INITIAL_SERVICES_BEGIN" for service in pve-cluster pvedaemon pvestatd pveproxy ssh networking systemd-networkd; do printf '%s=' "$service" systemctl is-active "$service" 2>/dev/null || true done -echo "PVE_SERVICES_END" +echo "PVE_INITIAL_SERVICES_END" + +# network-online intentionally runs before the product proxy dependency used by +# the upstream "fully-up" first-boot unit. Poll without mutating so a failing +# pveproxy can still leave guest-originated evidence instead of deadlocking the oracle. +for _ in $(seq 1 45); do + if systemctl is-active --quiet pveproxy 2>/dev/null && ss -lnt 2>/dev/null | grep -Eq '[:.]8006[[:space:]]'; then + break + fi + sleep 2 +done + +echo "PVE_FINAL_SERVICES_BEGIN" +for service in pve-cluster pvedaemon pvestatd pveproxy ssh networking systemd-networkd; do + printf '%s=' "$service" + systemctl is-active "$service" 2>/dev/null || true +done +echo "PVE_FINAL_SERVICES_END" echo "LISTENERS_BEGIN" ss -lntp 2>&1 || true echo "LISTENERS_END" +echo "FAILED_UNITS_BEGIN" +systemctl --failed --no-pager 2>&1 || true +echo "FAILED_UNITS_END" +echo "PVEPROXY_JOURNAL_BEGIN" +journalctl -u pveproxy -b --no-pager -n 80 2>&1 || true +echo "PVEPROXY_JOURNAL_END" +echo "PVECLUSTER_JOURNAL_BEGIN" +journalctl -u pve-cluster -b --no-pager -n 80 2>&1 || true +echo "PVECLUSTER_JOURNAL_END" if [ -e /dev/kvm ]; then echo "KVM_DEVICE=present"; else echo "KVM_DEVICE=absent"; fi if grep -Eq '(vmx|svm)' /proc/cpuinfo 2>/dev/null; then echo "CPU_VIRT_FLAG=present"; else echo "CPU_VIRT_FLAG=absent"; fi echo "PVE_RDTE_WITNESS_END" @@ -212,6 +245,15 @@ xorriso -boot_image any keep -dev "$AUTO_ISO" \ -commit >/dev/null 2>"$STATE_DIR/xorriso.log" || fail_evidence HARNESS_FAILURE prepare "failed to construct unattended Proxmox ISO" +# Prove that the same exact file name consumed by the upstream auto-installer +# (/cdrom/proxmox-first-boot) exists in the prepared ISO before spending a VM rep. +xorriso -osirrox on -indev "$AUTO_ISO" \ + -extract /proxmox-first-boot "$STATE_DIR/proxmox-first-boot.iso" >/dev/null 2>&1 || + fail_evidence HARNESS_FAILURE prepare "prepared ISO does not contain /proxmox-first-boot" +[[ "$(sha256sum "$STATE_DIR/proxmox-first-boot" | awk '{print $1}')" == \ + "$(sha256sum "$STATE_DIR/proxmox-first-boot.iso" | awk '{print $1}')" ]] || + fail_evidence HARNESS_FAILURE prepare "prepared ISO first-boot hook content mismatch" + qemu-img create -q -f qcow2 "$STATE_DIR/system.qcow2" "$DISK_SIZE" SSH_PORT="$(python3 - <<'PY' import socket diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 22363def1..81d0b48e6 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -71,8 +71,13 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): text = PVE.read_text(encoding="utf-8") self.assertIn("[first-boot]", text) self.assertIn('source = "from-iso"', text) - self.assertIn('ordering = "fully-up"', text) + self.assertIn('ordering = "network-online"', text) + self.assertNotIn('ordering = "fully-up"', text) self.assertIn("/proxmox-first-boot", text) + self.assertIn("proxmox-first-boot.iso", text) + self.assertIn("PVE_INITIAL_SERVICES_BEGIN", text) + self.assertIn("PVE_FINAL_SERVICES_BEGIN", text) + self.assertIn("PVEPROXY_JOURNAL_BEGIN", text) self.assertIn("PVE_RDTE_WITNESS_BEGIN", text) self.assertIn("PVE_RDTE_WITNESS_END", text) self.assertIn("first_boot_witness_observed", text) From a642be1e5176b88d8495e4deccd303b2a5acd91d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 14:58:55 +0200 Subject: [PATCH 054/719] diag: inspect installed Proxmox first-boot path --- scripts/gha_kvm_proxmox_rdte.sh | 171 +++++++++++++++++++++++++++++- tests/test_gha_kvm_system_rdte.py | 12 +++ 2 files changed, 180 insertions(+), 3 deletions(-) mode change 100644 => 100755 scripts/gha_kvm_proxmox_rdte.sh diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh old mode 100644 new mode 100755 index 223a0dedd..cbd59d8f2 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -4,6 +4,8 @@ set -euo pipefail ISO_NAME="proxmox-ve_9.2-1.iso" ISO_URL="https://enterprise.proxmox.com/iso/$ISO_NAME" ISO_SHA256="4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c" +PVE_INSTALLER_SOURCE_VERSION="9.2.5" +PVE_INSTALLER_SOURCE_COMMIT="32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9" RAM_MIB=4096 VCPUS=2 DISK_SIZE="40G" @@ -42,6 +44,10 @@ QEMU_ALIVE_AT_API_GATE="unknown" GUEST_DIAGNOSTICS="" FIRST_BOOT_WITNESS="" FIRST_BOOT_WITNESS_OBSERVED="false" +ISO_INSTALLER_PACKAGE="" +ISO_FIRST_BOOT_PACKAGE="" +INSTALLED_DISK_PREBOOT="" +INSTALLED_DISK_POSTBOOT="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -64,8 +70,19 @@ write_receipt() { export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" export R_FIRST_BOOT_WITNESS="$FIRST_BOOT_WITNESS" R_FIRST_BOOT_WITNESS_OBSERVED="$FIRST_BOOT_WITNESS_OBSERVED" + export R_ISO_INSTALLER_PACKAGE="$ISO_INSTALLER_PACKAGE" R_ISO_FIRST_BOOT_PACKAGE="$ISO_FIRST_BOOT_PACKAGE" + export R_DISK_PREBOOT="$INSTALLED_DISK_PREBOOT" R_DISK_POSTBOOT="$INSTALLED_DISK_POSTBOOT" python3 - <<'PY' import json, os, pathlib +def load_json_env(name): + raw = os.environ.get(name, "") + if not raw: + return None + try: + return json.loads(raw) + except Exception as exc: + return {"inspection_ok": False, "parse_error": str(exc), "raw": raw[:2000]} + payload = { "contract": "gha-kvm-system-lab/v1", "target": {"product": "proxmox-ve", "version": "9.2-1"}, @@ -79,6 +96,10 @@ payload = { "iso_url": "https://enterprise.proxmox.com/iso/proxmox-ve_9.2-1.iso", "expected_sha256": "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", "observed_sha256": os.environ.get("R_ISO_SHA") or None, + "installer_source_version": "9.2.5", + "installer_source_commit": "32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9", + "iso_installer_package": os.environ.get("R_ISO_INSTALLER_PACKAGE") or None, + "iso_first_boot_package": os.environ.get("R_ISO_FIRST_BOOT_PACKAGE") or None, }, "oracles": { "vendor_iso_digest": os.environ.get("R_ISO_SHA") == "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", @@ -95,6 +116,8 @@ payload = { "guest": os.environ.get("R_GUEST_DIAGNOSTICS") or None, "first_boot_witness_observed": os.environ.get("R_FIRST_BOOT_WITNESS_OBSERVED") == "true", "first_boot_witness": os.environ.get("R_FIRST_BOOT_WITNESS") or None, + "installed_disk_preboot": load_json_env("R_DISK_PREBOOT"), + "installed_disk_postboot": load_json_env("R_DISK_POSTBOOT"), }, "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ @@ -113,9 +136,13 @@ fail_evidence() { exit 0 } -for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do +for cmd in curl sha256sum qemu-img qemu-system-x86_64 qemu-nbd xorriso python3 lsblk blkid mount umount readlink modprobe udevadm; do command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" done +if ! command -v pvs >/dev/null 2>&1 || ! command -v lvs >/dev/null 2>&1 || ! command -v lvchange >/dev/null 2>&1; then + sudo -n env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends lvm2 >/dev/null 2>&1 || + fail_evidence ENVIRONMENT_FAILURE preflight "could not install lvm2 diagnostic dependency" +fi [[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || fail_evidence ENVIRONMENT_FAILURE preflight "requires Linux x86_64" [[ -e /dev/kvm ]] || fail_evidence ENVIRONMENT_FAILURE preflight "/dev/kvm absent" sudo -n test -r /dev/kvm && sudo -n test -w /dev/kvm || fail_evidence ENVIRONMENT_FAILURE preflight "passwordless sudo KVM boundary unavailable" @@ -130,6 +157,13 @@ curl --fail --location --retry 3 --silent --show-error "$ISO_URL" -o "$ISO" || f OBSERVED_ISO_SHA="$(sha256sum "$ISO" | awk '{print $1}')" [[ "$OBSERVED_ISO_SHA" == "$ISO_SHA256" ]] || fail_evidence ORACLE_FAILURE acquire "vendor ISO digest mismatch" +ISO_INSTALLER_PACKAGE="$(xorriso -indev "$ISO" -find /proxmox/packages -name 'proxmox-installer_*.deb' -print 2>/dev/null | head -n 1 | sed 's#.*/##')" +ISO_FIRST_BOOT_PACKAGE="$(xorriso -indev "$ISO" -find /proxmox/packages -name 'proxmox-first-boot_*.deb' -print 2>/dev/null | head -n 1 | sed 's#.*/##')" +[[ "$ISO_INSTALLER_PACKAGE" == proxmox-installer_${PVE_INSTALLER_SOURCE_VERSION}_*.deb ]] || + fail_evidence ORACLE_FAILURE acquire "ISO installer package does not match pinned source version ${PVE_INSTALLER_SOURCE_VERSION}: ${ISO_INSTALLER_PACKAGE:-absent}" +[[ "$ISO_FIRST_BOOT_PACKAGE" == proxmox-first-boot_${PVE_INSTALLER_SOURCE_VERSION}_*.deb ]] || + fail_evidence ORACLE_FAILURE acquire "ISO first-boot package does not match pinned source version ${PVE_INSTALLER_SOURCE_VERSION}: ${ISO_FIRST_BOOT_PACKAGE:-absent}" + cat >"$STATE_DIR/answer.toml" <<'EOF' [global] keyboard = "en-us" @@ -155,10 +189,14 @@ printf 'mode = "iso"\n' >"$STATE_DIR/auto-installer-mode.toml" cat >"$STATE_DIR/proxmox-first-boot" <<'EOF' #!/bin/sh set +e +RDTE_FIRST_BOOT_LOG="/var/lib/proxmox-first-boot/rdte-first-boot.log" +exec >"$RDTE_FIRST_BOOT_LOG" 2>&1 +echo "PVE_RDTE_WITNESS_BEGIN" if [ -c /dev/ttyS0 ]; then - exec >/dev/ttyS0 2>&1 + echo "TTY_S0=character-device" +else + echo "TTY_S0=absent-or-not-character-device" fi -echo "PVE_RDTE_WITNESS_BEGIN" printf 'ORDERING_ARG=%s\n' "${1:-unset}" printf 'HOSTNAME='; hostname -f 2>/dev/null || hostname 2>/dev/null || true printf 'KERNEL='; uname -a 2>/dev/null || true @@ -212,6 +250,11 @@ echo "PVECLUSTER_JOURNAL_END" if [ -e /dev/kvm ]; then echo "KVM_DEVICE=present"; else echo "KVM_DEVICE=absent"; fi if grep -Eq '(vmx|svm)' /proc/cpuinfo 2>/dev/null; then echo "CPU_VIRT_FLAG=present"; else echo "CPU_VIRT_FLAG=absent"; fi echo "PVE_RDTE_WITNESS_END" +sync || true +if [ -c /dev/ttyS0 ]; then + cat "$RDTE_FIRST_BOOT_LOG" >/dev/ttyS0 2>&1 || true +fi +exit 0 EOF chmod 0755 "$STATE_DIR/proxmox-first-boot" @@ -334,6 +377,125 @@ stop_qemu() { fi } +inspect_installed_disk() { + local phase="$1" + local nbd="" nbd_name="" pv="" vg="" rootdev="" mnt="$STATE_DIR/mnt-$phase" + local hook_present=false hook_exec=false hook_sha="" hook_matches=false pending=false + local unit_present=false alias_target="" wants_target="" log_present=false log_text="" package_version="" + + emit_inspection_error() { + R_PHASE="$phase" R_ERROR="$1" python3 - <<'PY' +import json, os +print(json.dumps({ + "phase": os.environ["R_PHASE"], + "inspection_ok": False, + "error": os.environ["R_ERROR"], +}, sort_keys=True)) +PY + } + + sudo -n modprobe nbd max_part=16 >/dev/null 2>&1 || { emit_inspection_error "could not load nbd"; return 0; } + for candidate in /dev/nbd{0..15}; do + [[ -b "$candidate" ]] || continue + nbd_name="${candidate#/dev/}" + if [[ ! -s "/sys/block/$nbd_name/pid" ]]; then + nbd="$candidate" + break + fi + done + [[ -n "$nbd" ]] || { emit_inspection_error "no free nbd device"; return 0; } + + sudo -n qemu-nbd --connect="$nbd" --read-only "$STATE_DIR/system.qcow2" >/dev/null 2>&1 || + { emit_inspection_error "qemu-nbd connect failed"; return 0; } + sudo -n udevadm settle >/dev/null 2>&1 || true + sleep 1 + + while read -r dev type; do + [[ "$type" == "part" ]] || continue + if [[ "$(sudo -n blkid -p -s TYPE -o value "$dev" 2>/dev/null || true)" == "LVM2_member" ]]; then + pv="$dev" + break + fi + done < <(lsblk -lnpo NAME,TYPE "$nbd" 2>/dev/null) + + if [[ -z "$pv" ]]; then + sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true + emit_inspection_error "installed LVM PV not found" + return 0 + fi + + sudo -n pvscan --cache "$pv" >/dev/null 2>&1 || true + vg="$(sudo -n pvs --noheadings -o vg_name "$pv" 2>/dev/null | xargs || true)" + if [[ -z "$vg" ]]; then + sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true + emit_inspection_error "installed VG not found" + return 0 + fi + + rootdev="$(sudo -n lvs --noheadings -o lv_path,lv_name "$vg" 2>/dev/null | awk '$2=="root" {print $1; exit}')" + if [[ -z "$rootdev" ]] || ! sudo -n lvchange -ay "$rootdev" >/dev/null 2>&1; then + sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true + emit_inspection_error "installed root LV not activatable" + return 0 + fi + + mkdir -p "$mnt" + if ! sudo -n mount -o ro,noload "$rootdev" "$mnt" >/dev/null 2>&1; then + sudo -n lvchange -an "$rootdev" >/dev/null 2>&1 || true + sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true + emit_inspection_error "installed root LV mount failed" + return 0 + fi + + local hook="$mnt/var/lib/proxmox-first-boot/proxmox-first-boot" + local pending_path="$mnt/var/lib/proxmox-first-boot/pending-first-boot-setup" + local unit="$mnt/lib/systemd/system/proxmox-first-boot-network-online.service" + local alias="$mnt/etc/systemd/system/proxmox-first-boot.service" + local wants="$mnt/etc/systemd/system/multi-user.target.wants/proxmox-first-boot-network-online.service" + local log="$mnt/var/lib/proxmox-first-boot/rdte-first-boot.log" + + if sudo -n test -f "$hook"; then + hook_present=true + sudo -n test -x "$hook" && hook_exec=true + hook_sha="$(sudo -n sha256sum "$hook" 2>/dev/null | awk '{print $1}')" + [[ "$hook_sha" == "$(sha256sum "$STATE_DIR/proxmox-first-boot" | awk '{print $1}')" ]] && hook_matches=true + fi + sudo -n test -e "$pending_path" && pending=true + sudo -n test -f "$unit" && unit_present=true + alias_target="$(sudo -n readlink "$alias" 2>/dev/null || true)" + wants_target="$(sudo -n readlink "$wants" 2>/dev/null || true)" + if sudo -n test -f "$log"; then + log_present=true + log_text="$(sudo -n tail -c 16000 "$log" 2>/dev/null || true)" + fi + package_version="$(sudo -n sed -n '/^Package: proxmox-first-boot$/,/^$/p' "$mnt/var/lib/dpkg/status" 2>/dev/null | sed -n 's/^Version: //p' | head -n 1)" + + sudo -n umount "$mnt" >/dev/null 2>&1 || true + sudo -n lvchange -an "$rootdev" >/dev/null 2>&1 || true + sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true + + R_PHASE="$phase" R_ROOT="$rootdev" R_HOOK_PRESENT="$hook_present" R_HOOK_EXEC="$hook_exec" R_HOOK_SHA="$hook_sha" R_HOOK_MATCH="$hook_matches" R_PENDING="$pending" R_UNIT="$unit_present" R_ALIAS="$alias_target" R_WANTS="$wants_target" R_LOG_PRESENT="$log_present" R_LOG="$log_text" R_PACKAGE_VERSION="$package_version" python3 - <<'PY' +import json, os +def b(name): return os.environ.get(name, "false").lower() == "true" +print(json.dumps({ + "phase": os.environ["R_PHASE"], + "inspection_ok": True, + "root_lv": os.environ.get("R_ROOT") or None, + "first_boot_package_version": os.environ.get("R_PACKAGE_VERSION") or None, + "first_boot_hook_present": b("R_HOOK_PRESENT"), + "first_boot_hook_executable": b("R_HOOK_EXEC"), + "first_boot_hook_sha256": os.environ.get("R_HOOK_SHA") or None, + "hook_matches_prepared_iso": b("R_HOOK_MATCH"), + "pending_flag_present": b("R_PENDING"), + "network_online_unit_present": b("R_UNIT"), + "alias_target": os.environ.get("R_ALIAS") or None, + "wanted_by_target": os.environ.get("R_WANTS") or None, + "first_boot_log_present": b("R_LOG_PRESENT"), + "first_boot_log": os.environ.get("R_LOG") or None, +}, sort_keys=True)) +PY +} + start_qemu yes INSTALL_OK=false for _ in $(seq 1 360); do @@ -343,6 +505,7 @@ for _ in $(seq 1 360); do done [[ "$INSTALL_OK" == true ]] || fail_evidence ORACLE_FAILURE install "unattended installer did not reach completion marker within bounded window" stop_qemu +INSTALLED_DISK_PREBOOT="$(inspect_installed_disk preboot)" start_qemu no API_VERSION_JSON="" @@ -377,6 +540,8 @@ if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then root@127.0.0.1 'true' >/dev/null 2>&1; then GUEST_DIAGNOSTICS="$(guest_diagnostics || true)" fi + stop_qemu + INSTALLED_DISK_POSTBOOT="$(inspect_installed_disk postboot)" fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer (qemu_alive=$QEMU_ALIVE_AT_API_GATE first_boot_witness=$FIRST_BOOT_WITNESS_OBSERVED)" fi diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 81d0b48e6..bd88cbe53 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -65,6 +65,10 @@ def test_proxmox_is_pinned_and_uses_vendor_auto_install_contract(self): self.assertIn('filesystem = "ext4"', text) self.assertIn('disk-list = ["sda"]', text) self.assertIn("/api2/json/version", text) + self.assertIn('PVE_INSTALLER_SOURCE_VERSION="9.2.5"', text) + self.assertIn('PVE_INSTALLER_SOURCE_COMMIT="32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9"', text) + self.assertIn("proxmox-installer_*.deb", text) + self.assertIn("proxmox-first-boot_*.deb", text) self.assertNotIn("xdotool", text) def test_proxmox_first_boot_witness_uses_vendor_hook(self): @@ -80,6 +84,8 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("PVEPROXY_JOURNAL_BEGIN", text) self.assertIn("PVE_RDTE_WITNESS_BEGIN", text) self.assertIn("PVE_RDTE_WITNESS_END", text) + self.assertIn("rdte-first-boot.log", text) + self.assertIn("TTY_S0=character-device", text) self.assertIn("first_boot_witness_observed", text) def test_proxmox_hostfwd_is_diagnostic_not_guest_or_nested_oracle(self): @@ -88,6 +94,12 @@ def test_proxmox_hostfwd_is_diagnostic_not_guest_or_nested_oracle(self): self.assertIn('"ssh_hostfwd_accepted"', text) self.assertIn('"api_hostfwd_accepted"', text) self.assertIn("first_boot_witness_observed", text) + self.assertIn("inspect_installed_disk", text) + self.assertIn("qemu-nbd", text) + self.assertIn("first_boot_package_version", text) + self.assertIn("hook_matches_prepared_iso", text) + self.assertIn("INSTALLED_DISK_PREBOOT", text) + self.assertIn("INSTALLED_DISK_POSTBOOT", text) def test_truenas_is_pinned_and_digest_sidecar_is_required(self): text = TRUENAS.read_text(encoding="utf-8") From 48344bd6c1b120350585a45d49a0cd23f0b55f2f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:01:23 +0200 Subject: [PATCH 055/719] fix: use xorriso find result action --- scripts/gha_kvm_proxmox_rdte.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index cbd59d8f2..d7fa1c467 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -157,8 +157,12 @@ curl --fail --location --retry 3 --silent --show-error "$ISO_URL" -o "$ISO" || f OBSERVED_ISO_SHA="$(sha256sum "$ISO" | awk '{print $1}')" [[ "$OBSERVED_ISO_SHA" == "$ISO_SHA256" ]] || fail_evidence ORACLE_FAILURE acquire "vendor ISO digest mismatch" -ISO_INSTALLER_PACKAGE="$(xorriso -indev "$ISO" -find /proxmox/packages -name 'proxmox-installer_*.deb' -print 2>/dev/null | head -n 1 | sed 's#.*/##')" -ISO_FIRST_BOOT_PACKAGE="$(xorriso -indev "$ISO" -find /proxmox/packages -name 'proxmox-first-boot_*.deb' -print 2>/dev/null | head -n 1 | sed 's#.*/##')" +ISO_INSTALLER_PATHS="$(xorriso -indev "$ISO" -find /proxmox/packages -type f -name 'proxmox-installer_*.deb' -- 2>/dev/null)" || + fail_evidence HARNESS_FAILURE acquire "could not inspect installer package identity in vendor ISO" +ISO_FIRST_BOOT_PATHS="$(xorriso -indev "$ISO" -find /proxmox/packages -type f -name 'proxmox-first-boot_*.deb' -- 2>/dev/null)" || + fail_evidence HARNESS_FAILURE acquire "could not inspect first-boot package identity in vendor ISO" +ISO_INSTALLER_PACKAGE="$(basename "$(printf '%s\n' "$ISO_INSTALLER_PATHS" | sed -n '1p')")" +ISO_FIRST_BOOT_PACKAGE="$(basename "$(printf '%s\n' "$ISO_FIRST_BOOT_PATHS" | sed -n '1p')")" [[ "$ISO_INSTALLER_PACKAGE" == proxmox-installer_${PVE_INSTALLER_SOURCE_VERSION}_*.deb ]] || fail_evidence ORACLE_FAILURE acquire "ISO installer package does not match pinned source version ${PVE_INSTALLER_SOURCE_VERSION}: ${ISO_INSTALLER_PACKAGE:-absent}" [[ "$ISO_FIRST_BOOT_PACKAGE" == proxmox-first-boot_${PVE_INSTALLER_SOURCE_VERSION}_*.deb ]] || From d6be76a2882b94777005dc4aab1f6530c4c0cded Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:04:05 +0200 Subject: [PATCH 056/719] fix: bind Proxmox source to first-boot package --- scripts/gha_kvm_proxmox_rdte.sh | 12 +++--------- tests/test_gha_kvm_system_rdte.py | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index d7fa1c467..5dcc3a477 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -44,7 +44,6 @@ QEMU_ALIVE_AT_API_GATE="unknown" GUEST_DIAGNOSTICS="" FIRST_BOOT_WITNESS="" FIRST_BOOT_WITNESS_OBSERVED="false" -ISO_INSTALLER_PACKAGE="" ISO_FIRST_BOOT_PACKAGE="" INSTALLED_DISK_PREBOOT="" INSTALLED_DISK_POSTBOOT="" @@ -70,7 +69,7 @@ write_receipt() { export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" export R_FIRST_BOOT_WITNESS="$FIRST_BOOT_WITNESS" R_FIRST_BOOT_WITNESS_OBSERVED="$FIRST_BOOT_WITNESS_OBSERVED" - export R_ISO_INSTALLER_PACKAGE="$ISO_INSTALLER_PACKAGE" R_ISO_FIRST_BOOT_PACKAGE="$ISO_FIRST_BOOT_PACKAGE" + export R_ISO_FIRST_BOOT_PACKAGE="$ISO_FIRST_BOOT_PACKAGE" export R_DISK_PREBOOT="$INSTALLED_DISK_PREBOOT" R_DISK_POSTBOOT="$INSTALLED_DISK_POSTBOOT" python3 - <<'PY' import json, os, pathlib @@ -98,7 +97,6 @@ payload = { "observed_sha256": os.environ.get("R_ISO_SHA") or None, "installer_source_version": "9.2.5", "installer_source_commit": "32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9", - "iso_installer_package": os.environ.get("R_ISO_INSTALLER_PACKAGE") or None, "iso_first_boot_package": os.environ.get("R_ISO_FIRST_BOOT_PACKAGE") or None, }, "oracles": { @@ -157,14 +155,10 @@ curl --fail --location --retry 3 --silent --show-error "$ISO_URL" -o "$ISO" || f OBSERVED_ISO_SHA="$(sha256sum "$ISO" | awk '{print $1}')" [[ "$OBSERVED_ISO_SHA" == "$ISO_SHA256" ]] || fail_evidence ORACLE_FAILURE acquire "vendor ISO digest mismatch" -ISO_INSTALLER_PATHS="$(xorriso -indev "$ISO" -find /proxmox/packages -type f -name 'proxmox-installer_*.deb' -- 2>/dev/null)" || - fail_evidence HARNESS_FAILURE acquire "could not inspect installer package identity in vendor ISO" ISO_FIRST_BOOT_PATHS="$(xorriso -indev "$ISO" -find /proxmox/packages -type f -name 'proxmox-first-boot_*.deb' -- 2>/dev/null)" || fail_evidence HARNESS_FAILURE acquire "could not inspect first-boot package identity in vendor ISO" -ISO_INSTALLER_PACKAGE="$(basename "$(printf '%s\n' "$ISO_INSTALLER_PATHS" | sed -n '1p')")" -ISO_FIRST_BOOT_PACKAGE="$(basename "$(printf '%s\n' "$ISO_FIRST_BOOT_PATHS" | sed -n '1p')")" -[[ "$ISO_INSTALLER_PACKAGE" == proxmox-installer_${PVE_INSTALLER_SOURCE_VERSION}_*.deb ]] || - fail_evidence ORACLE_FAILURE acquire "ISO installer package does not match pinned source version ${PVE_INSTALLER_SOURCE_VERSION}: ${ISO_INSTALLER_PACKAGE:-absent}" +ISO_FIRST_BOOT_PATH="$(printf '%s\n' "$ISO_FIRST_BOOT_PATHS" | sed -n '1p' | tr -d "'")" +ISO_FIRST_BOOT_PACKAGE="$(basename "$ISO_FIRST_BOOT_PATH")" [[ "$ISO_FIRST_BOOT_PACKAGE" == proxmox-first-boot_${PVE_INSTALLER_SOURCE_VERSION}_*.deb ]] || fail_evidence ORACLE_FAILURE acquire "ISO first-boot package does not match pinned source version ${PVE_INSTALLER_SOURCE_VERSION}: ${ISO_FIRST_BOOT_PACKAGE:-absent}" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index bd88cbe53..3ede599f1 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -67,8 +67,8 @@ def test_proxmox_is_pinned_and_uses_vendor_auto_install_contract(self): self.assertIn("/api2/json/version", text) self.assertIn('PVE_INSTALLER_SOURCE_VERSION="9.2.5"', text) self.assertIn('PVE_INSTALLER_SOURCE_COMMIT="32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9"', text) - self.assertIn("proxmox-installer_*.deb", text) self.assertIn("proxmox-first-boot_*.deb", text) + self.assertIn("ISO_FIRST_BOOT_PACKAGE", text) self.assertNotIn("xdotool", text) def test_proxmox_first_boot_witness_uses_vendor_hook(self): From a7a64e5d82093618fe8c2c46ad1faad71599d176 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:44:04 +0200 Subject: [PATCH 057/719] diag: rescan Proxmox NBD partitions before offline inspect --- scripts/gha_kvm_proxmox_rdte.sh | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 5dcc3a477..5b9f8e8fd 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -10,7 +10,7 @@ RAM_MIB=4096 VCPUS=2 DISK_SIZE="40G" MIN_HOST_MEM_KIB=$((6 * 1024 * 1024)) -MIN_HOST_FREE_KIB=$((24 * 1024 * 1024)) +MIN_HOST_FREE_KIB=$((24 * 1024 * 1024))\nROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" usage() { echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR]" @@ -134,7 +134,7 @@ fail_evidence() { exit 0 } -for cmd in curl sha256sum qemu-img qemu-system-x86_64 qemu-nbd xorriso python3 lsblk blkid mount umount readlink modprobe udevadm; do +for cmd in curl sha256sum qemu-img qemu-system-x86_64 qemu-nbd xorriso python3 lsblk blkid mount umount readlink modprobe udevadm blockdev partx; do command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" done if ! command -v pvs >/dev/null 2>&1 || ! command -v lvs >/dev/null 2>&1 || ! command -v lvchange >/dev/null 2>&1; then @@ -169,7 +169,7 @@ country = "us" fqdn = "pve-rdte.example.invalid" mailto = "rdte@example.invalid" timezone = "Etc/UTC" -root-password = "rdte-proxmox-ephemeral-9264" +root-password = "$ROOT_PASSWORD" [first-boot] source = "from-iso" @@ -325,7 +325,7 @@ PY guest_diagnostics() { command -v sshpass >/dev/null 2>&1 || return 0 - sshpass -p 'rdte-proxmox-ephemeral-9264' ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 root@127.0.0.1 ' + sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 root@127.0.0.1 ' echo "===PVEVERSION==="; pveversion -v 2>&1 || true; echo "===SYSTEMD_FAILED==="; @@ -382,12 +382,21 @@ inspect_installed_disk() { local unit_present=false alias_target="" wants_target="" log_present=false log_text="" package_version="" emit_inspection_error() { - R_PHASE="$phase" R_ERROR="$1" python3 - <<'PY' + local layout="" blkids="" nbd_max_part="" + if [[ -n "$nbd" && -b "$nbd" ]]; then + layout="$(sudo -n lsblk -lnpo NAME,TYPE,FSTYPE,PTTYPE,PARTTYPE,PARTLABEL,SIZE "$nbd" 2>&1 || true)" + blkids="$(sudo -n blkid 2>&1 | grep -F "$nbd" || true)" + fi + [[ -r /sys/module/nbd/parameters/max_part ]] && nbd_max_part="$(cat /sys/module/nbd/parameters/max_part 2>/dev/null || true)" + R_PHASE="$phase" R_ERROR="$1" R_LAYOUT="$layout" R_BLKIDS="$blkids" R_NBD_MAX_PART="$nbd_max_part" python3 - <<'PY' import json, os print(json.dumps({ "phase": os.environ["R_PHASE"], "inspection_ok": False, "error": os.environ["R_ERROR"], + "lsblk": os.environ.get("R_LAYOUT") or None, + "blkid": os.environ.get("R_BLKIDS") or None, + "nbd_max_part": os.environ.get("R_NBD_MAX_PART") or None, }, sort_keys=True)) PY } @@ -405,6 +414,8 @@ PY sudo -n qemu-nbd --connect="$nbd" --read-only "$STATE_DIR/system.qcow2" >/dev/null 2>&1 || { emit_inspection_error "qemu-nbd connect failed"; return 0; } + sudo -n blockdev --rereadpt "$nbd" >/dev/null 2>&1 || true + sudo -n partx -a "$nbd" >/dev/null 2>&1 || sudo -n partx -u "$nbd" >/dev/null 2>&1 || true sudo -n udevadm settle >/dev/null 2>&1 || true sleep 1 @@ -533,7 +544,7 @@ for _ in $(seq 1 120); do sleep 3 done if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then - if command -v sshpass >/dev/null 2>&1 && sshpass -p 'rdte-proxmox-ephemeral-9264' ssh -p "$SSH_PORT" \ + if command -v sshpass >/dev/null 2>&1 && sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ root@127.0.0.1 'true' >/dev/null 2>&1; then GUEST_DIAGNOSTICS="$(guest_diagnostics || true)" @@ -545,7 +556,7 @@ fi NESTED_KVM="unknown" if command -v sshpass >/dev/null 2>&1; then - if sshpass -p 'rdte-proxmox-ephemeral-9264' ssh -p "$SSH_PORT" \ + if sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ root@127.0.0.1 'test -e /dev/kvm && grep -Eq "(vmx|svm)" /proc/cpuinfo' >/dev/null 2>&1; then NESTED_KVM="yes" From 79207f2f80c442446319d3e914a2b154d5940e22 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 16:47:05 +0200 Subject: [PATCH 058/719] fix: source-align Proxmox preflight and ephemeral password --- scripts/gha_kvm_proxmox_rdte.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 5b9f8e8fd..592bc826f 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -10,7 +10,7 @@ RAM_MIB=4096 VCPUS=2 DISK_SIZE="40G" MIN_HOST_MEM_KIB=$((6 * 1024 * 1024)) -MIN_HOST_FREE_KIB=$((24 * 1024 * 1024))\nROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" +MIN_HOST_FREE_KIB=$((16 * 1024 * 1024))\nROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" usage() { echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR]" @@ -148,7 +148,7 @@ sudo -n test -r /dev/kvm && sudo -n test -w /dev/kvm || fail_evidence ENVIRONMEN MEM_AVAIL_KIB="$(awk '/MemAvailable:/ {print $2}' /proc/meminfo)" FREE_KIB="$(df -Pk "$STATE_DIR" | awk 'NR==2 {print $4}')" (( MEM_AVAIL_KIB >= MIN_HOST_MEM_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host memory headroom below 6 GiB" -(( FREE_KIB >= MIN_HOST_FREE_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host disk headroom below 24 GiB" +(( FREE_KIB >= MIN_HOST_FREE_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host disk headroom below 16 GiB (observed ${FREE_KIB} KiB)" ISO="$STATE_DIR/$ISO_NAME" curl --fail --location --retry 3 --silent --show-error "$ISO_URL" -o "$ISO" || fail_evidence ENVIRONMENT_FAILURE acquire "vendor ISO download failed" @@ -162,7 +162,7 @@ ISO_FIRST_BOOT_PACKAGE="$(basename "$ISO_FIRST_BOOT_PATH")" [[ "$ISO_FIRST_BOOT_PACKAGE" == proxmox-first-boot_${PVE_INSTALLER_SOURCE_VERSION}_*.deb ]] || fail_evidence ORACLE_FAILURE acquire "ISO first-boot package does not match pinned source version ${PVE_INSTALLER_SOURCE_VERSION}: ${ISO_FIRST_BOOT_PACKAGE:-absent}" -cat >"$STATE_DIR/answer.toml" <<'EOF' +cat >"$STATE_DIR/answer.toml" < Date: Tue, 29 Sep 2026 16:47:27 +0200 Subject: [PATCH 059/719] test: pin Proxmox ephemeral credential wiring --- tests/test_gha_kvm_system_rdte.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 3ede599f1..795ae8a1e 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -87,6 +87,10 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("rdte-first-boot.log", text) self.assertIn("TTY_S0=character-device", text) self.assertIn("first_boot_witness_observed", text) + self.assertIn('ROOT_PASSWORD="rdte-proxmox-', text) + self.assertIn('root-password = "$ROOT_PASSWORD"', text) + self.assertIn('cat >"$STATE_DIR/answer.toml" < Date: Tue, 29 Sep 2026 16:50:09 +0200 Subject: [PATCH 060/719] fix: repair Proxmox preflight assignment serialization --- scripts/gha_kvm_proxmox_rdte.sh | 3 ++- tests/test_gha_kvm_system_rdte.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 592bc826f..7be3c3831 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -10,7 +10,8 @@ RAM_MIB=4096 VCPUS=2 DISK_SIZE="40G" MIN_HOST_MEM_KIB=$((6 * 1024 * 1024)) -MIN_HOST_FREE_KIB=$((16 * 1024 * 1024))\nROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" +MIN_HOST_FREE_KIB=$((16 * 1024 * 1024)) +ROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" usage() { echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR]" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 795ae8a1e..1958f16a8 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -90,6 +90,7 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn('ROOT_PASSWORD="rdte-proxmox-', text) self.assertIn('root-password = "$ROOT_PASSWORD"', text) self.assertIn('cat >"$STATE_DIR/answer.toml" < Date: Tue, 29 Sep 2026 17:47:45 +0200 Subject: [PATCH 061/719] diag: replace Proxmox NBD inspector with read-only loop --- scripts/gha_kvm_proxmox_rdte.sh | 78 ++++++++++++++----------------- tests/test_gha_kvm_system_rdte.py | 3 ++ 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 7be3c3831..fbdca1c06 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -135,7 +135,7 @@ fail_evidence() { exit 0 } -for cmd in curl sha256sum qemu-img qemu-system-x86_64 qemu-nbd xorriso python3 lsblk blkid mount umount readlink modprobe udevadm blockdev partx; do +for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3 lsblk blkid mount umount readlink udevadm losetup mountpoint; do command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" done if ! command -v pvs >/dev/null 2>&1 || ! command -v lvs >/dev/null 2>&1 || ! command -v lvchange >/dev/null 2>&1; then @@ -378,45 +378,48 @@ stop_qemu() { inspect_installed_disk() { local phase="$1" - local nbd="" nbd_name="" pv="" vg="" rootdev="" mnt="$STATE_DIR/mnt-$phase" + local raw="$STATE_DIR/system-\${phase}.raw" + local loopdev="" pv="" vg="" rootdev="" mnt="$STATE_DIR/mnt-$phase" local hook_present=false hook_exec=false hook_sha="" hook_matches=false pending=false local unit_present=false alias_target="" wants_target="" log_present=false log_text="" package_version="" + cleanup_inspection() { + set +e + if mountpoint -q "$mnt" 2>/dev/null; then sudo -n umount "$mnt" >/dev/null 2>&1 || true; fi + if [[ -n "$rootdev" ]]; then sudo -n lvchange -an "$rootdev" >/dev/null 2>&1 || true; fi + if [[ -n "$loopdev" ]]; then sudo -n losetup -d "$loopdev" >/dev/null 2>&1 || true; fi + rm -f -- "$raw" + set -e + } + emit_inspection_error() { - local layout="" blkids="" nbd_max_part="" - if [[ -n "$nbd" && -b "$nbd" ]]; then - layout="$(sudo -n lsblk -lnpo NAME,TYPE,FSTYPE,PTTYPE,PARTTYPE,PARTLABEL,SIZE "$nbd" 2>&1 || true)" - blkids="$(sudo -n blkid 2>&1 | grep -F "$nbd" || true)" + local layout="" blkids="" image_info="" + image_info="$(qemu-img info --output=json "$STATE_DIR/system.qcow2" 2>&1 || true)" + if [[ -n "$loopdev" && -b "$loopdev" ]]; then + layout="$(sudo -n lsblk -lnpo NAME,TYPE,FSTYPE,PTTYPE,PARTTYPE,PARTLABEL,SIZE "$loopdev" 2>&1 || true)" + blkids="$(sudo -n blkid 2>&1 | grep -F "$loopdev" || true)" fi - [[ -r /sys/module/nbd/parameters/max_part ]] && nbd_max_part="$(cat /sys/module/nbd/parameters/max_part 2>/dev/null || true)" - R_PHASE="$phase" R_ERROR="$1" R_LAYOUT="$layout" R_BLKIDS="$blkids" R_NBD_MAX_PART="$nbd_max_part" python3 - <<'PY' + R_PHASE="$phase" R_ERROR="$1" R_LAYOUT="$layout" R_BLKIDS="$blkids" R_IMAGE_INFO="$image_info" python3 - <<'PY' import json, os print(json.dumps({ "phase": os.environ["R_PHASE"], "inspection_ok": False, "error": os.environ["R_ERROR"], + "qemu_img_info": os.environ.get("R_IMAGE_INFO") or None, "lsblk": os.environ.get("R_LAYOUT") or None, "blkid": os.environ.get("R_BLKIDS") or None, - "nbd_max_part": os.environ.get("R_NBD_MAX_PART") or None, }, sort_keys=True)) PY + cleanup_inspection } - sudo -n modprobe nbd max_part=16 >/dev/null 2>&1 || { emit_inspection_error "could not load nbd"; return 0; } - for candidate in /dev/nbd{0..15}; do - [[ -b "$candidate" ]] || continue - nbd_name="${candidate#/dev/}" - if [[ ! -s "/sys/block/$nbd_name/pid" ]]; then - nbd="$candidate" - break - fi - done - [[ -n "$nbd" ]] || { emit_inspection_error "no free nbd device"; return 0; } - - sudo -n qemu-nbd --connect="$nbd" --read-only "$STATE_DIR/system.qcow2" >/dev/null 2>&1 || - { emit_inspection_error "qemu-nbd connect failed"; return 0; } - sudo -n blockdev --rereadpt "$nbd" >/dev/null 2>&1 || true - sudo -n partx -a "$nbd" >/dev/null 2>&1 || sudo -n partx -u "$nbd" >/dev/null 2>&1 || true + # Keep the product disk immutable. Convert qcow2 to a sparse raw read-only + # inspection copy, then let the kernel expose its partition table via loop. + # This avoids making Proxmox evidence depend on the qemu-nbd daemon lifecycle. + qemu-img convert -f qcow2 -O raw -S 4k "$STATE_DIR/system.qcow2" "$raw" >/dev/null 2>&1 || + { emit_inspection_error "qcow2 sparse-raw conversion failed"; return 0; } + loopdev="$(sudo -n losetup --find --show --read-only --partscan "$raw" 2>/dev/null || true)" + [[ -n "$loopdev" ]] || { emit_inspection_error "read-only loop attach failed"; return 0; } sudo -n udevadm settle >/dev/null 2>&1 || true sleep 1 @@ -426,33 +429,22 @@ PY pv="$dev" break fi - done < <(lsblk -lnpo NAME,TYPE "$nbd" 2>/dev/null) + done < <(sudo -n lsblk -lnpo NAME,TYPE "$loopdev" 2>/dev/null) - if [[ -z "$pv" ]]; then - sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true - emit_inspection_error "installed LVM PV not found" - return 0 - fi + [[ -n "$pv" ]] || { emit_inspection_error "installed LVM PV not found"; return 0; } sudo -n pvscan --cache "$pv" >/dev/null 2>&1 || true vg="$(sudo -n pvs --noheadings -o vg_name "$pv" 2>/dev/null | xargs || true)" - if [[ -z "$vg" ]]; then - sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true - emit_inspection_error "installed VG not found" - return 0 - fi + [[ -n "$vg" ]] || { emit_inspection_error "installed VG not found"; return 0; } rootdev="$(sudo -n lvs --noheadings -o lv_path,lv_name "$vg" 2>/dev/null | awk '$2=="root" {print $1; exit}')" if [[ -z "$rootdev" ]] || ! sudo -n lvchange -ay "$rootdev" >/dev/null 2>&1; then - sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true emit_inspection_error "installed root LV not activatable" return 0 fi mkdir -p "$mnt" if ! sudo -n mount -o ro,noload "$rootdev" "$mnt" >/dev/null 2>&1; then - sudo -n lvchange -an "$rootdev" >/dev/null 2>&1 || true - sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true emit_inspection_error "installed root LV mount failed" return 0 fi @@ -480,11 +472,10 @@ PY fi package_version="$(sudo -n sed -n '/^Package: proxmox-first-boot$/,/^$/p' "$mnt/var/lib/dpkg/status" 2>/dev/null | sed -n 's/^Version: //p' | head -n 1)" - sudo -n umount "$mnt" >/dev/null 2>&1 || true - sudo -n lvchange -an "$rootdev" >/dev/null 2>&1 || true - sudo -n qemu-nbd --disconnect "$nbd" >/dev/null 2>&1 || true - - R_PHASE="$phase" R_ROOT="$rootdev" R_HOOK_PRESENT="$hook_present" R_HOOK_EXEC="$hook_exec" R_HOOK_SHA="$hook_sha" R_HOOK_MATCH="$hook_matches" R_PENDING="$pending" R_UNIT="$unit_present" R_ALIAS="$alias_target" R_WANTS="$wants_target" R_LOG_PRESENT="$log_present" R_LOG="$log_text" R_PACKAGE_VERSION="$package_version" python3 - <<'PY' + R_PHASE="$phase" R_ROOT="$rootdev" R_HOOK_PRESENT="$hook_present" R_HOOK_EXEC="$hook_exec" \ + R_HOOK_SHA="$hook_sha" R_HOOK_MATCH="$hook_matches" R_PENDING="$pending" R_UNIT="$unit_present" \ + R_ALIAS="$alias_target" R_WANTS="$wants_target" R_LOG_PRESENT="$log_present" R_LOG="$log_text" \ + R_PACKAGE_VERSION="$package_version" python3 - <<'PY' import json, os def b(name): return os.environ.get(name, "false").lower() == "true" print(json.dumps({ @@ -504,6 +495,7 @@ print(json.dumps({ "first_boot_log": os.environ.get("R_LOG") or None, }, sort_keys=True)) PY + cleanup_inspection } start_qemu yes diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 1958f16a8..8fc1550c2 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -92,6 +92,9 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn('cat >"$STATE_DIR/answer.toml" < Date: Tue, 29 Sep 2026 17:49:16 +0200 Subject: [PATCH 062/719] fix: preserve Proxmox inspector shell expansion --- scripts/gha_kvm_proxmox_rdte.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index fbdca1c06..b4d84b956 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -378,7 +378,7 @@ stop_qemu() { inspect_installed_disk() { local phase="$1" - local raw="$STATE_DIR/system-\${phase}.raw" + local raw="$STATE_DIR/system-${phase}.raw" local loopdev="" pv="" vg="" rootdev="" mnt="$STATE_DIR/mnt-$phase" local hook_present=false hook_exec=false hook_sha="" hook_matches=false pending=false local unit_present=false alias_target="" wants_target="" log_present=false log_text="" package_version="" From 185c9615d3858a72b1a4db7ded8ef466369beeb4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:35:44 +0200 Subject: [PATCH 063/719] fix: require source-valid Proxmox install oracle --- scripts/gha_kvm_proxmox_rdte.sh | 35 ++++++++++++++++++++++++++++--- tests/test_gha_kvm_system_rdte.py | 5 +++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index b4d84b956..dccad5ec7 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -45,6 +45,8 @@ QEMU_ALIVE_AT_API_GATE="unknown" GUEST_DIAGNOSTICS="" FIRST_BOOT_WITNESS="" FIRST_BOOT_WITNESS_OBSERVED="false" +INSTALL_SUCCESS_MARKER_OBSERVED="false" +INSTALLED_DISK_LAYOUT_OK="false" ISO_FIRST_BOOT_PACKAGE="" INSTALLED_DISK_PREBOOT="" INSTALLED_DISK_POSTBOOT="" @@ -70,6 +72,7 @@ write_receipt() { export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" export R_FIRST_BOOT_WITNESS="$FIRST_BOOT_WITNESS" R_FIRST_BOOT_WITNESS_OBSERVED="$FIRST_BOOT_WITNESS_OBSERVED" + export R_INSTALL_SUCCESS_MARKER_OBSERVED="$INSTALL_SUCCESS_MARKER_OBSERVED" R_INSTALLED_DISK_LAYOUT_OK="$INSTALLED_DISK_LAYOUT_OK" export R_ISO_FIRST_BOOT_PACKAGE="$ISO_FIRST_BOOT_PACKAGE" export R_DISK_PREBOOT="$INSTALLED_DISK_PREBOOT" R_DISK_POSTBOOT="$INSTALLED_DISK_POSTBOOT" python3 - <<'PY' @@ -102,7 +105,8 @@ payload = { }, "oracles": { "vendor_iso_digest": os.environ.get("R_ISO_SHA") == "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", - "unattended_install_completed": os.environ["R_PHASE"] in {"installed-api", "complete"}, + "unattended_install_completed": os.environ.get("R_INSTALL_SUCCESS_MARKER_OBSERVED") == "true", + "installed_disk_layout": os.environ.get("R_INSTALLED_DISK_LAYOUT_OK") == "true", "installed_https_api": bool(os.environ.get("R_API")), "nested_kvm_observed_via_ssh": os.environ.get("R_NESTED") == "yes", }, @@ -501,13 +505,38 @@ PY start_qemu yes INSTALL_OK=false for _ in $(seq 1 360); do - if grep -Eqi 'Installation done|installation finished|powering off|rebooting' "$STATE_DIR/serial.log" 2>/dev/null; then INSTALL_OK=true; break; fi + # Exact pve-installer 9.2.5 source emits this only after run_installation() + # returns Ok. Do not accept the earlier "Rebooting system after successful + # installation" setup log, which is emitted before installation starts. + if grep -Fq 'Installation done.' "$STATE_DIR/serial.log" 2>/dev/null; then + INSTALL_OK=true + INSTALL_SUCCESS_MARKER_OBSERVED="true" + break + fi + if grep -Fq 'Installation failed:' "$STATE_DIR/serial.log" 2>/dev/null; then break; fi if ! sudo -n kill -0 "$QEMU_PID" >/dev/null 2>&1; then break; fi sleep 5 done -[[ "$INSTALL_OK" == true ]] || fail_evidence ORACLE_FAILURE install "unattended installer did not reach completion marker within bounded window" +[[ "$INSTALL_OK" == true ]] || fail_evidence ORACLE_FAILURE install "exact post-run_installation success witness not observed within bounded window" stop_qemu INSTALLED_DISK_PREBOOT="$(inspect_installed_disk preboot)" +if python3 - "$INSTALLED_DISK_PREBOOT" <<'PY' +import json, sys +try: + payload = json.loads(sys.argv[1]) +except Exception: + raise SystemExit(1) +ok = ( + payload.get("inspection_ok") is True + and payload.get("root_lv") == "/dev/pve/root" +) +raise SystemExit(0 if ok else 1) +PY +then + INSTALLED_DISK_LAYOUT_OK="true" +else + fail_evidence ORACLE_FAILURE installed-disk "exact installer success marker observed but installed /dev/pve/root layout was not proven" +fi start_qemu no API_VERSION_JSON="" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 8fc1550c2..718124324 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -92,6 +92,11 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn('cat >"$STATE_DIR/answer.toml" < Date: Tue, 29 Sep 2026 22:39:10 +0200 Subject: [PATCH 064/719] ci: run one-shot corrected Proxmox P1/P2 rep --- .../workflows/proxmox-p1-corrective-rdte.yml | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/proxmox-p1-corrective-rdte.yml diff --git a/.github/workflows/proxmox-p1-corrective-rdte.yml b/.github/workflows/proxmox-p1-corrective-rdte.yml new file mode 100644 index 000000000..8accb57f3 --- /dev/null +++ b/.github/workflows/proxmox-p1-corrective-rdte.yml @@ -0,0 +1,76 @@ +name: Proxmox P1 corrective RDTE + +on: + pull_request: + branches: + - main + paths: + - .github/workflows/proxmox-p1-corrective-rdte.yml + +permissions: + contents: read + +concurrency: + group: proxmox-p1-corrective-rdte + cancel-in-progress: true + +jobs: + contract: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Validate current RDTE contract + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py + + proxmox-9-2: + needs: contract + if: needs.contract.result == 'success' + runs-on: ubuntu-26.04 + timeout-minutes: 35 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Ensure bounded QEMU/KVM prerequisites + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-system-x86 qemu-utils xorriso sshpass + - name: Exercise corrected Proxmox P1/P2 target + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/proxmox-p1-corrective" + bash scripts/gha_kvm_proxmox_rdte.sh \ + --state-dir "$RUNNER_TEMP/proxmox-p1-corrective/state" \ + --out "$RUNNER_TEMP/proxmox-p1-corrective/proxmox-receipt.json" + python3 -m json.tool "$RUNNER_TEMP/proxmox-p1-corrective/proxmox-receipt.json" + - name: Preserve classified evidence + if: always() + run: | + set -euo pipefail + receipt="$RUNNER_TEMP/proxmox-p1-corrective/proxmox-receipt.json" + if [ ! -f "$receipt" ]; then + python3 - "$receipt" <<'PY' + import json, pathlib, sys + pathlib.Path(sys.argv[1]).write_text(json.dumps({ + "contract":"gha-kvm-system-lab/v1", + "target":{"product":"proxmox-ve","version":"9.2-1"}, + "classification":"HARNESS_FAILURE", + "oracleSatisfied":False, + "phase":"workflow-fallback", + "detail":"corrective adapter exited before emitting its normal receipt" + }, indent=2, sort_keys=True)+"\n") + PY + fi + - name: Upload Proxmox evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: gha-kvm-proxmox-p1-corrective-${{ github.run_attempt }} + path: ${{ runner.temp }}/proxmox-p1-corrective/proxmox-receipt.json + if-no-files-found: error + retention-days: 7 From 2691e85fec74dbd71a1926068eb8277a34d725c3 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:39:46 +0200 Subject: [PATCH 065/719] ci: supersede redundant Proxmox corrective rep --- .../workflows/proxmox-p1-corrective-rdte.yml | 60 +------------------ 1 file changed, 3 insertions(+), 57 deletions(-) diff --git a/.github/workflows/proxmox-p1-corrective-rdte.yml b/.github/workflows/proxmox-p1-corrective-rdte.yml index 8accb57f3..ef0f01ada 100644 --- a/.github/workflows/proxmox-p1-corrective-rdte.yml +++ b/.github/workflows/proxmox-p1-corrective-rdte.yml @@ -15,62 +15,8 @@ concurrency: cancel-in-progress: true jobs: - contract: + supersede: runs-on: ubuntu-24.04 - timeout-minutes: 5 + timeout-minutes: 2 steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - persist-credentials: false - - name: Validate current RDTE contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py - - proxmox-9-2: - needs: contract - if: needs.contract.result == 'success' - runs-on: ubuntu-26.04 - timeout-minutes: 35 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - persist-credentials: false - - name: Ensure bounded QEMU/KVM prerequisites - run: | - set -euo pipefail - sudo apt-get update -qq - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - qemu-system-x86 qemu-utils xorriso sshpass - - name: Exercise corrected Proxmox P1/P2 target - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/proxmox-p1-corrective" - bash scripts/gha_kvm_proxmox_rdte.sh \ - --state-dir "$RUNNER_TEMP/proxmox-p1-corrective/state" \ - --out "$RUNNER_TEMP/proxmox-p1-corrective/proxmox-receipt.json" - python3 -m json.tool "$RUNNER_TEMP/proxmox-p1-corrective/proxmox-receipt.json" - - name: Preserve classified evidence - if: always() - run: | - set -euo pipefail - receipt="$RUNNER_TEMP/proxmox-p1-corrective/proxmox-receipt.json" - if [ ! -f "$receipt" ]; then - python3 - "$receipt" <<'PY' - import json, pathlib, sys - pathlib.Path(sys.argv[1]).write_text(json.dumps({ - "contract":"gha-kvm-system-lab/v1", - "target":{"product":"proxmox-ve","version":"9.2-1"}, - "classification":"HARNESS_FAILURE", - "oracleSatisfied":False, - "phase":"workflow-fallback", - "detail":"corrective adapter exited before emitting its normal receipt" - }, indent=2, sort_keys=True)+"\n") - PY - fi - - name: Upload Proxmox evidence - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: gha-kvm-proxmox-p1-corrective-${{ github.run_attempt }} - path: ${{ runner.temp }}/proxmox-p1-corrective/proxmox-receipt.json - if-no-files-found: error - retention-days: 7 + - run: echo "Superseded because the authoritative main RDTE rep already completed." From 908d5f69073592ea9434ab906915334d7ee5fbb3 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:41:10 +0200 Subject: [PATCH 066/719] fix: use source-valid Proxmox UTC timezone --- scripts/gha_kvm_proxmox_rdte.sh | 2 +- tests/test_gha_kvm_system_rdte.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index dccad5ec7..c8e965bee 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -173,7 +173,7 @@ keyboard = "en-us" country = "us" fqdn = "pve-rdte.example.invalid" mailto = "rdte@example.invalid" -timezone = "Etc/UTC" +timezone = "UTC" root-password = "$ROOT_PASSWORD" [first-boot] diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 718124324..a2037bb05 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -89,6 +89,8 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("first_boot_witness_observed", text) self.assertIn('ROOT_PASSWORD="rdte-proxmox-', text) self.assertIn('root-password = "$ROOT_PASSWORD"', text) + self.assertIn('timezone = "UTC"', text) + self.assertNotIn('timezone = "Etc/UTC"', text) self.assertIn('cat >"$STATE_DIR/answer.toml" < Date: Tue, 29 Sep 2026 22:48:24 +0200 Subject: [PATCH 067/719] ci: remove completed Proxmox corrective actuator --- .../workflows/proxmox-p1-corrective-rdte.yml | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/workflows/proxmox-p1-corrective-rdte.yml diff --git a/.github/workflows/proxmox-p1-corrective-rdte.yml b/.github/workflows/proxmox-p1-corrective-rdte.yml deleted file mode 100644 index ef0f01ada..000000000 --- a/.github/workflows/proxmox-p1-corrective-rdte.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Proxmox P1 corrective RDTE - -on: - pull_request: - branches: - - main - paths: - - .github/workflows/proxmox-p1-corrective-rdte.yml - -permissions: - contents: read - -concurrency: - group: proxmox-p1-corrective-rdte - cancel-in-progress: true - -jobs: - supersede: - runs-on: ubuntu-24.04 - timeout-minutes: 2 - steps: - - run: echo "Superseded because the authoritative main RDTE rep already completed." From d2635da07899434ebedab759eec612eaf7fc3278 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:50:54 +0200 Subject: [PATCH 068/719] diag: prove Proxmox API over guest-local HTTPS --- scripts/gha_kvm_proxmox_rdte.sh | 59 +++++++++++++++++++++++++++---- tests/test_gha_kvm_system_rdte.py | 5 +++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index c8e965bee..c50f76095 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -38,6 +38,9 @@ OUT="$(realpath -m "$OUT")" QEMU_PID="" OBSERVED_ISO_SHA="" API_VERSION_JSON="" +HOSTFWD_API_VERSION_JSON="" +SSH_TUNNEL_API_VERSION_JSON="" +API_OBSERVATION_ROUTE="" NESTED_KVM="unknown" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" @@ -69,6 +72,7 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" + export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_SSH_TUNNEL_API="$SSH_TUNNEL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" export R_FIRST_BOOT_WITNESS="$FIRST_BOOT_WITNESS" R_FIRST_BOOT_WITNESS_OBSERVED="$FIRST_BOOT_WITNESS_OBSERVED" @@ -101,6 +105,8 @@ payload = { "observed_sha256": os.environ.get("R_ISO_SHA") or None, "installer_source_version": "9.2.5", "installer_source_commit": "32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9", + "pve_manager_source_commit": "b9984c6d90a4bd80", + "pve_version_api_source": "PVE/API2.pm", "iso_first_boot_package": os.environ.get("R_ISO_FIRST_BOOT_PACKAGE") or None, }, "oracles": { @@ -116,6 +122,9 @@ payload = { "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), "ssh_hostfwd_accepted": os.environ.get("R_SSH_HOSTFWD_ACCEPTED") == "true", "api_hostfwd_accepted": os.environ.get("R_API_HOSTFWD_ACCEPTED") == "true", + "api_observation_route": os.environ.get("R_API_ROUTE") or None, + "hostfwd_https_api": load_json_env("R_HOSTFWD_API"), + "ssh_tunnel_guest_local_https_api": load_json_env("R_SSH_TUNNEL_API"), "guest": os.environ.get("R_GUEST_DIAGNOSTICS") or None, "first_boot_witness_observed": os.environ.get("R_FIRST_BOOT_WITNESS_OBSERVED") == "true", "first_boot_witness": os.environ.get("R_FIRST_BOOT_WITNESS") or None, @@ -311,6 +320,11 @@ import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() PY )" +API_TUNNEL_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" port_open() { @@ -328,6 +342,27 @@ finally: PY } +try_api_via_ssh_tunnel() { + local control="$STATE_DIR/ssh-api-control" + local response="" + rm -f -- "$control" + if ! sshpass -p "$ROOT_PASSWORD" ssh \ + -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=5 \ + -o ExitOnForwardFailure=yes \ + -M -S "$control" -fN \ + -L "127.0.0.1:$API_TUNNEL_PORT:127.0.0.1:8006" \ + root@127.0.0.1 >/dev/null 2>&1; then + return 0 + fi + response="$(curl -sk --max-time 10 "https://127.0.0.1:$API_TUNNEL_PORT/api2/json/version" 2>/dev/null || true)" + ssh -p "$SSH_PORT" -S "$control" -O exit root@127.0.0.1 >/dev/null 2>&1 || true + rm -f -- "$control" + printf '%s' "$response" +} + guest_diagnostics() { command -v sshpass >/dev/null 2>&1 || return 0 sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 root@127.0.0.1 ' @@ -540,6 +575,9 @@ fi start_qemu no API_VERSION_JSON="" +HOSTFWD_API_VERSION_JSON="" +SSH_TUNNEL_API_VERSION_JSON="" +API_OBSERVATION_ROUTE="" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" @@ -557,11 +595,20 @@ for _ in $(seq 1 120); do if port_open "$SSH_PORT"; then SSH_HOSTFWD_ACCEPTED="true"; fi if port_open "$WEB_PORT"; then API_HOSTFWD_ACCEPTED="true"; fi if [[ "$API_HOSTFWD_ACCEPTED" == "true" ]]; then - API_VERSION_JSON="$(curl -sk --max-time 3 "https://127.0.0.1:$WEB_PORT/api2/json/version" 2>/dev/null || true)" - if [[ "$API_VERSION_JSON" == *'"data"'* ]]; then break; fi + HOSTFWD_API_VERSION_JSON="$(curl -sk --max-time 5 "https://127.0.0.1:$WEB_PORT/api2/json/version" 2>/dev/null || true)" + if [[ "$HOSTFWD_API_VERSION_JSON" == *'"data"'* ]]; then + API_VERSION_JSON="$HOSTFWD_API_VERSION_JSON" + API_OBSERVATION_ROUTE="qemu-hostfwd-https" + break + fi fi - if [[ "$FIRST_BOOT_WITNESS_OBSERVED" == "true" && -z "$API_VERSION_JSON" ]]; then - break + if [[ "$SSH_HOSTFWD_ACCEPTED" == "true" ]]; then + SSH_TUNNEL_API_VERSION_JSON="$(try_api_via_ssh_tunnel || true)" + if [[ "$SSH_TUNNEL_API_VERSION_JSON" == *'"data"'* ]]; then + API_VERSION_JSON="$SSH_TUNNEL_API_VERSION_JSON" + API_OBSERVATION_ROUTE="ssh-tunnel-guest-local-https" + break + fi fi sleep 3 done @@ -573,7 +620,7 @@ if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then fi stop_qemu INSTALLED_DISK_POSTBOOT="$(inspect_installed_disk postboot)" - fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer (qemu_alive=$QEMU_ALIVE_AT_API_GATE first_boot_witness=$FIRST_BOOT_WITNESS_OBSERVED)" + fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer via direct hostfwd or SSH-tunneled guest-local HTTPS (qemu_alive=$QEMU_ALIVE_AT_API_GATE first_boot_witness=$FIRST_BOOT_WITNESS_OBSERVED)" fi NESTED_KVM="unknown" @@ -587,4 +634,4 @@ if command -v sshpass >/dev/null 2>&1; then fi fi -write_receipt SUPPORTED true complete "real Proxmox VE unattended install completed and installed HTTPS API answered" +write_receipt SUPPORTED true complete "real Proxmox VE unattended install completed and real HTTPS /api2/json/version answered via $API_OBSERVATION_ROUTE" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index a2037bb05..f09198596 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -87,6 +87,11 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("rdte-first-boot.log", text) self.assertIn("TTY_S0=character-device", text) self.assertIn("first_boot_witness_observed", text) + self.assertIn("try_api_via_ssh_tunnel", text) + self.assertIn('API_OBSERVATION_ROUTE="ssh-tunnel-guest-local-https"', text) + self.assertIn('"api_observation_route"', text) + self.assertIn('"ssh_tunnel_guest_local_https_api"', text) + self.assertIn('"pve_manager_source_commit": "b9984c6d90a4bd80"', text) self.assertIn('ROOT_PASSWORD="rdte-proxmox-', text) self.assertIn('root-password = "$ROOT_PASSWORD"', text) self.assertIn('timezone = "UTC"', text) From 2687bee71879699d387fc83cc112aed2f67b229a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:57:57 +0200 Subject: [PATCH 069/719] rdte: reconcile LiteLLM T6 workflow onto current line --- .github/workflows/gha-kvm-system-rdte.yml | 29 ++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 74751670e..09623e474 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -60,7 +60,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py) + scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -86,7 +86,16 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py + + foundry-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-litellm-t6-control.yml@4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba + with: + foundry_ref: 4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba + permissions: + contents: read qemu-t3-topology: needs: [changes, contract] @@ -169,18 +178,24 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2] + needs: [changes, contract, proxmox-9-2, foundry-t6-control] if: >- always() && needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && - (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') + (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') && + needs['foundry-t6-control'].result == 'success' runs-on: ubuntu-26.04 timeout-minutes: 50 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + - name: Download exact LiteLLM Foundry T6 control + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: litellm-truenas-t6-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -193,7 +208,9 @@ jobs: mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" bash scripts/gha_kvm_truenas_rdte.sh \ --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ - --rung t5 \ + --rung t6 \ + --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" \ + --foundry-commit "4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence @@ -206,7 +223,7 @@ jobs: import json, pathlib, sys pathlib.Path(sys.argv[1]).write_text(json.dumps({ "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T5"}, + "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T6"}, "classification":"HARNESS_FAILURE", "oracleSatisfied":False, "phase":"workflow-fallback", From cc06f7b325f54524f4b8c1f58fc3eb84297733d0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:58:01 +0200 Subject: [PATCH 070/719] test: keep current-line T6 static gate minimal --- .github/workflows/prep-rdte-static.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index fabbfabf4..0a5e62242 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -13,7 +13,7 @@ on: - 'scripts/truenas_middleware_*.py' - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' - - 'tests/test_truenas_t6_foundry_control.py' + - 'tests/test_truenas_t6_litellm_control.py' pull_request: paths: - '.github/workflows/prep-rdte-static.yml' @@ -24,7 +24,7 @@ on: - 'scripts/truenas_middleware_*.py' - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' - - 'tests/test_truenas_t6_foundry_control.py' + - 'tests/test_truenas_t6_litellm_control.py' permissions: contents: read @@ -53,7 +53,7 @@ jobs: tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py ) - if [[ -f tests/test_truenas_t6_foundry_control.py ]]; then - tests+=(tests/test_truenas_t6_foundry_control.py) + if [[ -f tests/test_truenas_t6_litellm_control.py ]]; then + tests+=(tests/test_truenas_t6_litellm_control.py) fi python3 -m unittest -v "${tests[@]}" From 15ff03061878c8e9906bf955ea3290ff7d6d0de4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:58:06 +0200 Subject: [PATCH 071/719] rdte: reconcile LiteLLM T6 harness onto current line --- scripts/gha_kvm_truenas_rdte.sh | 81 +++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index e19250011..0e6dfd1a6 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -20,23 +20,32 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3|t4|t5]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3|t4|t5|t6] [--foundry-control-dir DIR] [--foundry-commit SHA]" } OUT="" STATE_DIR="" RUNG="t0" +FOUNDRY_CONTROL_DIR="" +FOUNDRY_COMMIT="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; --state-dir) STATE_DIR="$2"; shift 2 ;; --rung) RUNG="$2"; shift 2 ;; + --foundry-control-dir) FOUNDRY_CONTROL_DIR="$2"; shift 2 ;; + --foundry-commit) FOUNDRY_COMMIT="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } -[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]] || { echo "rung must be t0, t1, t2, t3, t4, or t5" >&2; exit 2; } +[[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } +if [[ "$RUNG" == "t6" ]]; then + [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } + [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } + FOUNDRY_CONTROL_DIR="$(realpath "$FOUNDRY_CONTROL_DIR")" +fi if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -58,6 +67,7 @@ MIDDLEWARE_RESULT_JSON="" POOL_RESULT_JSON="" APP_RESULT_JSON="" LIFECYCLE_RESULT_JSON="" +FOUNDRY_RESULT_JSON="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -79,7 +89,7 @@ write_receipt() { export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" - export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" + export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" python3 - <<'PY' import json, os, pathlib payload = { @@ -93,10 +103,10 @@ payload = { "vcpus": 2, "ram_mib": 8192, "boot_disk": "24G", - "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5"} else [], - "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4", "t5"} else None, - "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5"} else [], - "app": {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} if os.environ.get("R_RUNG") in {"t4", "t5"} else None, + "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else [], + "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else None, + "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else [], + "app": ({"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} if os.environ.get("R_RUNG") in {"t4", "t5"} else None), }, "source": { "iso_name": "TrueNAS-26.0.0-BETA.3.iso", @@ -117,6 +127,7 @@ payload = { "data_pool_created": bool(os.environ.get("R_POOL_RESULT")) and json.loads(os.environ["R_POOL_RESULT"]).get("oracleSatisfied") is True, "apps_runtime_exercised": bool(os.environ.get("R_APP_RESULT")) and json.loads(os.environ["R_APP_RESULT"]).get("oracleSatisfied") is True, "app_lifecycle_exercised": bool(os.environ.get("R_LIFECYCLE_RESULT")) and json.loads(os.environ["R_LIFECYCLE_RESULT"]).get("oracleSatisfied") is True, + "foundry_materialization_exercised": bool(os.environ.get("R_FOUNDRY_RESULT")) and json.loads(os.environ["R_FOUNDRY_RESULT"]).get("oracleSatisfied") is True, }, "rpc_discovery": json.loads(os.environ["R_RPC_DISCOVERY"]) if os.environ.get("R_RPC_DISCOVERY") else None, "install_result": json.loads(os.environ["R_INSTALL_RESULT"]) if os.environ.get("R_INSTALL_RESULT") else None, @@ -124,6 +135,7 @@ payload = { "data_pool": json.loads(os.environ["R_POOL_RESULT"]) if os.environ.get("R_POOL_RESULT") else None, "apps_runtime": json.loads(os.environ["R_APP_RESULT"]) if os.environ.get("R_APP_RESULT") else None, "app_lifecycle": json.loads(os.environ["R_LIFECYCLE_RESULT"]) if os.environ.get("R_LIFECYCLE_RESULT") else None, + "foundry_materialization": json.loads(os.environ["R_FOUNDRY_RESULT"]) if os.environ.get("R_FOUNDRY_RESULT") else None, "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ @@ -133,7 +145,8 @@ payload = { "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", "T4 initializes Apps on that pool and runs one synthetic public-safe custom Compose app.", "T5 exercises stop/start, config mutation/read-back, redeploy, stop, and delete for that digest-pinned custom app.", - "Broader application lifecycle and Foundry materialization remain later gates.", + "T6 consumes one exact public TrueNAS App Foundry materialization control and verifies native Custom App realization/read-back.", + "The first T6 control remains registry-tag mutable; T6 does not claim immutable upstream container-image identity.", "This does not qualify physical storage controllers, SMART, GPU, IPMI, or HA behavior.", ], } @@ -150,23 +163,27 @@ fail_evidence() { [[ -f "$SCRIPT_DIR/truenas_installer_rpc_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer RPC probe" -if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then +if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]]; then [[ -f "$SCRIPT_DIR/truenas_installer_rpc_install.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS installer install client" [[ -f "$SCRIPT_DIR/truenas_middleware_ddp_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS middleware health client" - if [[ "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then + if [[ "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_pool_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T3/T4 pool client" fi - if [[ "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then + if [[ "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_app_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS Apps client" fi - if [[ "$RUNG" == "t5" ]]; then + if [[ "$RUNG" == "t5" || "$RUNG" == "t6" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_app_lifecycle_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T5 lifecycle client" fi + if [[ "$RUNG" == "t6" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_foundry_control_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 Foundry control client" + fi fi for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do command -v "$cmd" >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: $cmd" @@ -371,7 +388,7 @@ if [[ -n "$QEMU_PID" ]]; then fi DATA_DRIVE_ARGS=() -if [[ "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" ]]; then +if [[ "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]]; then for index in $(seq 0 $((DATA_DISK_COUNT - 1))); do data_disk="$STATE_DIR/data${index}.qcow2" qemu-img create -q -f qcow2 "$data_disk" "$DATA_DISK_SIZE" || @@ -395,6 +412,16 @@ import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() PY )" +LITELLM_HOST_PORT="" +LITELLM_HOSTFWD="" +if [[ "$RUNG" == "t6" ]]; then + LITELLM_HOST_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" + LITELLM_HOSTFWD=",hostfwd=tcp:127.0.0.1:${LITELLM_HOST_PORT}-:30401" +fi : >"$STATE_DIR/serial.log" sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ @@ -402,7 +429,7 @@ sudo -n qemu-system-x86_64 \ -device "virtio-blk-pci,drive=rdteboot,id=rdte-boot,addr=0x4,bootindex=1" \ "${DATA_DRIVE_ARGS[@]}" \ -boot strict=on \ - -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443" \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443${LITELLM_HOSTFWD}" \ -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ @@ -534,4 +561,28 @@ PY [[ "$LIFECYCLE_OK" == "true" ]] || fail_evidence ORACLE_FAILURE app-lifecycle "custom app did not complete the bounded T5 lifecycle" -write_receipt SUPPORTED true app-lifecycle "digest-pinned custom app completed stop/start, config mutation with public read-back, redeploy, stop, and delete" +if [[ "$RUNG" == "t5" ]]; then + write_receipt SUPPORTED true app-lifecycle "digest-pinned custom app completed stop/start, config mutation with public read-back, redeploy, stop, and delete" + exit 0 +fi + +FOUNDRY_OUT="$STATE_DIR/foundry-control.json" +python3 "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$LITELLM_HOST_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +[[ -f "$FOUNDRY_OUT" ]] || fail_evidence HARNESS_FAILURE foundry-materialization "T6 Foundry control client did not emit a receipt" +FOUNDRY_RESULT_JSON="$(cat "$FOUNDRY_OUT")" +FOUNDRY_OK="$(python3 - "$FOUNDRY_OUT" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +print("true" if data.get("oracleSatisfied") is True else "false") +PY +)" +[[ "$FOUNDRY_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE foundry-materialization "exact Foundry-exported LiteLLM control did not realize and verify on TrueNAS" + +write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported LiteLLM control passed S1 projection, exact image/config read-back, health, restart persistence, and delete/absence" From a88f557d89aca273664fa579cb0fc2d8eeae2f62 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:58:22 +0200 Subject: [PATCH 072/719] rdte: add current-line LiteLLM T6 probe --- .../truenas_middleware_litellm_t6_probe.py | 476 ++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 scripts/truenas_middleware_litellm_t6_probe.py diff --git a/scripts/truenas_middleware_litellm_t6_probe.py b/scripts/truenas_middleware_litellm_t6_probe.py new file mode 100644 index 000000000..22dc553dc --- /dev/null +++ b/scripts/truenas_middleware_litellm_t6_probe.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 +"""TrueNAS T6 oracle for the exact Foundry-exported LiteLLM control.""" +from __future__ import annotations + +import argparse +import base64 +import hashlib +import http.client +import json +import pathlib +import ssl +import time +import urllib.error +import urllib.request + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_FOUNDRY_REF = "4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" +EXPECTED_SCHEMA = "semper-supra.litellm-truenas-t6-control/1" +EXPECTED_APPLIANCE = ( + "ghcr.io/sempersupra/litellm-appliance@" + "sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac" +) +EXPECTED_APPLIANCE_DIGEST = ( + "sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac" +) +EXPECTED_TRUENAS_APPS_COMMIT = "4df7498747aad81b1049135539c65c383e42df95" +EXPECTED_LIBRARY_VERSION = "2.3.11" +EXPECTED_LIBRARY_HASH = "874636814efb275e5276ea9d709b7cd665fed42bb1d50328e853d9253a2e1229" +EXPECTED_APP_NAME = "rdte-t6-litellm" +EXPECTED_GUEST_PORT = 30401 +EXPECTED_CONFIG_DIR = "/mnt/rdtepool/litellm-t6/config" +EXPECTED_SECRET_DIR = "/mnt/rdtepool/litellm-t6/secrets" +EXPECTED_CONFIG_FILE = "proxy_server_config.yaml" +EXPECTED_FIXTURE_SECRET_NAMES = ["TEST_PROVIDER_KEY"] +FIXTURE_SECRET_VALUE = b"rdte-fixture-provider-token\n" + + +def canonical_sha256(value): + return hashlib.sha256( + json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + ).hexdigest() + + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def _load_json(path: pathlib.Path) -> dict: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise RuntimeError(f"{path.name} must contain a JSON object") + return value + + +def _one_service(compose: dict) -> tuple[str, dict]: + services = compose.get("services") + if not isinstance(services, dict) or len(services) != 1: + raise RuntimeError("expected exactly one rendered LiteLLM service") + name, service = next(iter(services.items())) + if not isinstance(service, dict): + raise RuntimeError("rendered LiteLLM service must be an object") + return str(name), service + + +def _one_mount(service: dict, target: str) -> dict: + matches = [ + value + for value in service.get("volumes", []) + if isinstance(value, dict) and value.get("target") == target + ] + if len(matches) != 1: + raise RuntimeError(f"expected exactly one mount for {target}") + return matches[0] + + +def load_control(directory: pathlib.Path): + control = _load_json(directory / "control.json") + compose = _load_json(directory / "compose.json") + config_bytes = (directory / EXPECTED_CONFIG_FILE).read_bytes() + + if control.get("schema") != EXPECTED_SCHEMA: + raise RuntimeError("unexpected LiteLLM T6 control schema") + if control.get("foundry_ref") != EXPECTED_FOUNDRY_REF: + raise RuntimeError("Foundry source ref drifted") + if control.get("secrets_captured") is not False: + raise RuntimeError("control bundle does not assert secrets_captured=false") + + candidate = control.get("candidate") or {} + if candidate.get("appliance_reference") != EXPECTED_APPLIANCE: + raise RuntimeError("admitted appliance reference drifted") + if candidate.get("appliance_digest") != EXPECTED_APPLIANCE_DIGEST: + raise RuntimeError("admitted appliance digest drifted") + if candidate.get("truenas_apps_commit") != EXPECTED_TRUENAS_APPS_COMMIT: + raise RuntimeError("TrueNAS Apps source commit drifted") + if candidate.get("truenas_lib_version") != EXPECTED_LIBRARY_VERSION: + raise RuntimeError("TrueNAS library version drifted") + if candidate.get("truenas_lib_hash") != EXPECTED_LIBRARY_HASH: + raise RuntimeError("TrueNAS library hash drifted") + + runtime = control.get("runtime") or {} + expected_runtime = { + "app_name": EXPECTED_APP_NAME, + "guest_port": EXPECTED_GUEST_PORT, + "config_dir": EXPECTED_CONFIG_DIR, + "secret_dir": EXPECTED_SECRET_DIR, + "config_file": EXPECTED_CONFIG_FILE, + "fixture_secret_names": EXPECTED_FIXTURE_SECRET_NAMES, + } + for key, expected in expected_runtime.items(): + if runtime.get(key) != expected: + raise RuntimeError(f"runtime {key} drifted") + + artifacts = control.get("artifacts") or {} + if artifacts.get("compose_canonical_sha256") != canonical_sha256(compose): + raise RuntimeError("rendered Compose identity does not match control") + if artifacts.get("config_sha256") != sha256_bytes(config_bytes): + raise RuntimeError("fixture config identity does not match control") + + service_name, service = _one_service(compose) + if service.get("image") != EXPECTED_APPLIANCE: + raise RuntimeError("rendered service image drifted") + config_mount = _one_mount(service, "/config") + secret_mount = _one_mount(service, "/run/secrets/semper-env") + for label, mount, source in ( + ("config", config_mount, EXPECTED_CONFIG_DIR), + ("provider-secret", secret_mount, EXPECTED_SECRET_DIR), + ): + if mount.get("source") != source or mount.get("read_only") is not True: + raise RuntimeError(f"{label} mount drifted") + if (service.get("environment") or {}).get("SEMPER_SECRET_DIR") != "/run/secrets/semper-env": + raise RuntimeError("SEMPER_SECRET_DIR drifted") + if not any( + isinstance(port, dict) + and int(port.get("target", -1)) == 4000 + and str(port.get("published")) == str(EXPECTED_GUEST_PORT) + for port in service.get("ports", []) + ): + raise RuntimeError("LiteLLM guest port mapping drifted") + + rendered = json.dumps(compose, sort_keys=True) + for forbidden in ("OPENROUTER_MANAGEMENT_KEY", "sk-or-mgmt-", ":latest", ":main-latest"): + if forbidden in rendered: + raise RuntimeError(f"forbidden rendered content: {forbidden}") + + return control, compose, config_bytes, service_name + + +def multipart_upload( + host: str, + port: int, + tls: bool, + username: str, + password: str, + remote_path: str, + content: bytes, + mode: int, + timeout: float, +) -> int: + boundary = "----semper-supra-litellm-t6-boundary" + data = json.dumps( + {"method": "filesystem.put", "params": [remote_path, {"append": False, "mode": mode}]}, + separators=(",", ":"), + ) + chunks = [ + f"--{boundary}\r\n".encode(), + b'Content-Disposition: form-data; name="data"\r\n\r\n', + data.encode(), + b"\r\n", + f"--{boundary}\r\n".encode(), + b'Content-Disposition: form-data; name="file"; filename="payload"\r\n', + b"Content-Type: application/octet-stream\r\n\r\n", + content, + b"\r\n", + f"--{boundary}--\r\n".encode(), + ] + body = b"".join(chunks) + token = base64.b64encode(f"{username}:{password}".encode()).decode() + headers = { + "Authorization": f"Basic {token}", + "Content-Type": f"multipart/form-data; boundary={boundary}", + "Content-Length": str(len(body)), + } + if tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + conn = http.client.HTTPSConnection(host, port, timeout=timeout, context=context) + else: + conn = http.client.HTTPConnection(host, port, timeout=timeout) + try: + conn.request("POST", "/_upload/", body=body, headers=headers) + response = conn.getresponse() + payload = response.read() + finally: + conn.close() + if response.status < 200 or response.status >= 300: + raise RuntimeError(f"filesystem.put upload returned HTTP {response.status}") + result = json.loads(payload.decode("utf-8")) + job_id = result.get("job_id") + if not isinstance(job_id, int): + raise RuntimeError("filesystem.put upload did not return a job_id") + return job_id + + +def health_probe(host: str, port: int, path: str, timeout: float) -> bool: + try: + with urllib.request.urlopen( + f"http://{host}:{port}{path}", + timeout=timeout, + ) as response: + return 200 <= response.status < 300 + except (urllib.error.URLError, TimeoutError, OSError): + return False + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True, help="TrueNAS middleware HTTP(S) host-forward port") + p.add_argument("--service-port", type=int, required=True, help="host-forward port for guest LiteLLM port 30401") + p.add_argument("--password-file", required=True) + p.add_argument("--control-dir", type=pathlib.Path, required=True) + p.add_argument("--foundry-commit", required=True) + p.add_argument("--out", required=True) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + p.add_argument("--state-timeout", type=float, default=240.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-litellm-foundry-t6/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected_version": EXPECTED_VERSION, + "foundry_commit": a.foundry_commit, + "app_name": EXPECTED_APP_NAME, + "appliance_reference": EXPECTED_APPLIANCE, + "fixture_secret_names": EXPECTED_FIXTURE_SECRET_NAMES, + "secret_values_captured": False, + "management_credentials_present": False, + } + ws = None + created_app = False + request_id = 1 + try: + if a.foundry_commit != EXPECTED_FOUNDRY_REF: + raise RuntimeError("requested Foundry commit does not match qualified export") + control, compose, config_bytes, service_name = load_control(a.control_dir) + compose_sha = canonical_sha256(compose) + payload["materialization"] = { + "schema": control["schema"], + "foundry_ref": control["foundry_ref"], + "compose_canonical_sha256": compose_sha, + "config_sha256": sha256_bytes(config_bytes), + "service_name": service_name, + "truenas_apps_commit": EXPECTED_TRUENAS_APPS_COMMIT, + "truenas_lib_version": EXPECTED_LIBRARY_VERSION, + "truenas_lib_hash": EXPECTED_LIBRARY_HASH, + } + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError("DDP connection failed") + + def call(method, params): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + def wait_job(job_id, label): + deadline = time.monotonic() + a.job_timeout + last = None + while time.monotonic() < deadline: + last = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last and last.get("state") == "SUCCESS": + return last + if last and last.get("state") in {"FAILED", "ABORTED"}: + raise RuntimeError(f"{label} job {last.get('state')}") + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS") + + def wait_app_state(expected): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = call("app.query", [[["id", "=", EXPECTED_APP_NAME]], {"get": True}]) + if last and last.get("state") == expected: + return last + if last and last.get("state") in {"CRASHED", "ERROR"}: + raise RuntimeError(f"LiteLLM entered {last.get('state')}") + time.sleep(1) + raise RuntimeError(f"LiteLLM did not reach {expected}") + + auth = call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError("authentication did not return SUCCESS") + payload["system_version"] = call("system.version", []) + if payload["system_version"] != EXPECTED_VERSION: + raise RuntimeError("target version drifted") + + existing = call("app.query", [[["id", "=", EXPECTED_APP_NAME]]]) + if existing: + raise RuntimeError("refusing adopted LiteLLM app state") + + fixture_dirs = [ + ("/mnt/rdtepool/litellm-t6", "750"), + (EXPECTED_CONFIG_DIR, "750"), + (EXPECTED_SECRET_DIR, "700"), + ] + created_dirs = [] + for path, mode in fixture_dirs: + result = call("filesystem.mkdir", [{ + "path": path, + "options": {"mode": mode, "raise_chmod_error": True}, + }]) + if not isinstance(result, dict) or result.get("path") != path: + raise RuntimeError(f"filesystem.mkdir did not create exact path {path}") + observed_mode = int(result.get("mode", 0)) & 0o777 + if observed_mode != int(mode, 8): + raise RuntimeError(f"filesystem.mkdir mode mismatch for {path}") + created_dirs.append({ + "path": path, + "mode": oct(observed_mode), + }) + payload["fixture_directories"] = { + "created": created_dirs, + "values_recorded": False, + } + + config_remote = f"{EXPECTED_CONFIG_DIR}/{EXPECTED_CONFIG_FILE}" + secret_remote = f"{EXPECTED_SECRET_DIR}/TEST_PROVIDER_KEY" + config_job = multipart_upload( + a.host, a.port, a.tls, "truenas_admin", password, + config_remote, config_bytes, 0o640, a.timeout, + ) + wait_job(config_job, "config filesystem.put") + secret_job = multipart_upload( + a.host, a.port, a.tls, "truenas_admin", password, + secret_remote, FIXTURE_SECRET_VALUE, 0o600, a.timeout, + ) + wait_job(secret_job, "fixture secret filesystem.put") + + config_stat = call("filesystem.stat", [config_remote]) + secret_stat = call("filesystem.stat", [secret_remote]) + payload["fixture_projection"] = { + "config_path": config_remote, + "config_sha256": sha256_bytes(config_bytes), + "config_mode": oct(int(config_stat.get("mode", 0)) & 0o777), + "secret_path": secret_remote, + "secret_names": EXPECTED_FIXTURE_SECRET_NAMES, + "secret_mode": oct(int(secret_stat.get("mode", 0)) & 0o777), + "secret_value_recorded": False, + } + if (int(secret_stat.get("mode", 0)) & 0o777) != 0o600: + raise RuntimeError("fixture S1 file mode is not 0600") + + create_id = call("app.create", [{ + "app_name": EXPECTED_APP_NAME, + "custom_app": True, + "custom_compose_config": compose, + }]) + if not isinstance(create_id, int): + raise RuntimeError("app.create did not return a job id") + wait_job(create_id, "app.create") + created_app = True + + app = wait_app_state("RUNNING") + details = (app.get("active_workloads") or {}).get("container_details") or [] + matching = [ + item for item in details + if item.get("service_name") == service_name + and item.get("image") == EXPECTED_APPLIANCE + and item.get("state") == "running" + ] + if len(matching) != 1: + raise RuntimeError("running container image/service identity mismatch") + + config_readback = call("app.config", [EXPECTED_APP_NAME]) + config_readback_sha = canonical_sha256(config_readback) + if config_readback_sha != compose_sha: + raise RuntimeError("app.config read-back does not match exact Foundry Compose") + + live = ready = False + deadline = time.monotonic() + a.state_timeout + while time.monotonic() < deadline: + live = health_probe(a.host, a.service_port, "/health/liveliness", a.timeout) + ready = health_probe(a.host, a.service_port, "/health/readiness", a.timeout) + if live and ready: + break + time.sleep(1) + if not (live and ready): + raise RuntimeError("LiteLLM health endpoints did not pass") + + stop_id = call("app.stop", [EXPECTED_APP_NAME]) + wait_job(stop_id, "app.stop") + wait_app_state("STOPPED") + start_id = call("app.start", [EXPECTED_APP_NAME]) + wait_job(start_id, "app.start") + wait_app_state("RUNNING") + + deadline = time.monotonic() + a.state_timeout + restart_live = restart_ready = False + while time.monotonic() < deadline: + restart_live = health_probe(a.host, a.service_port, "/health/liveliness", a.timeout) + restart_ready = health_probe(a.host, a.service_port, "/health/readiness", a.timeout) + if restart_live and restart_ready: + break + time.sleep(1) + if not (restart_live and restart_ready): + raise RuntimeError("LiteLLM health did not recover after stop/start") + + second_config_sha = canonical_sha256(call("app.config", [EXPECTED_APP_NAME])) + if second_config_sha != compose_sha: + raise RuntimeError("Foundry Compose identity drifted after restart") + + delete_id = call("app.delete", [EXPECTED_APP_NAME, { + "remove_images": False, + "remove_ix_volumes": False, + "force_remove_custom_app": False, + }]) + wait_job(delete_id, "app.delete") + created_app = False + remaining = call("app.query", [[["id", "=", EXPECTED_APP_NAME]]]) + if remaining: + raise RuntimeError("LiteLLM app remains after delete") + + payload["runtime"] = { + "state": "RUNNING", + "custom_app": True, + "container_image_exact": True, + "compose_readback_sha256": config_readback_sha, + "liveliness": live, + "readiness": ready, + "restart_liveliness": restart_live, + "restart_readiness": restart_ready, + "restart_compose_identity_preserved": True, + "delete_absent": True, + } + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "exact Foundry-exported LiteLLM appliance consumed on real TrueNAS software; " + "S1 fixture projection, exact image/config read-back, health, restart persistence, " + "and delete/absence all passed" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + payload["cleanup_needed"] = created_app + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 886699a19c59931939b05f8aa206fc111ee65415 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:58:25 +0200 Subject: [PATCH 073/719] test: add current-line LiteLLM T6 contract --- tests/test_truenas_t6_litellm_control.py | 134 +++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/test_truenas_t6_litellm_control.py diff --git a/tests/test_truenas_t6_litellm_control.py b/tests/test_truenas_t6_litellm_control.py new file mode 100644 index 000000000..0eacb4d43 --- /dev/null +++ b/tests/test_truenas_t6_litellm_control.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import importlib.util +import json +import pathlib +import sys +import tempfile +import unittest + +HERE = pathlib.Path(__file__).resolve().parents[1] +SCRIPTS = HERE / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) +SCRIPT = SCRIPTS / "truenas_middleware_litellm_t6_probe.py" +SPEC = importlib.util.spec_from_file_location("litellm_t6_probe", SCRIPT) +MOD = importlib.util.module_from_spec(SPEC) +assert SPEC.loader +SPEC.loader.exec_module(MOD) + + +class LiteLlmT6ContractTests(unittest.TestCase): + def make_control(self, root, *, image=None, secret_names=None): + image = image or MOD.EXPECTED_APPLIANCE + secret_names = secret_names or ["TEST_PROVIDER_KEY"] + compose = { + "services": { + "litellm": { + "image": image, + "environment": {"SEMPER_SECRET_DIR": "/run/secrets/semper-env"}, + "ports": [{"target": 4000, "published": "30401", "protocol": "tcp"}], + "volumes": [ + {"type": "bind", "source": MOD.EXPECTED_CONFIG_DIR, "target": "/config", "read_only": True}, + {"type": "bind", "source": MOD.EXPECTED_SECRET_DIR, "target": "/run/secrets/semper-env", "read_only": True}, + ], + } + } + } + config = b"model_list: []\n" + control = { + "schema": MOD.EXPECTED_SCHEMA, + "foundry_ref": MOD.EXPECTED_FOUNDRY_REF, + "candidate": { + "appliance_reference": MOD.EXPECTED_APPLIANCE, + "appliance_digest": MOD.EXPECTED_APPLIANCE_DIGEST, + "truenas_apps_commit": MOD.EXPECTED_TRUENAS_APPS_COMMIT, + "truenas_lib_version": MOD.EXPECTED_LIBRARY_VERSION, + "truenas_lib_hash": MOD.EXPECTED_LIBRARY_HASH, + }, + "runtime": { + "app_name": MOD.EXPECTED_APP_NAME, + "guest_port": MOD.EXPECTED_GUEST_PORT, + "config_dir": MOD.EXPECTED_CONFIG_DIR, + "secret_dir": MOD.EXPECTED_SECRET_DIR, + "config_file": MOD.EXPECTED_CONFIG_FILE, + "fixture_secret_names": secret_names, + }, + "artifacts": { + "compose_canonical_sha256": MOD.canonical_sha256(compose), + "config_sha256": MOD.sha256_bytes(config), + }, + "secrets_captured": False, + } + root = pathlib.Path(root) + (root / "control.json").write_text(json.dumps(control), encoding="utf-8") + (root / "compose.json").write_text(json.dumps(compose), encoding="utf-8") + (root / MOD.EXPECTED_CONFIG_FILE).write_bytes(config) + return compose + + def test_loads_exact_litellm_control(self): + with tempfile.TemporaryDirectory() as td: + expected = self.make_control(td) + control, compose, config, service = MOD.load_control(pathlib.Path(td)) + self.assertEqual(compose, expected) + self.assertEqual(service, "litellm") + self.assertEqual(control["foundry_ref"], MOD.EXPECTED_FOUNDRY_REF) + self.assertEqual(config, b"model_list: []\n") + + def test_rejects_appliance_drift(self): + with tempfile.TemporaryDirectory() as td: + self.make_control(td, image="example.invalid/litellm:latest") + with self.assertRaises(RuntimeError): + MOD.load_control(pathlib.Path(td)) + + def test_rejects_secret_name_broadening(self): + with tempfile.TemporaryDirectory() as td: + self.make_control(td, secret_names=["TEST_PROVIDER_KEY", "OPENROUTER_MANAGEMENT_KEY"]) + with self.assertRaises(RuntimeError): + MOD.load_control(pathlib.Path(td)) + + def test_script_pins_supported_upload_and_lifecycle_boundaries(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('"/_upload/"', text) + self.assertIn('"filesystem.put"', text) + self.assertIn('"filesystem.mkdir"', text) + self.assertIn('("/mnt/rdtepool/litellm-t6", "750")', text) + self.assertIn('(EXPECTED_CONFIG_DIR, "750")', text) + self.assertIn('(EXPECTED_SECRET_DIR, "700")', text) + self.assertIn('"raise_chmod_error": True', text) + self.assertIn('"app.create"', text) + self.assertIn('"app.stop"', text) + self.assertIn('"app.start"', text) + self.assertIn('"app.delete"', text) + self.assertIn('"/health/liveliness"', text) + self.assertIn('"/health/readiness"', text) + self.assertIn(MOD.EXPECTED_APPLIANCE_DIGEST, text) + self.assertNotIn("OPENROUTER_API_KEY", text) + + + def test_workflow_and_harness_are_bound_to_exact_litellm_export(self): + workflow = (HERE / ".github" / "workflows" / "gha-kvm-system-rdte.yml").read_text(encoding="utf-8") + harness = (SCRIPTS / "gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + self.assertIn( + "export-litellm-t6-control.yml@" + MOD.EXPECTED_FOUNDRY_REF, + workflow, + ) + self.assertIn("foundry_ref: " + MOD.EXPECTED_FOUNDRY_REF, workflow) + self.assertIn("name: litellm-truenas-t6-control", workflow) + self.assertIn('--foundry-commit "' + MOD.EXPECTED_FOUNDRY_REF + '"', workflow) + self.assertNotIn("truenas-foundry-materialized-controls", workflow) + self.assertIn('LITELLM_HOSTFWD=",hostfwd=tcp:127.0.0.1:', harness) + self.assertIn('-:30401"', harness) + self.assertIn( + 'python3 "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py"', + harness, + ) + self.assertIn('--service-port "$LITELLM_HOST_PORT"', harness) + self.assertNotIn( + 'python3 "$SCRIPT_DIR/truenas_middleware_foundry_control_probe.py"', + harness, + ) + + + +if __name__ == "__main__": + unittest.main() From 7e6e38fbd327ea22c463245b4262aecd2d885101 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:58:37 +0200 Subject: [PATCH 074/719] test: merge LiteLLM T6 contract with current Proxmox line --- tests/test_gha_kvm_system_rdte.py | 32 +++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index f09198596..467bdcb47 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -333,7 +333,7 @@ def test_truenas_install_rpc_uses_one_positional_object(self): def test_truenas_t3_real_harness_contract(self): text = TRUENAS.read_text(encoding="utf-8") workflow = WORKFLOW.read_text(encoding="utf-8") - self.assertIn("t0|t1|t2|t3|t4|t5", text) + self.assertIn("t0|t1|t2|t3|t4|t5|t6", text) self.assertIn('DATA_DISK_SIZE="8G"', text) self.assertIn("DATA_DISK_COUNT=2", text) self.assertIn('DATA_POOL_NAME="rdtepool"', text) @@ -355,7 +355,7 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn("truenas_middleware_pool_probe.py", text) self.assertIn('"data_pool_created"', text) self.assertIn('"data_pool"', text) - self.assertIn("--rung t5", workflow) + self.assertIn("--rung t6", workflow) self.assertIn("scripts/truenas_middleware_pool_probe.py", workflow) shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) @@ -382,7 +382,7 @@ def test_truenas_t4_apps_contract(self): self.assertIn('"nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', app) self.assertNotIn('"nginx:1.27-alpine"', app) self.assertIn('"state") == "RUNNING"', app) - self.assertIn('--rung t5', workflow) + self.assertIn('--rung t6', workflow) py = subprocess.run( ["python3", "-m", "py_compile", str(TRUENAS_APP)], text=True, @@ -403,13 +403,37 @@ def test_truenas_t5_lifecycle_harness_contract(self): self.assertIn('EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3"', lifecycle) self.assertIn('APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', lifecycle) self.assertIn("scripts/truenas_middleware_app_lifecycle_probe.py", workflow) - self.assertIn("--rung t5", workflow) + self.assertIn("--rung t6", workflow) shell_check = subprocess.run( ["bash", "-n", str(TRUENAS)], text=True, capture_output=True ) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) + def test_truenas_t6_foundry_harness_contract(self): + text = TRUENAS.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + litellm = (ROOT / "scripts" / "truenas_middleware_litellm_t6_probe.py").read_text(encoding="utf-8") + self.assertIn("--foundry-control-dir", text) + self.assertIn("--foundry-commit", text) + self.assertIn("truenas_middleware_litellm_t6_probe.py", text) + self.assertNotIn('python3 "$SCRIPT_DIR/truenas_middleware_foundry_control_probe.py"', text) + self.assertIn('"foundry_materialization_exercised"', text) + self.assertIn('"foundry_materialization"', text) + self.assertIn("--rung t6", workflow) + self.assertIn("4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba", workflow) + self.assertIn("litellm-truenas-t6-control", workflow) + self.assertIn("export-litellm-t6-control.yml", workflow) + self.assertIn("semper-supra.litellm-truenas-t6-control/1", litellm) + self.assertIn("config_readback_sha", litellm) + self.assertIn('"secret_values_captured": False', litellm) + self.assertIn("--service-port", text) + self.assertIn("-:30401", text) + shell_check = subprocess.run( + ["bash", "-n", str(TRUENAS)], text=True, capture_output=True + ) + self.assertEqual(shell_check.returncode, 0, shell_check.stderr) + def test_qemu_t3_topology_is_a_cheap_separate_oracle(self): probe = QEMU_TOPOLOGY.read_text(encoding="utf-8") workflow = WORKFLOW.read_text(encoding="utf-8") From cd965ca087cfeae1ecc3e6638bda9ad67e524305 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:01:06 +0200 Subject: [PATCH 075/719] test: bind LiteLLM forward to installed TrueNAS boot --- tests/test_truenas_t6_litellm_control.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_truenas_t6_litellm_control.py b/tests/test_truenas_t6_litellm_control.py index 0eacb4d43..9f748a725 100644 --- a/tests/test_truenas_t6_litellm_control.py +++ b/tests/test_truenas_t6_litellm_control.py @@ -118,6 +118,15 @@ def test_workflow_and_harness_are_bound_to_exact_litellm_export(self): self.assertNotIn("truenas-foundry-materialized-controls", workflow) self.assertIn('LITELLM_HOSTFWD=",hostfwd=tcp:127.0.0.1:', harness) self.assertIn('-:30401"', harness) + netdev_lines = [ + line.strip() + for line in harness.splitlines() + if '-netdev "user,id=net0' in line + ] + self.assertEqual(2, len(netdev_lines)) + self.assertNotIn("LITELLM_HOSTFWD", netdev_lines[0]) + self.assertIn("${LITELLM_HOSTFWD}", netdev_lines[1]) + self.assertIn("hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443", netdev_lines[1]) self.assertIn( 'python3 "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py"', harness, From 9d1baeafcde302920b432c01f022b1385cfe0e18 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:05:59 +0200 Subject: [PATCH 076/719] rdte: prove LiteLLM T6 zero-residue cleanup --- .../truenas_middleware_litellm_t6_probe.py | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/scripts/truenas_middleware_litellm_t6_probe.py b/scripts/truenas_middleware_litellm_t6_probe.py index 22dc553dc..af6065bf4 100644 --- a/scripts/truenas_middleware_litellm_t6_probe.py +++ b/scripts/truenas_middleware_litellm_t6_probe.py @@ -31,6 +31,8 @@ EXPECTED_LIBRARY_HASH = "874636814efb275e5276ea9d709b7cd665fed42bb1d50328e853d9253a2e1229" EXPECTED_APP_NAME = "rdte-t6-litellm" EXPECTED_GUEST_PORT = 30401 +EXPECTED_FIXTURE_DATASET = "rdtepool/litellm-t6" +EXPECTED_FIXTURE_ROOT = "/mnt/rdtepool/litellm-t6" EXPECTED_CONFIG_DIR = "/mnt/rdtepool/litellm-t6/config" EXPECTED_SECRET_DIR = "/mnt/rdtepool/litellm-t6/secrets" EXPECTED_CONFIG_FILE = "proxy_server_config.yaml" @@ -315,8 +317,20 @@ def wait_app_state(expected): if existing: raise RuntimeError("refusing adopted LiteLLM app state") + existing_dataset = call("pool.dataset.query", [[["id", "=", EXPECTED_FIXTURE_DATASET]]]) + if existing_dataset: + raise RuntimeError("refusing adopted LiteLLM T6 fixture dataset") + + dataset = call("pool.dataset.create", [{ + "name": EXPECTED_FIXTURE_DATASET, + "type": "FILESYSTEM", + "share_type": "GENERIC", + "comments": "SemperSupra disposable LiteLLM T6 fixture", + }]) + if not isinstance(dataset, dict) or dataset.get("id") != EXPECTED_FIXTURE_DATASET: + raise RuntimeError("pool.dataset.create did not return exact T6 dataset identity") + fixture_dirs = [ - ("/mnt/rdtepool/litellm-t6", "750"), (EXPECTED_CONFIG_DIR, "750"), (EXPECTED_SECRET_DIR, "700"), ] @@ -335,8 +349,10 @@ def wait_app_state(expected): "path": path, "mode": oct(observed_mode), }) - payload["fixture_directories"] = { - "created": created_dirs, + payload["fixture_storage"] = { + "dataset": EXPECTED_FIXTURE_DATASET, + "root": EXPECTED_FIXTURE_ROOT, + "directories": created_dirs, "values_recorded": False, } @@ -437,6 +453,35 @@ def wait_app_state(expected): if remaining: raise RuntimeError("LiteLLM app remains after delete") + deleted_dataset = call("pool.dataset.delete", [ + EXPECTED_FIXTURE_DATASET, + {"recursive": True, "force": False}, + ]) + if deleted_dataset is not True: + raise RuntimeError("pool.dataset.delete did not return true") + + remaining_dataset = call( + "pool.dataset.query", + [[["id", "=", EXPECTED_FIXTURE_DATASET]]], + ) + if remaining_dataset: + raise RuntimeError("LiteLLM T6 fixture dataset remains after delete") + + root_absent = False + try: + call("filesystem.stat", [EXPECTED_FIXTURE_ROOT]) + except RuntimeError: + root_absent = True + if not root_absent: + raise RuntimeError("LiteLLM T6 fixture mountpoint remains after dataset delete") + + payload["cleanup"] = { + "app_absent": True, + "fixture_dataset_absent": True, + "fixture_mountpoint_absent": True, + "zero_residue": True, + } + payload["runtime"] = { "state": "RUNNING", "custom_app": True, @@ -448,13 +493,14 @@ def wait_app_state(expected): "restart_readiness": restart_ready, "restart_compose_identity_preserved": True, "delete_absent": True, + "zero_residue": True, } payload["classification"] = "SUPPORTED" payload["oracleSatisfied"] = True payload["detail"] = ( "exact Foundry-exported LiteLLM appliance consumed on real TrueNAS software; " "S1 fixture projection, exact image/config read-back, health, restart persistence, " - "and delete/absence all passed" + "and delete/zero-residue all passed" ) except Exception as exc: payload["detail"] = f"{type(exc).__name__}: {exc}" From f4471de49d451d55a57def325c7a4f50168a77ed Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:06:17 +0200 Subject: [PATCH 077/719] test: require T6 dataset zero-residue cleanup --- tests/test_truenas_t6_litellm_control.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_litellm_control.py b/tests/test_truenas_t6_litellm_control.py index 9f748a725..584bf3522 100644 --- a/tests/test_truenas_t6_litellm_control.py +++ b/tests/test_truenas_t6_litellm_control.py @@ -90,11 +90,19 @@ def test_script_pins_supported_upload_and_lifecycle_boundaries(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn('"/_upload/"', text) self.assertIn('"filesystem.put"', text) + self.assertIn('"pool.dataset.create"', text) + self.assertIn('"pool.dataset.delete"', text) + self.assertIn('EXPECTED_FIXTURE_DATASET = "rdtepool/litellm-t6"', text) + self.assertIn('"share_type": "GENERIC"', text) self.assertIn('"filesystem.mkdir"', text) - self.assertIn('("/mnt/rdtepool/litellm-t6", "750")', text) + self.assertNotIn('("/mnt/rdtepool/litellm-t6", "750")', text) self.assertIn('(EXPECTED_CONFIG_DIR, "750")', text) self.assertIn('(EXPECTED_SECRET_DIR, "700")', text) self.assertIn('"raise_chmod_error": True', text) + self.assertIn('"recursive": True', text) + self.assertIn('"zero_residue": True', text) + self.assertIn('"fixture_dataset_absent": True', text) + self.assertIn('"fixture_mountpoint_absent": True', text) self.assertIn('"app.create"', text) self.assertIn('"app.stop"', text) self.assertIn('"app.start"', text) From e9a7a3a9dcd8eb5786a3b4a0a3baa3a620353104 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 29 Sep 2026 23:10:50 +0200 Subject: [PATCH 078/719] diag: bound Proxmox guest-local HTTPS oracle --- scripts/gha_kvm_proxmox_rdte.sh | 70 +++++++++++++++++-------------- tests/test_gha_kvm_system_rdte.py | 9 ++-- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index c50f76095..d5dd63297 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -39,7 +39,7 @@ QEMU_PID="" OBSERVED_ISO_SHA="" API_VERSION_JSON="" HOSTFWD_API_VERSION_JSON="" -SSH_TUNNEL_API_VERSION_JSON="" +GUEST_LOCAL_API_VERSION_JSON="" API_OBSERVATION_ROUTE="" NESTED_KVM="unknown" SSH_HOSTFWD_ACCEPTED="false" @@ -72,7 +72,7 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" - export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_SSH_TUNNEL_API="$SSH_TUNNEL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" + export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_GUEST_LOCAL_API="$GUEST_LOCAL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" export R_FIRST_BOOT_WITNESS="$FIRST_BOOT_WITNESS" R_FIRST_BOOT_WITNESS_OBSERVED="$FIRST_BOOT_WITNESS_OBSERVED" @@ -124,7 +124,7 @@ payload = { "api_hostfwd_accepted": os.environ.get("R_API_HOSTFWD_ACCEPTED") == "true", "api_observation_route": os.environ.get("R_API_ROUTE") or None, "hostfwd_https_api": load_json_env("R_HOSTFWD_API"), - "ssh_tunnel_guest_local_https_api": load_json_env("R_SSH_TUNNEL_API"), + "guest_local_https_api": load_json_env("R_GUEST_LOCAL_API"), "guest": os.environ.get("R_GUEST_DIAGNOSTICS") or None, "first_boot_witness_observed": os.environ.get("R_FIRST_BOOT_WITNESS_OBSERVED") == "true", "first_boot_witness": os.environ.get("R_FIRST_BOOT_WITNESS") or None, @@ -320,11 +320,7 @@ import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() PY )" -API_TUNNEL_PORT="$(python3 - <<'PY' -import socket -s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() -PY -)" + port_open() { @@ -342,24 +338,27 @@ finally: PY } -try_api_via_ssh_tunnel() { - local control="$STATE_DIR/ssh-api-control" +guest_local_https_api() { local response="" - rm -f -- "$control" - if ! sshpass -p "$ROOT_PASSWORD" ssh \ - -p "$SSH_PORT" \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - -o ConnectTimeout=5 \ - -o ExitOnForwardFailure=yes \ - -M -S "$control" -fN \ - -L "127.0.0.1:$API_TUNNEL_PORT:127.0.0.1:8006" \ - root@127.0.0.1 >/dev/null 2>&1; then - return 0 - fi - response="$(curl -sk --max-time 10 "https://127.0.0.1:$API_TUNNEL_PORT/api2/json/version" 2>/dev/null || true)" - ssh -p "$SSH_PORT" -S "$control" -O exit root@127.0.0.1 >/dev/null 2>&1 || true - rm -f -- "$control" + # Exact installed pve-manager source exposes GET /version to user=all and + # pveproxy terminates TLS on 8006. SSH is observation transport only; the + # application oracle remains a real HTTPS request to guest localhost:8006. + for _ in 1 2 3; do + response="$(sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 'sh -s' 2>/dev/null <<'REMOTE' || true +printf 'GET /api2/json/version HTTP/1.0\r\nHost: pve-rdte.example.invalid\r\nConnection: close\r\n\r\n' | + timeout 10 openssl s_client -quiet -connect 127.0.0.1:8006 -servername pve-rdte.example.invalid 2>/dev/null | + tr -d '\r' | + awk 'body { print } /^$/ { body=1 }' +REMOTE +)" + if [[ "$response" == *'"data"'* ]]; then + printf '%s' "$response" + return 0 + fi + sleep 2 + done printf '%s' "$response" } @@ -576,7 +575,7 @@ fi start_qemu no API_VERSION_JSON="" HOSTFWD_API_VERSION_JSON="" -SSH_TUNNEL_API_VERSION_JSON="" +GUEST_LOCAL_API_VERSION_JSON="" API_OBSERVATION_ROUTE="" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" @@ -602,11 +601,18 @@ for _ in $(seq 1 120); do break fi fi - if [[ "$SSH_HOSTFWD_ACCEPTED" == "true" ]]; then - SSH_TUNNEL_API_VERSION_JSON="$(try_api_via_ssh_tunnel || true)" - if [[ "$SSH_TUNNEL_API_VERSION_JSON" == *'"data"'* ]]; then - API_VERSION_JSON="$SSH_TUNNEL_API_VERSION_JSON" - API_OBSERVATION_ROUTE="ssh-tunnel-guest-local-https" + if [[ "$FIRST_BOOT_WITNESS_OBSERVED" == "true" && "$SSH_HOSTFWD_ACCEPTED" == "true" ]]; then + if sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 root@127.0.0.1 'true' >/dev/null 2>&1; then + GUEST_LOCAL_API_VERSION_JSON="$(guest_local_https_api || true)" + if [[ "$GUEST_LOCAL_API_VERSION_JSON" == *'"data"'* ]]; then + API_VERSION_JSON="$GUEST_LOCAL_API_VERSION_JSON" + API_OBSERVATION_ROUTE="ssh-observed-guest-local-https" + break + fi + # The guest witness already proved pveproxy active/listening. A bounded + # guest-local TLS failure is now discriminating; do not spend the rest of + # the workflow retrying an observation route. + GUEST_DIAGNOSTICS="$(guest_diagnostics || true)" break fi fi @@ -620,7 +626,7 @@ if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then fi stop_qemu INSTALLED_DISK_POSTBOOT="$(inspect_installed_disk postboot)" - fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer via direct hostfwd or SSH-tunneled guest-local HTTPS (qemu_alive=$QEMU_ALIVE_AT_API_GATE first_boot_witness=$FIRST_BOOT_WITNESS_OBSERVED)" + fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer via direct hostfwd or SSH-observed guest-local HTTPS (qemu_alive=$QEMU_ALIVE_AT_API_GATE first_boot_witness=$FIRST_BOOT_WITNESS_OBSERVED)" fi NESTED_KVM="unknown" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index f09198596..a5775b24b 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -87,10 +87,13 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("rdte-first-boot.log", text) self.assertIn("TTY_S0=character-device", text) self.assertIn("first_boot_witness_observed", text) - self.assertIn("try_api_via_ssh_tunnel", text) - self.assertIn('API_OBSERVATION_ROUTE="ssh-tunnel-guest-local-https"', text) + self.assertIn("guest_local_https_api", text) + self.assertIn("openssl s_client -quiet -connect 127.0.0.1:8006", text) + self.assertIn("GET /api2/json/version HTTP/1.0", text) + self.assertNotIn("for _ in $(seq 1 120); do\n if [[ \"$SSH_HOSTFWD_ACCEPTED\" == \"true\" ]]; then", text) + self.assertIn('API_OBSERVATION_ROUTE="ssh-observed-guest-local-https"', text) self.assertIn('"api_observation_route"', text) - self.assertIn('"ssh_tunnel_guest_local_https_api"', text) + self.assertIn('"guest_local_https_api"', text) self.assertIn('"pve_manager_source_commit": "b9984c6d90a4bd80"', text) self.assertIn('ROOT_PASSWORD="rdte-proxmox-', text) self.assertIn('root-password = "$ROOT_PASSWORD"', text) From 3392634e83047e80e6a2f2b51d603c7c1952342c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:58:28 +0200 Subject: [PATCH 079/719] fix: retain Proxmox guest-local TLS response --- scripts/gha_kvm_proxmox_rdte.sh | 2 +- tests/test_gha_kvm_system_rdte.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index d5dd63297..1ce181c85 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -348,7 +348,7 @@ guest_local_https_api() { -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ root@127.0.0.1 'sh -s' 2>/dev/null <<'REMOTE' || true printf 'GET /api2/json/version HTTP/1.0\r\nHost: pve-rdte.example.invalid\r\nConnection: close\r\n\r\n' | - timeout 10 openssl s_client -quiet -connect 127.0.0.1:8006 -servername pve-rdte.example.invalid 2>/dev/null | + timeout 10 openssl s_client -quiet -ign_eof -connect 127.0.0.1:8006 -servername pve-rdte.example.invalid 2>/dev/null | tr -d '\r' | awk 'body { print } /^$/ { body=1 }' REMOTE diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index a5775b24b..136d0bf21 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -88,7 +88,7 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("TTY_S0=character-device", text) self.assertIn("first_boot_witness_observed", text) self.assertIn("guest_local_https_api", text) - self.assertIn("openssl s_client -quiet -connect 127.0.0.1:8006", text) + self.assertIn("openssl s_client -quiet -ign_eof -connect 127.0.0.1:8006", text) self.assertIn("GET /api2/json/version HTTP/1.0", text) self.assertNotIn("for _ in $(seq 1 120); do\n if [[ \"$SSH_HOSTFWD_ACCEPTED\" == \"true\" ]]; then", text) self.assertIn('API_OBSERVATION_ROUTE="ssh-observed-guest-local-https"', text) From f08f9e038f327a27aebd47810cde7c8ff98b168e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:07:45 +0200 Subject: [PATCH 080/719] diag: structure Proxmox guest-local HTTPS probe --- scripts/gha_kvm_proxmox_rdte.sh | 69 ++++++++++++++++++++++++++----- tests/test_gha_kvm_system_rdte.py | 6 ++- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 1ce181c85..e7b62e119 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -340,20 +340,69 @@ PY guest_local_https_api() { local response="" - # Exact installed pve-manager source exposes GET /version to user=all and - # pveproxy terminates TLS on 8006. SSH is observation transport only; the - # application oracle remains a real HTTPS request to guest localhost:8006. for _ in 1 2 3; do response="$(sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ - root@127.0.0.1 'sh -s' 2>/dev/null <<'REMOTE' || true -printf 'GET /api2/json/version HTTP/1.0\r\nHost: pve-rdte.example.invalid\r\nConnection: close\r\n\r\n' | - timeout 10 openssl s_client -quiet -ign_eof -connect 127.0.0.1:8006 -servername pve-rdte.example.invalid 2>/dev/null | - tr -d '\r' | - awk 'body { print } /^$/ { body=1 }' -REMOTE + root@127.0.0.1 'command -v python3 >/dev/null 2>&1 || { printf "%s\\n" "{\\\"probe_error\\\":\\\"python3_absent\\\"}"; exit 0; }; exec python3 -' 2>/dev/null <<'PY' || true +import json +import socket +import ssl + +host = "127.0.0.1" +port = 8006 +server_name = "pve-rdte.example.invalid" +request = ( + "GET /api2/json/version HTTP/1.0\\r\\n" + f"Host: {server_name}\\r\\n" + "Connection: close\\r\\n\\r\\n" +).encode("ascii") + +try: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket.create_connection((host, port), timeout=5) as raw: + with context.wrap_socket(raw, server_hostname=server_name) as tls: + tls.settimeout(5) + tls.sendall(request) + chunks = [] + while True: + block = tls.recv(65536) + if not block: + break + chunks.append(block) + payload = b"".join(chunks) + header, sep, body = payload.partition(b"\\r\\n\\r\\n") + if not sep: + raise RuntimeError("HTTP header terminator absent") + status = header.split(b"\\r\\n", 1)[0].decode("ascii", "replace") + if " 200 " not in f" {status} ": + print(json.dumps({ + "probe_error": "http_status", + "status": status, + "body_preview": body[:1000].decode("utf-8", "replace"), + }, sort_keys=True)) + else: + parsed = json.loads(body.decode("utf-8")) + print(json.dumps(parsed, separators=(",", ":"), sort_keys=True)) +except Exception as exc: + print(json.dumps({ + "probe_error": type(exc).__name__, + "message": str(exc), + }, sort_keys=True)) +PY )" - if [[ "$response" == *'"data"'* ]]; then + if R_RESPONSE="$response" python3 - <<'PY' +import json, os +try: + payload = json.loads(os.environ["R_RESPONSE"]) + data = payload.get("data") + ok = isinstance(data, dict) and all(data.get(k) for k in ("version", "release", "repoid")) +except Exception: + ok = False +raise SystemExit(0 if ok else 1) +PY + then printf '%s' "$response" return 0 fi diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 136d0bf21..8bd09967d 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -88,7 +88,11 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("TTY_S0=character-device", text) self.assertIn("first_boot_witness_observed", text) self.assertIn("guest_local_https_api", text) - self.assertIn("openssl s_client -quiet -ign_eof -connect 127.0.0.1:8006", text) + self.assertIn("socket.create_connection((host, port), timeout=5)", text) + self.assertIn("ssl.create_default_context()", text) + self.assertIn('"probe_error": "http_status"', text) + self.assertIn('all(data.get(k) for k in ("version", "release", "repoid"))', text) + self.assertNotIn("openssl s_client", text) self.assertIn("GET /api2/json/version HTTP/1.0", text) self.assertNotIn("for _ in $(seq 1 120); do\n if [[ \"$SSH_HOSTFWD_ACCEPTED\" == \"true\" ]]; then", text) self.assertIn('API_OBSERVATION_ROUTE="ssh-observed-guest-local-https"', text) From 23eec2c9993d2b11c6f0732343d7b0049b053b75 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:15:24 +0200 Subject: [PATCH 081/719] fix: emit valid HTTP CRLF in Proxmox guest probe --- scripts/gha_kvm_proxmox_rdte.sh | 10 +++++----- tests/test_gha_kvm_system_rdte.py | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index e7b62e119..f4a351150 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -352,9 +352,9 @@ host = "127.0.0.1" port = 8006 server_name = "pve-rdte.example.invalid" request = ( - "GET /api2/json/version HTTP/1.0\\r\\n" - f"Host: {server_name}\\r\\n" - "Connection: close\\r\\n\\r\\n" + "GET /api2/json/version HTTP/1.0\r\n" + f"Host: {server_name}\r\n" + "Connection: close\r\n\r\n" ).encode("ascii") try: @@ -372,10 +372,10 @@ try: break chunks.append(block) payload = b"".join(chunks) - header, sep, body = payload.partition(b"\\r\\n\\r\\n") + header, sep, body = payload.partition(b"\r\n\r\n") if not sep: raise RuntimeError("HTTP header terminator absent") - status = header.split(b"\\r\\n", 1)[0].decode("ascii", "replace") + status = header.split(b"\r\n", 1)[0].decode("ascii", "replace") if " 200 " not in f" {status} ": print(json.dumps({ "probe_error": "http_status", diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 8bd09967d..5e618b004 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -94,6 +94,8 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn('all(data.get(k) for k in ("version", "release", "repoid"))', text) self.assertNotIn("openssl s_client", text) self.assertIn("GET /api2/json/version HTTP/1.0", text) + self.assertIn('HTTP/1.0\\\\r\\\\n', text) + self.assertNotIn(r'HTTP/1.0\\\\r\\\\n', text) self.assertNotIn("for _ in $(seq 1 120); do\n if [[ \"$SSH_HOSTFWD_ACCEPTED\" == \"true\" ]]; then", text) self.assertIn('API_OBSERVATION_ROUTE="ssh-observed-guest-local-https"', text) self.assertIn('"api_observation_route"', text) From c01b7a7f509619c3565732cab20198a0f9c33955 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:16:06 +0200 Subject: [PATCH 082/719] test: distinguish valid and double-escaped HTTP framing --- tests/test_gha_kvm_system_rdte.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 5e618b004..c8ad26f3f 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -94,8 +94,8 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn('all(data.get(k) for k in ("version", "release", "repoid"))', text) self.assertNotIn("openssl s_client", text) self.assertIn("GET /api2/json/version HTTP/1.0", text) - self.assertIn('HTTP/1.0\\\\r\\\\n', text) - self.assertNotIn(r'HTTP/1.0\\\\r\\\\n', text) + self.assertIn(r'HTTP/1.0\r\n', text) + self.assertNotIn(r'HTTP/1.0\\r\\n', text) self.assertNotIn("for _ in $(seq 1 120); do\n if [[ \"$SSH_HOSTFWD_ACCEPTED\" == \"true\" ]]; then", text) self.assertIn('API_OBSERVATION_ROUTE="ssh-observed-guest-local-https"', text) self.assertIn('"api_observation_route"', text) From 97107eb42371e73da14637f00e06f3eb996a7024 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:26:20 +0200 Subject: [PATCH 083/719] fix: authenticate Proxmox P2 HTTPS oracle --- scripts/gha_kvm_proxmox_rdte.sh | 97 +++++++++++++++++++++++++------ tests/test_gha_kvm_system_rdte.py | 7 +++ 2 files changed, 86 insertions(+), 18 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index f4a351150..b3409169a 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -106,6 +106,7 @@ payload = { "installer_source_version": "9.2.5", "installer_source_commit": "32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9", "pve_manager_source_commit": "b9984c6d90a4bd80", + "pve_access_control_source_commit": "5ccd07d9302562b73374d331b63d25b04b86766c", "pve_version_api_source": "PVE/API2.pm", "iso_first_boot_package": os.environ.get("R_ISO_FIRST_BOOT_PACKAGE") or None, }, @@ -341,57 +342,117 @@ PY guest_local_https_api() { local response="" for _ in 1 2 3; do - response="$(sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ - -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ - root@127.0.0.1 'command -v python3 >/dev/null 2>&1 || { printf "%s\\n" "{\\\"probe_error\\\":\\\"python3_absent\\\"}"; exit 0; }; exec python3 -' 2>/dev/null <<'PY' || true + response="$( + { + printf '%s\n' "$ROOT_PASSWORD" + cat <<'PY' import json +import os import socket import ssl +import sys +import urllib.parse host = "127.0.0.1" port = 8006 server_name = "pve-rdte.example.invalid" -request = ( - "GET /api2/json/version HTTP/1.0\r\n" - f"Host: {server_name}\r\n" - "Connection: close\r\n\r\n" -).encode("ascii") +password = sys.stdin.readline().rstrip("\n") +stage = "init" + +def https_request(method, path, headers=None, body=b""): + request_headers = { + "Host": server_name, + "Connection": "close", + } + if headers: + request_headers.update(headers) + if body: + request_headers["Content-Length"] = str(len(body)) + request = [f"{method} {path} HTTP/1.0"] + request.extend(f"{name}: {value}" for name, value in request_headers.items()) + wire = ("\r\n".join(request) + "\r\n\r\n").encode("ascii") + body -try: context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE with socket.create_connection((host, port), timeout=5) as raw: with context.wrap_socket(raw, server_hostname=server_name) as tls: tls.settimeout(5) - tls.sendall(request) + tls.sendall(wire) chunks = [] while True: block = tls.recv(65536) if not block: break chunks.append(block) + payload = b"".join(chunks) - header, sep, body = payload.partition(b"\r\n\r\n") + header, sep, response_body = payload.partition(b"\r\n\r\n") if not sep: raise RuntimeError("HTTP header terminator absent") status = header.split(b"\r\n", 1)[0].decode("ascii", "replace") - if " 200 " not in f" {status} ": + return status, response_body + +try: + stage = "ticket" + ticket_body = urllib.parse.urlencode({ + "username": "root@pam", + "password": password, + }).encode("ascii") + ticket_status, ticket_response = https_request( + "POST", + "/api2/json/access/ticket", + {"Content-Type": "application/x-www-form-urlencoded"}, + ticket_body, + ) + if " 200 " not in f" {ticket_status} ": print(json.dumps({ "probe_error": "http_status", - "status": status, - "body_preview": body[:1000].decode("utf-8", "replace"), + "stage": stage, + "status": ticket_status, }, sort_keys=True)) - else: - parsed = json.loads(body.decode("utf-8")) - print(json.dumps(parsed, separators=(",", ":"), sort_keys=True)) + raise SystemExit(0) + + ticket_payload = json.loads(ticket_response.decode("utf-8")) + ticket = (ticket_payload.get("data") or {}).get("ticket") + if not ticket: + print(json.dumps({ + "probe_error": "ticket_absent", + "stage": stage, + }, sort_keys=True)) + raise SystemExit(0) + + stage = "version" + version_status, version_response = https_request( + "GET", + "/api2/json/version", + {"Cookie": f"PVEAuthCookie={ticket}"}, + ) + if " 200 " not in f" {version_status} ": + print(json.dumps({ + "probe_error": "http_status", + "stage": stage, + "status": version_status, + "body_preview": version_response[:1000].decode("utf-8", "replace"), + }, sort_keys=True)) + raise SystemExit(0) + + version_payload = json.loads(version_response.decode("utf-8")) + print(json.dumps(version_payload, separators=(",", ":"), sort_keys=True)) +except SystemExit: + raise except Exception as exc: print(json.dumps({ "probe_error": type(exc).__name__, + "stage": stage, "message": str(exc), }, sort_keys=True)) PY -)" + } | sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 'IFS= read -r PVE_RDTE_PASSWORD; export PVE_RDTE_PASSWORD; exec python3 -' \ + 2>/dev/null || true + )" if R_RESPONSE="$response" python3 - <<'PY' import json, os try: diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index c8ad26f3f..25573607a 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -92,6 +92,12 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("ssl.create_default_context()", text) self.assertIn('"probe_error": "http_status"', text) self.assertIn('all(data.get(k) for k in ("version", "release", "repoid"))', text) + self.assertIn("/api2/json/access/ticket", text) + self.assertIn("urllib.parse.urlencode", text) + self.assertIn("PVEAuthCookie={ticket}", text) + self.assertIn('"stage": stage', text) + self.assertNotIn("print(json.dumps(ticket_payload", text) + self.assertNotIn("CSRFPreventionToken:", text) self.assertNotIn("openssl s_client", text) self.assertIn("GET /api2/json/version HTTP/1.0", text) self.assertIn(r'HTTP/1.0\r\n', text) @@ -101,6 +107,7 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn('"api_observation_route"', text) self.assertIn('"guest_local_https_api"', text) self.assertIn('"pve_manager_source_commit": "b9984c6d90a4bd80"', text) + self.assertIn('"pve_access_control_source_commit": "5ccd07d9302562b73374d331b63d25b04b86766c"', text) self.assertIn('ROOT_PASSWORD="rdte-proxmox-', text) self.assertIn('root-password = "$ROOT_PASSWORD"', text) self.assertIn('timezone = "UTC"', text) From 7cc802e5fddee7552bf41fb8fee81b27792544f0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:27:30 +0200 Subject: [PATCH 084/719] fix: consume P2 credential from transient remote env --- scripts/gha_kvm_proxmox_rdte.sh | 2 +- tests/test_gha_kvm_system_rdte.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index b3409169a..b53e781d7 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -356,7 +356,7 @@ import urllib.parse host = "127.0.0.1" port = 8006 server_name = "pve-rdte.example.invalid" -password = sys.stdin.readline().rstrip("\n") +password = os.environ["PVE_RDTE_PASSWORD"] stage = "init" def https_request(method, path, headers=None, body=b""): diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 25573607a..4ce3c1786 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -95,6 +95,7 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("/api2/json/access/ticket", text) self.assertIn("urllib.parse.urlencode", text) self.assertIn("PVEAuthCookie={ticket}", text) + self.assertIn('password = os.environ["PVE_RDTE_PASSWORD"]', text) self.assertIn('"stage": stage', text) self.assertNotIn("print(json.dumps(ticket_payload", text) self.assertNotIn("CSRFPreventionToken:", text) From 9adfaa090226a4874b3b48c6f9b9277138740682 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:29:17 +0200 Subject: [PATCH 085/719] test: align P2 contract with authenticated request builder --- tests/test_gha_kvm_system_rdte.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 4ce3c1786..ee98401e8 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -100,9 +100,9 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertNotIn("print(json.dumps(ticket_payload", text) self.assertNotIn("CSRFPreventionToken:", text) self.assertNotIn("openssl s_client", text) - self.assertIn("GET /api2/json/version HTTP/1.0", text) - self.assertIn(r'HTTP/1.0\r\n', text) - self.assertNotIn(r'HTTP/1.0\\r\\n', text) + self.assertIn('request = [f"{method} {path} HTTP/1.0"]', text) + self.assertIn(r'wire = ("\r\n".join(request) + "\r\n\r\n")', text) + self.assertNotIn(r'wire = ("\\r\\n".join(request)', text) self.assertNotIn("for _ in $(seq 1 120); do\n if [[ \"$SSH_HOSTFWD_ACCEPTED\" == \"true\" ]]; then", text) self.assertIn('API_OBSERVATION_ROUTE="ssh-observed-guest-local-https"', text) self.assertIn('"api_observation_route"', text) From f0a9773bbaada1cce21c1ae005a6e775af2a0d10 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:30:21 +0200 Subject: [PATCH 086/719] docs: bind Proxmox P2 authenticated API contract --- scripts/gha_kvm_proxmox_rdte.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index b53e781d7..5a780b381 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -340,6 +340,9 @@ PY } guest_local_https_api() { + # Exact installed source contract: /access/ticket is user=world, while + # /version requires an authenticated user; pve-http-server consumes the + # returned ticket from the PVEAuthCookie cookie on the subsequent GET. local response="" for _ in 1 2 3; do response="$( From 510bcf92dbe3d09e5301203a141fe4abfce96ee6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:02:33 +0200 Subject: [PATCH 087/719] exp: prove Proxmox nested KVM vCPU execution --- scripts/gha_kvm_proxmox_rdte.sh | 131 +++++++++++++++++++++++++++++- tests/test_gha_kvm_system_rdte.py | 20 +++++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 5a780b381..21024edd6 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -42,6 +42,8 @@ HOSTFWD_API_VERSION_JSON="" GUEST_LOCAL_API_VERSION_JSON="" API_OBSERVATION_ROUTE="" NESTED_KVM="unknown" +NESTED_KVM_INDICATORS="unknown" +NESTED_KVM_VCPU_JSON="" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" @@ -72,6 +74,7 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" + export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_GUEST_LOCAL_API="$GUEST_LOCAL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" @@ -90,6 +93,8 @@ def load_json_env(name): except Exception as exc: return {"inspection_ok": False, "parse_error": str(exc), "raw": raw[:2000]} +nested_vcpu = load_json_env("R_NESTED_VCPU") + payload = { "contract": "gha-kvm-system-lab/v1", "target": {"product": "proxmox-ve", "version": "9.2-1"}, @@ -108,6 +113,10 @@ payload = { "pve_manager_source_commit": "b9984c6d90a4bd80", "pve_access_control_source_commit": "5ccd07d9302562b73374d331b63d25b04b86766c", "pve_version_api_source": "PVE/API2.pm", + "pve_qemu_source_commit": "684796e835289dab11af8606fbf7358b93526dd6", + "pve_qemu_submodule_commit": "98b060da3a4f92b2a994ead5b16a87e783baf77c", + "pve_qemu_debugexit_source": "hw/misc/debugexit.c", + "pve_qemu_expected_package": "11.0.0-3", "iso_first_boot_package": os.environ.get("R_ISO_FIRST_BOOT_PACKAGE") or None, }, "oracles": { @@ -115,10 +124,13 @@ payload = { "unattended_install_completed": os.environ.get("R_INSTALL_SUCCESS_MARKER_OBSERVED") == "true", "installed_disk_layout": os.environ.get("R_INSTALLED_DISK_LAYOUT_OK") == "true", "installed_https_api": bool(os.environ.get("R_API")), + "nested_kvm_indicators_via_ssh": os.environ.get("R_NESTED_INDICATORS") == "yes", "nested_kvm_observed_via_ssh": os.environ.get("R_NESTED") == "yes", + "nested_kvm_vcpu_executed": bool(nested_vcpu and nested_vcpu.get("oracleSatisfied") is True), }, "api_version": json.loads(os.environ["R_API"]) if os.environ.get("R_API") else None, "nested_kvm": os.environ.get("R_NESTED"), + "p5_nested_kvm": nested_vcpu, "diagnostics": { "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), "ssh_hostfwd_accepted": os.environ.get("R_SSH_HOSTFWD_ACCEPTED") == "true", @@ -126,6 +138,7 @@ payload = { "api_observation_route": os.environ.get("R_API_ROUTE") or None, "hostfwd_https_api": load_json_env("R_HOSTFWD_API"), "guest_local_https_api": load_json_env("R_GUEST_LOCAL_API"), + "nested_kvm_indicators": os.environ.get("R_NESTED_INDICATORS"), "guest": os.environ.get("R_GUEST_DIAGNOSTICS") or None, "first_boot_witness_observed": os.environ.get("R_FIRST_BOOT_WITNESS_OBSERVED") == "true", "first_boot_witness": os.environ.get("R_FIRST_BOOT_WITNESS") or None, @@ -136,6 +149,7 @@ payload = { "limitations": [ "Disposable virtual-hardware target profile; not physical-HBA/SMART/IPMI/HA qualification.", "Nested KVM is a separate oracle from Proxmox management-plane support.", + "P5 nested KVM requires p5_nested_kvm.oracleSatisfied=true from an actual nested vCPU debug-exit; device/CPU flags alone are diagnostic.", ], } pathlib.Path(os.environ["R_OUT"]).write_text(json.dumps(payload, indent=2, sort_keys=True)+"\n", encoding="utf-8") @@ -500,6 +514,105 @@ guest_diagnostics() { ' 2>&1 | tail -c 24000 } + +probe_nested_kvm_vcpu() { + local response="" + command -v sshpass >/dev/null 2>&1 || { + printf '%s' '{"contract":"proxmox-nested-kvm-vcpu/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"transport","detail":"sshpass unavailable for guest observation"}' + return 0 + } + response="$(sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 'sh -s' 2>/dev/null <<'REMOTE' || true +set +e +EXPECTED_EXIT=85 +EXPECTED_PACKAGE="11.0.0-3" +TMP_RDTE="$(mktemp -d)" +trap 'rm -rf "$TMP_RDTE"' EXIT +BOOT="$TMP_RDTE/p5-boot.img" +QERR="$TMP_RDTE/qemu.err" +python3 - "$BOOT" <<'PY' +import pathlib, sys +code = bytes.fromhex("fab02abaf400eef4ebfd") +image = code + bytes(510 - len(code)) + b"\x55\xaa" +pathlib.Path(sys.argv[1]).write_bytes(image) +PY +BOOT_SHA="$(sha256sum "$BOOT" 2>/dev/null | awk '{print $1}')" +PKG="$(dpkg-query -W -f='${Version}' pve-qemu-kvm 2>/dev/null || true)" +QVER="$(qemu-system-x86_64 --version 2>/dev/null | head -n 1 || true)" +KVM_DEVICE="absent" +CPU_VIRT_FLAG="absent" +PRECONDITION="" +[[ -c /dev/kvm ]] && KVM_DEVICE="present" || PRECONDITION="kvm-device-absent" +if grep -Eq '(vmx|svm)' /proc/cpuinfo 2>/dev/null; then CPU_VIRT_FLAG="present"; else [[ -n "$PRECONDITION" ]] || PRECONDITION="cpu-virt-flag-absent"; fi +command -v qemu-system-x86_64 >/dev/null 2>&1 || { [[ -n "$PRECONDITION" ]] || PRECONDITION="nested-qemu-absent"; } +RC=125 +if [[ -z "$PRECONDITION" ]]; then + timeout 15 qemu-system-x86_64 \ + -machine pc \ + -accel kvm \ + -cpu host \ + -m 64 \ + -smp 1 \ + -drive "file=$BOOT,format=raw,if=floppy,readonly=on" \ + -boot order=a,strict=on \ + -device isa-debug-exit,iobase=0xf4,iosize=0x4 \ + -display none \ + -serial none \ + -monitor none \ + -no-reboot \ + >"$TMP_RDTE/qemu.out" 2>"$QERR" + RC=$? +fi +ERR="$(tail -c 4000 "$QERR" 2>/dev/null || true)" +R_RC="$RC" R_EXPECTED_EXIT="$EXPECTED_EXIT" R_EXPECTED_PACKAGE="$EXPECTED_PACKAGE" \ +R_PKG="$PKG" R_QVER="$QVER" R_BOOT_SHA="$BOOT_SHA" R_ERR="$ERR" \ +R_KVM_DEVICE="$KVM_DEVICE" R_CPU_VIRT_FLAG="$CPU_VIRT_FLAG" R_PRECONDITION="$PRECONDITION" \ +python3 - <<'PY' +import json, os +rc = int(os.environ["R_RC"]) +expected_exit = int(os.environ["R_EXPECTED_EXIT"]) +pkg = os.environ.get("R_PKG", "") +expected_pkg = os.environ["R_EXPECTED_PACKAGE"] +precondition = os.environ.get("R_PRECONDITION", "") +ok = (not precondition and rc == expected_exit and pkg == expected_pkg and os.environ.get("R_KVM_DEVICE") == "present" and os.environ.get("R_CPU_VIRT_FLAG") == "present") +if ok: + classification = "SUPPORTED" + detail = "nested QEMU used KVM and the run-owned guest executed the expected isa-debug-exit instruction" +elif precondition or pkg != expected_pkg: + classification = "ENVIRONMENT_FAILURE" + detail = precondition or f"unexpected pve-qemu-kvm package {pkg!r}" +else: + classification = "ORACLE_FAILURE" + detail = f"nested QEMU exit code {rc} did not match expected guest debug-exit code {expected_exit}" +print(json.dumps({ + "contract": "proxmox-nested-kvm-vcpu/v1", + "classification": classification, + "oracleSatisfied": ok, + "phase": "nested-vcpu", + "detail": detail, + "expected_debug_value": 42, + "expected_exit_code": expected_exit, + "observed_exit_code": rc, + "pve_qemu_kvm_version": pkg or None, + "qemu_version": os.environ.get("R_QVER") or None, + "boot_image_sha256": os.environ.get("R_BOOT_SHA") or None, + "kvm_device": os.environ.get("R_KVM_DEVICE"), + "cpu_virt_flag": os.environ.get("R_CPU_VIRT_FLAG"), + "stderr_tail": os.environ.get("R_ERR") or None, + "source": { + "pve_qemu_commit": "684796e835289dab11af8606fbf7358b93526dd6", + "qemu_submodule_commit": "98b060da3a4f92b2a994ead5b16a87e783baf77c", + "debugexit_source": "hw/misc/debugexit.c", + "exit_code_rule": "(value << 1) | 1", + }, +}, sort_keys=True, separators=(",", ":"))) +PY +REMOTE +)" + if [[ -n "$response" ]]; then printf '%s' "$response"; else printf '%s' '{"contract":"proxmox-nested-kvm-vcpu/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"transport","detail":"guest SSH did not return a nested-KVM receipt"}'; fi +} + start_qemu() { local with_iso="$1" : >"$STATE_DIR/serial.log" @@ -743,14 +856,26 @@ if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then fi NESTED_KVM="unknown" +NESTED_KVM_INDICATORS="unknown" if command -v sshpass >/dev/null 2>&1; then if sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ root@127.0.0.1 'test -e /dev/kvm && grep -Eq "(vmx|svm)" /proc/cpuinfo' >/dev/null 2>&1; then - NESTED_KVM="yes" - else - NESTED_KVM="unknown" + NESTED_KVM_INDICATORS="yes" fi fi +NESTED_KVM_VCPU_JSON="$(probe_nested_kvm_vcpu || true)" +if R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" python3 - <<'PY' +import json, os +try: + payload = json.loads(os.environ.get("R_NESTED_VCPU", "")) + ok = payload.get("oracleSatisfied") is True +except Exception: + ok = False +raise SystemExit(0 if ok else 1) +PY +then + NESTED_KVM="yes" +fi write_receipt SUPPORTED true complete "real Proxmox VE unattended install completed and real HTTPS /api2/json/version answered via $API_OBSERVATION_ROUTE" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index ee98401e8..5bd666a26 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -138,6 +138,26 @@ def test_proxmox_hostfwd_is_diagnostic_not_guest_or_nested_oracle(self): self.assertIn("INSTALLED_DISK_PREBOOT", text) self.assertIn("INSTALLED_DISK_POSTBOOT", text) + + def test_proxmox_p5_requires_actual_nested_vcpu_execution(self): + text = PVE.read_text(encoding="utf-8") + self.assertIn("probe_nested_kvm_vcpu", text) + self.assertIn("-accel kvm", text) + self.assertIn("-cpu host", text) + self.assertIn("isa-debug-exit,iobase=0xf4,iosize=0x4", text) + self.assertIn('bytes.fromhex("fab02abaf400eef4ebfd")', text) + self.assertIn('"expected_debug_value": 42', text) + self.assertIn('"expected_exit_code": expected_exit', text) + self.assertIn("rc == expected_exit", text) + self.assertIn("pkg == expected_pkg", text) + self.assertIn('"nested_kvm_vcpu_executed"', text) + self.assertIn('"p5_nested_kvm"', text) + self.assertIn('"nested_kvm_indicators_via_ssh"', text) + self.assertIn('"pve_qemu_source_commit": "684796e835289dab11af8606fbf7358b93526dd6"', text) + self.assertIn('"pve_qemu_submodule_commit": "98b060da3a4f92b2a994ead5b16a87e783baf77c"', text) + self.assertIn('"exit_code_rule": "(value << 1) | 1"', text) + self.assertNotIn('NESTED_KVM="no"', text) + def test_truenas_is_pinned_and_digest_sidecar_is_required(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn("TrueNAS-26.0.0-BETA.3.iso", text) From 06958310c6e33582019b1b6cf64990e628a20ed2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:10:54 +0200 Subject: [PATCH 088/719] fix: run Proxmox P5 probe under bash --- scripts/gha_kvm_proxmox_rdte.sh | 2 +- tests/test_gha_kvm_system_rdte.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 21024edd6..ffc5255d5 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -523,7 +523,7 @@ probe_nested_kvm_vcpu() { } response="$(sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ - root@127.0.0.1 'sh -s' 2>/dev/null <<'REMOTE' || true + root@127.0.0.1 'bash -s' 2>/dev/null <<'REMOTE' || true set +e EXPECTED_EXIT=85 EXPECTED_PACKAGE="11.0.0-3" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 5bd666a26..005b20c60 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -142,6 +142,8 @@ def test_proxmox_hostfwd_is_diagnostic_not_guest_or_nested_oracle(self): def test_proxmox_p5_requires_actual_nested_vcpu_execution(self): text = PVE.read_text(encoding="utf-8") self.assertIn("probe_nested_kvm_vcpu", text) + self.assertIn("root@127.0.0.1 'bash -s'", text) + self.assertNotIn("root@127.0.0.1 'sh -s'", text) self.assertIn("-accel kvm", text) self.assertIn("-cpu host", text) self.assertIn("isa-debug-exit,iobase=0xf4,iosize=0x4", text) From 41df67908b52eb51565a418288a17ab9b299137d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:16:58 +0200 Subject: [PATCH 089/719] exp: exercise Proxmox P3 LXC lifecycle --- scripts/gha_kvm_proxmox_rdte.sh | 163 +++++++++++++++++++++++++++++- tests/test_gha_kvm_system_rdte.py | 20 ++++ 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index ffc5255d5..79347fb2c 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -6,6 +6,11 @@ ISO_URL="https://enterprise.proxmox.com/iso/$ISO_NAME" ISO_SHA256="4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c" PVE_INSTALLER_SOURCE_VERSION="9.2.5" PVE_INSTALLER_SOURCE_COMMIT="32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9" +P3_PVE_CONTAINER_SOURCE_COMMIT="5eb5574ee9158ac40a5230de2cf18d7d6345709f" +P3_TEMPLATE_NAME="debian-13-standard_13.1-2_amd64.tar.zst" +P3_TEMPLATE_URL="https://download.proxmox.com/images/system/$P3_TEMPLATE_NAME" +P3_TEMPLATE_SHA512="5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8" +P3_VMID=9101 RAM_MIB=4096 VCPUS=2 DISK_SIZE="40G" @@ -44,6 +49,7 @@ API_OBSERVATION_ROUTE="" NESTED_KVM="unknown" NESTED_KVM_INDICATORS="unknown" NESTED_KVM_VCPU_JSON="" +P3_LXC_JSON="" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" @@ -74,7 +80,7 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" - export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" + export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" R_P3_LXC="$P3_LXC_JSON" export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_GUEST_LOCAL_API="$GUEST_LOCAL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" @@ -94,6 +100,7 @@ def load_json_env(name): return {"inspection_ok": False, "parse_error": str(exc), "raw": raw[:2000]} nested_vcpu = load_json_env("R_NESTED_VCPU") +p3_lxc = load_json_env("R_P3_LXC") payload = { "contract": "gha-kvm-system-lab/v1", @@ -117,6 +124,9 @@ payload = { "pve_qemu_submodule_commit": "98b060da3a4f92b2a994ead5b16a87e783baf77c", "pve_qemu_debugexit_source": "hw/misc/debugexit.c", "pve_qemu_expected_package": "11.0.0-3", + "pve_container_source_commit": "5eb5574ee9158ac40a5230de2cf18d7d6345709f", + "p3_template": "debian-13-standard_13.1-2_amd64.tar.zst", + "p3_template_catalog_sha512": "5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8", "iso_first_boot_package": os.environ.get("R_ISO_FIRST_BOOT_PACKAGE") or None, }, "oracles": { @@ -127,10 +137,12 @@ payload = { "nested_kvm_indicators_via_ssh": os.environ.get("R_NESTED_INDICATORS") == "yes", "nested_kvm_observed_via_ssh": os.environ.get("R_NESTED") == "yes", "nested_kvm_vcpu_executed": bool(nested_vcpu and nested_vcpu.get("oracleSatisfied") is True), + "p3_lxc_lifecycle_exercised": bool(p3_lxc and p3_lxc.get("oracleSatisfied") is True), }, "api_version": json.loads(os.environ["R_API"]) if os.environ.get("R_API") else None, "nested_kvm": os.environ.get("R_NESTED"), "p5_nested_kvm": nested_vcpu, + "p3_lxc": p3_lxc, "diagnostics": { "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), "ssh_hostfwd_accepted": os.environ.get("R_SSH_HOSTFWD_ACCEPTED") == "true", @@ -150,6 +162,7 @@ payload = { "Disposable virtual-hardware target profile; not physical-HBA/SMART/IPMI/HA qualification.", "Nested KVM is a separate oracle from Proxmox management-plane support.", "P5 nested KVM requires p5_nested_kvm.oracleSatisfied=true from an actual nested vCPU debug-exit; device/CPU flags alone are diagnostic.", + "P3 LXC is a separate lifecycle oracle using one exact public Debian template; P2/P5 are not inferred from it.", ], } pathlib.Path(os.environ["R_OUT"]).write_text(json.dumps(payload, indent=2, sort_keys=True)+"\n", encoding="utf-8") @@ -515,6 +528,148 @@ guest_diagnostics() { } + +probe_lxc_lifecycle() { + local template_host="$STATE_DIR/$P3_TEMPLATE_NAME" + local template_guest="/var/lib/vz/template/cache/$P3_TEMPLATE_NAME" + local observed_sha="" response="" + + if ! command -v sha512sum >/dev/null 2>&1 || ! command -v scp >/dev/null 2>&1; then + printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"host-prerequisite","detail":"sha512sum or scp unavailable on GHA host"}' + return 0 + fi + if ! curl --fail --location --retry 3 --silent --show-error "$P3_TEMPLATE_URL" -o "$template_host"; then + printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"template-acquire","detail":"exact public LXC template download failed"}' + return 0 + fi + observed_sha="$(sha512sum "$template_host" | awk '{print $1}')" + if [[ "$observed_sha" != "$P3_TEMPLATE_SHA512" ]]; then + R_OBS="$observed_sha" R_EXP="$P3_TEMPLATE_SHA512" python3 - <<'PY' +import json, os +print(json.dumps({"contract":"proxmox-lxc-lifecycle/v1","classification":"ORACLE_FAILURE","oracleSatisfied":False,"phase":"template-integrity","detail":"downloaded template SHA-512 did not match exact admitted PVE catalog","expected_sha512":os.environ["R_EXP"],"observed_sha512":os.environ["R_OBS"]},separators=(",",":"))) +PY + rm -f -- "$template_host" + return 0 + fi + + if ! sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 "test ! -e '$template_guest' && install -d -m 0755 /var/lib/vz/template/cache" >/dev/null 2>&1; then + rm -f -- "$template_host" + printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"template-stage","detail":"template destination already existed or guest cache could not be prepared"}' + return 0 + fi + if ! sshpass -p "$ROOT_PASSWORD" scp -q -P "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "$template_host" "root@127.0.0.1:$template_guest"; then + rm -f -- "$template_host" + printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"template-stage","detail":"exact template could not be copied into PVE"}' + return 0 + fi + rm -f -- "$template_host" + + response="$(sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 'bash -s' 2>/dev/null <<'REMOTE' || true +set +e +VMID=9101 +TEMPLATE_NAME="debian-13-standard_13.1-2_amd64.tar.zst" +TEMPLATE="/var/lib/vz/template/cache/$TEMPLATE_NAME" +EXPECTED_SHA512="5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8" +EXPECTED_PVE_CONTAINER="6.1.10" +CENSUS="$(mktemp)"; CREATE_LOG="$(mktemp)"; START_LOG="$(mktemp)"; STOP_LOG="$(mktemp)"; DESTROY_LOG="$(mktemp)"; CONFIG_SNAPSHOT="$(mktemp)" +cleanup() { + if pct status "$VMID" >/dev/null 2>&1; then + pct stop "$VMID" >/dev/null 2>&1 || true + pct destroy "$VMID" --purge 1 >/dev/null 2>&1 || true + fi + rm -f -- "$TEMPLATE" +} +trap 'cleanup >/dev/null 2>&1' EXIT + +OBS_SHA="$(sha512sum "$TEMPLATE" 2>/dev/null | awk '{print $1}')" +PKG="$(dpkg-query -W -f='${Version}' pve-container 2>/dev/null || true)" +STORAGE_STATUS="$(pvesm status --storage local-lvm 2>&1 || true)" +STORAGE_ACTIVE=false +printf '%s\n' "$STORAGE_STATUS" | awk 'NR>1 && $1=="local-lvm" && $3=="active"{ok=1} END{exit !ok}' && STORAGE_ACTIVE=true +PRECONDITION="" +[[ "$OBS_SHA" == "$EXPECTED_SHA512" ]] || PRECONDITION="template-sha512-mismatch" +[[ "$PKG" == "$EXPECTED_PVE_CONTAINER" ]] || [[ -n "$PRECONDITION" ]] || PRECONDITION="pve-container-version-mismatch" +[[ "$STORAGE_ACTIVE" == true ]] || [[ -n "$PRECONDITION" ]] || PRECONDITION="local-lvm-inactive" +if pct status "$VMID" >/dev/null 2>&1 || [[ -e "/etc/pve/lxc/$VMID.conf" ]]; then [[ -n "$PRECONDITION" ]] || PRECONDITION="vmid-preexisting"; fi + +CREATE_RC=125; START_RC=125; EXEC_RC=125; STOP_RC=125; DESTROY_RC=125; RUNNING=false; CLEANUP_OK=false +if [[ -z "$PRECONDITION" ]]; then + pct create "$VMID" "local:vztmpl/$TEMPLATE_NAME" --rootfs local-lvm:2 --memory 256 --cores 1 --unprivileged 1 \ + --hostname rdte-p3 --net0 "name=eth0,bridge=vmbr0,ip=10.0.2.16/24,type=veth" >"$CREATE_LOG" 2>&1 + CREATE_RC=$? + if [[ "$CREATE_RC" -eq 0 ]]; then + pct config "$VMID" >"$CONFIG_SNAPSHOT" 2>&1 || true + pct start "$VMID" >"$START_LOG" 2>&1; START_RC=$? + if [[ "$START_RC" -eq 0 ]]; then + for _ in $(seq 1 20); do if pct status "$VMID" 2>/dev/null | grep -Fq 'status: running'; then RUNNING=true; break; fi; sleep 1; done + if [[ "$RUNNING" == true ]]; then + pct exec "$VMID" -- sh -c ' + echo P3_CENSUS_BEGIN + printf "HOSTNAME="; hostname 2>&1 || true + echo UNAME_BEGIN; uname -a 2>&1 || true; echo UNAME_END + echo CPU_BEGIN; grep -E "^(processor|vendor_id|model name|flags|Features)" /proc/cpuinfo 2>&1 | head -n 40 || true; echo CPU_END + echo MEM_BEGIN; head -n 30 /proc/meminfo 2>&1 || true; echo MEM_END + echo CGROUP_BEGIN; cat /proc/self/cgroup 2>&1 || true; echo CGROUP_END + echo MOUNTS_BEGIN; head -n 80 /proc/mounts 2>&1 || true; echo MOUNTS_END + echo NET_BEGIN; ip -brief address 2>&1 || true; echo NET_END + echo ROUTE_BEGIN; ip route 2>&1 || true; echo ROUTE_END + echo DNS_BEGIN; cat /etc/resolv.conf 2>&1 || true; echo DNS_END + echo DEV_BEGIN; ls -la /dev 2>&1 | head -n 80 || true; echo DEV_END + echo P3_CENSUS_END + ' >"$CENSUS" 2>&1 + EXEC_RC=$? + fi + fi + fi +fi + +if pct status "$VMID" >/dev/null 2>&1; then + if pct status "$VMID" 2>/dev/null | grep -Fq 'status: running'; then pct stop "$VMID" >"$STOP_LOG" 2>&1; STOP_RC=$?; else STOP_RC=0; fi + pct destroy "$VMID" --purge 1 >"$DESTROY_LOG" 2>&1; DESTROY_RC=$? +else + STOP_RC=0; DESTROY_RC=0 +fi +rm -f -- "$TEMPLATE" +CONFIG_ABSENT=true; VOLUME_ABSENT=true; TEMPLATE_ABSENT=true +[[ ! -e "/etc/pve/lxc/$VMID.conf" ]] || CONFIG_ABSENT=false +if pvesm list local-lvm 2>/dev/null | grep -Eq "(vm|subvol)-$VMID-"; then VOLUME_ABSENT=false; fi +[[ ! -e "$TEMPLATE" ]] || TEMPLATE_ABSENT=false +[[ "$CONFIG_ABSENT" == true && "$VOLUME_ABSENT" == true && "$TEMPLATE_ABSENT" == true ]] && CLEANUP_OK=true + +CENSUS_TEXT="$(tail -c 12000 "$CENSUS" 2>/dev/null || true)" +CONFIG_TEXT="$(tail -c 6000 "$CONFIG_SNAPSHOT" 2>/dev/null || true)" +CREATE_TEXT="$(tail -c 4000 "$CREATE_LOG" 2>/dev/null || true)"; START_TEXT="$(tail -c 4000 "$START_LOG" 2>/dev/null || true)" +STOP_TEXT="$(tail -c 4000 "$STOP_LOG" 2>/dev/null || true)"; DESTROY_TEXT="$(tail -c 4000 "$DESTROY_LOG" 2>/dev/null || true)" +R_PRE="$PRECONDITION" R_SHA="$OBS_SHA" R_PKG="$PKG" R_STORAGE="$STORAGE_STATUS" R_CREATE="$CREATE_RC" R_START="$START_RC" R_EXEC="$EXEC_RC" \ +R_STOP="$STOP_RC" R_DESTROY="$DESTROY_RC" R_RUNNING="$RUNNING" R_CLEAN="$CLEANUP_OK" R_CFGABS="$CONFIG_ABSENT" R_VOLABS="$VOLUME_ABSENT" R_TPLABS="$TEMPLATE_ABSENT" \ +R_CENSUS="$CENSUS_TEXT" R_CONFIG="$CONFIG_TEXT" R_CREATE_LOG="$CREATE_TEXT" R_START_LOG="$START_TEXT" R_STOP_LOG="$STOP_TEXT" R_DESTROY_LOG="$DESTROY_TEXT" \ +python3 - <<'PY' +import json, os +pre=os.environ.get("R_PRE",""); create=int(os.environ["R_CREATE"]); start=int(os.environ["R_START"]); exe=int(os.environ["R_EXEC"]); stop=int(os.environ["R_STOP"]); destroy=int(os.environ["R_DESTROY"]) +running=os.environ["R_RUNNING"]=="true"; cleanup=os.environ["R_CLEAN"]=="true" +ok=(not pre and create==0 and start==0 and running and exe==0 and stop==0 and destroy==0 and cleanup) +classification="SUPPORTED" if ok else ("ENVIRONMENT_FAILURE" if pre else "ORACLE_FAILURE") +detail="run-owned unprivileged LXC completed create/start/exec/stop/destroy with zero experiment residue" if ok else (pre or "LXC lifecycle did not satisfy the bounded create/start/exec/stop/destroy/cleanup oracle") +print(json.dumps({ + "contract":"proxmox-lxc-lifecycle/v1","classification":classification,"oracleSatisfied":ok,"phase":"lxc-lifecycle","detail":detail,"vmid":9101, + "template":{"name":"debian-13-standard_13.1-2_amd64.tar.zst","expected_sha512":"5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8","observed_sha512":os.environ.get("R_SHA") or None}, + "pve_container_version":os.environ.get("R_PKG") or None,"source":{"pve_container_commit":"5eb5574ee9158ac40a5230de2cf18d7d6345709f"},"storage_status":os.environ.get("R_STORAGE") or None, + "steps":{"create":create,"start":start,"running":running,"exec":exe,"stop":stop,"destroy":destroy}, + "cleanup":{"config_absent":os.environ["R_CFGABS"]=="true","root_volume_absent":os.environ["R_VOLABS"]=="true","template_absent":os.environ["R_TPLABS"]=="true","zero_residue":cleanup}, + "container_config":os.environ.get("R_CONFIG") or None,"census":os.environ.get("R_CENSUS") or None, + "logs":{"create":os.environ.get("R_CREATE_LOG") or None,"start":os.environ.get("R_START_LOG") or None,"stop":os.environ.get("R_STOP_LOG") or None,"destroy":os.environ.get("R_DESTROY_LOG") or None}, +},sort_keys=True,separators=(",",":"))) +PY +trap - EXIT +rm -f -- "$CENSUS" "$CREATE_LOG" "$START_LOG" "$STOP_LOG" "$DESTROY_LOG" "$CONFIG_SNAPSHOT" +REMOTE +)" + if [[ -n "$response" ]]; then printf '%s' "$response"; else printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"transport","detail":"guest SSH did not return an LXC receipt"}'; fi +} + probe_nested_kvm_vcpu() { local response="" command -v sshpass >/dev/null 2>&1 || { @@ -878,4 +1033,10 @@ then NESTED_KVM="yes" fi +if [[ "$NESTED_KVM" == "yes" ]]; then + P3_LXC_JSON="$(probe_lxc_lifecycle || true)" +else + P3_LXC_JSON='{"contract":"proxmox-lxc-lifecycle/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"prerequisite","detail":"P5 nested-vCPU oracle was not satisfied in this rep"}' +fi + write_receipt SUPPORTED true complete "real Proxmox VE unattended install completed and real HTTPS /api2/json/version answered via $API_OBSERVATION_ROUTE" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 005b20c60..04ccfbcd0 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -160,6 +160,26 @@ def test_proxmox_p5_requires_actual_nested_vcpu_execution(self): self.assertIn('"exit_code_rule": "(value << 1) | 1"', text) self.assertNotIn('NESTED_KVM="no"', text) + + def test_proxmox_p3_uses_exact_public_template_and_supported_pct_lifecycle(self): + text = PVE.read_text(encoding="utf-8") + self.assertIn('P3_TEMPLATE_NAME="debian-13-standard_13.1-2_amd64.tar.zst"', text) + self.assertIn('P3_TEMPLATE_SHA512="5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8"', text) + self.assertIn('P3_PVE_CONTAINER_SOURCE_COMMIT="5eb5574ee9158ac40a5230de2cf18d7d6345709f"', text) + self.assertIn("probe_lxc_lifecycle", text) + self.assertIn('pct create "$VMID"', text) + self.assertIn('pct start "$VMID"', text) + self.assertIn('pct exec "$VMID"', text) + self.assertIn('pct stop "$VMID"', text) + self.assertIn('pct destroy "$VMID" --purge 1', text) + self.assertIn("--unprivileged 1", text) + self.assertIn("--rootfs local-lvm:2", text) + self.assertIn('"p3_lxc_lifecycle_exercised"', text) + self.assertIn('"p3_lxc": p3_lxc', text) + self.assertIn('"zero_residue":cleanup', text) + self.assertIn('if [[ "$NESTED_KVM" == "yes" ]]; then', text) + self.assertNotIn("nsenter", text) + def test_truenas_is_pinned_and_digest_sidecar_is_required(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn("TrueNAS-26.0.0-BETA.3.iso", text) From e1ce49db96f969efe4e9079f874b88ec19b0c61b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:43:48 +0200 Subject: [PATCH 090/719] Fix LiteLLM T6 preflight client check --- scripts/gha_kvm_truenas_rdte.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 0e6dfd1a6..833a5cab1 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -181,8 +181,8 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T5 lifecycle client" fi if [[ "$RUNG" == "t6" ]]; then - [[ -f "$SCRIPT_DIR/truenas_middleware_foundry_control_probe.py" ]] || - fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 Foundry control client" + [[ -f "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 LiteLLM control client" fi fi for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do From 68f327abc3b8aae43f90bc4c446898e3e7240117 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:44:03 +0200 Subject: [PATCH 091/719] Lock LiteLLM T6 preflight client contract --- tests/test_truenas_t6_litellm_control.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_truenas_t6_litellm_control.py b/tests/test_truenas_t6_litellm_control.py index 584bf3522..778f3e699 100644 --- a/tests/test_truenas_t6_litellm_control.py +++ b/tests/test_truenas_t6_litellm_control.py @@ -144,6 +144,15 @@ def test_workflow_and_harness_are_bound_to_exact_litellm_export(self): 'python3 "$SCRIPT_DIR/truenas_middleware_foundry_control_probe.py"', harness, ) + self.assertIn( + '[[ -f "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" ]] ||', + harness, + ) + self.assertIn("missing TrueNAS T6 LiteLLM control client", harness) + self.assertNotIn( + '[[ -f "$SCRIPT_DIR/truenas_middleware_foundry_control_probe.py" ]] ||', + harness, + ) From 96c55ffafe8e31d1d21a658be210a80f3ea7c3b1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:20:56 +0200 Subject: [PATCH 092/719] RDTE: add exact TrueNAS target registry --- config/truenas-rdte-targets.json | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 config/truenas-rdte-targets.json diff --git a/config/truenas-rdte-targets.json b/config/truenas-rdte-targets.json new file mode 100644 index 000000000..d8bee7404 --- /dev/null +++ b/config/truenas-rdte-targets.json @@ -0,0 +1,50 @@ +{ + "schema": "gha-kvm-truenas-targets/v1", + "policy": { + "exact_version_required": true, + "vendor_digest_sidecar_required": true, + "floating_target_prohibited": true, + "heavyweight_slot": "shared-one-at-a-time" + }, + "targets": [ + { + "version": "25.04.1", + "channel": "deployed-legacy", + "system_version": "TrueNAS-SCALE-25.04.1", + "iso_name": "TrueNAS-SCALE-25.04.1.iso", + "iso_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Fangtooth/25.04.1/TrueNAS-SCALE-25.04.1.iso", + "sha256_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Fangtooth/25.04.1/TrueNAS-SCALE-25.04.1.iso.sha256", + "middleware_ref": "release/25.04.1", + "middleware_commit": "74ab5a2d373be4097dece257d00e1086376333ba", + "foundry_profile": ".foundry/truenas-compatibility/25.04.1-materialization.json", + "ha_apps_gate": "system.license:JAILS", + "authority_issue": 411 + }, + { + "version": "25.10.7", + "channel": "stable", + "system_version": "TrueNAS-SCALE-25.10.7", + "iso_name": "TrueNAS-SCALE-25.10.7.iso", + "iso_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Goldeye/25.10.7/TrueNAS-SCALE-25.10.7.iso", + "sha256_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Goldeye/25.10.7/TrueNAS-SCALE-25.10.7.iso.sha256", + "middleware_ref": "TS-25.10.7", + "middleware_commit": "8ede398839710e56893d88ce85088139d8fab18e", + "foundry_profile": ".foundry/truenas-compatibility/25.10.7-materialization.json", + "ha_apps_gate": "system.license:JAILS", + "authority_issue": 410 + }, + { + "version": "26.0.0-BETA.3", + "channel": "early-release", + "system_version": "TrueNAS-26.0.0-BETA.3", + "iso_name": "TrueNAS-26.0.0-BETA.3.iso", + "iso_url": "https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3/TrueNAS-26.0.0-BETA.3.iso", + "sha256_url": "https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3/TrueNAS-26.0.0-BETA.3.iso.sha256", + "middleware_ref": "TS-26.0.0-BETA.3", + "middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", + "foundry_profile": ".foundry/truenas-compatibility/26.0.0-BETA.3-materialization.json", + "ha_apps_gate": "system.feature_enabled:APPS", + "authority_issue": 396 + } + ] +} From cdf60c89a238ca7f283362b26f9dd7d09b21ca7c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:20:59 +0200 Subject: [PATCH 093/719] RDTE: add exact TrueNAS target resolver --- scripts/truenas_rdte_target.py | 74 ++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 scripts/truenas_rdte_target.py diff --git a/scripts/truenas_rdte_target.py b/scripts/truenas_rdte_target.py new file mode 100644 index 000000000..e59e0f087 --- /dev/null +++ b/scripts/truenas_rdte_target.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +from __future__ import annotations +import argparse, json, pathlib, re, shlex, sys + +SHA_RE = re.compile(r"^[0-9a-f]{40}$") + +class TargetError(RuntimeError): + pass + +def load_registry(path: pathlib.Path): + try: + doc=json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise TargetError(f"cannot read target registry: {exc}") from exc + if not isinstance(doc, dict) or doc.get("schema") != "gha-kvm-truenas-targets/v1": + raise TargetError("unsupported target registry schema") + targets=doc.get("targets") + if not isinstance(targets, list) or not targets: + raise TargetError("target registry has no targets") + seen=set() + result={} + for i,t in enumerate(targets): + if not isinstance(t, dict): + raise TargetError(f"targets[{i}] must be an object") + version=t.get("version") + if not isinstance(version,str) or not version or version in seen: + raise TargetError(f"invalid or duplicate target version at index {i}") + seen.add(version) + for key in ("system_version","iso_name","iso_url","sha256_url","middleware_ref","middleware_commit","foundry_profile","ha_apps_gate"): + if not isinstance(t.get(key),str) or not t[key]: + raise TargetError(f"{version}: missing {key}") + if not SHA_RE.fullmatch(t["middleware_commit"]): + raise TargetError(f"{version}: middleware_commit must be exact 40-hex") + if any(token in t["iso_url"].lower() for token in ("latest","nightly","master+")): + raise TargetError(f"{version}: supported target ISO must be exact, not floating") + if t["sha256_url"] != t["iso_url"] + ".sha256": + raise TargetError(f"{version}: sha256_url must be exact ISO sidecar") + result[version]=t + return result + +def shell(target): + fields={ + "VERSION":target["version"], + "EXPECTED_SYSTEM_VERSION":target["system_version"], + "ISO_NAME":target["iso_name"], + "ISO_URL":target["iso_url"], + "SHA_URL":target["sha256_url"], + "MIDDLEWARE_REF":target["middleware_ref"], + "MIDDLEWARE_COMMIT":target["middleware_commit"], + "FOUNDRY_PROFILE":target["foundry_profile"], + "HA_APPS_GATE":target["ha_apps_gate"], + "AUTHORITY_ISSUE":str(target.get("authority_issue","")), + } + return "\n".join(f"{k}={shlex.quote(v)}" for k,v in fields.items()) + +def main(): + p=argparse.ArgumentParser() + p.add_argument("--registry", type=pathlib.Path, required=True) + p.add_argument("--version", required=True) + p.add_argument("--shell", action="store_true") + a=p.parse_args() + try: + targets=load_registry(a.registry) + if a.version not in targets: + raise TargetError(f"exact TrueNAS target is not registered: {a.version}") + target=targets[a.version] + print(shell(target) if a.shell else json.dumps(target, indent=2, sort_keys=True)) + return 0 + except TargetError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + +if __name__ == "__main__": + raise SystemExit(main()) From b04eb1452fe5d38feeadb6f27546c714778f58e6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:21:01 +0200 Subject: [PATCH 094/719] RDTE: test exact TrueNAS target matrix --- tests/test_truenas_rdte_target.py | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/test_truenas_rdte_target.py diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py new file mode 100644 index 000000000..d1a53fb37 --- /dev/null +++ b/tests/test_truenas_rdte_target.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import importlib.util, json, pathlib, unittest + +ROOT=pathlib.Path(__file__).resolve().parents[1] +SPEC=importlib.util.spec_from_file_location("truenas_rdte_target", ROOT/"scripts"/"truenas_rdte_target.py") +MOD=importlib.util.module_from_spec(SPEC); assert SPEC.loader; SPEC.loader.exec_module(MOD) +REG=ROOT/"config"/"truenas-rdte-targets.json" + +class TargetRegistryTests(unittest.TestCase): + def setUp(self): + self.targets=MOD.load_registry(REG) + def test_exact_supported_matrix(self): + self.assertEqual(set(self.targets), {"25.04.1","25.10.7","26.0.0-BETA.3"}) + def test_exact_source_anchors(self): + self.assertEqual(self.targets["25.04.1"]["middleware_commit"], "74ab5a2d373be4097dece257d00e1086376333ba") + self.assertEqual(self.targets["25.10.7"]["middleware_commit"], "8ede398839710e56893d88ce85088139d8fab18e") + self.assertEqual(self.targets["26.0.0-BETA.3"]["middleware_commit"], "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3") + def test_vendor_media_is_exact_and_has_digest_sidecar(self): + for version,target in self.targets.items(): + self.assertIn(version, target["iso_name"]) + self.assertEqual(target["sha256_url"], target["iso_url"]+".sha256") + lower=target["iso_url"].lower() + self.assertNotIn("latest", lower) + self.assertNotIn("nightly", lower) + self.assertNotIn("master+", lower) + def test_apps_gate_deltas_are_not_normalized_away(self): + self.assertEqual(self.targets["25.04.1"]["ha_apps_gate"], "system.license:JAILS") + self.assertEqual(self.targets["25.10.7"]["ha_apps_gate"], "system.license:JAILS") + self.assertEqual(self.targets["26.0.0-BETA.3"]["ha_apps_gate"], "system.feature_enabled:APPS") + def test_unknown_exact_version_fails_closed(self): + with self.assertRaises(MOD.TargetError): + targets=self.targets + if "26.0.0-RC.1" not in targets: + raise MOD.TargetError("exact TrueNAS target is not registered: 26.0.0-RC.1") + +if __name__=="__main__": + unittest.main() From fbbed556247ba377e5246276f400ccc5a23be43d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:24:35 +0200 Subject: [PATCH 095/719] RDTE: make T4 Apps probe exact-target driven --- scripts/truenas_middleware_app_probe.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/truenas_middleware_app_probe.py b/scripts/truenas_middleware_app_probe.py index 8dce26b8d..9f70f41b7 100755 --- a/scripts/truenas_middleware_app_probe.py +++ b/scripts/truenas_middleware_app_probe.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Bounded TrueNAS BETA.3 Apps T4 oracle using source-defined middleware APIs.""" +"""Bounded exact-version TrueNAS Apps T4 oracle using source-defined middleware APIs.""" from __future__ import annotations import argparse @@ -21,6 +21,10 @@ def main(): p.add_argument("--password-file", required=True) p.add_argument("--out", required=True) p.add_argument("--pool-name", default="rdtepool") + p.add_argument("--expected-version", required=True) + p.add_argument("--middleware-ref", required=True) + p.add_argument("--middleware-commit", required=True) + p.add_argument("--ha-apps-gate", required=True) p.add_argument("--tls", action="store_true") p.add_argument("--timeout", type=float, default=8.0) p.add_argument("--job-timeout", type=float, default=300.0) @@ -31,10 +35,11 @@ def main(): payload = { "schema": "truenas-middleware-app-t4/v1", "source_contract": { - "middleware_tag": "TS-26.0.0-BETA.3", - "middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", + "middleware_ref": a.middleware_ref, + "middleware_commit": a.middleware_commit, "apps_gate": "docker.license_active", - "apps_gate_behavior": "non-HA targets permitted directly; HA consults system.feature_enabled(APPS)", + "ha_apps_gate": a.ha_apps_gate, + "expected_version": a.expected_version, }, "oracleSatisfied": False, "classification": "ORACLE_FAILURE", @@ -89,7 +94,10 @@ def wait_job(job_id, label): raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") payload["system_version"] = call("system.version", []) - # BETA.3 uses docker.license_active internally; the later truenas.entitlements API is absent. + if payload["system_version"] != a.expected_version: + raise RuntimeError( + f"target version changed: expected {a.expected_version!r}, got {payload['system_version']!r}" + ) payload["docker_before"] = call("docker.config", []) existing = call("app.query", [[[ "id", "=", APP_NAME ]]]) if existing: From 3d39ef56c018531ec9dc47a07e9f44bbe4af253d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:24:46 +0200 Subject: [PATCH 096/719] RDTE: make T5 lifecycle probe exact-target driven --- scripts/truenas_middleware_app_lifecycle_probe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/truenas_middleware_app_lifecycle_probe.py b/scripts/truenas_middleware_app_lifecycle_probe.py index c51eba64e..53dd7f391 100755 --- a/scripts/truenas_middleware_app_lifecycle_probe.py +++ b/scripts/truenas_middleware_app_lifecycle_probe.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Bounded TrueNAS 26.0.0-BETA.3 T5 custom-app lifecycle oracle.""" +"""Bounded exact-version TrueNAS T5 custom-app lifecycle oracle.""" from __future__ import annotations import argparse @@ -10,7 +10,6 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for -EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" APP_NAME = "rdte-t4-probe" APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10" @@ -21,6 +20,7 @@ def main() -> int: p.add_argument("--port", type=int, required=True) p.add_argument("--password-file", required=True) p.add_argument("--out", required=True) + p.add_argument("--expected-version", required=True) p.add_argument("--tls", action="store_true") p.add_argument("--timeout", type=float, default=8.0) p.add_argument("--job-timeout", type=float, default=300.0) @@ -32,7 +32,7 @@ def main() -> int: "schema": "truenas-app-lifecycle-t5/v2", "classification": "ORACLE_FAILURE", "oracleSatisfied": False, - "expected_version": EXPECTED_VERSION, + "expected_version": a.expected_version, "app_name": APP_NAME, "app_image": APP_IMAGE, "transport": "wss" if a.tls else "ws", @@ -132,9 +132,9 @@ def wait_state(expected, require_running=False): version = call("system.version", []) payload["system_version"] = version - if version != EXPECTED_VERSION: + if version != a.expected_version: raise RuntimeError( - f"target version changed: expected {EXPECTED_VERSION!r}, got {version!r}" + f"target version changed: expected {a.expected_version!r}, got {version!r}" ) initial = app_query() From 4017fba81b2fe45428ef3d49fc9a1fa312e88d06 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:25:15 +0200 Subject: [PATCH 097/719] RDTE: drive TrueNAS harness from exact target registry --- scripts/gha_kvm_truenas_rdte.sh | 39 +++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 833a5cab1..a6b7974c9 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -3,11 +3,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VERSION="26.0.0-BETA.3" -ISO_NAME="TrueNAS-26.0.0-BETA.3.iso" -BASE_URL="https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3" -ISO_URL="$BASE_URL/$ISO_NAME" -SHA_URL="$ISO_URL.sha256" +TARGET_REGISTRY="$SCRIPT_DIR/../config/truenas-rdte-targets.json" +TARGET_VERSION="26.0.0-BETA.3" RAM_MIB=8192 VCPUS=2 DISK_SIZE="24G" @@ -20,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--rung t0|t1|t2|t3|t4|t5|t6] [--foundry-control-dir DIR] [--foundry-commit SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--foundry-control-dir DIR] [--foundry-commit SHA]" } OUT="" @@ -32,6 +29,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; --state-dir) STATE_DIR="$2"; shift 2 ;; + --target-version) TARGET_VERSION="$2"; shift 2 ;; --rung) RUNG="$2"; shift 2 ;; --foundry-control-dir) FOUNDRY_CONTROL_DIR="$2"; shift 2 ;; --foundry-commit) FOUNDRY_COMMIT="$2"; shift 2 ;; @@ -40,8 +38,13 @@ while [[ $# -gt 0 ]]; do esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } +[[ -f "$TARGET_REGISTRY" ]] || { echo "missing exact TrueNAS target registry: $TARGET_REGISTRY" >&2; exit 2; } +TARGET_ENV="$(python3 "$SCRIPT_DIR/truenas_rdte_target.py" --registry "$TARGET_REGISTRY" --version "$TARGET_VERSION" --shell)" || exit 2 +# truenas_rdte_target.py emits only shell-quoted values after strict registry validation. +eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } if [[ "$RUNG" == "t6" ]]; then + [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "LiteLLM T6 is currently admitted only for exact TrueNAS 26.0.0-BETA.3; use Foundry F1-F5 for other targets" >&2; exit 2; } [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } FOUNDRY_CONTROL_DIR="$(realpath "$FOUNDRY_CONTROL_DIR")" @@ -87,6 +90,10 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_EXPECTED="$EXPECTED_ISO_SHA" R_GRUB="$GRUB_PATH" + export R_TARGET_VERSION="$VERSION" R_EXPECTED_SYSTEM_VERSION="$EXPECTED_SYSTEM_VERSION" + export R_ISO_NAME="$ISO_NAME" R_ISO_URL="$ISO_URL" R_SHA_URL="$SHA_URL" + export R_MIDDLEWARE_REF="$MIDDLEWARE_REF" R_MIDDLEWARE_COMMIT="$MIDDLEWARE_COMMIT" + export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" @@ -94,7 +101,7 @@ write_receipt() { import json, os, pathlib payload = { "contract": "gha-kvm-system-lab/v1", - "target": {"product": "truenas", "version": "26.0.0-BETA.3", "rung": os.environ.get("R_RUNG", "t0").upper()}, + "target": {"product": "truenas", "version": os.environ["R_TARGET_VERSION"], "rung": os.environ.get("R_RUNG", "t0").upper()}, "classification": os.environ["R_CLASS"], "oracleSatisfied": os.environ["R_ORACLE"].lower() == "true", "phase": os.environ["R_PHASE"], @@ -109,11 +116,17 @@ payload = { "app": ({"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} if os.environ.get("R_RUNG") in {"t4", "t5"} else None), }, "source": { - "iso_name": "TrueNAS-26.0.0-BETA.3.iso", - "iso_url": "https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3/TrueNAS-26.0.0-BETA.3.iso", - "vendor_sha256_url": "https://download.sys.truenas.net/TrueNAS-26-BETA/26.0.0-BETA.3/TrueNAS-26.0.0-BETA.3.iso.sha256", + "iso_name": os.environ["R_ISO_NAME"], + "iso_url": os.environ["R_ISO_URL"], + "vendor_sha256_url": os.environ["R_SHA_URL"], "expected_sha256": os.environ.get("R_EXPECTED") or None, "observed_sha256": os.environ.get("R_ISO_SHA") or None, + "middleware_ref": os.environ["R_MIDDLEWARE_REF"], + "middleware_commit": os.environ["R_MIDDLEWARE_COMMIT"], + "foundry_profile": os.environ["R_FOUNDRY_PROFILE"], + "ha_apps_gate": os.environ["R_HA_APPS_GATE"], + "system_version_expected": os.environ["R_EXPECTED_SYSTEM_VERSION"], + "authority_issue": int(os.environ["R_AUTHORITY_ISSUE"]) if os.environ.get("R_AUTHORITY_ISSUE") else None, }, "installer_grub_path": os.environ.get("R_GRUB") or None, "oracles": { @@ -527,6 +540,9 @@ python3 "$SCRIPT_DIR/truenas_middleware_app_probe.py" \ "${MIDDLEWARE_TLS_ARG[@]}" \ --password-file "$PASSWORD_FILE" \ --out "$APP_OUT" --pool-name "$DATA_POOL_NAME" \ + --expected-version "$EXPECTED_SYSTEM_VERSION" \ + --middleware-ref "$MIDDLEWARE_REF" --middleware-commit "$MIDDLEWARE_COMMIT" \ + --ha-apps-gate "$HA_APPS_GATE" \ --timeout 8 --job-timeout 300 --state-timeout 180 >/dev/null 2>&1 || true [[ -f "$APP_OUT" ]] || fail_evidence HARNESS_FAILURE apps-runtime "T4 Apps client did not emit a receipt" APP_RESULT_JSON="$(cat "$APP_OUT")" @@ -549,7 +565,8 @@ python3 "$SCRIPT_DIR/truenas_middleware_app_lifecycle_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ "${MIDDLEWARE_TLS_ARG[@]}" \ --password-file "$PASSWORD_FILE" \ - --out "$LIFECYCLE_OUT" --timeout 8 --job-timeout 300 --state-timeout 180 >/dev/null 2>&1 || true + --out "$LIFECYCLE_OUT" --expected-version "$EXPECTED_SYSTEM_VERSION" \ + --timeout 8 --job-timeout 300 --state-timeout 180 >/dev/null 2>&1 || true [[ -f "$LIFECYCLE_OUT" ]] || fail_evidence HARNESS_FAILURE app-lifecycle "T5 lifecycle client did not emit a receipt" LIFECYCLE_RESULT_JSON="$(cat "$LIFECYCLE_OUT")" LIFECYCLE_OK="$(python3 - "$LIFECYCLE_OUT" <<'PY' From 5fb0a1f8e2cbab7a447454583eddc1e7fde8cc29 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:26:54 +0200 Subject: [PATCH 098/719] RDTE: select exact TrueNAS version and system rung --- .github/workflows/gha-kvm-system-rdte.yml | 65 ++++++++++++++++++----- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 09623e474..bfb411c9e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -15,6 +15,28 @@ on: - both - proxmox - truenas + truenas_version: + description: "Exact TrueNAS target version" + required: true + default: "26.0.0-BETA.3" + type: choice + options: + - "26.0.0-BETA.3" + - "25.10.7" + - "25.04.1" + truenas_rung: + description: "TrueNAS system qualification rung" + required: true + default: t6 + type: choice + options: + - t0 + - t1 + - t2 + - t3 + - t4 + - t5 + - t6 permissions: contents: read @@ -31,6 +53,8 @@ jobs: proxmox: ${{ steps.scope.outputs.proxmox }} truenas: ${{ steps.scope.outputs.truenas }} qemu_topology: ${{ steps.scope.outputs.qemu_topology }} + truenas_version: ${{ steps.scope.outputs.truenas_version }} + truenas_rung: ${{ steps.scope.outputs.truenas_rung }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -40,6 +64,8 @@ jobs: env: EVENT_NAME: ${{ github.event_name }} DISPATCH_TARGET: ${{ inputs.target }} + DISPATCH_TRUENAS_VERSION: ${{ inputs.truenas_version }} + DISPATCH_TRUENAS_RUNG: ${{ inputs.truenas_rung }} BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.sha }} run: | @@ -47,6 +73,8 @@ jobs: proxmox=false truenas=false qemu_topology=false + truenas_version="${DISPATCH_TRUENAS_VERSION:-26.0.0-BETA.3}" + truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then case "$DISPATCH_TARGET" in proxmox) proxmox=true ;; @@ -60,7 +88,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -76,7 +104,9 @@ jobs: echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" echo "truenas=$truenas" >>"$GITHUB_OUTPUT" echo "qemu_topology=$qemu_topology" >>"$GITHUB_OUTPUT" - printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s\n' "$proxmox" "$truenas" "$qemu_topology" + echo "truenas_version=$truenas_version" >>"$GITHUB_OUTPUT" + echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" + printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" contract: runs-on: ubuntu-24.04 @@ -86,11 +116,11 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] - if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' uses: SemperSupra/truenas-app-foundry/.github/workflows/export-litellm-t6-control.yml@4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba with: foundry_ref: 4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba @@ -184,7 +214,7 @@ jobs: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') && - needs['foundry-t6-control'].result == 'success' + (needs.changes.outputs.truenas_rung != 't6' || needs['foundry-t6-control'].result == 'success') runs-on: ubuntu-26.04 timeout-minutes: 50 steps: @@ -192,6 +222,7 @@ jobs: with: persist-credentials: false - name: Download exact LiteLLM Foundry T6 control + if: needs.changes.outputs.truenas_rung == 't6' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: litellm-truenas-t6-control @@ -206,24 +237,34 @@ jobs: run: | set -euo pipefail mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" - bash scripts/gha_kvm_truenas_rdte.sh \ - --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" \ - --rung t6 \ - --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" \ - --foundry-commit "4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" \ + args=( + --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" + --target-version "${{ needs.changes.outputs.truenas_version }}" + --rung "${{ needs.changes.outputs.truenas_rung }}" --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + ) + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then + args+=( + --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" + --foundry-commit "4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" + ) + fi + bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence if: always() + env: + TRUENAS_VERSION: ${{ needs.changes.outputs.truenas_version }} + TRUENAS_RUNG: ${{ needs.changes.outputs.truenas_rung }} run: | set -euo pipefail receipt="$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" if [ ! -f "$receipt" ]; then python3 - "$receipt" <<'PY' - import json, pathlib, sys + import json, os, pathlib, sys pathlib.Path(sys.argv[1]).write_text(json.dumps({ "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":"26.0.0-BETA.3","rung":"T6"}, + "target":{"product":"truenas","version":os.environ["TRUENAS_VERSION"],"rung":os.environ["TRUENAS_RUNG"].upper()}, "classification":"HARNESS_FAILURE", "oracleSatisfied":False, "phase":"workflow-fallback", From 207f9e6c693754c2f27767b36957f9ae89b7838e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:27:20 +0200 Subject: [PATCH 099/719] RDTE: include exact target registry in static gate --- .github/workflows/prep-rdte-static.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 0a5e62242..d76c2ea38 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -9,11 +9,15 @@ on: - '.github/workflows/gha-kvm-system-rdte.yml' - 'config/truenas-rdte-targets.json' - 'scripts/gha_kvm_truenas_rdte.sh' + - 'scripts/truenas_rdte_target.py' + - 'scripts/truenas_rdte_target.py' - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_middleware_*.py' - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' - 'tests/test_truenas_t6_litellm_control.py' + - 'tests/test_truenas_rdte_target.py' + - 'tests/test_truenas_rdte_target.py' pull_request: paths: - '.github/workflows/prep-rdte-static.yml' @@ -56,4 +60,7 @@ jobs: if [[ -f tests/test_truenas_t6_litellm_control.py ]]; then tests+=(tests/test_truenas_t6_litellm_control.py) fi + if [[ -f tests/test_truenas_rdte_target.py ]]; then + tests+=(tests/test_truenas_rdte_target.py) + fi python3 -m unittest -v "${tests[@]}" From 796e0eb3e19282a0925b13066d5bcbd12baec19e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:28:03 +0200 Subject: [PATCH 100/719] RDTE: regression-lock exact TrueNAS target selection --- tests/test_gha_kvm_system_rdte.py | 38 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index e15633e7e..ea25243e5 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -180,15 +180,18 @@ def test_proxmox_p3_uses_exact_public_template_and_supported_pct_lifecycle(self) self.assertIn('if [[ "$NESTED_KVM" == "yes" ]]; then', text) self.assertNotIn("nsenter", text) - def test_truenas_is_pinned_and_digest_sidecar_is_required(self): + def test_truenas_uses_exact_target_registry_and_vendor_digest_sidecar(self): text = TRUENAS.read_text(encoding="utf-8") - self.assertIn("TrueNAS-26.0.0-BETA.3.iso", text) - self.assertIn("TrueNAS-26.0.0-BETA.3.iso.sha256", text) + self.assertIn('TARGET_REGISTRY="$SCRIPT_DIR/../config/truenas-rdte-targets.json"', text) + self.assertIn('TARGET_VERSION="26.0.0-BETA.3"', text) + self.assertIn("--target-version", text) + self.assertIn("truenas_rdte_target.py", text) + self.assertIn('curl --fail --location --retry 3 --silent --show-error "$SHA_URL"', text) + self.assertIn('[[ "$OBSERVED_ISO_SHA" == "$EXPECTED_ISO_SHA" ]]', text) self.assertIn("RAM_MIB=8192", text) self.assertIn("installer-boot", text) self.assertIn("installer-rpc", text) self.assertIn("truenas_installer_rpc_probe.py", text) - self.assertNotIn("nightly", text.lower()) self.assertNotIn("xdotool", text) def test_no_literal_escaped_shell_parameter_expansions(self): @@ -219,6 +222,10 @@ def test_workflow_scopes_heavy_targets(self): self.assertIn("BEFORE_SHA:", text) self.assertIn('git diff --name-only "$BEFORE_SHA" "$AFTER_SHA"', text) self.assertIn("scripts/truenas_installer_rpc_probe.py", text) + self.assertIn("truenas_version:", text) + self.assertIn("truenas_rung:", text) + self.assertIn("needs.changes.outputs.truenas_version", text) + self.assertIn("needs.changes.outputs.truenas_rung", text) def test_truenas_t1_requires_local_rpc_probe(self): text = TRUENAS.read_text(encoding="utf-8") @@ -432,8 +439,12 @@ def test_truenas_t4_apps_contract(self): self.assertIn("docker.status", app) self.assertNotIn("truenas.entitlements.check", app) self.assertIn('"apps_gate": "docker.license_active"', app) - self.assertIn('"apps_gate_behavior"', app) - self.assertIn('"middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3"', app) + self.assertIn('"ha_apps_gate": a.ha_apps_gate', app) + self.assertIn('"middleware_commit": a.middleware_commit', app) + self.assertIn('"middleware_ref": a.middleware_ref', app) + self.assertIn('--expected-version "$EXPECTED_SYSTEM_VERSION"', text) + self.assertIn('--middleware-commit "$MIDDLEWARE_COMMIT"', text) + self.assertIn('--ha-apps-gate "$HA_APPS_GATE"', text) self.assertIn("app.create", app) self.assertIn("app.query", app) self.assertIn('payload["docker_update_result"]', app) @@ -459,7 +470,10 @@ def test_truenas_t5_lifecycle_harness_contract(self): self.assertIn('"app_lifecycle_exercised"', text) self.assertIn('"app_lifecycle"', text) self.assertIn("nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10", text) - self.assertIn('EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3"', lifecycle) + self.assertNotIn("EXPECTED_VERSION =", lifecycle) + self.assertIn('p.add_argument("--expected-version", required=True)', lifecycle) + self.assertIn('"expected_version": a.expected_version', lifecycle) + self.assertIn('--expected-version "$EXPECTED_SYSTEM_VERSION"', text) self.assertIn('APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', lifecycle) self.assertIn("scripts/truenas_middleware_app_lifecycle_probe.py", workflow) self.assertIn("--rung t6", workflow) @@ -522,12 +536,16 @@ def test_all_post_t0_rungs_share_installer_rpc_gate(self): self.assertNotIn('if [[ "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" ]]; then', text) self.assertIn("try_rpc_discovery", text) - def test_t4_uses_tagged_beta3_apps_gate(self): + def test_t4_uses_exact_target_profile_metadata(self): text = (ROOT / "scripts" / "truenas_middleware_app_probe.py").read_text(encoding="utf-8") self.assertNotIn("truenas.entitlements.check", text) - self.assertIn('"middleware_tag": "TS-26.0.0-BETA.3"', text) - self.assertIn('"middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3"', text) + self.assertIn('p.add_argument("--middleware-ref", required=True)', text) + self.assertIn('p.add_argument("--middleware-commit", required=True)', text) + self.assertIn('p.add_argument("--ha-apps-gate", required=True)', text) + self.assertIn('"middleware_ref": a.middleware_ref', text) + self.assertIn('"middleware_commit": a.middleware_commit', text) self.assertIn('"apps_gate": "docker.license_active"', text) + self.assertIn('"ha_apps_gate": a.ha_apps_gate', text) if __name__ == "__main__": From 9392029ee61d81377db6c29228d5170395d62aae Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:29:13 +0200 Subject: [PATCH 101/719] RDTE: align workflow assertions with version-rung matrix --- tests/test_gha_kvm_system_rdte.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index ea25243e5..bcf36bda9 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -421,7 +421,8 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn("truenas_middleware_pool_probe.py", text) self.assertIn('"data_pool_created"', text) self.assertIn('"data_pool"', text) - self.assertIn("--rung t6", workflow) + self.assertIn("truenas_rung:", workflow) + self.assertIn("needs.changes.outputs.truenas_rung", workflow) self.assertIn("scripts/truenas_middleware_pool_probe.py", workflow) shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) @@ -452,7 +453,8 @@ def test_truenas_t4_apps_contract(self): self.assertIn('"nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', app) self.assertNotIn('"nginx:1.27-alpine"', app) self.assertIn('"state") == "RUNNING"', app) - self.assertIn('--rung t6', workflow) + self.assertIn("truenas_rung:", workflow) + self.assertIn("needs.changes.outputs.truenas_rung", workflow) py = subprocess.run( ["python3", "-m", "py_compile", str(TRUENAS_APP)], text=True, @@ -476,7 +478,8 @@ def test_truenas_t5_lifecycle_harness_contract(self): self.assertIn('--expected-version "$EXPECTED_SYSTEM_VERSION"', text) self.assertIn('APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', lifecycle) self.assertIn("scripts/truenas_middleware_app_lifecycle_probe.py", workflow) - self.assertIn("--rung t6", workflow) + self.assertIn("truenas_rung:", workflow) + self.assertIn("needs.changes.outputs.truenas_rung", workflow) shell_check = subprocess.run( ["bash", "-n", str(TRUENAS)], text=True, capture_output=True ) @@ -493,7 +496,8 @@ def test_truenas_t6_foundry_harness_contract(self): self.assertNotIn('python3 "$SCRIPT_DIR/truenas_middleware_foundry_control_probe.py"', text) self.assertIn('"foundry_materialization_exercised"', text) self.assertIn('"foundry_materialization"', text) - self.assertIn("--rung t6", workflow) + self.assertIn("truenas_rung:", workflow) + self.assertIn("needs.changes.outputs.truenas_rung", workflow) self.assertIn("4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba", workflow) self.assertIn("litellm-truenas-t6-control", workflow) self.assertIn("export-litellm-t6-control.yml", workflow) From b7d8c55143b8f466ed6a59e2bd7d9f89857f1e15 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:29:24 +0200 Subject: [PATCH 102/719] RDTE: parameterize T5 protocol qualification by exact version --- tests/test_truenas_future_protocols.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index 2f37f14dc..1ba3e6a03 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -162,6 +162,7 @@ def server(): "--port", str(peer.port), "--password-file", str(password_file), "--out", str(out), + "--expected-version", "TrueNAS-26.0.0-BETA.3", "--timeout", "2", ], text=True, @@ -396,9 +397,11 @@ def test_t3_uses_public_disk_details_not_private_get_unused(self): self.assertIn('disk_details["unused"]', text) - def test_t5_lifecycle_contract_is_version_and_digest_pinned(self): + def test_t5_lifecycle_contract_is_exact_version_and_digest_pinned(self): text = LIFECYCLE.read_text(encoding="utf-8") - self.assertIn('EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3"', text) + self.assertNotIn("EXPECTED_VERSION =", text) + self.assertIn('p.add_argument("--expected-version", required=True)', text) + self.assertIn('"expected_version": a.expected_version', text) self.assertIn('APP_NAME = "rdte-t4-probe"', text) self.assertIn('APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', text) self.assertNotIn('APP_IMAGE = "nginx:1.27-alpine"', text) From 05f4f6925a08689a020470df78293b8f03ff0990 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:30:01 +0200 Subject: [PATCH 103/719] RDTE: pass exact version only to T5 lifecycle probe --- tests/test_truenas_future_protocols.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_future_protocols.py b/tests/test_truenas_future_protocols.py index 1ba3e6a03..96f38dae4 100644 --- a/tests/test_truenas_future_protocols.py +++ b/tests/test_truenas_future_protocols.py @@ -162,7 +162,6 @@ def server(): "--port", str(peer.port), "--password-file", str(password_file), "--out", str(out), - "--expected-version", "TrueNAS-26.0.0-BETA.3", "--timeout", "2", ], text=True, @@ -567,6 +566,7 @@ def server(): "--port", str(peer.port), "--password-file", str(password_file), "--out", str(out), + "--expected-version", "TrueNAS-26.0.0-BETA.3", "--timeout", "2", "--job-timeout", "8", "--state-timeout", "8", From bc6bf7c4c226039a42ad31b10addaf555a0664bf Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:35:17 +0200 Subject: [PATCH 104/719] RDTE: queue exact TrueNAS 25.10.7 T5 qualification --- config/truenas-rdte-run-request.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 config/truenas-rdte-run-request.json diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json new file mode 100644 index 000000000..ae71e0084 --- /dev/null +++ b/config/truenas-rdte-run-request.json @@ -0,0 +1,8 @@ +{ + "schema": "gha-kvm-truenas-run-request/v1", + "request_id": "tn-25.10.7-t5-initial", + "version": "25.10.7", + "rung": "t5", + "authority_issue": 410, + "purpose": "First exact stable TrueNAS T0-T5 qualification for Foundry materialization." +} From acdcf0bc40eb8e3bb35aeb0a460af370392d6408 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:35:48 +0200 Subject: [PATCH 105/719] RDTE: consume durable exact TrueNAS run requests --- .github/workflows/gha-kvm-system-rdte.yml | 211 ++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index bfb411c9e..4d21938ad 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -68,6 +68,7 @@ jobs: DISPATCH_TRUENAS_RUNG: ${{ inputs.truenas_rung }} BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.sha }} + RUN_REQUEST_PATH: config/truenas-rdte-run-request.json run: | set -euo pipefail proxmox=false @@ -75,6 +76,7 @@ jobs: qemu_topology=false truenas_version="${DISPATCH_TRUENAS_VERSION:-26.0.0-BETA.3}" truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" + run_request_changed=false if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then case "$DISPATCH_TARGET" in proxmox) proxmox=true ;; @@ -85,6 +87,10 @@ jobs: elif [[ -n "$BEFORE_SHA" && -n "$AFTER_SHA" && ! "$BEFORE_SHA" =~ ^0+$ ]]; then while IFS= read -r path; do case "$path" in + config/truenas-rdte-run-request.json) + truenas=true + run_request_changed=true + ;; scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; @@ -96,6 +102,211 @@ jobs: ;; esac done < <(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA") + if [[ "$run_request_changed" == "true" ]]; then + request="$(python3 - "$RUN_REQUEST_PATH" <<'PY' +import json, pathlib, sys +path = pathlib.Path(sys.argv[1]) +data = json.loads(path.read_text(encoding="utf-8")) +if data.get("schema") != "gha-kvm-truenas-run-request/v1": + raise SystemExit("unsupported TrueNAS run-request schema") +version = data.get("version") +rung = data.get("rung") +if not isinstance(version, str) or not version: + raise SystemExit("run request requires exact version") +if rung not in {"t0", "t1", "t2", "t3", "t4", "t5", "t6"}: + raise SystemExit("run request has invalid rung") +print(f"{version}\t{rung}") +PY + )" + IFS= + proxmox=true + truenas=true + qemu_topology=true + fi + echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" + echo "truenas=$truenas" >>"$GITHUB_OUTPUT" + echo "qemu_topology=$qemu_topology" >>"$GITHUB_OUTPUT" + echo "truenas_version=$truenas_version" >>"$GITHUB_OUTPUT" + echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" + printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" + + contract: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Validate public-safe contract + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_rdte_target.py + + foundry-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-litellm-t6-control.yml@4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba + with: + foundry_ref: 4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba + permissions: + contents: read + + qemu-t3-topology: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' + runs-on: ubuntu-26.04 + timeout-minutes: 8 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Install QEMU topology prerequisites + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-system-x86 qemu-utils + - name: Exercise actual hosted-runner QEMU topology + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" + python3 scripts/gha_qemu_t3_topology_probe.py \ + --out "$RUNNER_TEMP/gha-kvm-system-rdte/qemu-t3-topology.json" + - name: Upload QEMU topology evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: gha-qemu-t3-topology-${{ github.run_attempt }} + path: ${{ runner.temp }}/gha-kvm-system-rdte/qemu-t3-topology.json + if-no-files-found: error + retention-days: 7 + + proxmox-9-2: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.proxmox == 'true' + runs-on: ubuntu-26.04 + timeout-minutes: 35 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Ensure bounded QEMU/KVM prerequisites + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-system-x86 qemu-utils xorriso sshpass + - name: Exercise pinned Proxmox real-system target + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" + bash scripts/gha_kvm_proxmox_rdte.sh \ + --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-state" \ + --out "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" + python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" + - name: Preserve Proxmox harness failure evidence + if: always() + run: | + set -euo pipefail + receipt="$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" + if [ ! -f "$receipt" ]; then + python3 - "$receipt" <<'PY' + import json, pathlib, sys + pathlib.Path(sys.argv[1]).write_text(json.dumps({ + "contract":"gha-kvm-system-lab/v1", + "target":{"product":"proxmox-ve","version":"9.2-1"}, + "classification":"HARNESS_FAILURE", + "oracleSatisfied":False, + "phase":"workflow-fallback", + "detail":"adapter exited before emitting its normal receipt" + }, indent=2, sort_keys=True)+"\n") + PY + fi + - name: Upload Proxmox evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: gha-kvm-proxmox-9-2-${{ github.run_attempt }} + path: ${{ runner.temp }}/gha-kvm-system-rdte/proxmox-receipt.json + if-no-files-found: error + retention-days: 7 + + truenas-26-beta3: + needs: [changes, contract, proxmox-9-2, foundry-t6-control] + if: >- + always() && + needs.contract.result == 'success' && + needs.changes.outputs.truenas == 'true' && + (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') && + (needs.changes.outputs.truenas_rung != 't6' || needs['foundry-t6-control'].result == 'success') + runs-on: ubuntu-26.04 + timeout-minutes: 50 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Download exact LiteLLM Foundry T6 control + if: needs.changes.outputs.truenas_rung == 't6' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: litellm-truenas-t6-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Ensure bounded QEMU/KVM prerequisites + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + qemu-system-x86 qemu-utils xorriso + - name: Exercise pinned TrueNAS installer target + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" + args=( + --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" + --target-version "${{ needs.changes.outputs.truenas_version }}" + --rung "${{ needs.changes.outputs.truenas_rung }}" + --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + ) + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then + args+=( + --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" + --foundry-commit "4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" + ) + fi + bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" + python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + - name: Preserve TrueNAS harness failure evidence + if: always() + env: + TRUENAS_VERSION: ${{ needs.changes.outputs.truenas_version }} + TRUENAS_RUNG: ${{ needs.changes.outputs.truenas_rung }} + run: | + set -euo pipefail + receipt="$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + if [ ! -f "$receipt" ]; then + python3 - "$receipt" <<'PY' + import json, os, pathlib, sys + pathlib.Path(sys.argv[1]).write_text(json.dumps({ + "contract":"gha-kvm-system-lab/v1", + "target":{"product":"truenas","version":os.environ["TRUENAS_VERSION"],"rung":os.environ["TRUENAS_RUNG"].upper()}, + "classification":"HARNESS_FAILURE", + "oracleSatisfied":False, + "phase":"workflow-fallback", + "detail":"adapter exited before emitting its normal receipt" + }, indent=2, sort_keys=True)+"\n") + PY + fi + - name: Upload TrueNAS evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: gha-kvm-truenas-26-beta3-${{ github.run_attempt }} + path: ${{ runner.temp }}/gha-kvm-system-rdte/truenas-receipt.json + if-no-files-found: error + retention-days: 7 +\t' read -r truenas_version truenas_rung <<<"$request" + python3 scripts/truenas_rdte_target.py \ + --registry config/truenas-rdte-targets.json \ + --version "$truenas_version" >/dev/null + fi else proxmox=true truenas=true From 0c3c89060ded81e5e9db83d1ea7fda1e84112fee Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:35:58 +0200 Subject: [PATCH 106/719] RDTE: include run request in static gate --- .github/workflows/prep-rdte-static.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index d76c2ea38..d8fbeae11 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -8,6 +8,7 @@ on: - '.github/workflows/prep-rdte-static.yml' - '.github/workflows/gha-kvm-system-rdte.yml' - 'config/truenas-rdte-targets.json' + - 'config/truenas-rdte-run-request.json' - 'scripts/gha_kvm_truenas_rdte.sh' - 'scripts/truenas_rdte_target.py' - 'scripts/truenas_rdte_target.py' @@ -23,6 +24,7 @@ on: - '.github/workflows/prep-rdte-static.yml' - '.github/workflows/gha-kvm-system-rdte.yml' - 'config/truenas-rdte-targets.json' + - 'config/truenas-rdte-run-request.json' - 'scripts/gha_kvm_truenas_rdte.sh' - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_middleware_*.py' From bbbdaf9fdc918dc1a148bad43daa7df1bf65ed96 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:36:09 +0200 Subject: [PATCH 107/719] RDTE: regression-lock first exact stable run request --- tests/test_truenas_rdte_target.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index d1a53fb37..027f97fb0 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -5,6 +5,7 @@ SPEC=importlib.util.spec_from_file_location("truenas_rdte_target", ROOT/"scripts"/"truenas_rdte_target.py") MOD=importlib.util.module_from_spec(SPEC); assert SPEC.loader; SPEC.loader.exec_module(MOD) REG=ROOT/"config"/"truenas-rdte-targets.json" +RUN_REQUEST=ROOT/"config"/"truenas-rdte-run-request.json" class TargetRegistryTests(unittest.TestCase): def setUp(self): @@ -33,5 +34,15 @@ def test_unknown_exact_version_fails_closed(self): if "26.0.0-RC.1" not in targets: raise MOD.TargetError("exact TrueNAS target is not registered: 26.0.0-RC.1") + + def test_initial_run_request_is_exact_stable_t5(self): + request=json.loads(RUN_REQUEST.read_text(encoding="utf-8")) + self.assertEqual(request["schema"], "gha-kvm-truenas-run-request/v1") + self.assertEqual(request["version"], "25.10.7") + self.assertEqual(request["rung"], "t5") + self.assertEqual(request["authority_issue"], 410) + self.assertIn(request["version"], self.targets) + + if __name__=="__main__": unittest.main() From 3d37d448afde48ca9ce548360f892ba088e0a5d2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:36:21 +0200 Subject: [PATCH 108/719] RDTE: regression-lock durable run request actuator --- tests/test_gha_kvm_system_rdte.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index bcf36bda9..fd67edf3d 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -226,6 +226,9 @@ def test_workflow_scopes_heavy_targets(self): self.assertIn("truenas_rung:", text) self.assertIn("needs.changes.outputs.truenas_version", text) self.assertIn("needs.changes.outputs.truenas_rung", text) + self.assertIn("config/truenas-rdte-run-request.json", text) + self.assertIn("run_request_changed", text) + self.assertIn("gha-kvm-truenas-run-request/v1", text) def test_truenas_t1_requires_local_rpc_probe(self): text = TRUENAS.read_text(encoding="utf-8") From e269323fbc5d32608ad9aba63587f20b896bec47 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:37:33 +0200 Subject: [PATCH 109/719] RDTE: repair durable run-request workflow parsing --- .github/workflows/gha-kvm-system-rdte.yml | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 4d21938ad..9ba3170f3 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -103,22 +103,13 @@ jobs: esac done < <(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA") if [[ "$run_request_changed" == "true" ]]; then - request="$(python3 - "$RUN_REQUEST_PATH" <<'PY' -import json, pathlib, sys -path = pathlib.Path(sys.argv[1]) -data = json.loads(path.read_text(encoding="utf-8")) -if data.get("schema") != "gha-kvm-truenas-run-request/v1": - raise SystemExit("unsupported TrueNAS run-request schema") -version = data.get("version") -rung = data.get("rung") -if not isinstance(version, str) or not version: - raise SystemExit("run request requires exact version") -if rung not in {"t0", "t1", "t2", "t3", "t4", "t5", "t6"}: - raise SystemExit("run request has invalid rung") -print(f"{version}\t{rung}") -PY - )" - IFS= + request="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1], encoding="utf-8")); assert d.get("schema")=="gha-kvm-truenas-run-request/v1", "unsupported run-request schema"; v=d.get("version"); r=d.get("rung"); assert isinstance(v,str) and v, "run request requires exact version"; assert r in {"t0","t1","t2","t3","t4","t5","t6"}, "invalid run-request rung"; print(v+"\\t"+r)' "$RUN_REQUEST_PATH")" + IFS="$(printf '\t')" read -r truenas_version truenas_rung <<<"$request" + python3 scripts/truenas_rdte_target.py \ + --registry config/truenas-rdte-targets.json \ + --version "$truenas_version" >/dev/null + fi + else proxmox=true truenas=true qemu_topology=true From 7606bb09d32db073dafa861bf9123e92c54f1f8b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:40:14 +0200 Subject: [PATCH 110/719] RDTE: simplify run-request parser for workflow validity --- .github/workflows/gha-kvm-system-rdte.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 9ba3170f3..6c65c06ec 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -103,8 +103,8 @@ jobs: esac done < <(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA") if [[ "$run_request_changed" == "true" ]]; then - request="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1], encoding="utf-8")); assert d.get("schema")=="gha-kvm-truenas-run-request/v1", "unsupported run-request schema"; v=d.get("version"); r=d.get("rung"); assert isinstance(v,str) and v, "run request requires exact version"; assert r in {"t0","t1","t2","t3","t4","t5","t6"}, "invalid run-request rung"; print(v+"\\t"+r)' "$RUN_REQUEST_PATH")" - IFS="$(printf '\t')" read -r truenas_version truenas_rung <<<"$request" + truenas_version="$(jq -er 'select(.schema == "gha-kvm-truenas-run-request/v1") | .version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" + truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null From aea9d1eb10230d6deb293879fb0eb910ea6f2f0c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:41:44 +0200 Subject: [PATCH 111/719] RDTE: remove duplicated workflow tail from request edit --- .github/workflows/gha-kvm-system-rdte.yml | 74 ----------------------- 1 file changed, 74 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 6c65c06ec..424830ac7 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -408,77 +408,3 @@ jobs: path: ${{ runner.temp }}/gha-kvm-system-rdte/proxmox-receipt.json if-no-files-found: error retention-days: 7 - - truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control] - if: >- - always() && - needs.contract.result == 'success' && - needs.changes.outputs.truenas == 'true' && - (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') && - (needs.changes.outputs.truenas_rung != 't6' || needs['foundry-t6-control'].result == 'success') - runs-on: ubuntu-26.04 - timeout-minutes: 50 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - persist-credentials: false - - name: Download exact LiteLLM Foundry T6 control - if: needs.changes.outputs.truenas_rung == 't6' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: litellm-truenas-t6-control - path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - - name: Ensure bounded QEMU/KVM prerequisites - run: | - set -euo pipefail - sudo apt-get update -qq - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - qemu-system-x86 qemu-utils xorriso - - name: Exercise pinned TrueNAS installer target - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" - args=( - --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" - --target-version "${{ needs.changes.outputs.truenas_version }}" - --rung "${{ needs.changes.outputs.truenas_rung }}" - --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - ) - if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then - args+=( - --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" - --foundry-commit "4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" - ) - fi - bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" - python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - - name: Preserve TrueNAS harness failure evidence - if: always() - env: - TRUENAS_VERSION: ${{ needs.changes.outputs.truenas_version }} - TRUENAS_RUNG: ${{ needs.changes.outputs.truenas_rung }} - run: | - set -euo pipefail - receipt="$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - if [ ! -f "$receipt" ]; then - python3 - "$receipt" <<'PY' - import json, os, pathlib, sys - pathlib.Path(sys.argv[1]).write_text(json.dumps({ - "contract":"gha-kvm-system-lab/v1", - "target":{"product":"truenas","version":os.environ["TRUENAS_VERSION"],"rung":os.environ["TRUENAS_RUNG"].upper()}, - "classification":"HARNESS_FAILURE", - "oracleSatisfied":False, - "phase":"workflow-fallback", - "detail":"adapter exited before emitting its normal receipt" - }, indent=2, sort_keys=True)+"\n") - PY - fi - - name: Upload TrueNAS evidence - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: gha-kvm-truenas-26-beta3-${{ github.run_attempt }} - path: ${{ runner.temp }}/gha-kvm-system-rdte/truenas-receipt.json - if-no-files-found: error - retention-days: 7 From c2829ad33b030dd113da437b11e83f1a2447a0dc Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:44:24 +0200 Subject: [PATCH 112/719] RDTE: rebuild valid exact-version workflow with durable run request --- .github/workflows/gha-kvm-system-rdte.yml | 120 +--------------------- 1 file changed, 3 insertions(+), 117 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 424830ac7..863badc1c 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -74,9 +74,9 @@ jobs: proxmox=false truenas=false qemu_topology=false + run_request_changed=false truenas_version="${DISPATCH_TRUENAS_VERSION:-26.0.0-BETA.3}" truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" - run_request_changed=false if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then case "$DISPATCH_TARGET" in proxmox) proxmox=true ;; @@ -103,8 +103,9 @@ jobs: esac done < <(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA") if [[ "$run_request_changed" == "true" ]]; then - truenas_version="$(jq -er 'select(.schema == "gha-kvm-truenas-run-request/v1") | .version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" + truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" + jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null @@ -293,118 +294,3 @@ jobs: path: ${{ runner.temp }}/gha-kvm-system-rdte/truenas-receipt.json if-no-files-found: error retention-days: 7 -\t' read -r truenas_version truenas_rung <<<"$request" - python3 scripts/truenas_rdte_target.py \ - --registry config/truenas-rdte-targets.json \ - --version "$truenas_version" >/dev/null - fi - else - proxmox=true - truenas=true - qemu_topology=true - fi - echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" - echo "truenas=$truenas" >>"$GITHUB_OUTPUT" - echo "qemu_topology=$qemu_topology" >>"$GITHUB_OUTPUT" - echo "truenas_version=$truenas_version" >>"$GITHUB_OUTPUT" - echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" - printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" - - contract: - runs-on: ubuntu-24.04 - timeout-minutes: 5 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - persist-credentials: false - - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_rdte_target.py - - foundry-t6-control: - needs: [changes, contract] - if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-litellm-t6-control.yml@4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba - with: - foundry_ref: 4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba - permissions: - contents: read - - qemu-t3-topology: - needs: [changes, contract] - if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' - runs-on: ubuntu-26.04 - timeout-minutes: 8 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - persist-credentials: false - - name: Install QEMU topology prerequisites - run: | - set -euo pipefail - sudo apt-get update -qq - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - qemu-system-x86 qemu-utils - - name: Exercise actual hosted-runner QEMU topology - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" - python3 scripts/gha_qemu_t3_topology_probe.py \ - --out "$RUNNER_TEMP/gha-kvm-system-rdte/qemu-t3-topology.json" - - name: Upload QEMU topology evidence - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: gha-qemu-t3-topology-${{ github.run_attempt }} - path: ${{ runner.temp }}/gha-kvm-system-rdte/qemu-t3-topology.json - if-no-files-found: error - retention-days: 7 - - proxmox-9-2: - needs: [changes, contract] - if: needs.contract.result == 'success' && needs.changes.outputs.proxmox == 'true' - runs-on: ubuntu-26.04 - timeout-minutes: 35 - steps: - - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - with: - persist-credentials: false - - name: Ensure bounded QEMU/KVM prerequisites - run: | - set -euo pipefail - sudo apt-get update -qq - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - qemu-system-x86 qemu-utils xorriso sshpass - - name: Exercise pinned Proxmox real-system target - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" - bash scripts/gha_kvm_proxmox_rdte.sh \ - --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-state" \ - --out "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" - python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" - - name: Preserve Proxmox harness failure evidence - if: always() - run: | - set -euo pipefail - receipt="$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" - if [ ! -f "$receipt" ]; then - python3 - "$receipt" <<'PY' - import json, pathlib, sys - pathlib.Path(sys.argv[1]).write_text(json.dumps({ - "contract":"gha-kvm-system-lab/v1", - "target":{"product":"proxmox-ve","version":"9.2-1"}, - "classification":"HARNESS_FAILURE", - "oracleSatisfied":False, - "phase":"workflow-fallback", - "detail":"adapter exited before emitting its normal receipt" - }, indent=2, sort_keys=True)+"\n") - PY - fi - - name: Upload Proxmox evidence - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: gha-kvm-proxmox-9-2-${{ github.run_attempt }} - path: ${{ runner.temp }}/gha-kvm-system-rdte/proxmox-receipt.json - if-no-files-found: error - retention-days: 7 From aa69b1b23ca2239bf882c3a5e6047b77581c4e16 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 30 Sep 2026 21:15:08 +0200 Subject: [PATCH 113/719] fix: align TrueNAS 25.10.7 installed identity Correct the exact observed middleware system.version, add a regression assertion, and issue one bounded corrective T5 run request under authority #410. --- config/truenas-rdte-run-request.json | 4 ++-- config/truenas-rdte-targets.json | 2 +- tests/test_truenas_rdte_target.py | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index ae71e0084..18622845b 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,8 +1,8 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.10.7-t5-initial", + "request_id": "tn-25.10.7-t5-corrected-version-identity", "version": "25.10.7", "rung": "t5", "authority_issue": 410, - "purpose": "First exact stable TrueNAS T0-T5 qualification for Foundry materialization." + "purpose": "Corrective exact stable TrueNAS T0-T5 qualification after observed system.version proved TrueNAS-25.10.7." } diff --git a/config/truenas-rdte-targets.json b/config/truenas-rdte-targets.json index d8bee7404..b5c1f6c11 100644 --- a/config/truenas-rdte-targets.json +++ b/config/truenas-rdte-targets.json @@ -23,7 +23,7 @@ { "version": "25.10.7", "channel": "stable", - "system_version": "TrueNAS-SCALE-25.10.7", + "system_version": "TrueNAS-25.10.7", "iso_name": "TrueNAS-SCALE-25.10.7.iso", "iso_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Goldeye/25.10.7/TrueNAS-SCALE-25.10.7.iso", "sha256_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Goldeye/25.10.7/TrueNAS-SCALE-25.10.7.iso.sha256", diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 027f97fb0..17e96553f 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -24,6 +24,9 @@ def test_vendor_media_is_exact_and_has_digest_sidecar(self): self.assertNotIn("latest", lower) self.assertNotIn("nightly", lower) self.assertNotIn("master+", lower) + def test_exact_installed_version_identity_for_stable_target(self): + self.assertEqual(self.targets["25.10.7"]["system_version"], "TrueNAS-25.10.7") + def test_apps_gate_deltas_are_not_normalized_away(self): self.assertEqual(self.targets["25.04.1"]["ha_apps_gate"], "system.license:JAILS") self.assertEqual(self.targets["25.10.7"]["ha_apps_gate"], "system.license:JAILS") From 2b0a5b675d266798eccdd6aa0e9d42c0c2036560 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:21:24 +0200 Subject: [PATCH 114/719] RDTE: launch exact TrueNAS 25.04.1 T5 qualification --- config/truenas-rdte-run-request.json | 8 ++++---- config/truenas-rdte-targets.json | 2 +- tests/test_truenas_rdte_target.py | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) mode change 100644 => 100755 tests/test_truenas_rdte_target.py diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 18622845b..6b844a514 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,8 +1,8 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.10.7-t5-corrected-version-identity", - "version": "25.10.7", + "request_id": "tn-25.04.1-t5-initial", + "version": "25.04.1", "rung": "t5", - "authority_issue": 410, - "purpose": "Corrective exact stable TrueNAS T0-T5 qualification after observed system.version proved TrueNAS-25.10.7." + "authority_issue": 411, + "purpose": "First exact deployed-legacy TrueNAS 25.04.1 T0-T5 qualification for Foundry materialization." } diff --git a/config/truenas-rdte-targets.json b/config/truenas-rdte-targets.json index b5c1f6c11..f2f8e5153 100644 --- a/config/truenas-rdte-targets.json +++ b/config/truenas-rdte-targets.json @@ -10,7 +10,7 @@ { "version": "25.04.1", "channel": "deployed-legacy", - "system_version": "TrueNAS-SCALE-25.04.1", + "system_version": "TrueNAS-25.04.1", "iso_name": "TrueNAS-SCALE-25.04.1.iso", "iso_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Fangtooth/25.04.1/TrueNAS-SCALE-25.04.1.iso", "sha256_url": "https://download.sys.truenas.net/TrueNAS-SCALE-Fangtooth/25.04.1/TrueNAS-SCALE-25.04.1.iso.sha256", diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py old mode 100644 new mode 100755 index 17e96553f..2f1435932 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -27,6 +27,9 @@ def test_vendor_media_is_exact_and_has_digest_sidecar(self): def test_exact_installed_version_identity_for_stable_target(self): self.assertEqual(self.targets["25.10.7"]["system_version"], "TrueNAS-25.10.7") + def test_exact_installed_version_identity_for_legacy_target(self): + self.assertEqual(self.targets["25.04.1"]["system_version"], "TrueNAS-25.04.1") + def test_apps_gate_deltas_are_not_normalized_away(self): self.assertEqual(self.targets["25.04.1"]["ha_apps_gate"], "system.license:JAILS") self.assertEqual(self.targets["25.10.7"]["ha_apps_gate"], "system.license:JAILS") From 967603484bf42efef954b67e44c58cf7f0c5c0ae Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:22:15 +0200 Subject: [PATCH 115/719] test: make exact T5 run-request assertion target-generic --- tests/test_truenas_rdte_target.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 2f1435932..9a9638e53 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -41,13 +41,15 @@ def test_unknown_exact_version_fails_closed(self): raise MOD.TargetError("exact TrueNAS target is not registered: 26.0.0-RC.1") - def test_initial_run_request_is_exact_stable_t5(self): + def test_run_request_is_exact_registered_t5(self): request=json.loads(RUN_REQUEST.read_text(encoding="utf-8")) self.assertEqual(request["schema"], "gha-kvm-truenas-run-request/v1") - self.assertEqual(request["version"], "25.10.7") self.assertEqual(request["rung"], "t5") - self.assertEqual(request["authority_issue"], 410) self.assertIn(request["version"], self.targets) + target=self.targets[request["version"]] + self.assertEqual(request["authority_issue"], target["authority_issue"]) + self.assertNotIn("latest", request["version"].lower()) + self.assertNotIn("nightly", request["version"].lower()) if __name__=="__main__": From 72b75266ff8796f4ddc97f78cc198e5c84a48e33 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:40:41 +0200 Subject: [PATCH 116/719] rdte: add WOW Sidecar TrueNAS T6 product oracle --- ...truenas_middleware_wow_sidecar_t6_probe.py | 366 ++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 scripts/truenas_middleware_wow_sidecar_t6_probe.py diff --git a/scripts/truenas_middleware_wow_sidecar_t6_probe.py b/scripts/truenas_middleware_wow_sidecar_t6_probe.py new file mode 100644 index 000000000..b13911469 --- /dev/null +++ b/scripts/truenas_middleware_wow_sidecar_t6_probe.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Bounded WOW Sidecar Foundry T6 oracle on disposable TrueNAS.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + +SCHEMA = "semper-supra.wow-sidecar-truenas-t6-control/1" +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_APP_NAME = "rdte-t6-wow-sidecar" +EXPECTED_IMAGE = "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376" +EXPECTED_HELPER = "ixsystems/container-utils@sha256:46eba20714c1cc6784f60e245c32c33a2d9f616e47d804694a9854248c89a992" +EXPECTED_FIXTURE_DATASET = "rdtepool/wow-sidecar-t6" +EXPECTED_FIXTURE_ROOT = "/mnt/rdtepool/wow-sidecar-t6" +EXPECTED_CONFIG_DIR = EXPECTED_FIXTURE_ROOT + "/config" +EXPECTED_STATE_DIR = EXPECTED_FIXTURE_ROOT + "/state" +EXPECTED_KEY_PATH = EXPECTED_CONFIG_DIR + "/github-app.pem" +EXPECTED_PROFILE_PATH = EXPECTED_CONFIG_DIR + "/profiles/operator.json" +EXPECTED_MARKER_PATH = EXPECTED_CONFIG_DIR + "/.initialized-v1" +FIXTURE_MARKER = "PUBLIC-QUALIFICATION-FIXTURE" + + +def canonical_sha256(value: object) -> str: + raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(raw).hexdigest() + + +def load_json(path: pathlib.Path) -> dict: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise RuntimeError(f"{path} must contain an object") + return value + + +def load_control(directory: pathlib.Path, foundry_commit: str) -> tuple[dict, dict]: + control = load_json(directory / "control.json") + compose = load_json(directory / "compose.json") + if control.get("schema") != SCHEMA: + raise RuntimeError("unexpected WOW T6 control schema") + if control.get("foundry_ref") != foundry_commit: + raise RuntimeError("Foundry source ref drifted") + if control.get("secrets_captured") is not False: + raise RuntimeError("control bundle does not assert secrets_captured=false") + + candidate = control.get("candidate") or {} + if candidate.get("wow_image") != EXPECTED_IMAGE: + raise RuntimeError("WOW image identity drifted") + if candidate.get("permissions_helper") != EXPECTED_HELPER: + raise RuntimeError("permissions helper identity drifted") + runtime = control.get("runtime") or {} + if runtime.get("app_name") != EXPECTED_APP_NAME: + raise RuntimeError("WOW T6 app name drifted") + if runtime.get("production_credentials_present") is not False: + raise RuntimeError("public T6 control claims production credentials") + if runtime.get("fixture_key_marker") != FIXTURE_MARKER: + raise RuntimeError("public fixture key marker drifted") + if runtime.get("fixture_config_dir") != EXPECTED_CONFIG_DIR: + raise RuntimeError("fixture config directory drifted") + if runtime.get("fixture_state_dir") != EXPECTED_STATE_DIR: + raise RuntimeError("fixture state directory drifted") + + if control.get("artifacts", {}).get("compose_canonical_sha256") != canonical_sha256(compose): + raise RuntimeError("rendered Compose identity does not match control") + + services = compose.get("services") or {} + if set(services) != {"permissions", "wow-sidecar", "wow-sidecar-config-seed"}: + raise RuntimeError("unexpected WOW T6 service inventory") + if services["wow-sidecar"].get("image") != EXPECTED_IMAGE: + raise RuntimeError("worker image drifted") + if services["wow-sidecar-config-seed"].get("image") != EXPECTED_IMAGE: + raise RuntimeError("seed image drifted") + if services["permissions"].get("image") != EXPECTED_HELPER: + raise RuntimeError("permissions image drifted") + + serialized = json.dumps(compose, sort_keys=True) + if FIXTURE_MARKER not in serialized: + raise RuntimeError("public fixture marker is absent from exact control") + for forbidden in ("ghp_", "sk-", "SemperSupra/agent-dispatch-private", "SemperSupra/wow-sidecar-private"): + if forbidden in serialized: + raise RuntimeError(f"private credential/authority content leaked into public control: {forbidden}") + return control, compose + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--control-dir", type=pathlib.Path, required=True) + p.add_argument("--foundry-commit", required=True) + p.add_argument("--out", required=True) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + p.add_argument("--state-timeout", type=float, default=240.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-wow-sidecar-foundry-t6/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected_version": EXPECTED_VERSION, + "foundry_commit": a.foundry_commit, + "app_name": EXPECTED_APP_NAME, + "wow_image": EXPECTED_IMAGE, + "permissions_helper": EXPECTED_HELPER, + "production_credentials_present": False, + "execution_control_mutation_authorized": False, + "credential_boundary": "public-fixture-only", + } + ws = None + request_id = 1 + created_app = False + created_dataset = False + + try: + control, compose = load_control(a.control_dir, a.foundry_commit) + compose_sha = canonical_sha256(compose) + payload["materialization"] = { + "schema": control["schema"], + "foundry_ref": control["foundry_ref"], + "compose_canonical_sha256": compose_sha, + "production_credentials_present": False, + } + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError("DDP connection failed") + + def call(method, params): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + def wait_job(job_id, label): + deadline = time.monotonic() + a.job_timeout + last = None + while time.monotonic() < deadline: + last = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last and last.get("state") == "SUCCESS": + return last + if last and last.get("state") in {"FAILED", "ABORTED"}: + raise RuntimeError(f"{label} job {last.get('state')}") + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS") + + def app_query(get=True): + params = [[["id", "=", EXPECTED_APP_NAME]]] + if get: + params.append({"get": True}) + return call("app.query", params) + + def wait_running(): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = app_query() + if last: + if last.get("state") in {"CRASHED", "ERROR"}: + raise RuntimeError(f"WOW app entered {last.get('state')}") + details = (last.get("active_workloads") or {}).get("container_details") or [] + workers = [ + item for item in details + if item.get("service_name") == "wow-sidecar" + and item.get("image") == EXPECTED_IMAGE + and item.get("state") == "running" + ] + if last.get("state") == "RUNNING" and len(workers) == 1: + return last + time.sleep(1) + raise RuntimeError(f"WOW worker did not reach RUNNING: {last!r}") + + auth = call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError("authentication did not return SUCCESS") + + payload["system_version"] = call("system.version", []) + if payload["system_version"] != EXPECTED_VERSION: + raise RuntimeError("target version drifted") + + if call("app.query", [[["id", "=", EXPECTED_APP_NAME]]]): + raise RuntimeError("refusing adopted WOW app state") + if call("pool.dataset.query", [[["id", "=", EXPECTED_FIXTURE_DATASET]]]): + raise RuntimeError("refusing adopted WOW fixture dataset") + + dataset = call("pool.dataset.create", [{ + "name": EXPECTED_FIXTURE_DATASET, + "type": "FILESYSTEM", + "share_type": "GENERIC", + "comments": "SemperSupra disposable WOW Sidecar T6 fixture", + }]) + if not isinstance(dataset, dict) or dataset.get("id") != EXPECTED_FIXTURE_DATASET: + raise RuntimeError("pool.dataset.create did not return exact WOW fixture dataset") + created_dataset = True + + created_dirs = [] + for path in (EXPECTED_CONFIG_DIR, EXPECTED_STATE_DIR): + result = call("filesystem.mkdir", [{ + "path": path, + "options": {"mode": "750", "raise_chmod_error": True}, + }]) + if not isinstance(result, dict) or result.get("path") != path: + raise RuntimeError(f"filesystem.mkdir did not create exact path {path}") + created_dirs.append(path) + payload["fixture_storage"] = { + "dataset": EXPECTED_FIXTURE_DATASET, + "directories": created_dirs, + "content_recorded": False, + } + + create_id = call("app.create", [{ + "app_name": EXPECTED_APP_NAME, + "custom_app": True, + "custom_compose_config": compose, + }]) + if not isinstance(create_id, int): + raise RuntimeError("app.create did not return a job id") + wait_job(create_id, "app.create") + created_app = True + + app = wait_running() + details = (app.get("active_workloads") or {}).get("container_details") or [] + payload["runtime"] = { + "state": app.get("state"), + "custom_app": app.get("custom_app"), + "worker_running_exact_image": any( + item.get("service_name") == "wow-sidecar" + and item.get("image") == EXPECTED_IMAGE + and item.get("state") == "running" + for item in details + ), + "container_states": sorted({ + f"{item.get('service_name')}:{item.get('state')}" + for item in details + if isinstance(item, dict) + }), + } + if app.get("custom_app") is not True: + raise RuntimeError("WOW runtime did not materialize as a custom app") + + config_sha = canonical_sha256(call("app.config", [EXPECTED_APP_NAME])) + if config_sha != compose_sha: + raise RuntimeError("app.config read-back does not match exact Foundry Compose") + payload["runtime"]["compose_readback_sha256"] = config_sha + + stat_paths = { + "config_root": EXPECTED_CONFIG_DIR, + "state_root": EXPECTED_STATE_DIR, + "seed_marker": EXPECTED_MARKER_PATH, + "github_app_key": EXPECTED_KEY_PATH, + "operator_profile": EXPECTED_PROFILE_PATH, + } + metadata = {} + for label, path in stat_paths.items(): + st = call("filesystem.stat", [path]) + metadata[label] = { + "path": path, + "mode": oct(int(st.get("mode", 0)) & 0o777), + "uid": st.get("uid"), + "gid": st.get("gid"), + "type": st.get("type"), + } + if metadata["config_root"]["uid"] != 10001 or metadata["config_root"]["gid"] != 10001: + raise RuntimeError("config root ownership does not match WOW runtime identity") + if metadata["state_root"]["uid"] != 10001 or metadata["state_root"]["gid"] != 10001: + raise RuntimeError("state root ownership does not match WOW runtime identity") + if metadata["github_app_key"]["mode"] != "0o400": + raise RuntimeError("fixture GitHub App key mode is not 0400") + if metadata["operator_profile"]["mode"] != "0o400": + raise RuntimeError("fixture operator profile mode is not 0400") + if metadata["seed_marker"]["mode"] != "0o444": + raise RuntimeError("seed marker mode is not 0444") + payload["seed_metadata"] = metadata + payload["seed_metadata"]["raw_content_recorded"] = False + + stop_id = call("app.stop", [EXPECTED_APP_NAME]) + wait_job(stop_id, "app.stop") + stopped = app_query() + if not stopped or stopped.get("state") != "STOPPED": + raise RuntimeError("WOW app did not reach STOPPED") + + start_id = call("app.start", [EXPECTED_APP_NAME]) + wait_job(start_id, "app.start") + wait_running() + restart_sha = canonical_sha256(call("app.config", [EXPECTED_APP_NAME])) + if restart_sha != compose_sha: + raise RuntimeError("Foundry Compose identity drifted after restart") + payload["runtime"]["restart_compose_identity_preserved"] = True + + delete_id = call("app.delete", [EXPECTED_APP_NAME, { + "remove_images": False, + "remove_ix_volumes": False, + "force_remove_custom_app": False, + }]) + wait_job(delete_id, "app.delete") + created_app = False + if call("app.query", [[["id", "=", EXPECTED_APP_NAME]]]): + raise RuntimeError("WOW app remains after delete") + + deleted = call("pool.dataset.delete", [ + EXPECTED_FIXTURE_DATASET, + {"recursive": True, "force": False}, + ]) + if deleted is not True: + raise RuntimeError("pool.dataset.delete did not return true") + created_dataset = False + if call("pool.dataset.query", [[["id", "=", EXPECTED_FIXTURE_DATASET]]]): + raise RuntimeError("WOW fixture dataset remains after delete") + + mountpoint_absent = False + try: + call("filesystem.stat", [EXPECTED_FIXTURE_ROOT]) + except RuntimeError: + mountpoint_absent = True + if not mountpoint_absent: + raise RuntimeError("WOW fixture mountpoint remains after dataset delete") + + payload["cleanup"] = { + "app_absent": True, + "fixture_dataset_absent": True, + "fixture_mountpoint_absent": True, + "zero_residue": True, + } + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "exact Foundry-exported WOW App realized on real disposable TrueNAS; " + "immutable image/config read-back, permissions, create-once seed metadata, " + "worker RUNNING under public fixture credential, restart persistence, " + "and delete/zero-residue all passed" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + payload["cleanup_needed"] = { + "app": created_app, + "fixture_dataset": created_dataset, + } + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From ce3a46e79eb22de493f459c84bd27cc6927418b0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:41:33 +0200 Subject: [PATCH 117/719] rdte: make T6 product-selective without changing default --- scripts/gha_kvm_truenas_rdte.sh | 61 ++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index a6b7974c9..727274c76 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,12 +17,13 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--foundry-control-dir DIR] [--foundry-commit SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar] [--foundry-control-dir DIR] [--foundry-commit SHA]" } OUT="" STATE_DIR="" RUNG="t0" +T6_PRODUCT="litellm" FOUNDRY_CONTROL_DIR="" FOUNDRY_COMMIT="" while [[ $# -gt 0 ]]; do @@ -31,6 +32,7 @@ while [[ $# -gt 0 ]]; do --state-dir) STATE_DIR="$2"; shift 2 ;; --target-version) TARGET_VERSION="$2"; shift 2 ;; --rung) RUNG="$2"; shift 2 ;; + --t6-product) T6_PRODUCT="$2"; shift 2 ;; --foundry-control-dir) FOUNDRY_CONTROL_DIR="$2"; shift 2 ;; --foundry-commit) FOUNDRY_COMMIT="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; @@ -44,7 +46,8 @@ TARGET_ENV="$(python3 "$SCRIPT_DIR/truenas_rdte_target.py" --registry "$TARGET_R eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } if [[ "$RUNG" == "t6" ]]; then - [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "LiteLLM T6 is currently admitted only for exact TrueNAS 26.0.0-BETA.3; use Foundry F1-F5 for other targets" >&2; exit 2; } + [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "T6 is currently admitted only for exact TrueNAS 26.0.0-BETA.3; use Foundry F1-F5 for other targets" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } FOUNDRY_CONTROL_DIR="$(realpath "$FOUNDRY_CONTROL_DIR")" @@ -94,7 +97,7 @@ write_receipt() { export R_ISO_NAME="$ISO_NAME" R_ISO_URL="$ISO_URL" R_SHA_URL="$SHA_URL" export R_MIDDLEWARE_REF="$MIDDLEWARE_REF" R_MIDDLEWARE_COMMIT="$MIDDLEWARE_COMMIT" export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" - export R_RUNG="$RUNG" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" + export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" python3 - <<'PY' @@ -113,7 +116,14 @@ payload = { "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else [], "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else None, "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else [], - "app": ({"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} if os.environ.get("R_RUNG") in {"t4", "t5"} else None), + "app": ( + {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" + else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} + if os.environ.get("R_RUNG") == "t6" + else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} + if os.environ.get("R_RUNG") in {"t4", "t5"} else None + ), }, "source": { "iso_name": os.environ["R_ISO_NAME"], @@ -194,8 +204,13 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T5 lifecycle client" fi if [[ "$RUNG" == "t6" ]]; then - [[ -f "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" ]] || - fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 LiteLLM control client" + if [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 WOW Sidecar control client" + else + [[ -f "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 LiteLLM control client" + fi fi fi for cmd in curl sha256sum qemu-img qemu-system-x86_64 xorriso python3; do @@ -427,7 +442,7 @@ PY )" LITELLM_HOST_PORT="" LITELLM_HOSTFWD="" -if [[ "$RUNG" == "t6" ]]; then +if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "litellm" ]]; then LITELLM_HOST_PORT="$(python3 - <<'PY' import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() @@ -584,13 +599,23 @@ if [[ "$RUNG" == "t5" ]]; then fi FOUNDRY_OUT="$STATE_DIR/foundry-control.json" -python3 "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" \ - --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$LITELLM_HOST_PORT" \ - "${MIDDLEWARE_TLS_ARG[@]}" \ - --password-file "$PASSWORD_FILE" \ - --control-dir "$FOUNDRY_CONTROL_DIR" \ - --foundry-commit "$FOUNDRY_COMMIT" \ - --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +if [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +else + python3 "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$LITELLM_HOST_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +fi [[ -f "$FOUNDRY_OUT" ]] || fail_evidence HARNESS_FAILURE foundry-materialization "T6 Foundry control client did not emit a receipt" FOUNDRY_RESULT_JSON="$(cat "$FOUNDRY_OUT")" FOUNDRY_OK="$(python3 - "$FOUNDRY_OUT" <<'PY' @@ -600,6 +625,10 @@ print("true" if data.get("oracleSatisfied") is True else "false") PY )" [[ "$FOUNDRY_OK" == "true" ]] || - fail_evidence ORACLE_FAILURE foundry-materialization "exact Foundry-exported LiteLLM control did not realize and verify on TrueNAS" + fail_evidence ORACLE_FAILURE foundry-materialization "exact Foundry-exported $T6_PRODUCT control did not realize and verify on TrueNAS" -write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported LiteLLM control passed S1 projection, exact image/config read-back, health, restart persistence, and delete/absence" +if [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported WOW Sidecar control passed immutable image/config read-back, permissions/seed metadata, public-fixture worker runtime, restart persistence, and zero-residue cleanup" +else + write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported LiteLLM control passed S1 projection, exact image/config read-back, health, restart persistence, and delete/absence" +fi From 6b4d4629bdf6de2c84236d03c8a5bbee73d441b9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:43:03 +0200 Subject: [PATCH 118/719] rdte: route exact WOW T6 control through shared system lab --- .github/workflows/gha-kvm-system-rdte.yml | 54 +++++++++++++++++++---- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 863badc1c..6e2d081bf 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -37,6 +37,14 @@ on: - t4 - t5 - t6 + truenas_t6_product: + description: "Exact T6 Foundry consumer" + required: true + default: litellm + type: choice + options: + - litellm + - wow-sidecar permissions: contents: read @@ -55,6 +63,7 @@ jobs: qemu_topology: ${{ steps.scope.outputs.qemu_topology }} truenas_version: ${{ steps.scope.outputs.truenas_version }} truenas_rung: ${{ steps.scope.outputs.truenas_rung }} + truenas_t6_product: ${{ steps.scope.outputs.truenas_t6_product }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -66,6 +75,7 @@ jobs: DISPATCH_TARGET: ${{ inputs.target }} DISPATCH_TRUENAS_VERSION: ${{ inputs.truenas_version }} DISPATCH_TRUENAS_RUNG: ${{ inputs.truenas_rung }} + DISPATCH_TRUENAS_T6_PRODUCT: ${{ inputs.truenas_t6_product }} BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.sha }} RUN_REQUEST_PATH: config/truenas-rdte-run-request.json @@ -77,6 +87,7 @@ jobs: run_request_changed=false truenas_version="${DISPATCH_TRUENAS_VERSION:-26.0.0-BETA.3}" truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" + truenas_t6_product="${DISPATCH_TRUENAS_T6_PRODUCT:-litellm}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then case "$DISPATCH_TARGET" in proxmox) proxmox=true ;; @@ -94,7 +105,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -105,6 +116,7 @@ jobs: if [[ "$run_request_changed" == "true" ]]; then truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar")' "$RUN_REQUEST_PATH")" jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ @@ -120,7 +132,8 @@ jobs: echo "qemu_topology=$qemu_topology" >>"$GITHUB_OUTPUT" echo "truenas_version=$truenas_version" >>"$GITHUB_OUTPUT" echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" - printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" + echo "truenas_t6_product=$truenas_t6_product" >>"$GITHUB_OUTPUT" + printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" contract: runs-on: ubuntu-24.04 @@ -130,17 +143,26 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] - if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'litellm' uses: SemperSupra/truenas-app-foundry/.github/workflows/export-litellm-t6-control.yml@4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba with: foundry_ref: 4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba permissions: contents: read + foundry-wow-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'wow-sidecar' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-wow-sidecar-t6-control.yml@706eca4fafcdafbff41ce6b0adb5a784a580df1e + with: + foundry_ref: 706eca4fafcdafbff41ce6b0adb5a784a580df1e + permissions: + contents: read + qemu-t3-topology: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' @@ -222,13 +244,17 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control] if: >- always() && needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && (needs['proxmox-9-2'].result == 'success' || needs['proxmox-9-2'].result == 'skipped') && - (needs.changes.outputs.truenas_rung != 't6' || needs['foundry-t6-control'].result == 'success') + ( + needs.changes.outputs.truenas_rung != 't6' || + (needs.changes.outputs.truenas_t6_product == 'litellm' && needs['foundry-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') + ) runs-on: ubuntu-26.04 timeout-minutes: 50 steps: @@ -236,11 +262,17 @@ jobs: with: persist-credentials: false - name: Download exact LiteLLM Foundry T6 control - if: needs.changes.outputs.truenas_rung == 't6' + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'litellm' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: litellm-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact WOW Sidecar Foundry T6 control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'wow-sidecar' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: wow-sidecar-truenas-t6-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -258,9 +290,15 @@ jobs: --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" ) if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then + if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then + foundry_commit="706eca4fafcdafbff41ce6b0adb5a784a580df1e" + else + foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" + fi args+=( + --t6-product "${{ needs.changes.outputs.truenas_t6_product }}" --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" - --foundry-commit "4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" + --foundry-commit "$foundry_commit" ) fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" From 3489109758951cf6661174ce28a7deb178a29c6e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:43:38 +0200 Subject: [PATCH 119/719] rdte: request exact WOW Sidecar BETA.3 T6 rep --- config/truenas-rdte-run-request.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 6b844a514..091f08f2a 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,8 +1,9 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t5-initial", - "version": "25.04.1", - "rung": "t5", - "authority_issue": 411, - "purpose": "First exact deployed-legacy TrueNAS 25.04.1 T0-T5 qualification for Foundry materialization." + "request_id": "tn-26-beta3-t6-wow-sidecar-001", + "version": "26.0.0-BETA.3", + "rung": "t6", + "product": "wow-sidecar", + "authority_issue": 276, + "purpose": "Pre-hardware WOW Sidecar Foundry T6 qualification on the already-accepted disposable TrueNAS BETA.3 substrate." } From 923ef0addd5089908205db1f57960918499833d1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:43:41 +0200 Subject: [PATCH 120/719] rdte: include WOW T6 consumer in cheap static gate --- .github/workflows/prep-rdte-static.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index d8fbeae11..346d6d832 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -17,6 +17,7 @@ on: - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' - 'tests/test_truenas_t6_litellm_control.py' + - 'tests/test_truenas_t6_wow_sidecar_control.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_rdte_target.py' pull_request: @@ -62,6 +63,9 @@ jobs: if [[ -f tests/test_truenas_t6_litellm_control.py ]]; then tests+=(tests/test_truenas_t6_litellm_control.py) fi + if [[ -f tests/test_truenas_t6_wow_sidecar_control.py ]]; then + tests+=(tests/test_truenas_t6_wow_sidecar_control.py) + fi if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi From 68e6c89111c58181f27bec295ff37dde854bed26 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:43:43 +0200 Subject: [PATCH 121/719] test: lock WOW T6 product and public-fixture boundary --- tests/test_truenas_t6_wow_sidecar_control.py | 105 +++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 tests/test_truenas_t6_wow_sidecar_control.py diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py new file mode 100644 index 000000000..fcccc99a7 --- /dev/null +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "truenas_middleware_wow_sidecar_t6_probe.py" +SPEC = importlib.util.spec_from_file_location("wow_t6_probe", SCRIPT) +assert SPEC and SPEC.loader +MOD = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MOD) + + +class WowSidecarT6ControlTests(unittest.TestCase): + def test_probe_contract_is_public_fixture_only(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn("production_credentials_present", text) + self.assertIn('"public-fixture-only"', text) + self.assertIn(MOD.EXPECTED_IMAGE, text) + self.assertIn(MOD.EXPECTED_HELPER, text) + self.assertIn(MOD.EXPECTED_FIXTURE_DATASET, text) + self.assertIn('"execution_control_mutation_authorized": False', text) + self.assertNotIn("GITHUB_APP_PRIVATE_KEY=", text) + self.assertNotIn("SemperSupra/wow-sidecar-private", text) + + def test_probe_binds_seed_permissions_restart_and_cleanup_oracles(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + '"filesystem.stat"', + "EXPECTED_KEY_PATH", + "EXPECTED_PROFILE_PATH", + "EXPECTED_MARKER_PATH", + '"0o400"', + '"0o444"', + '"app.create"', + '"app.stop"', + '"app.start"', + '"app.delete"', + '"pool.dataset.delete"', + '"zero_residue": True', + "compose_readback_sha256", + "restart_compose_identity_preserved", + ): + self.assertIn(required, text) + + def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self): + workflow = (ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml").read_text(encoding="utf-8") + harness = (ROOT / "scripts" / "gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + request = json.loads((ROOT / "config" / "truenas-rdte-run-request.json").read_text(encoding="utf-8")) + + self.assertIn( + "export-wow-sidecar-t6-control.yml@706eca4fafcdafbff41ce6b0adb5a784a580df1e", + workflow, + ) + self.assertIn("name: wow-sidecar-truenas-t6-control", workflow) + self.assertIn("--t6-product", workflow) + self.assertIn('"wow-sidecar"', harness) + self.assertIn("truenas_middleware_wow_sidecar_t6_probe.py", harness) + self.assertIn('T6_PRODUCT="litellm"', harness) + self.assertEqual("wow-sidecar", request["product"]) + self.assertEqual("t6", request["rung"]) + self.assertEqual("26.0.0-BETA.3", request["version"]) + self.assertEqual(276, request["authority_issue"]) + + def test_load_control_rejects_foundry_drift(self): + compose = { + "services": { + "permissions": {"image": MOD.EXPECTED_HELPER}, + "wow-sidecar": {"image": MOD.EXPECTED_IMAGE}, + "wow-sidecar-config-seed": {"image": MOD.EXPECTED_IMAGE}, + }, + "configs": { + "fixture": {"content": MOD.FIXTURE_MARKER}, + }, + } + with tempfile.TemporaryDirectory() as td: + root=Path(td) + (root/"compose.json").write_text(json.dumps(compose),encoding="utf-8") + control={ + "schema":MOD.SCHEMA, + "foundry_ref":"a"*40, + "secrets_captured":False, + "candidate":{ + "wow_image":MOD.EXPECTED_IMAGE, + "permissions_helper":MOD.EXPECTED_HELPER, + }, + "runtime":{ + "app_name":MOD.EXPECTED_APP_NAME, + "production_credentials_present":False, + "fixture_key_marker":MOD.FIXTURE_MARKER, + "fixture_config_dir":MOD.EXPECTED_CONFIG_DIR, + "fixture_state_dir":MOD.EXPECTED_STATE_DIR, + }, + "artifacts":{"compose_canonical_sha256":MOD.canonical_sha256(compose)}, + } + (root/"control.json").write_text(json.dumps(control),encoding="utf-8") + with self.assertRaisesRegex(RuntimeError,"Foundry source ref drifted"): + MOD.load_control(root,"b"*40) + + +if __name__ == "__main__": + unittest.main() From 7578d804c02ab187bb7a030b40d259a233babfab Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:45:04 +0200 Subject: [PATCH 122/719] rdte: separate substrate and WOW consumer authority --- config/truenas-rdte-run-request.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 091f08f2a..f62fbe4b4 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -4,6 +4,7 @@ "version": "26.0.0-BETA.3", "rung": "t6", "product": "wow-sidecar", - "authority_issue": 276, - "purpose": "Pre-hardware WOW Sidecar Foundry T6 qualification on the already-accepted disposable TrueNAS BETA.3 substrate." + "authority_issue": 396, + "purpose": "Pre-hardware WOW Sidecar Foundry T6 qualification on the already-accepted disposable TrueNAS BETA.3 substrate.", + "consumer_authority_issue": 276 } From 3ae99fc472689197819f3f2201ef09e1af341e8b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:45:07 +0200 Subject: [PATCH 123/719] test: preserve LiteLLM exact-ref invariant under T6 product selection --- tests/test_truenas_t6_litellm_control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_litellm_control.py b/tests/test_truenas_t6_litellm_control.py index 778f3e699..0100e5646 100644 --- a/tests/test_truenas_t6_litellm_control.py +++ b/tests/test_truenas_t6_litellm_control.py @@ -122,7 +122,9 @@ def test_workflow_and_harness_are_bound_to_exact_litellm_export(self): ) self.assertIn("foundry_ref: " + MOD.EXPECTED_FOUNDRY_REF, workflow) self.assertIn("name: litellm-truenas-t6-control", workflow) - self.assertIn('--foundry-commit "' + MOD.EXPECTED_FOUNDRY_REF + '"', workflow) + self.assertIn('foundry_commit="' + MOD.EXPECTED_FOUNDRY_REF + '"', workflow) + self.assertIn('--foundry-commit "$foundry_commit"', workflow) + self.assertIn("needs.changes.outputs.truenas_t6_product == 'litellm'", workflow) self.assertNotIn("truenas-foundry-materialized-controls", workflow) self.assertIn('LITELLM_HOSTFWD=",hostfwd=tcp:127.0.0.1:', harness) self.assertIn('-:30401"', harness) From 29a6726303fa354c429517d08638ea49850f963b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:45:10 +0200 Subject: [PATCH 124/719] test: keep exact target authority while admitting bounded T6 product --- tests/test_truenas_rdte_target.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 9a9638e53..b6a0103f6 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -41,13 +41,17 @@ def test_unknown_exact_version_fails_closed(self): raise MOD.TargetError("exact TrueNAS target is not registered: 26.0.0-RC.1") - def test_run_request_is_exact_registered_t5(self): + def test_run_request_is_exact_registered_target(self): request=json.loads(RUN_REQUEST.read_text(encoding="utf-8")) self.assertEqual(request["schema"], "gha-kvm-truenas-run-request/v1") - self.assertEqual(request["rung"], "t5") + self.assertIn(request["rung"], {"t0","t1","t2","t3","t4","t5","t6"}) self.assertIn(request["version"], self.targets) target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) + if request["rung"] == "t6": + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar"}) + if request.get("product") == "wow-sidecar": + self.assertEqual(request.get("consumer_authority_issue"), 276) self.assertNotIn("latest", request["version"].lower()) self.assertNotIn("nightly", request["version"].lower()) From 562e037979c1fa5102d6a504231519230d9018f3 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:45:12 +0200 Subject: [PATCH 125/719] test: distinguish forbidden private identifiers from substrate authority --- tests/test_truenas_t6_wow_sidecar_control.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index fcccc99a7..65923f37d 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -24,7 +24,7 @@ def test_probe_contract_is_public_fixture_only(self): self.assertIn(MOD.EXPECTED_FIXTURE_DATASET, text) self.assertIn('"execution_control_mutation_authorized": False', text) self.assertNotIn("GITHUB_APP_PRIVATE_KEY=", text) - self.assertNotIn("SemperSupra/wow-sidecar-private", text) + self.assertIn('"SemperSupra/wow-sidecar-private"', text) def test_probe_binds_seed_permissions_restart_and_cleanup_oracles(self): text = SCRIPT.read_text(encoding="utf-8") @@ -63,7 +63,8 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertEqual("wow-sidecar", request["product"]) self.assertEqual("t6", request["rung"]) self.assertEqual("26.0.0-BETA.3", request["version"]) - self.assertEqual(276, request["authority_issue"]) + self.assertEqual(396, request["authority_issue"]) + self.assertEqual(276, request["consumer_authority_issue"]) def test_load_control_rejects_foundry_drift(self): compose = { From 5039bb671385fdc974964e16fd56fa44364fcf4e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:46:21 +0200 Subject: [PATCH 126/719] rdte: reject private identities without naming them publicly --- scripts/truenas_middleware_wow_sidecar_t6_probe.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_wow_sidecar_t6_probe.py b/scripts/truenas_middleware_wow_sidecar_t6_probe.py index b13911469..37bb3e3a2 100644 --- a/scripts/truenas_middleware_wow_sidecar_t6_probe.py +++ b/scripts/truenas_middleware_wow_sidecar_t6_probe.py @@ -6,6 +6,7 @@ import hashlib import json import pathlib +import re import time from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for @@ -23,6 +24,7 @@ EXPECTED_PROFILE_PATH = EXPECTED_CONFIG_DIR + "/profiles/operator.json" EXPECTED_MARKER_PATH = EXPECTED_CONFIG_DIR + "/.initialized-v1" FIXTURE_MARKER = "PUBLIC-QUALIFICATION-FIXTURE" +PRIVATE_REPO_RE = re.compile(r"(?:https://github\\.com/)?SemperSupra/[A-Za-z0-9_.-]+-private(?![A-Za-z0-9_.-])", re.IGNORECASE) def canonical_sha256(value: object) -> str: @@ -80,9 +82,11 @@ def load_control(directory: pathlib.Path, foundry_commit: str) -> tuple[dict, di serialized = json.dumps(compose, sort_keys=True) if FIXTURE_MARKER not in serialized: raise RuntimeError("public fixture marker is absent from exact control") - for forbidden in ("ghp_", "sk-", "SemperSupra/agent-dispatch-private", "SemperSupra/wow-sidecar-private"): + if PRIVATE_REPO_RE.search(serialized): + raise RuntimeError("private repository identity leaked into public control") + for forbidden in ("ghp_", "sk-"): if forbidden in serialized: - raise RuntimeError(f"private credential/authority content leaked into public control: {forbidden}") + raise RuntimeError(f"private credential-like content leaked into public control: {forbidden}") return control, compose From 1ca22e0be9d27c2e56f21a18ff5055d5e19b5787 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:46:23 +0200 Subject: [PATCH 127/719] test: keep private repo names out of public WOW oracle --- tests/test_truenas_t6_wow_sidecar_control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 65923f37d..2e2841089 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -24,7 +24,9 @@ def test_probe_contract_is_public_fixture_only(self): self.assertIn(MOD.EXPECTED_FIXTURE_DATASET, text) self.assertIn('"execution_control_mutation_authorized": False', text) self.assertNotIn("GITHUB_APP_PRIVATE_KEY=", text) - self.assertIn('"SemperSupra/wow-sidecar-private"', text) + self.assertIn("PRIVATE_REPO_RE", text) + self.assertNotIn("SemperSupra/wow-sidecar-private", text) + self.assertNotIn("SemperSupra/agent-dispatch-private", text) def test_probe_binds_seed_permissions_restart_and_cleanup_oracles(self): text = SCRIPT.read_text(encoding="utf-8") From 9f8c621cc4b31b22fabf5a88cb0521e29d24d447 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:49:10 +0200 Subject: [PATCH 128/719] rdte: repin WOW T6 exporter after least-authority fix --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 6e2d081bf..9d7cee566 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -157,9 +157,9 @@ jobs: foundry-wow-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'wow-sidecar' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-wow-sidecar-t6-control.yml@706eca4fafcdafbff41ce6b0adb5a784a580df1e + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-wow-sidecar-t6-control.yml@68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2 with: - foundry_ref: 706eca4fafcdafbff41ce6b0adb5a784a580df1e + foundry_ref: 68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2 permissions: contents: read @@ -291,7 +291,7 @@ jobs: ) if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then - foundry_commit="706eca4fafcdafbff41ce6b0adb5a784a580df1e" + foundry_commit="68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 79ffba48aa42641fd1fb1f957f575039535afb80 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:49:13 +0200 Subject: [PATCH 129/719] test: bind least-authority WOW T6 exporter head --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 2e2841089..7d4781951 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -54,7 +54,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self request = json.loads((ROOT / "config" / "truenas-rdte-run-request.json").read_text(encoding="utf-8")) self.assertIn( - "export-wow-sidecar-t6-control.yml@706eca4fafcdafbff41ce6b0adb5a784a580df1e", + "export-wow-sidecar-t6-control.yml@68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2", workflow, ) self.assertIn("name: wow-sidecar-truenas-t6-control", workflow) From beda91278cb9ad3200e9745939cbfd9f803409ea Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:51:59 +0200 Subject: [PATCH 130/719] rdte: request permission-corrected WOW Sidecar T6 rep --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index f62fbe4b4..93fcf0cbd 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-wow-sidecar-001", + "request_id": "tn-26-beta3-t6-wow-sidecar-002", "version": "26.0.0-BETA.3", "rung": "t6", "product": "wow-sidecar", "authority_issue": 396, - "purpose": "Pre-hardware WOW Sidecar Foundry T6 qualification on the already-accepted disposable TrueNAS BETA.3 substrate.", + "purpose": "Execute the permission-corrected WOW Sidecar pre-hardware T6 consumer rep on exact disposable TrueNAS BETA.3.", "consumer_authority_issue": 276 } From 0b94652775b2548bb15c72e475dc16918fbbdbf2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:55:06 +0200 Subject: [PATCH 131/719] prep: carry GARM T6 truenas_middleware_garm_t6_probe.py onto latest RDTE base --- scripts/truenas_middleware_garm_t6_probe.py | 490 ++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 scripts/truenas_middleware_garm_t6_probe.py diff --git a/scripts/truenas_middleware_garm_t6_probe.py b/scripts/truenas_middleware_garm_t6_probe.py new file mode 100644 index 000000000..c0d54202c --- /dev/null +++ b/scripts/truenas_middleware_garm_t6_probe.py @@ -0,0 +1,490 @@ +#!/usr/bin/env python3 +"""Bounded GARM controller Foundry T6 oracle on disposable TrueNAS.""" +from __future__ import annotations + +import argparse +import copy +import hashlib +import http.client +import json +import pathlib +import secrets +import ssl +import string +import subprocess +import tempfile +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + +SCHEMA = "semper-supra.garm-truenas-t6-control/1" +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_APP_NAME = "rdte-t6-garm" +EXPECTED_IMAGE = "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9" +EXPECTED_CONTROLLER_SOURCE = "8a72cb23deacc712786945ac5e7ee63f8b4976d6" +EXPECTED_PROVIDER_SOURCE = "14535745dc3aa3c0b5466da7c704bca4d23dcec5" +EXPECTED_FIXTURE_DATASET = "rdtepool/garm-t6" +EXPECTED_FIXTURE_ROOT = "/mnt/rdtepool/garm-t6" +EXPECTED_CONFIG_DIR = EXPECTED_FIXTURE_ROOT + "/config" +EXPECTED_CONFIG_PATH = EXPECTED_CONFIG_DIR + "/config.toml" +EXPECTED_DB_PATH = EXPECTED_CONFIG_DIR + "/garm.db" +BOOTSTRAP_CONFIG = "garm-initial-config" +TLS_CERT_CONFIG = "garm-tls-certificate" +TLS_KEY_CONFIG = "garm-tls-private-key" +BOOTSTRAP_PLACEHOLDER = "__EPHEMERAL_NESTED_GARM_CONFIG__" +TLS_PREFIX = "__EPHEMERAL_NESTED_TLS__:" + + +def canonical_sha256(value: object) -> str: + raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(raw).hexdigest() + + +def load_json(path: pathlib.Path) -> dict: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise RuntimeError(f"{path} must contain an object") + return value + + +def normalize_secret_configs(compose: dict) -> dict: + normalized = copy.deepcopy(compose) + configs = normalized.get("configs") or {} + if BOOTSTRAP_CONFIG not in configs or TLS_CERT_CONFIG not in configs or TLS_KEY_CONFIG not in configs: + raise RuntimeError("GARM secret-bearing config inventory drifted") + configs[BOOTSTRAP_CONFIG]["content"] = BOOTSTRAP_PLACEHOLDER + configs[TLS_CERT_CONFIG]["content"] = TLS_PREFIX + TLS_CERT_CONFIG + configs[TLS_KEY_CONFIG]["content"] = TLS_PREFIX + TLS_KEY_CONFIG + return normalized + + +def load_control(directory: pathlib.Path, foundry_commit: str) -> tuple[dict, dict]: + control = load_json(directory / "control.json") + compose = load_json(directory / "compose.json") + deployment = load_json(directory / "deployment.json") + + if control.get("schema") != SCHEMA: + raise RuntimeError("unexpected GARM T6 control schema") + if control.get("foundry_ref") != foundry_commit: + raise RuntimeError("Foundry source ref drifted") + if control.get("appliance_reference") != EXPECTED_IMAGE: + raise RuntimeError("GARM appliance identity drifted") + if control.get("controller_source") != EXPECTED_CONTROLLER_SOURCE: + raise RuntimeError("GARM controller source identity drifted") + if control.get("provider_source") != EXPECTED_PROVIDER_SOURCE: + raise RuntimeError("TrueNAS provider source identity drifted") + if control.get("private_secrets_captured") is not False: + raise RuntimeError("control does not assert private_secrets_captured=false") + if control.get("fixture_secret_values_retained") is not False: + raise RuntimeError("control retained fixture secret values") + if control.get("github_credentials_present") is not False: + raise RuntimeError("control claims GitHub credentials are present") + + runtime = control.get("runtime") or {} + if runtime.get("app_name") != EXPECTED_APP_NAME: + raise RuntimeError("GARM T6 app name drifted") + if runtime.get("config_root") != EXPECTED_CONFIG_DIR: + raise RuntimeError("GARM T6 config root drifted") + if int(runtime.get("host_port") or 0) != 30880: + raise RuntimeError("GARM T6 guest service port drifted") + if runtime.get("bootstrap_replacement") != BOOTSTRAP_CONFIG: + raise RuntimeError("GARM bootstrap replacement contract drifted") + + if control.get("runtime_compose_sha256") != canonical_sha256(compose): + raise RuntimeError("GARM runtime Compose identity does not match control") + if deployment.get("artifact_sha256") != control.get("deployment_artifact_sha256"): + raise RuntimeError("GARM deployment artifact identity drifted") + if deployment.get("materialization_identity") != control.get("materialization_identity"): + raise RuntimeError("GARM materialization identity drifted") + + services = compose.get("services") or {} + if set(services) != {"garm", "garm-config-seed"}: + raise RuntimeError("unexpected GARM T6 service inventory") + for name in ("garm", "garm-config-seed"): + service = services[name] + if service.get("image") != EXPECTED_IMAGE: + raise RuntimeError(f"{name} image identity drifted") + if service.get("privileged") is True: + raise RuntimeError(f"{name} became privileged") + if "ALL" not in {str(x).upper() for x in service.get("cap_drop") or []}: + raise RuntimeError(f"{name} lost cap_drop ALL") + options = {str(x).lower().replace(":", "=") for x in service.get("security_opt") or []} + if not any(x.startswith("no-new-privileges=true") for x in options): + raise RuntimeError(f"{name} lost no-new-privileges") + + serialized = json.dumps(compose, sort_keys=True) + for forbidden in ( + "BEGIN PRIVATE KEY", + "PRIVATE-QUALIFICATION-FIXTURE", + "PUBLIC-QUALIFICATION-FIXTURE", + "ghp_", + "github_pat_", + ): + if forbidden in serialized: + raise RuntimeError(f"secret-like retained control content: {forbidden}") + if BOOTSTRAP_PLACEHOLDER not in serialized or TLS_PREFIX not in serialized: + raise RuntimeError("run-local secret placeholders are absent") + return control, compose + + +def build_ephemeral_runtime_compose(compose: dict) -> tuple[dict, dict[str, str]]: + runtime = copy.deepcopy(compose) + jwt_secret = secrets.token_urlsafe(36) + alphabet = string.ascii_letters + string.digits + db_passphrase = "".join(secrets.choice(alphabet) for _ in range(32)) + + with tempfile.TemporaryDirectory(prefix="garm-t6-tls-") as td: + root = pathlib.Path(td) + key_path = root / "tls.key" + cert_path = root / "tls.crt" + subprocess.run( + [ + "openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", + "-subj", "/CN=garm-t6.invalid", + "-addext", "subjectAltName=DNS:garm-t6.invalid,IP:127.0.0.1", + "-keyout", str(key_path), "-out", str(cert_path), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + tls_key = key_path.read_text(encoding="utf-8") + tls_cert = cert_path.read_text(encoding="utf-8") + + bootstrap = f"""[default] +enable_webhook_management = false + +[logging] +log_level = "info" + +[metrics] +disable_auth = false + +[jwt_auth] +secret = "{jwt_secret}" +time_to_live = "1h" + +[apiserver] +bind = "0.0.0.0" +port = 8080 +use_tls = true + +[apiserver.tls] +certificate = "/garm-tls.crt" +key = "/garm-tls.key" + +[apiserver.webui] +enable = true + +[database] +backend = "sqlite3" +passphrase = "{db_passphrase}" + +[database.sqlite3] +db_file = "/etc/garm/garm.db" +""" + configs = runtime["configs"] + configs[BOOTSTRAP_CONFIG]["content"] = bootstrap + configs[TLS_CERT_CONFIG]["content"] = tls_cert + configs[TLS_KEY_CONFIG]["content"] = tls_key + return runtime, { + "jwt_secret_sha256": hashlib.sha256(jwt_secret.encode()).hexdigest(), + "database_passphrase_sha256": hashlib.sha256(db_passphrase.encode()).hexdigest(), + "certificate_sha256": hashlib.sha256(tls_cert.encode()).hexdigest(), + } + + +def https_ui_probe(port: int, timeout: float = 8.0) -> int: + context = ssl._create_unverified_context() + conn = http.client.HTTPSConnection("127.0.0.1", port, timeout=timeout, context=context) + try: + conn.request("GET", "/ui/") + response = conn.getresponse() + response.read(4096) + return int(response.status) + finally: + conn.close() + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--service-port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--control-dir", type=pathlib.Path, required=True) + p.add_argument("--foundry-commit", required=True) + p.add_argument("--out", required=True) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + p.add_argument("--state-timeout", type=float, default=240.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-garm-foundry-t6/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected_version": EXPECTED_VERSION, + "foundry_commit": a.foundry_commit, + "app_name": EXPECTED_APP_NAME, + "appliance": EXPECTED_IMAGE, + "controller_source": EXPECTED_CONTROLLER_SOURCE, + "provider_source": EXPECTED_PROVIDER_SOURCE, + "github_credentials_present": False, + "github_jit_registration_exercised": False, + "physical_truenas_mutation": False, + "capacity_promotion": False, + "secret_values_recorded": False, + } + ws = None + request_id = 1 + created_app = False + created_dataset = False + + try: + control, retained_compose = load_control(a.control_dir, a.foundry_commit) + runtime_compose, secret_fingerprints = build_ephemeral_runtime_compose(retained_compose) + retained_sha = canonical_sha256(retained_compose) + payload["materialization"] = { + "schema": control["schema"], + "foundry_ref": control["foundry_ref"], + "retained_compose_sha256": retained_sha, + "deployment_artifact_sha256": control["deployment_artifact_sha256"], + "materialization_identity": control["materialization_identity"], + "run_local_secret_fingerprints": secret_fingerprints, + "secret_values_recorded": False, + } + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError("DDP connection failed") + + def call(method, params): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + def wait_job(job_id, label): + deadline = time.monotonic() + a.job_timeout + last = None + while time.monotonic() < deadline: + last = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last and last.get("state") == "SUCCESS": + return last + if last and last.get("state") in {"FAILED", "ABORTED"}: + raise RuntimeError(f"{label} job {last.get('state')}") + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS") + + def app_query(): + return call("app.query", [[["id", "=", EXPECTED_APP_NAME]], {"get": True}]) + + def wait_running(): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = app_query() + if last: + if last.get("state") in {"CRASHED", "ERROR"}: + raise RuntimeError(f"GARM app entered {last.get('state')}") + details = (last.get("active_workloads") or {}).get("container_details") or [] + controllers = [ + item for item in details + if item.get("service_name") == "garm" + and item.get("image") == EXPECTED_IMAGE + and item.get("state") == "running" + ] + if last.get("state") == "RUNNING" and len(controllers) == 1: + return last + time.sleep(1) + raise RuntimeError(f"GARM controller did not reach RUNNING: {last!r}") + + auth = call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError("authentication did not return SUCCESS") + + payload["system_version"] = call("system.version", []) + if payload["system_version"] != EXPECTED_VERSION: + raise RuntimeError("target version drifted") + + if call("app.query", [[["id", "=", EXPECTED_APP_NAME]]]): + raise RuntimeError("refusing adopted GARM app state") + if call("pool.dataset.query", [[["id", "=", EXPECTED_FIXTURE_DATASET]]]): + raise RuntimeError("refusing adopted GARM fixture dataset") + + dataset = call("pool.dataset.create", [{ + "name": EXPECTED_FIXTURE_DATASET, + "type": "FILESYSTEM", + "share_type": "GENERIC", + "comments": "SemperSupra disposable GARM T6 fixture", + }]) + if not isinstance(dataset, dict) or dataset.get("id") != EXPECTED_FIXTURE_DATASET: + raise RuntimeError("pool.dataset.create did not return exact GARM fixture dataset") + created_dataset = True + + mkdir = call("filesystem.mkdir", [{ + "path": EXPECTED_CONFIG_DIR, + "options": {"mode": "750", "raise_chmod_error": True}, + }]) + if not isinstance(mkdir, dict) or mkdir.get("path") != EXPECTED_CONFIG_DIR: + raise RuntimeError("filesystem.mkdir did not create exact GARM config directory") + payload["fixture_storage"] = { + "dataset": EXPECTED_FIXTURE_DATASET, + "config_dir": EXPECTED_CONFIG_DIR, + "content_recorded": False, + } + + create_id = call("app.create", [{ + "app_name": EXPECTED_APP_NAME, + "custom_app": True, + "custom_compose_config": runtime_compose, + }]) + if not isinstance(create_id, int): + raise RuntimeError("app.create did not return a job id") + wait_job(create_id, "app.create") + created_app = True + + app = wait_running() + details = (app.get("active_workloads") or {}).get("container_details") or [] + payload["runtime"] = { + "state": app.get("state"), + "custom_app": app.get("custom_app"), + "controller_running_exact_image": any( + item.get("service_name") == "garm" + and item.get("image") == EXPECTED_IMAGE + and item.get("state") == "running" + for item in details + ), + "container_states": sorted({ + f"{item.get('service_name')}:{item.get('state')}" + for item in details + if isinstance(item, dict) + }), + } + if app.get("custom_app") is not True: + raise RuntimeError("GARM runtime did not materialize as a custom app") + + readback = call("app.config", [EXPECTED_APP_NAME]) + normalized_readback = normalize_secret_configs(readback) + readback_sha = canonical_sha256(normalized_readback) + if readback_sha != retained_sha: + raise RuntimeError("secret-normalized app.config read-back does not match retained Foundry Compose") + payload["runtime"]["secret_normalized_compose_readback_sha256"] = readback_sha + + config_meta = call("filesystem.stat", [EXPECTED_CONFIG_PATH]) + db_meta = call("filesystem.stat", [EXPECTED_DB_PATH]) + payload["persistent_state"] = { + "config_toml": { + "path": EXPECTED_CONFIG_PATH, + "type": config_meta.get("type"), + "size_positive": int(config_meta.get("size") or 0) > 0, + }, + "garm_db": { + "path": EXPECTED_DB_PATH, + "type": db_meta.get("type"), + "size_positive": int(db_meta.get("size") or 0) > 0, + }, + "raw_content_recorded": False, + } + if not payload["persistent_state"]["config_toml"]["size_positive"]: + raise RuntimeError("persistent config.toml is empty") + if not payload["persistent_state"]["garm_db"]["size_positive"]: + raise RuntimeError("persistent garm.db is empty") + + status = https_ui_probe(a.service_port, timeout=a.timeout) + if status < 200 or status >= 400: + raise RuntimeError(f"GARM external /ui/ probe returned HTTP {status}") + payload["runtime"]["external_https_ui_status"] = status + + stop_id = call("app.stop", [EXPECTED_APP_NAME]) + wait_job(stop_id, "app.stop") + stopped = app_query() + if not stopped or stopped.get("state") != "STOPPED": + raise RuntimeError("GARM app did not reach STOPPED") + + start_id = call("app.start", [EXPECTED_APP_NAME]) + wait_job(start_id, "app.start") + wait_running() + + restart_readback = normalize_secret_configs(call("app.config", [EXPECTED_APP_NAME])) + if canonical_sha256(restart_readback) != retained_sha: + raise RuntimeError("GARM secret-normalized Compose identity drifted after restart") + restart_status = https_ui_probe(a.service_port, timeout=a.timeout) + if restart_status < 200 or restart_status >= 400: + raise RuntimeError(f"GARM external /ui/ restart probe returned HTTP {restart_status}") + payload["runtime"]["restart_compose_identity_preserved"] = True + payload["runtime"]["restart_https_ui_status"] = restart_status + + delete_id = call("app.delete", [EXPECTED_APP_NAME, { + "remove_images": False, + "remove_ix_volumes": False, + "force_remove_custom_app": False, + }]) + wait_job(delete_id, "app.delete") + created_app = False + if call("app.query", [[["id", "=", EXPECTED_APP_NAME]]]): + raise RuntimeError("GARM app remains after delete") + + deleted = call("pool.dataset.delete", [ + EXPECTED_FIXTURE_DATASET, + {"recursive": True, "force": False}, + ]) + if deleted is not True: + raise RuntimeError("pool.dataset.delete did not return true") + created_dataset = False + if call("pool.dataset.query", [[["id", "=", EXPECTED_FIXTURE_DATASET]]]): + raise RuntimeError("GARM fixture dataset remains after delete") + + mountpoint_absent = False + try: + call("filesystem.stat", [EXPECTED_FIXTURE_ROOT]) + except RuntimeError: + mountpoint_absent = True + if not mountpoint_absent: + raise RuntimeError("GARM fixture mountpoint remains after dataset delete") + + payload["cleanup"] = { + "app_absent": True, + "fixture_dataset_absent": True, + "fixture_mountpoint_absent": True, + "zero_residue": True, + } + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "exact Foundry-exported GARM controller App realized on real disposable TrueNAS; " + "exact appliance and secret-normalized config read-back, persistent controller state, " + "external HTTPS UI, restart persistence, and delete/zero-residue all passed; " + "GitHub credentials/JIT registration intentionally not exercised" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + payload["cleanup_needed"] = { + "app": created_app, + "fixture_dataset": created_dataset, + } + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From bb6626a9963eb247ec465bbe7e2e95a6eedace63 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:55:08 +0200 Subject: [PATCH 132/719] prep: carry GARM T6 test_truenas_t6_garm_control.py onto latest RDTE base --- tests/test_truenas_t6_garm_control.py | 138 ++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/test_truenas_t6_garm_control.py diff --git a/tests/test_truenas_t6_garm_control.py b/tests/test_truenas_t6_garm_control.py new file mode 100644 index 000000000..6acd8a1d2 --- /dev/null +++ b/tests/test_truenas_t6_garm_control.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "truenas_middleware_garm_t6_probe.py" +SPEC = importlib.util.spec_from_file_location("garm_t6_probe", SCRIPT) +assert SPEC and SPEC.loader +MOD = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MOD) + + +class GarmT6ControlTests(unittest.TestCase): + def sample_compose(self): + service = { + "image": MOD.EXPECTED_IMAGE, + "cap_drop": ["ALL"], + "security_opt": ["no-new-privileges=true"], + "privileged": False, + } + return { + "services": { + "garm": dict(service), + "garm-config-seed": dict(service), + }, + "configs": { + MOD.BOOTSTRAP_CONFIG: {"content": MOD.BOOTSTRAP_PLACEHOLDER}, + MOD.TLS_CERT_CONFIG: {"content": MOD.TLS_PREFIX + MOD.TLS_CERT_CONFIG}, + MOD.TLS_KEY_CONFIG: {"content": MOD.TLS_PREFIX + MOD.TLS_KEY_CONFIG}, + }, + } + + def test_probe_contract_keeps_github_and_physical_boundaries_explicit(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + '"github_credentials_present": False', + '"github_jit_registration_exercised": False', + '"physical_truenas_mutation": False', + '"capacity_promotion": False', + '"secret_values_recorded": False', + "EXPECTED_PROVIDER_SOURCE", + "EXPECTED_CONTROLLER_SOURCE", + ): + self.assertIn(required, text) + self.assertNotIn("GITHUB_APP_PRIVATE_KEY=", text) + self.assertNotIn("local-truenas-linux-general", text) + + def test_probe_binds_controller_persistence_restart_and_cleanup(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + '"app.create"', + '"app.stop"', + '"app.start"', + '"app.delete"', + '"pool.dataset.delete"', + "EXPECTED_CONFIG_PATH", + "EXPECTED_DB_PATH", + "https_ui_probe", + '"zero_residue": True', + "secret_normalized_compose_readback_sha256", + "restart_compose_identity_preserved", + ): + self.assertIn(required, text) + + def test_normalization_restores_retained_placeholders(self): + compose = self.sample_compose() + compose["configs"][MOD.BOOTSTRAP_CONFIG]["content"] = "runtime-bootstrap" + compose["configs"][MOD.TLS_CERT_CONFIG]["content"] = "runtime-cert" + compose["configs"][MOD.TLS_KEY_CONFIG]["content"] = "runtime-key" + normalized = MOD.normalize_secret_configs(compose) + self.assertEqual( + normalized["configs"][MOD.BOOTSTRAP_CONFIG]["content"], + MOD.BOOTSTRAP_PLACEHOLDER, + ) + self.assertEqual( + normalized["configs"][MOD.TLS_CERT_CONFIG]["content"], + MOD.TLS_PREFIX + MOD.TLS_CERT_CONFIG, + ) + self.assertEqual( + normalized["configs"][MOD.TLS_KEY_CONFIG]["content"], + MOD.TLS_PREFIX + MOD.TLS_KEY_CONFIG, + ) + + def test_workflow_routes_exact_garm_export_through_existing_t6_selector(self): + workflow = (ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml").read_text(encoding="utf-8") + harness = (ROOT / "scripts" / "gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + self.assertIn( + "export-garm-t6-control.yml@34a0759fe390d7a92b270a599b6584f8949c6d07", + workflow, + ) + self.assertIn("name: garm-truenas-t6-control", workflow) + self.assertIn("needs.changes.outputs.truenas_t6_product == 'garm'", workflow) + self.assertIn("packages: read", workflow) + self.assertIn('T6_PRODUCT\" == \"garm', harness) + self.assertIn("truenas_middleware_garm_t6_probe.py", harness) + self.assertIn("GARM_HOSTFWD", harness) + self.assertIn("-:30880", harness) + + def test_load_control_rejects_foundry_drift(self): + compose = self.sample_compose() + with tempfile.TemporaryDirectory() as td: + root = Path(td) + deployment = { + "artifact_sha256": "a" * 64, + "materialization_identity": "sha256:" + "b" * 64, + } + control = { + "schema": MOD.SCHEMA, + "foundry_ref": "a" * 40, + "appliance_reference": MOD.EXPECTED_IMAGE, + "controller_source": MOD.EXPECTED_CONTROLLER_SOURCE, + "provider_source": MOD.EXPECTED_PROVIDER_SOURCE, + "private_secrets_captured": False, + "fixture_secret_values_retained": False, + "github_credentials_present": False, + "runtime_compose_sha256": MOD.canonical_sha256(compose), + "deployment_artifact_sha256": deployment["artifact_sha256"], + "materialization_identity": deployment["materialization_identity"], + "runtime": { + "app_name": MOD.EXPECTED_APP_NAME, + "config_root": MOD.EXPECTED_CONFIG_DIR, + "host_port": 30880, + "bootstrap_replacement": MOD.BOOTSTRAP_CONFIG, + }, + } + (root / "compose.json").write_text(json.dumps(compose), encoding="utf-8") + (root / "deployment.json").write_text(json.dumps(deployment), encoding="utf-8") + (root / "control.json").write_text(json.dumps(control), encoding="utf-8") + with self.assertRaisesRegex(RuntimeError, "Foundry source ref drifted"): + MOD.load_control(root, "b" * 40) + + +if __name__ == "__main__": + unittest.main() From b8186ccdf27a8b3330665031834550cff67afcec Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:55:20 +0200 Subject: [PATCH 133/719] prep: carry GARM T6 gha_kvm_truenas_rdte.sh onto latest RDTE base --- scripts/gha_kvm_truenas_rdte.sh | 39 +++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 727274c76..08ab22d59 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar] [--foundry-control-dir DIR] [--foundry-commit SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm] [--foundry-control-dir DIR] [--foundry-commit SHA]" } OUT="" @@ -47,7 +47,7 @@ eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } if [[ "$RUNG" == "t6" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "T6 is currently admitted only for exact TrueNAS 26.0.0-BETA.3; use Foundry F1-F5 for other targets" >&2; exit 2; } - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } FOUNDRY_CONTROL_DIR="$(realpath "$FOUNDRY_CONTROL_DIR")" @@ -117,7 +117,9 @@ payload = { "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else None, "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else [], "app": ( - {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} + {"name": "rdte-t6-garm", "image": "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm" + else {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" @@ -204,7 +206,10 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T5 lifecycle client" fi if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then + if [[ "$T6_PRODUCT" == "garm" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 GARM control client" + elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 WOW Sidecar control client" else @@ -442,6 +447,8 @@ PY )" LITELLM_HOST_PORT="" LITELLM_HOSTFWD="" +GARM_HOST_PORT="" +GARM_HOSTFWD="" if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "litellm" ]]; then LITELLM_HOST_PORT="$(python3 - <<'PY' import socket @@ -450,6 +457,14 @@ PY )" LITELLM_HOSTFWD=",hostfwd=tcp:127.0.0.1:${LITELLM_HOST_PORT}-:30401" fi +if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "garm" ]]; then + GARM_HOST_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" + GARM_HOSTFWD=",hostfwd=tcp:127.0.0.1:${GARM_HOST_PORT}-:30880" +fi : >"$STATE_DIR/serial.log" sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ @@ -457,7 +472,7 @@ sudo -n qemu-system-x86_64 \ -device "virtio-blk-pci,drive=rdteboot,id=rdte-boot,addr=0x4,bootindex=1" \ "${DATA_DRIVE_ARGS[@]}" \ -boot strict=on \ - -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443${LITELLM_HOSTFWD}" \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443${LITELLM_HOSTFWD}${GARM_HOSTFWD}" \ -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ @@ -599,7 +614,15 @@ if [[ "$RUNG" == "t5" ]]; then fi FOUNDRY_OUT="$STATE_DIR/foundry-control.json" -if [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then +if [[ "$T6_PRODUCT" == "garm" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$GARM_HOST_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ "${MIDDLEWARE_TLS_ARG[@]}" \ @@ -627,7 +650,9 @@ PY [[ "$FOUNDRY_OK" == "true" ]] || fail_evidence ORACLE_FAILURE foundry-materialization "exact Foundry-exported $T6_PRODUCT control did not realize and verify on TrueNAS" -if [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then +if [[ "$T6_PRODUCT" == "garm" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" +elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported WOW Sidecar control passed immutable image/config read-back, permissions/seed metadata, public-fixture worker runtime, restart persistence, and zero-residue cleanup" else write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported LiteLLM control passed S1 projection, exact image/config read-back, health, restart persistence, and delete/absence" From 7a963b92371499abeadd3d1c47eda2a64c079eb1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:55:47 +0200 Subject: [PATCH 134/719] ci: include GARM T6 contract in prep gate --- .github/workflows/prep-rdte-static.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 346d6d832..ac057ffab 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -18,6 +18,7 @@ on: - 'tests/test_truenas_future_protocols.py' - 'tests/test_truenas_t6_litellm_control.py' - 'tests/test_truenas_t6_wow_sidecar_control.py' + - 'tests/test_truenas_t6_garm_control.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_rdte_target.py' pull_request: @@ -66,6 +67,9 @@ jobs: if [[ -f tests/test_truenas_t6_wow_sidecar_control.py ]]; then tests+=(tests/test_truenas_t6_wow_sidecar_control.py) fi + if [[ -f tests/test_truenas_t6_garm_control.py ]]; then + tests+=(tests/test_truenas_t6_garm_control.py) + fi if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi From 5ef0d922efb29279bf24b599100f831c990efe21 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:56:13 +0200 Subject: [PATCH 135/719] exp: select exact GARM Foundry T6 consumer --- .github/workflows/gha-kvm-system-rdte.yml | 31 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 9d7cee566..fcc4d78a3 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -45,6 +45,7 @@ on: options: - litellm - wow-sidecar + - garm permissions: contents: read @@ -105,7 +106,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -116,7 +117,7 @@ jobs: if [[ "$run_request_changed" == "true" ]]; then truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm")' "$RUN_REQUEST_PATH")" jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ @@ -143,7 +144,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -163,6 +164,15 @@ jobs: permissions: contents: read + foundry-garm-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-garm-t6-control.yml@7fd0d2474248b61cf8e290fc80f40b761cea5e98 + with: + foundry_ref: 7fd0d2474248b61cf8e290fc80f40b761cea5e98 + permissions: + contents: read + qemu-t3-topology: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' @@ -202,6 +212,12 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + - name: Download exact GARM Foundry T6 control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-truenas-t6-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -244,7 +260,7 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control] if: >- always() && needs.contract.result == 'success' && @@ -253,7 +269,8 @@ jobs: ( needs.changes.outputs.truenas_rung != 't6' || (needs.changes.outputs.truenas_t6_product == 'litellm' && needs['foundry-t6-control'].result == 'success') || - (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') + (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') ) runs-on: ubuntu-26.04 timeout-minutes: 50 @@ -290,7 +307,9 @@ jobs: --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" ) if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then - if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then + if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then + foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" From e67f5e943321d7026ec6cc4f3d5e92db63b66d78 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:56:21 +0200 Subject: [PATCH 136/719] test: repin contents-read GARM T6 producer --- tests/test_truenas_t6_garm_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_garm_control.py b/tests/test_truenas_t6_garm_control.py index 6acd8a1d2..b59333e32 100644 --- a/tests/test_truenas_t6_garm_control.py +++ b/tests/test_truenas_t6_garm_control.py @@ -89,7 +89,7 @@ def test_workflow_routes_exact_garm_export_through_existing_t6_selector(self): workflow = (ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness = (ROOT / "scripts" / "gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn( - "export-garm-t6-control.yml@34a0759fe390d7a92b270a599b6584f8949c6d07", + "export-garm-t6-control.yml@7fd0d2474248b61cf8e290fc80f40b761cea5e98", workflow, ) self.assertIn("name: garm-truenas-t6-control", workflow) From fc35e43f0db2c443167928352c45771f56449078 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:58:51 +0200 Subject: [PATCH 137/719] test: lock GARM artifact to TrueNAS job --- tests/test_truenas_t6_garm_control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_garm_control.py b/tests/test_truenas_t6_garm_control.py index b59333e32..0c735dd03 100644 --- a/tests/test_truenas_t6_garm_control.py +++ b/tests/test_truenas_t6_garm_control.py @@ -94,11 +94,14 @@ def test_workflow_routes_exact_garm_export_through_existing_t6_selector(self): ) self.assertIn("name: garm-truenas-t6-control", workflow) self.assertIn("needs.changes.outputs.truenas_t6_product == 'garm'", workflow) - self.assertIn("packages: read", workflow) self.assertIn('T6_PRODUCT\" == \"garm', harness) self.assertIn("truenas_middleware_garm_t6_probe.py", harness) self.assertIn("GARM_HOSTFWD", harness) self.assertIn("-:30880", harness) + proxmox = workflow.split(" proxmox-9-2:", 1)[1].split(" truenas-26-beta3:", 1)[0] + truenas = workflow.split(" truenas-26-beta3:", 1)[1] + self.assertNotIn("Download exact GARM Foundry T6 control", proxmox) + self.assertIn("Download exact GARM Foundry T6 control", truenas) def test_load_control_rejects_foundry_drift(self): compose = self.sample_compose() From 0625a69f60875bf1704d6fb37fa84b292adc8e27 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:58:55 +0200 Subject: [PATCH 138/719] fix: scope GARM control download to TrueNAS job --- .github/workflows/gha-kvm-system-rdte.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index fcc4d78a3..305927a8e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -212,12 +212,6 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false - - name: Download exact GARM Foundry T6 control - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: garm-truenas-t6-control - path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -290,6 +284,12 @@ jobs: with: name: wow-sidecar-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact GARM Foundry T6 control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-truenas-t6-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail From 51b65cfb7893e588d8f91393c6b7d1e65d3a9675 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:00:11 +0200 Subject: [PATCH 139/719] rdte: stage GARM BETA.3 T6 rep 001 --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 93fcf0cbd..6b02f9831 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-wow-sidecar-002", + "request_id": "tn-26-beta3-t6-garm-001", "version": "26.0.0-BETA.3", "rung": "t6", - "product": "wow-sidecar", + "product": "garm", "authority_issue": 396, - "purpose": "Execute the permission-corrected WOW Sidecar pre-hardware T6 consumer rep on exact disposable TrueNAS BETA.3.", - "consumer_authority_issue": 276 + "purpose": "Execute the first GARM controller pre-HIL T6 rep on exact disposable TrueNAS BETA.3 using the accepted secret-free Foundry control.", + "consumer_authority_issue": 277 } From b1279b7f9f3bdbf72ea68fe986eae41d341ab720 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:00:14 +0200 Subject: [PATCH 140/719] test: admit exact GARM T6 run authority --- tests/test_truenas_rdte_target.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index b6a0103f6..e841045ba 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -49,9 +49,11 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) + if request.get("product") == "garm": + self.assertEqual(request.get("consumer_authority_issue"), 277) self.assertNotIn("latest", request["version"].lower()) self.assertNotIn("nightly", request["version"].lower()) From 6d2bd141c7c531aa458cc039fd1dd98d0329a5d7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:01:23 +0200 Subject: [PATCH 141/719] test: decouple WOW contract from shared T6 run pointer --- tests/test_truenas_t6_wow_sidecar_control.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 7d4781951..51df572e8 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -62,11 +62,12 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('"wow-sidecar"', harness) self.assertIn("truenas_middleware_wow_sidecar_t6_probe.py", harness) self.assertIn('T6_PRODUCT="litellm"', harness) - self.assertEqual("wow-sidecar", request["product"]) - self.assertEqual("t6", request["rung"]) - self.assertEqual("26.0.0-BETA.3", request["version"]) - self.assertEqual(396, request["authority_issue"]) - self.assertEqual(276, request["consumer_authority_issue"]) + self.assertIn(request["product"], {"litellm", "wow-sidecar", "garm"}) + if request["product"] == "wow-sidecar": + self.assertEqual("t6", request["rung"]) + self.assertEqual("26.0.0-BETA.3", request["version"]) + self.assertEqual(396, request["authority_issue"]) + self.assertEqual(276, request["consumer_authority_issue"]) def test_load_control_rejects_foundry_drift(self): compose = { From bc65e922dbf3f2bc6c091479083c7a89e5630e7e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:35:19 +0200 Subject: [PATCH 142/719] feat: add nested G2 provider adoption oracle --- ...uenas_middleware_garm_provider_g2_probe.py | 608 ++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 scripts/truenas_middleware_garm_provider_g2_probe.py diff --git a/scripts/truenas_middleware_garm_provider_g2_probe.py b/scripts/truenas_middleware_garm_provider_g2_probe.py new file mode 100644 index 000000000..ec5ce680c --- /dev/null +++ b/scripts/truenas_middleware_garm_provider_g2_probe.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python3 +"""G2 oracle: exact GARM TrueNAS provider transport/adoption on disposable TrueNAS.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import pathlib +import secrets +import shutil +import socket +import ssl +import subprocess +import tempfile +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_APPLIANCE = ( + "ghcr.io/sempersupra/garm-appliance@" + "sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9" +) +EXPECTED_PROVIDER_PRODUCT_SOURCE = "14535745dc3aa3c0b5466da7c704bca4d23dcec5" +EXPECTED_PROVIDER_BINARY_SHA256 = "911f076ba9421f7bb2a1ea72acc927327d799a5ad6265bffe2d869b2cbf5ecc2" +EXPECTED_FIXTURE_SCHEMA = "semper-supra.garm-provider-truenas-g2-fixtures/1" +EXPECTED_CONTROLLER_ID = "g2-controller" +EXPECTED_POOL_ID = "g2-pool" +PROVIDER_PATH_IN_IMAGE = "/opt/garm/providers.d/garm-provider-truenas" + + +class AdminSession: + def __init__(self, host: str, port: int, password: str, timeout: float): + self.host = host + self.port = port + self.password = password + self.timeout = timeout + self.ws: WebSocket | None = None + self.request_id = 1 + + def connect(self) -> None: + self.close() + self.ws = WebSocket(self.host, self.port, timeout=self.timeout, tls=False) + self.ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(self.ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError("DDP connection failed") + auth = self.call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": self.password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError("password authentication did not return SUCCESS") + + def call(self, method: str, params): + if self.ws is None: + raise RuntimeError("admin session is not connected") + result = ddp_call(self.ws, str(self.request_id), method, params) + self.request_id += 1 + return result + + def wait_job(self, job_id: int, label: str, timeout: float) -> dict: + deadline = time.monotonic() + timeout + last = None + while time.monotonic() < deadline: + last = self.call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last and last.get("state") == "SUCCESS": + return last + if last and last.get("state") in {"FAILED", "ABORTED"}: + raise RuntimeError(f"{label} job {last.get('state')}: {last.get('error')}") + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS: {last!r}") + + def close(self) -> None: + if self.ws is not None: + try: + self.ws.close() + finally: + self.ws = None + + +def load_bundle(directory: pathlib.Path, expected_producer: str) -> dict: + path = directory / "garm-provider-g2-fixtures.json" + bundle = json.loads(path.read_text(encoding="utf-8")) + if bundle.get("schema") != EXPECTED_FIXTURE_SCHEMA: + raise RuntimeError("unexpected G2 fixture schema") + if bundle.get("provider_product_source") != EXPECTED_PROVIDER_PRODUCT_SOURCE: + raise RuntimeError("G2 fixture product source drifted") + if bundle.get("producer_source") != expected_producer: + raise RuntimeError("G2 fixture producer source drifted") + if bundle.get("controller_id") != EXPECTED_CONTROLLER_ID: + raise RuntimeError("G2 fixture controller identity drifted") + if bundle.get("pool_id") != EXPECTED_POOL_ID: + raise RuntimeError("G2 fixture pool identity drifted") + fixtures = bundle.get("fixtures") + if not isinstance(fixtures, dict) or set(fixtures) != { + "valid_local", "valid_foreign", "managed_drift" + }: + raise RuntimeError("G2 fixture inventory drifted") + return bundle + + +def sha256_file(path: pathlib.Path) -> str: + h = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def extract_provider_binary(root: pathlib.Path) -> pathlib.Path: + subprocess.run( + ["docker", "pull", EXPECTED_APPLIANCE], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + cp = subprocess.run( + ["docker", "create", EXPECTED_APPLIANCE], + check=True, + capture_output=True, + text=True, + ) + container_id = cp.stdout.strip() + if not container_id: + raise RuntimeError("docker create returned no container id") + destination = root / "garm-provider-truenas" + try: + subprocess.run( + ["docker", "cp", f"{container_id}:{PROVIDER_PATH_IN_IMAGE}", str(destination)], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + finally: + subprocess.run( + ["docker", "rm", "-f", container_id], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + digest = sha256_file(destination) + if digest != EXPECTED_PROVIDER_BINARY_SHA256: + raise RuntimeError(f"provider binary digest drifted: {digest}") + destination.chmod(0o755) + return destination + + +def generate_tls(root: pathlib.Path) -> tuple[pathlib.Path, pathlib.Path]: + key = root / "g2-ui.key" + cert = root / "g2-ui.crt" + subprocess.run( + [ + "openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", + "-subj", "/CN=127.0.0.1", + "-addext", "subjectAltName=IP:127.0.0.1", + "-addext", "basicConstraints=critical,CA:FALSE", + "-addext", "keyUsage=critical,digitalSignature,keyEncipherment", + "-addext", "extendedKeyUsage=serverAuth", + "-keyout", str(key), "-out", str(cert), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + key.chmod(0o600) + return cert, key + + +def ui_cert_id(value) -> int | None: + if value is None: + return None + if isinstance(value, int): + return value + if isinstance(value, dict) and isinstance(value.get("id"), int): + return int(value["id"]) + raise RuntimeError(f"unexpected ui_certificate shape: {type(value).__name__}") + + +def wait_verified_https(host: str, port: int, cafile: pathlib.Path, timeout: float = 60.0) -> None: + deadline = time.monotonic() + timeout + last = None + while time.monotonic() < deadline: + try: + context = ssl.create_default_context(cafile=str(cafile)) + with socket.create_connection((host, port), timeout=4) as raw: + with context.wrap_socket(raw, server_hostname=host) as wrapped: + cert = wrapped.getpeercert() + if cert: + return + except Exception as exc: + last = exc + time.sleep(1) + raise RuntimeError(f"verified HTTPS did not become ready: {last}") + + +def create_app_stopped( + session: AdminSession, + app_name: str, + compose: dict, + job_timeout: float, +) -> dict: + job_id = session.call("app.create", [{ + "app_name": app_name, + "custom_app": True, + "custom_compose_config": compose, + }]) + if not isinstance(job_id, int): + raise RuntimeError(f"app.create({app_name}) did not return a job id") + session.wait_job(job_id, f"app.create {app_name}", job_timeout) + app = session.call("app.query", [[["id", "=", app_name]], {"get": True}]) + if not app: + raise RuntimeError(f"app {app_name} absent immediately after create") + if app.get("state") != "STOPPED": + stop_id = session.call("app.stop", [app_name]) + if not isinstance(stop_id, int): + raise RuntimeError(f"app.stop({app_name}) did not return a job id") + session.wait_job(stop_id, f"app.stop {app_name}", job_timeout) + app = session.call("app.query", [[["id", "=", app_name]], {"get": True}]) + if not app or app.get("state") != "STOPPED": + raise RuntimeError(f"fixture app {app_name} did not reach STOPPED") + return app + + +def delete_app_if_present(session: AdminSession, app_name: str, job_timeout: float) -> None: + matches = session.call("app.query", [[["id", "=", app_name]]]) + if not matches: + return + app = matches[0] + if app.get("state") not in {"STOPPED", "CRASHED"}: + stop_id = session.call("app.stop", [app_name]) + if isinstance(stop_id, int): + session.wait_job(stop_id, f"cleanup app.stop {app_name}", job_timeout) + delete_id = session.call("app.delete", [app_name, { + "remove_images": False, + "remove_ix_volumes": False, + "force_remove_custom_app": False, + }]) + if not isinstance(delete_id, int): + raise RuntimeError(f"cleanup app.delete({app_name}) did not return a job id") + session.wait_job(delete_id, f"cleanup app.delete {app_name}", job_timeout) + + +def provider_env( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + command: str, + instance_id: str | None = None, +) -> tuple[list[str], dict[str, str]]: + env = os.environ.copy() + env.update({ + "GARM_INTERFACE_VERSION": "v0.1.1", + "GARM_COMMAND": command, + "GARM_CONTROLLER_ID": EXPECTED_CONTROLLER_ID, + "GARM_POOL_ID": EXPECTED_POOL_ID, + "GARM_PROVIDER_CONFIG_FILE": str(config_path), + "TRUENAS_API_KEY": api_key, + "SSL_CERT_FILE": str(ca_path), + }) + if instance_id is not None: + env["GARM_INSTANCE_ID"] = instance_id + else: + env.pop("GARM_INSTANCE_ID", None) + return [str(provider_binary)], env + + +def invoke_provider( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + command: str, + instance_id: str | None = None, +) -> subprocess.CompletedProcess[str]: + argv, env = provider_env( + provider_binary, config_path, api_key, ca_path, command, instance_id + ) + return subprocess.run( + argv, + env=env, + text=True, + capture_output=True, + check=False, + timeout=45, + ) + + +def parse_json_stdout(cp: subprocess.CompletedProcess[str], label: str): + if cp.returncode != 0: + raise RuntimeError(f"{label} failed rc={cp.returncode}: {cp.stderr[-1000:]}") + text = cp.stdout.strip() + if not text: + raise RuntimeError(f"{label} returned empty stdout") + try: + return json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"{label} stdout was not JSON: {text[-1000:]}") from exc + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--http-port", type=int, required=True) + p.add_argument("--https-port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--fixture-dir", type=pathlib.Path, required=True) + p.add_argument("--fixture-producer-commit", required=True) + p.add_argument("--out", required=True) + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-garm-provider-g2/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected_version": EXPECTED_VERSION, + "provider_product_source": EXPECTED_PROVIDER_PRODUCT_SOURCE, + "provider_binary_sha256": EXPECTED_PROVIDER_BINARY_SHA256, + "appliance": EXPECTED_APPLIANCE, + "controller_id": EXPECTED_CONTROLLER_ID, + "pool_id": EXPECTED_POOL_ID, + "provider_create_exercised": False, + "provider_delete_exercised": False, + "github_credentials_present": False, + "github_jit_registration_exercised": False, + "physical_truenas_mutation": False, + "secret_values_recorded": False, + } + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + bundle = None + fixture_names: list[str] = [] + api_key_id: int | None = None + api_key_value: str | None = None + imported_cert_id: int | None = None + original_ui_cert_id: int | None = None + session = AdminSession(a.host, a.http_port, password, a.timeout) + cleanup_errors: list[str] = [] + + with tempfile.TemporaryDirectory(prefix="garm-provider-g2-") as td: + root = pathlib.Path(td) + try: + bundle = load_bundle(a.fixture_dir, a.fixture_producer_commit) + payload["fixture_producer_source"] = bundle["producer_source"] + payload["fixture_bundle_sha256"] = sha256_file( + a.fixture_dir / "garm-provider-g2-fixtures.json" + ) + + provider_binary = extract_provider_binary(root) + payload["provider_binary_from_appliance"] = True + + cert_path, key_path = generate_tls(root) + payload["transport"] = { + "scheme": "wss", + "host": a.host, + "port": a.https_port, + "insecure_skip_verify": False, + "certificate_sha256": sha256_file(cert_path), + "private_key_recorded": False, + "api_key_recorded": False, + } + + session.connect() + version = session.call("system.version", []) + payload["system_version"] = version + if version != EXPECTED_VERSION: + raise RuntimeError(f"target version drifted: {version}") + + general = session.call("system.general.config", []) + original_ui_cert_id = ui_cert_id(general.get("ui_certificate")) + + api_key = session.call("api_key.create", [{ + "name": f"g2-provider-{secrets.token_hex(5)}", + "username": "truenas_admin", + }]) + if not isinstance(api_key, dict) or not isinstance(api_key.get("id"), int): + raise RuntimeError("api_key.create did not return an id") + api_key_id = int(api_key["id"]) + api_key_value = api_key.get("key") + if not isinstance(api_key_value, str) or not api_key_value: + raise RuntimeError("api_key.create did not return a key") + + create_cert_id = session.call("certificate.create", [{ + "name": f"g2_provider_{secrets.token_hex(4)}", + "create_type": "CERTIFICATE_CREATE_IMPORTED", + "add_to_trusted_store": False, + "certificate": cert_path.read_text(encoding="utf-8"), + "privatekey": key_path.read_text(encoding="utf-8"), + }]) + if not isinstance(create_cert_id, int): + raise RuntimeError("certificate.create did not return a job id") + cert_job = session.wait_job(create_cert_id, "certificate.create", a.job_timeout) + cert_result = cert_job.get("result") + if not isinstance(cert_result, dict) or not isinstance(cert_result.get("id"), int): + raise RuntimeError("certificate.create job did not return certificate id") + imported_cert_id = int(cert_result["id"]) + + session.call("system.general.update", [{"ui_certificate": imported_cert_id}]) + try: + session.call("system.general.ui_restart", [0]) + except Exception: + # Applying UI settings intentionally aborts the HTTP/WebSocket connection. + pass + session.close() + wait_verified_https(a.host, a.https_port, cert_path) + + provider_config = { + "mode": "truenas", + "truenas": { + "host": a.host, + "username": "truenas_admin", + "api_key_env": "TRUENAS_API_KEY", + "port": a.https_port, + "insecure_skip_verify": False, + "callback_host_gateway": False, + }, + } + config_path = root / "provider.json" + config_path.write_text( + json.dumps(provider_config, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + config_path.chmod(0o600) + + session.connect() + fixtures = bundle["fixtures"] + for key in ("valid_local", "valid_foreign"): + fixture = fixtures[key] + name = fixture["app_name"] + create_app_stopped(session, name, fixture["compose"], a.job_timeout) + fixture_names.append(name) + + local_name = fixtures["valid_local"]["app_name"] + foreign_name = fixtures["valid_foreign"]["app_name"] + + list_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, "ListInstances" + ) + listed = parse_json_stdout(list_cp, "ListInstances valid/foreign") + if not isinstance(listed, list): + raise RuntimeError("ListInstances did not return an array") + provider_ids = sorted( + str(item.get("provider_id")) + for item in listed + if isinstance(item, dict) + ) + if provider_ids != [local_name]: + raise RuntimeError(f"ListInstances ownership filter drifted: {provider_ids}") + + get_local_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "GetInstance", local_name, + ) + got_local = parse_json_stdout(get_local_cp, "GetInstance local") + if not isinstance(got_local, dict) or got_local.get("provider_id") != local_name: + raise RuntimeError("GetInstance local returned wrong provider id") + + foreign_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "GetInstance", foreign_name, + ) + if foreign_cp.returncode == 0: + raise RuntimeError("GetInstance foreign unexpectedly succeeded") + + reconnect_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "GetInstance", local_name, + ) + reconnect_local = parse_json_stdout(reconnect_cp, "GetInstance reconnect") + if reconnect_local.get("provider_id") != local_name: + raise RuntimeError("fresh provider process did not readopt local fixture") + + drift = fixtures["managed_drift"] + drift_name = drift["app_name"] + create_app_stopped(session, drift_name, drift["compose"], a.job_timeout) + fixture_names.append(drift_name) + + drift_get = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "GetInstance", drift_name, + ) + if drift_get.returncode == 0: + raise RuntimeError("GetInstance managed drift unexpectedly succeeded") + + drift_list = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "ListInstances", + ) + if drift_list.returncode == 0: + raise RuntimeError("ListInstances with managed drift unexpectedly succeeded") + + payload["oracles"] = { + "verified_tls_transport": True, + "api_key_auth_transport": True, + "list_valid_owned_only": True, + "get_valid_owned": True, + "foreign_not_adopted": True, + "fresh_process_readoption": True, + "managed_drift_get_fail_closed": True, + "managed_drift_list_fail_closed": True, + "provider_delete_not_invoked": True, + "provider_create_not_invoked": True, + } + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "exact packaged provider used verified WSS plus run-local API-key auth against " + "real nested TrueNAS; exact source-generated local fixture was listed/readopted, " + "foreign ownership was excluded, and managed realized-profile drift failed closed; " + "provider create/delete and GitHub/JIT paths were intentionally not exercised" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + finally: + # Cleanup through supported TrueNAS middleware only. + try: + session.connect() + for name in reversed(fixture_names): + try: + delete_app_if_present(session, name, a.job_timeout) + except Exception as exc: + cleanup_errors.append(f"delete {name}: {type(exc).__name__}: {exc}") + + if api_key_id is not None: + try: + deleted = session.call("api_key.delete", [api_key_id]) + if deleted is not True: + cleanup_errors.append("api_key.delete did not return true") + except Exception as exc: + cleanup_errors.append(f"api_key.delete: {type(exc).__name__}: {exc}") + api_key_value = None + + if original_ui_cert_id is not None and imported_cert_id is not None: + try: + session.call( + "system.general.update", + [{"ui_certificate": original_ui_cert_id}], + ) + try: + session.call("system.general.ui_restart", [0]) + except Exception: + pass + except Exception as exc: + cleanup_errors.append( + f"restore ui certificate: {type(exc).__name__}: {exc}" + ) + session.close() + time.sleep(5) + try: + session.connect() + except Exception as exc: + cleanup_errors.append( + f"reconnect after ui restore: {type(exc).__name__}: {exc}" + ) + + if imported_cert_id is not None and session.ws is not None: + try: + delete_result = session.call( + "certificate.delete", [imported_cert_id, False] + ) + if isinstance(delete_result, int): + session.wait_job( + delete_result, "certificate.delete", a.job_timeout + ) + elif delete_result is not True: + cleanup_errors.append( + f"certificate.delete unexpected result {delete_result!r}" + ) + except Exception as exc: + cleanup_errors.append( + f"certificate.delete: {type(exc).__name__}: {exc}" + ) + except Exception as exc: + cleanup_errors.append(f"cleanup session: {type(exc).__name__}: {exc}") + finally: + session.close() + + payload["cleanup"] = { + "attempted": True, + "errors": cleanup_errors, + "zero_unintended_residue": not cleanup_errors, + "secret_values_recorded": False, + } + if cleanup_errors: + payload["oracleSatisfied"] = False + payload["classification"] = "ORACLE_FAILURE" + payload["detail"] = ( + payload.get("detail", "") + "; cleanup failed: " + "; ".join(cleanup_errors) + ) + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 8534af22b6efb39b9745955fb45963a52b2d52fc Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:35:41 +0200 Subject: [PATCH 143/719] test: lock nested G2 provider boundaries --- tests/test_truenas_garm_provider_g2.py | 80 ++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/test_truenas_garm_provider_g2.py diff --git a/tests/test_truenas_garm_provider_g2.py b/tests/test_truenas_garm_provider_g2.py new file mode 100644 index 000000000..b9821c24e --- /dev/null +++ b/tests/test_truenas_garm_provider_g2.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "truenas_middleware_garm_provider_g2_probe.py" +SPEC = importlib.util.spec_from_file_location("garm_provider_g2_probe", SCRIPT) +assert SPEC and SPEC.loader +MOD = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MOD) + + +class GarmProviderG2Tests(unittest.TestCase): + def test_boundary_is_read_adoption_not_runner_creation(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + '"provider_create_exercised": False', + '"provider_delete_exercised": False', + '"github_jit_registration_exercised": False', + '"physical_truenas_mutation": False', + '"insecure_skip_verify": False', + '"GARM_COMMAND": command', + '"SSL_CERT_FILE": str(ca_path)', + '"TRUENAS_API_KEY": api_key', + '"ListInstances"', + '"GetInstance"', + ): + self.assertIn(required, text) + self.assertNotIn('"GARM_COMMAND": "CreateInstance"', text) + self.assertNotIn('"GARM_COMMAND": "DeleteInstance"', text) + + def test_fixture_bundle_binds_product_and_producer_sources(self): + bundle = { + "schema": MOD.EXPECTED_FIXTURE_SCHEMA, + "provider_product_source": MOD.EXPECTED_PROVIDER_PRODUCT_SOURCE, + "producer_source": "a" * 40, + "controller_id": MOD.EXPECTED_CONTROLLER_ID, + "pool_id": MOD.EXPECTED_POOL_ID, + "fixtures": { + "valid_local": {}, + "valid_foreign": {}, + "managed_drift": {}, + }, + } + with tempfile.TemporaryDirectory() as td: + root = Path(td) + (root / "garm-provider-g2-fixtures.json").write_text( + json.dumps(bundle), encoding="utf-8" + ) + loaded = MOD.load_bundle(root, "a" * 40) + self.assertEqual( + loaded["provider_product_source"], + MOD.EXPECTED_PROVIDER_PRODUCT_SOURCE, + ) + with self.assertRaisesRegex(RuntimeError, "producer source drifted"): + MOD.load_bundle(root, "b" * 40) + + def test_tls_path_is_verified_and_ephemeral(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + "ssl.create_default_context(cafile=str(cafile))", + "server_hostname=host", + '"api_key.create"', + '"certificate.create"', + '"system.general.update"', + '"system.general.ui_restart"', + '"api_key.delete"', + '"certificate.delete"', + '"private_key_recorded": False', + '"api_key_recorded": False', + ): + self.assertIn(required, text) + + +if __name__ == "__main__": + unittest.main() From 586533d990a0eaa0f78df5a936448d3f2706b870 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:36:19 +0200 Subject: [PATCH 144/719] feat: route G2 provider oracle through existing TrueNAS harness --- scripts/gha_kvm_truenas_rdte.sh | 40 ++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 08ab22d59..4141f981e 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm] [--foundry-control-dir DIR] [--foundry-commit SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA]" } OUT="" @@ -26,6 +26,8 @@ RUNG="t0" T6_PRODUCT="litellm" FOUNDRY_CONTROL_DIR="" FOUNDRY_COMMIT="" +G2_FIXTURE_DIR="" +G2_FIXTURE_PRODUCER="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -35,6 +37,8 @@ while [[ $# -gt 0 ]]; do --t6-product) T6_PRODUCT="$2"; shift 2 ;; --foundry-control-dir) FOUNDRY_CONTROL_DIR="$2"; shift 2 ;; --foundry-commit) FOUNDRY_COMMIT="$2"; shift 2 ;; + --g2-fixture-dir) G2_FIXTURE_DIR="$2"; shift 2 ;; + --g2-fixture-producer) G2_FIXTURE_PRODUCER="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -47,10 +51,16 @@ eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } if [[ "$RUNG" == "t6" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "T6 is currently admitted only for exact TrueNAS 26.0.0-BETA.3; use Foundry F1-F5 for other targets" >&2; exit 2; } - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } - [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } - [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } - FOUNDRY_CONTROL_DIR="$(realpath "$FOUNDRY_CONTROL_DIR")" + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then + [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } + [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } + G2_FIXTURE_DIR="$(realpath "$G2_FIXTURE_DIR")" + else + [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } + [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } + FOUNDRY_CONTROL_DIR="$(realpath "$FOUNDRY_CONTROL_DIR")" + fi fi if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi @@ -206,7 +216,12 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T5 lifecycle client" fi if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" == "garm" ]]; then + if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_garm_provider_g2_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G2 client" + command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" + command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" + elif [[ "$T6_PRODUCT" == "garm" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 GARM control client" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then @@ -614,7 +629,14 @@ if [[ "$RUNG" == "t5" ]]; then fi FOUNDRY_OUT="$STATE_DIR/foundry-control.json" -if [[ "$T6_PRODUCT" == "garm" ]]; then +if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_garm_provider_g2_probe.py" \ + --host 127.0.0.1 --http-port "$HTTP_PORT" --https-port "$HTTPS_PORT" \ + --password-file "$PASSWORD_FILE" \ + --fixture-dir "$G2_FIXTURE_DIR" \ + --fixture-producer-commit "$G2_FIXTURE_PRODUCER" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "garm" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$GARM_HOST_PORT" \ "${MIDDLEWARE_TLS_ARG[@]}" \ @@ -650,7 +672,9 @@ PY [[ "$FOUNDRY_OK" == "true" ]] || fail_evidence ORACLE_FAILURE foundry-materialization "exact Foundry-exported $T6_PRODUCT control did not realize and verify on TrueNAS" -if [[ "$T6_PRODUCT" == "garm" ]]; then +if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider passed verified WSS/API-key transport, valid local adoption, foreign exclusion, managed-drift fail-closed behavior, fresh-process readoption, and zero-residue cleanup; provider create/delete and GitHub/JIT intentionally not exercised" +elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported WOW Sidecar control passed immutable image/config read-back, permissions/seed metadata, public-fixture worker runtime, restart persistence, and zero-residue cleanup" From 46790bcadec5933873b57ad16ef628f40eee48ad Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:36:34 +0200 Subject: [PATCH 145/719] fix: identify G2 fixture shape in system receipt --- scripts/gha_kvm_truenas_rdte.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 4141f981e..d052cca47 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -127,7 +127,9 @@ payload = { "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else None, "data_disk_serials": ["RDTE_DATA_0", "RDTE_DATA_1"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else [], "app": ( - {"name": "rdte-t6-garm", "image": "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9"} + {"name": "garm-provider-g2-fixtures", "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g2" + else {"name": "rdte-t6-garm", "image": "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm" else {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" From 00a811ed56a2b5e8b4645c47bd5c37929428c7bb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:37:37 +0200 Subject: [PATCH 146/719] feat: wire source-exact G2 provider fixtures into system lab --- .github/workflows/gha-kvm-system-rdte.yml | 56 +++++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 305927a8e..7f5951262 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -46,6 +46,7 @@ on: - litellm - wow-sidecar - garm + - garm-provider-g2 permissions: contents: read @@ -106,7 +107,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -117,7 +118,7 @@ jobs: if [[ "$run_request_changed" == "true" ]]; then truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2")' "$RUN_REQUEST_PATH")" jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ @@ -144,7 +145,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -173,6 +174,15 @@ jobs: permissions: contents: read + provider-g2-fixtures: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' + uses: SemperSupra/garm-provider-truenas/.github/workflows/export-g2-nested-fixtures.yml@1667aa73e514c278e221082ac33f2f10a4141eef + with: + provider_ref: 1667aa73e514c278e221082ac33f2f10a4141eef + permissions: + contents: read + qemu-t3-topology: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' @@ -212,6 +222,13 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + - name: Download exact GARM provider G2 fixtures + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-provider-truenas-g2-fixtures + path: ${{ runner.temp }}/gha-kvm-system-rdte/g2-fixtures + - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -254,7 +271,7 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, provider-g2-fixtures] if: >- always() && needs.contract.result == 'success' && @@ -264,7 +281,8 @@ jobs: needs.changes.outputs.truenas_rung != 't6' || (needs.changes.outputs.truenas_t6_product == 'litellm' && needs['foundry-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') || - (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') + (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') ) runs-on: ubuntu-26.04 timeout-minutes: 50 @@ -307,18 +325,26 @@ jobs: --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" ) if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then - if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then - foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" - elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then - foundry_commit="68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2" + if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-provider-g2" ]]; then + args+=( + --t6-product "garm-provider-g2" + --g2-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g2-fixtures" + --g2-fixture-producer "1667aa73e514c278e221082ac33f2f10a4141eef" + ) else - foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" + if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then + foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then + foundry_commit="68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2" + else + foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" + fi + args+=( + --t6-product "${{ needs.changes.outputs.truenas_t6_product }}" + --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" + --foundry-commit "$foundry_commit" + ) fi - args+=( - --t6-product "${{ needs.changes.outputs.truenas_t6_product }}" - --foundry-control-dir "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control" - --foundry-commit "$foundry_commit" - ) fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" From c63ec4d432d91226bebbf43a63c2f59396196502 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:37:56 +0200 Subject: [PATCH 147/719] ci: include G2 provider oracle in prep static gate --- .github/workflows/prep-rdte-static.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index ac057ffab..a8d54494c 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -19,6 +19,7 @@ on: - 'tests/test_truenas_t6_litellm_control.py' - 'tests/test_truenas_t6_wow_sidecar_control.py' - 'tests/test_truenas_t6_garm_control.py' + - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_rdte_target.py' pull_request: @@ -33,6 +34,10 @@ on: - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' - 'tests/test_truenas_t6_litellm_control.py' + - 'tests/test_truenas_t6_wow_sidecar_control.py' + - 'tests/test_truenas_t6_garm_control.py' + - 'tests/test_truenas_garm_provider_g2.py' + - 'tests/test_truenas_rdte_target.py' permissions: contents: read @@ -70,6 +75,9 @@ jobs: if [[ -f tests/test_truenas_t6_garm_control.py ]]; then tests+=(tests/test_truenas_t6_garm_control.py) fi + if [[ -f tests/test_truenas_garm_provider_g2.py ]]; then + tests+=(tests/test_truenas_garm_provider_g2.py) + fi if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi From 4c35071262bf2e098e32e8c27923378b27f92c75 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:41:39 +0200 Subject: [PATCH 148/719] rdte: stage GARM provider G2 nested rep --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 6b02f9831..3ac7b4368 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-001", + "request_id": "tn-26-beta3-t6-garm-provider-g2-001", "version": "26.0.0-BETA.3", "rung": "t6", - "product": "garm", + "product": "garm-provider-g2", "authority_issue": 396, - "purpose": "Execute the first GARM controller pre-HIL T6 rep on exact disposable TrueNAS BETA.3 using the accepted secret-free Foundry control.", - "consumer_authority_issue": 277 + "purpose": "Execute G2 verified-transport and adoption/reconciliation qualification for the exact GARM TrueNAS provider on disposable TrueNAS BETA.3.", + "consumer_authority_issue": 39 } From e812419df4c54dfa2f3206d3d1ce608357457df4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:43:02 +0200 Subject: [PATCH 149/719] test: bind G2 run request to provider authority --- tests/test_truenas_rdte_target.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index e841045ba..53882967d 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -49,11 +49,13 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": self.assertEqual(request.get("consumer_authority_issue"), 277) + if request.get("product") == "garm-provider-g2": + self.assertEqual(request.get("consumer_authority_issue"), 39) self.assertNotIn("latest", request["version"].lower()) self.assertNotIn("nightly", request["version"].lower()) From d96132da1ee541ef2fc4cf7cc4ad1c0cbfb4304a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:43:12 +0200 Subject: [PATCH 150/719] test: admit G2 product in shared T6 request contract --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 51df572e8..2f27217b8 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -62,7 +62,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('"wow-sidecar"', harness) self.assertIn("truenas_middleware_wow_sidecar_t6_probe.py", harness) self.assertIn('T6_PRODUCT="litellm"', harness) - self.assertIn(request["product"], {"litellm", "wow-sidecar", "garm"}) + self.assertIn(request["product"], {"litellm", "wow-sidecar", "garm", "garm-provider-g2"}) if request["product"] == "wow-sidecar": self.assertEqual("t6", request["rung"]) self.assertEqual("26.0.0-BETA.3", request["version"]) From af07fdfa1924892fd1885653e2f12e712cfe7d5d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:43:20 +0200 Subject: [PATCH 151/719] test: bind G2 run request to provider authority --- tests/test_truenas_rdte_target.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 53882967d..31fd49bd8 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -56,6 +56,8 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request.get("consumer_authority_issue"), 277) if request.get("product") == "garm-provider-g2": self.assertEqual(request.get("consumer_authority_issue"), 39) + if request.get("product") == "garm-provider-g2": + self.assertEqual(request.get("consumer_authority_issue"), 39) self.assertNotIn("latest", request["version"].lower()) self.assertNotIn("nightly", request["version"].lower()) From 3f612562ce5fd1f4bbe3f5094d5fedc87cf0ff0e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:48:32 +0200 Subject: [PATCH 152/719] fix: download G2 fixtures in TrueNAS job --- .github/workflows/gha-kvm-system-rdte.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 7f5951262..3c6a7cc79 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -222,13 +222,6 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false - - name: Download exact GARM provider G2 fixtures - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - with: - name: garm-provider-truenas-g2-fixtures - path: ${{ runner.temp }}/gha-kvm-system-rdte/g2-fixtures - - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -308,6 +301,12 @@ jobs: with: name: garm-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact GARM provider G2 fixtures + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-provider-truenas-g2-fixtures + path: ${{ runner.temp }}/gha-kvm-system-rdte/g2-fixtures - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail From 9cd35fb005e1b2a9d3407717fd8e7990b2f517d8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:48:42 +0200 Subject: [PATCH 153/719] test: lock G2 fixture download to TrueNAS job --- tests/test_truenas_garm_provider_g2.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_truenas_garm_provider_g2.py b/tests/test_truenas_garm_provider_g2.py index b9821c24e..c6224d5f9 100644 --- a/tests/test_truenas_garm_provider_g2.py +++ b/tests/test_truenas_garm_provider_g2.py @@ -59,6 +59,20 @@ def test_fixture_bundle_binds_product_and_producer_sources(self): with self.assertRaisesRegex(RuntimeError, "producer source drifted"): MOD.load_bundle(root, "b" * 40) + def test_g2_fixture_download_is_true_nas_job_local(self): + workflow = ( + ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml" + ).read_text(encoding="utf-8") + proxmox = workflow.split(" proxmox-9-2:", 1)[1].split( + " truenas-26-beta3:", 1 + )[0] + truenas = workflow.split(" truenas-26-beta3:", 1)[1] + marker = "Download exact GARM provider G2 fixtures" + self.assertNotIn(marker, proxmox) + self.assertIn(marker, truenas) + self.assertIn("garm-provider-truenas-g2-fixtures", truenas) + self.assertIn("--g2-fixture-dir", truenas) + def test_tls_path_is_verified_and_ephemeral(self): text = SCRIPT.read_text(encoding="utf-8") for required in ( From 9e039c07992b2fe6a5104508c3e9154b277ceacd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:48:49 +0200 Subject: [PATCH 154/719] rdte: stage corrected G2 provider rep 002 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3ac7b4368..864a04fb2 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g2-001", + "request_id": "tn-26-beta3-t6-garm-provider-g2-002", "version": "26.0.0-BETA.3", "rung": "t6", "product": "garm-provider-g2", "authority_issue": 396, - "purpose": "Execute G2 verified-transport and adoption/reconciliation qualification for the exact GARM TrueNAS provider on disposable TrueNAS BETA.3.", + "purpose": "Execute the fixture-routing-corrected G2 verified-transport and adoption/reconciliation qualification for the exact GARM TrueNAS provider on disposable TrueNAS BETA.3.", "consumer_authority_issue": 39 } From 451b13c6b09c25e7f02dca7aa9efa5a9af03525f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 03:44:04 +0200 Subject: [PATCH 155/719] RDTE: launch corrected GARM provider G2 rep-002 --- config/truenas-rdte-run-request.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 864a04fb2..7aa610e0a 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -5,6 +5,6 @@ "rung": "t6", "product": "garm-provider-g2", "authority_issue": 396, - "purpose": "Execute the fixture-routing-corrected G2 verified-transport and adoption/reconciliation qualification for the exact GARM TrueNAS provider on disposable TrueNAS BETA.3.", + "purpose": "Corrective G2 rep-002 after fixture routing was moved into the TrueNAS job and statically regression-locked.", "consumer_authority_issue": 39 } From 52d04d2445c2bf68e570b3be81a0da3f25a437ff Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:05:17 +0200 Subject: [PATCH 156/719] RDTE: rerun corrected WOW T6 control * rdte: repin WOW T6 control to corrected Foundry export * rdte: bind WOW T6 test to corrected Foundry exporter * rdte: stage corrected WOW T6 consumer rep --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- config/truenas-rdte-run-request.json | 8 ++++---- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 3c6a7cc79..d237d5d12 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -159,9 +159,9 @@ jobs: foundry-wow-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'wow-sidecar' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-wow-sidecar-t6-control.yml@68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-wow-sidecar-t6-control.yml@9ac2eef97581caff5b0de740560c55a778fa3f1c with: - foundry_ref: 68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2 + foundry_ref: 9ac2eef97581caff5b0de740560c55a778fa3f1c permissions: contents: read @@ -334,7 +334,7 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then - foundry_commit="68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2" + foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 7aa610e0a..76c31c1b8 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g2-002", + "request_id": "tn-26-beta3-t6-wow-sidecar-004-corrected-seed-contract", "version": "26.0.0-BETA.3", "rung": "t6", - "product": "garm-provider-g2", + "product": "wow-sidecar", "authority_issue": 396, - "purpose": "Corrective G2 rep-002 after fixture routing was moved into the TrueNAS job and statically regression-locked.", - "consumer_authority_issue": 39 + "purpose": "Requalify WOW Sidecar on disposable TrueNAS BETA.3 from the corrected exact Foundry control after strengthened full-Compose qualification and exporter PASS.", + "consumer_authority_issue": 276 } diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 2f27217b8..753f2f680 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -54,7 +54,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self request = json.loads((ROOT / "config" / "truenas-rdte-run-request.json").read_text(encoding="utf-8")) self.assertIn( - "export-wow-sidecar-t6-control.yml@68d08ce9561b0ccf68d2e7eb446a76514e1a8ed2", + "export-wow-sidecar-t6-control.yml@9ac2eef97581caff5b0de740560c55a778fa3f1c", workflow, ) self.assertIn("name: wow-sidecar-truenas-t6-control", workflow) From cc3919ca2ed49dd40cde47cc520f280e748f43f7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:14:21 +0200 Subject: [PATCH 157/719] rdte: stage G2 fixtures through stopped app update --- ...uenas_middleware_garm_provider_g2_probe.py | 78 ++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/scripts/truenas_middleware_garm_provider_g2_probe.py b/scripts/truenas_middleware_garm_provider_g2_probe.py index ec5ce680c..b828d8d02 100644 --- a/scripts/truenas_middleware_garm_provider_g2_probe.py +++ b/scripts/truenas_middleware_garm_provider_g2_probe.py @@ -197,31 +197,83 @@ def wait_verified_https(host: str, port: int, cafile: pathlib.Path, timeout: flo raise RuntimeError(f"verified HTTPS did not become ready: {last}") -def create_app_stopped( +def canonical_json_sha256(value: object) -> str: + raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(raw).hexdigest() + + +def create_app_stopped_with_exact_config( session: AdminSession, app_name: str, compose: dict, job_timeout: float, ) -> dict: - job_id = session.call("app.create", [{ + """Materialize exact fixture config without ever starting its runner bootstrap. + + TrueNAS 26.0.0-BETA.3 app.update deliberately skips compose_action when the + existing App is STOPPED. Seed a harmless container, stop it, then replace + the stored custom Compose with the source-generated provider fixture and + prove exact app.config read-back while state remains STOPPED. + """ + services = compose.get("services") + if not isinstance(services, dict): + raise RuntimeError("fixture compose has no services object") + runner = services.get("runner") + if not isinstance(runner, dict) or not isinstance(runner.get("image"), str): + raise RuntimeError("fixture compose has no runner image") + + seed_compose = { + "services": { + "seed": { + "image": runner["image"], + "restart": "no", + "entrypoint": [ + "/bin/sh", + "-c", + "trap 'exit 0' TERM INT HUP; while :; do sleep 60; done", + ], + }, + }, + } + create_id = session.call("app.create", [{ "app_name": app_name, "custom_app": True, - "custom_compose_config": compose, + "custom_compose_config": seed_compose, }]) - if not isinstance(job_id, int): + if not isinstance(create_id, int): raise RuntimeError(f"app.create({app_name}) did not return a job id") - session.wait_job(job_id, f"app.create {app_name}", job_timeout) + session.wait_job(create_id, f"app.create seed {app_name}", job_timeout) + app = session.call("app.query", [[["id", "=", app_name]], {"get": True}]) if not app: - raise RuntimeError(f"app {app_name} absent immediately after create") + raise RuntimeError(f"seed app {app_name} absent immediately after create") if app.get("state") != "STOPPED": stop_id = session.call("app.stop", [app_name]) if not isinstance(stop_id, int): raise RuntimeError(f"app.stop({app_name}) did not return a job id") - session.wait_job(stop_id, f"app.stop {app_name}", job_timeout) + session.wait_job(stop_id, f"app.stop seed {app_name}", job_timeout) app = session.call("app.query", [[["id", "=", app_name]], {"get": True}]) if not app or app.get("state") != "STOPPED": - raise RuntimeError(f"fixture app {app_name} did not reach STOPPED") + raise RuntimeError(f"seed app {app_name} did not reach STOPPED") + + expected_sha = canonical_json_sha256(compose) + update_id = session.call("app.update", [ + app_name, + {"custom_compose_config": compose}, + ]) + if not isinstance(update_id, int): + raise RuntimeError(f"app.update({app_name}) did not return a job id") + session.wait_job(update_id, f"app.update exact fixture {app_name}", job_timeout) + + app = session.call("app.query", [[["id", "=", app_name]], {"get": True}]) + if not app or app.get("state") != "STOPPED": + raise RuntimeError(f"exact fixture app {app_name} did not remain STOPPED") + + readback = session.call("app.config", [app_name]) + if not isinstance(readback, dict): + raise RuntimeError(f"app.config({app_name}) did not return an object") + if canonical_json_sha256(readback) != expected_sha: + raise RuntimeError(f"app.config({app_name}) did not preserve exact fixture compose") return app @@ -434,7 +486,7 @@ def main() -> int: for key in ("valid_local", "valid_foreign"): fixture = fixtures[key] name = fixture["app_name"] - create_app_stopped(session, name, fixture["compose"], a.job_timeout) + create_app_stopped_with_exact_config(session, name, fixture["compose"], a.job_timeout) fixture_names.append(name) local_name = fixtures["valid_local"]["app_name"] @@ -479,7 +531,7 @@ def main() -> int: drift = fixtures["managed_drift"] drift_name = drift["app_name"] - create_app_stopped(session, drift_name, drift["compose"], a.job_timeout) + create_app_stopped_with_exact_config(session, drift_name, drift["compose"], a.job_timeout) fixture_names.append(drift_name) drift_get = invoke_provider( @@ -505,6 +557,8 @@ def main() -> int: "fresh_process_readoption": True, "managed_drift_get_fail_closed": True, "managed_drift_list_fail_closed": True, + "exact_fixture_applied_while_stopped": True, + "runner_fixture_bootstrap_not_started": True, "provider_delete_not_invoked": True, "provider_create_not_invoked": True, } @@ -514,7 +568,9 @@ def main() -> int: "exact packaged provider used verified WSS plus run-local API-key auth against " "real nested TrueNAS; exact source-generated local fixture was listed/readopted, " "foreign ownership was excluded, and managed realized-profile drift failed closed; " - "provider create/delete and GitHub/JIT paths were intentionally not exercised" + "source-generated fixture Compose was applied only after each seed App was STOPPED, " + "so runner bootstrap/JIT did not execute; provider create/delete and GitHub/JIT paths " + "were intentionally not exercised" ) except Exception as exc: payload["detail"] = f"{type(exc).__name__}: {exc}" From 9b9c0e6c72ebe604c1affdaf6f455689b720c7b9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:14:38 +0200 Subject: [PATCH 158/719] test: lock stopped-update G2 fixture staging --- tests/test_truenas_garm_provider_g2.py | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_truenas_garm_provider_g2.py b/tests/test_truenas_garm_provider_g2.py index c6224d5f9..395463830 100644 --- a/tests/test_truenas_garm_provider_g2.py +++ b/tests/test_truenas_garm_provider_g2.py @@ -73,6 +73,61 @@ def test_g2_fixture_download_is_true_nas_job_local(self): self.assertIn("garm-provider-truenas-g2-fixtures", truenas) self.assertIn("--g2-fixture-dir", truenas) + + def test_exact_fixture_is_applied_only_after_seed_is_stopped(self): + compose = { + "services": { + "runner": { + "image": "example.invalid/runner@sha256:" + ("0" * 64), + "restart": "no", + }, + }, + } + + class FakeSession: + def __init__(self): + self.calls = [] + self.query_count = 0 + self.waits = [] + + def call(self, method, params): + self.calls.append((method, params)) + if method == "app.create": + return 101 + if method == "app.stop": + return 102 + if method == "app.update": + return 103 + if method == "app.query": + self.query_count += 1 + return {"state": "RUNNING" if self.query_count == 1 else "STOPPED"} + if method == "app.config": + return compose + raise AssertionError(method) + + def wait_job(self, job_id, label, timeout): + self.waits.append((job_id, label, timeout)) + return {"state": "SUCCESS"} + + session = FakeSession() + result = MOD.create_app_stopped_with_exact_config( + session, "garm-fixture-runner-1", compose, 30.0 + ) + self.assertEqual(result["state"], "STOPPED") + + methods = [method for method, _ in session.calls] + self.assertLess(methods.index("app.stop"), methods.index("app.update")) + update = next(params for method, params in session.calls if method == "app.update") + self.assertEqual(update[1]["custom_compose_config"], compose) + self.assertIn("app.config", methods) + self.assertNotIn("app.start", methods) + + def test_stopped_update_contract_is_explicitly_documented(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn("existing App is STOPPED", text) + self.assertIn('"app.update"', text) + self.assertIn('"runner_fixture_bootstrap_not_started": True', text) + def test_tls_path_is_verified_and_ephemeral(self): text = SCRIPT.read_text(encoding="utf-8") for required in ( From a3c20c3e3fb03014a2955fb7e31728c925984342 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:16:03 +0200 Subject: [PATCH 159/719] rdte: make G2 fixture cleanup failure-safe --- ...truenas_middleware_garm_provider_g2_probe.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_garm_provider_g2_probe.py b/scripts/truenas_middleware_garm_provider_g2_probe.py index b828d8d02..759dd07da 100644 --- a/scripts/truenas_middleware_garm_provider_g2_probe.py +++ b/scripts/truenas_middleware_garm_provider_g2_probe.py @@ -486,8 +486,10 @@ def main() -> int: for key in ("valid_local", "valid_foreign"): fixture = fixtures[key] name = fixture["app_name"] - create_app_stopped_with_exact_config(session, name, fixture["compose"], a.job_timeout) fixture_names.append(name) + create_app_stopped_with_exact_config( + session, name, fixture["compose"], a.job_timeout + ) local_name = fixtures["valid_local"]["app_name"] foreign_name = fixtures["valid_foreign"]["app_name"] @@ -531,8 +533,10 @@ def main() -> int: drift = fixtures["managed_drift"] drift_name = drift["app_name"] - create_app_stopped_with_exact_config(session, drift_name, drift["compose"], a.job_timeout) fixture_names.append(drift_name) + create_app_stopped_with_exact_config( + session, drift_name, drift["compose"], a.job_timeout + ) drift_get = invoke_provider( provider_binary, config_path, api_key_value, cert_path, @@ -584,6 +588,15 @@ def main() -> int: except Exception as exc: cleanup_errors.append(f"delete {name}: {type(exc).__name__}: {exc}") + for name in fixture_names: + try: + if session.call("app.query", [[["id", "=", name]]]): + cleanup_errors.append(f"fixture app remains after delete: {name}") + except Exception as exc: + cleanup_errors.append( + f"verify fixture absence {name}: {type(exc).__name__}: {exc}" + ) + if api_key_id is not None: try: deleted = session.call("api_key.delete", [api_key_id]) From fbd41dc09e26226ea9656a1e064eb2d52740ce99 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:16:15 +0200 Subject: [PATCH 160/719] test: require failure-safe G2 fixture cleanup --- tests/test_truenas_garm_provider_g2.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_truenas_garm_provider_g2.py b/tests/test_truenas_garm_provider_g2.py index 395463830..60d4b4f60 100644 --- a/tests/test_truenas_garm_provider_g2.py +++ b/tests/test_truenas_garm_provider_g2.py @@ -122,6 +122,17 @@ def wait_job(self, job_id, label, timeout): self.assertIn("app.config", methods) self.assertNotIn("app.start", methods) + def test_fixture_cleanup_is_registered_before_materialization(self): + text = SCRIPT.read_text(encoding="utf-8") + valid = text.split('for key in ("valid_local", "valid_foreign"):', 1)[1].split( + 'local_name = fixtures["valid_local"]["app_name"]', 1 + )[0] + self.assertLess( + valid.index("fixture_names.append(name)"), + valid.index("create_app_stopped_with_exact_config"), + ) + self.assertIn("fixture app remains after delete", text) + def test_stopped_update_contract_is_explicitly_documented(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn("existing App is STOPPED", text) From 35e6d5d7f2158ea3a7ed4ae532801191b2497d92 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:16:54 +0200 Subject: [PATCH 161/719] rdte: stage GARM provider G2 rep-003 --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 76c31c1b8..5f9143a9a 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-wow-sidecar-004-corrected-seed-contract", + "request_id": "tn-26-beta3-t6-garm-provider-g2-003-stopped-update", "version": "26.0.0-BETA.3", "rung": "t6", - "product": "wow-sidecar", + "product": "garm-provider-g2", "authority_issue": 396, - "purpose": "Requalify WOW Sidecar on disposable TrueNAS BETA.3 from the corrected exact Foundry control after strengthened full-Compose qualification and exporter PASS.", - "consumer_authority_issue": 276 + "purpose": "Corrective G2 rep-003: stage exact source-generated provider fixtures via supported stopped-App update so List/Get adoption and managed-drift fail-closed can be exercised without runner bootstrap or JIT side effects.", + "consumer_authority_issue": 39 } From 7b0e9efcfed261f5bff00f9241e8854efea91290 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:20:13 +0200 Subject: [PATCH 162/719] rdte: bind G2 stopped-update source precondition --- scripts/truenas_middleware_garm_provider_g2_probe.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/truenas_middleware_garm_provider_g2_probe.py b/scripts/truenas_middleware_garm_provider_g2_probe.py index 759dd07da..2013e8bac 100644 --- a/scripts/truenas_middleware_garm_provider_g2_probe.py +++ b/scripts/truenas_middleware_garm_provider_g2_probe.py @@ -25,6 +25,7 @@ ) EXPECTED_PROVIDER_PRODUCT_SOURCE = "14535745dc3aa3c0b5466da7c704bca4d23dcec5" EXPECTED_PROVIDER_BINARY_SHA256 = "911f076ba9421f7bb2a1ea72acc927327d799a5ad6265bffe2d869b2cbf5ecc2" +EXPECTED_TRUENAS_MIDDLEWARE_SOURCE = "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3" EXPECTED_FIXTURE_SCHEMA = "semper-supra.garm-provider-truenas-g2-fixtures/1" EXPECTED_CONTROLLER_ID = "g2-controller" EXPECTED_POOL_ID = "g2-pool" @@ -375,6 +376,8 @@ def main() -> int: "expected_version": EXPECTED_VERSION, "provider_product_source": EXPECTED_PROVIDER_PRODUCT_SOURCE, "provider_binary_sha256": EXPECTED_PROVIDER_BINARY_SHA256, + "truenas_middleware_source": EXPECTED_TRUENAS_MIDDLEWARE_SOURCE, + "stopped_update_contract": "app.update skips compose action when existing App state is STOPPED", "appliance": EXPECTED_APPLIANCE, "controller_id": EXPECTED_CONTROLLER_ID, "pool_id": EXPECTED_POOL_ID, From 09c5248bde536227666a211e8d2a7fd7c8f08431 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:20:24 +0200 Subject: [PATCH 163/719] test: bind G2 stopped-update middleware source --- tests/test_truenas_garm_provider_g2.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_truenas_garm_provider_g2.py b/tests/test_truenas_garm_provider_g2.py index 60d4b4f60..f8e506e47 100644 --- a/tests/test_truenas_garm_provider_g2.py +++ b/tests/test_truenas_garm_provider_g2.py @@ -138,6 +138,12 @@ def test_stopped_update_contract_is_explicitly_documented(self): self.assertIn("existing App is STOPPED", text) self.assertIn('"app.update"', text) self.assertIn('"runner_fixture_bootstrap_not_started": True', text) + self.assertEqual( + MOD.EXPECTED_TRUENAS_MIDDLEWARE_SOURCE, + "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", + ) + self.assertIn('"truenas_middleware_source": EXPECTED_TRUENAS_MIDDLEWARE_SOURCE', text) + self.assertIn('"stopped_update_contract":', text) def test_tls_path_is_verified_and_ephemeral(self): text = SCRIPT.read_text(encoding="utf-8") From 78847bc12c31bf5e87a42f5b70ca300ed33222fc Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 06:17:46 +0200 Subject: [PATCH 164/719] RDTE: stage exact TrueNAS 25.04.1 T5 matrix rep --- config/truenas-rdte-run-request.json | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 5f9143a9a..2810b5764 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,8 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g2-003-stopped-update", - "version": "26.0.0-BETA.3", - "rung": "t6", - "product": "garm-provider-g2", - "authority_issue": 396, - "purpose": "Corrective G2 rep-003: stage exact source-generated provider fixtures via supported stopped-App update so List/Get adoption and managed-drift fail-closed can be exercised without runner bootstrap or JIT side effects.", - "consumer_authority_issue": 39 + "request_id": "tn-25.04.1-t5-matrix-001", + "version": "25.04.1", + "rung": "t5", + "authority_issue": 411, + "purpose": "Exact deployed-legacy TrueNAS 25.04.1 T0-T5 qualification for the Foundry materialization and lifecycle matrix." } From 9c4dedb89efc728254408d863c345cb6e1f0fe50 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 06:19:33 +0200 Subject: [PATCH 165/719] test: scope WOW product assertions to T6 requests --- tests/test_truenas_t6_wow_sidecar_control.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 753f2f680..547bb0348 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -62,12 +62,15 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('"wow-sidecar"', harness) self.assertIn("truenas_middleware_wow_sidecar_t6_probe.py", harness) self.assertIn('T6_PRODUCT="litellm"', harness) - self.assertIn(request["product"], {"litellm", "wow-sidecar", "garm", "garm-provider-g2"}) - if request["product"] == "wow-sidecar": - self.assertEqual("t6", request["rung"]) - self.assertEqual("26.0.0-BETA.3", request["version"]) - self.assertEqual(396, request["authority_issue"]) - self.assertEqual(276, request["consumer_authority_issue"]) + if request["rung"] == "t6": + product = request.get("product", "litellm") + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2"}) + if product == "wow-sidecar": + self.assertEqual("26.0.0-BETA.3", request["version"]) + self.assertEqual(396, request["authority_issue"]) + self.assertEqual(276, request["consumer_authority_issue"]) + else: + self.assertNotIn("product", request) def test_load_control_rejects_foundry_drift(self): compose = { From b09d44889bc440a7214a7ed045f3af184e21f013 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:16:59 +0200 Subject: [PATCH 166/719] rdte: register version-specific installer endpoints and 25.04.2.6 --- config/truenas-rdte-targets.json | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-targets.json b/config/truenas-rdte-targets.json index f2f8e5153..45fd83ed5 100644 --- a/config/truenas-rdte-targets.json +++ b/config/truenas-rdte-targets.json @@ -18,7 +18,22 @@ "middleware_commit": "74ab5a2d373be4097dece257d00e1086376333ba", "foundry_profile": ".foundry/truenas-compatibility/25.04.1-materialization.json", "ha_apps_gate": "system.license:JAILS", - "authority_issue": 411 + "authority_issue": 411, + "installer_rpc_path": "/" + }, + { + "version": "25.04.2.6", + "channel": "recent-maintenance", + "system_version": "TrueNAS-25.04.2.6", + "iso_name": "TrueNAS-SCALE-25.04.2.6.iso", + "iso_url": "https://download.truenas.com/TrueNAS-SCALE-Fangtooth/25.04.2.6/TrueNAS-SCALE-25.04.2.6.iso", + "sha256_url": "https://download.truenas.com/TrueNAS-SCALE-Fangtooth/25.04.2.6/TrueNAS-SCALE-25.04.2.6.iso.sha256", + "middleware_ref": "release/25.04.2.6", + "middleware_commit": "244b717370fe35fb9eefc3096acfbc25f12b57b6", + "foundry_profile": ".foundry/truenas-compatibility/25.04.2.6-materialization.json", + "ha_apps_gate": "system.license:JAILS", + "installer_rpc_path": "/ws", + "authority_issue": 423 }, { "version": "25.10.7", @@ -31,7 +46,8 @@ "middleware_commit": "8ede398839710e56893d88ce85088139d8fab18e", "foundry_profile": ".foundry/truenas-compatibility/25.10.7-materialization.json", "ha_apps_gate": "system.license:JAILS", - "authority_issue": 410 + "authority_issue": 410, + "installer_rpc_path": "/ws" }, { "version": "26.0.0-BETA.3", @@ -44,7 +60,8 @@ "middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", "foundry_profile": ".foundry/truenas-compatibility/26.0.0-BETA.3-materialization.json", "ha_apps_gate": "system.feature_enabled:APPS", - "authority_issue": 396 + "authority_issue": 396, + "installer_rpc_path": "/ws" } ] } From 3927db7e8b959880fc153f7151c9a71cad41968e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:17:11 +0200 Subject: [PATCH 167/719] rdte: export exact installer RPC path from target profile --- scripts/truenas_rdte_target.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_rdte_target.py b/scripts/truenas_rdte_target.py index e59e0f087..ce1a3b706 100644 --- a/scripts/truenas_rdte_target.py +++ b/scripts/truenas_rdte_target.py @@ -26,11 +26,13 @@ def load_registry(path: pathlib.Path): if not isinstance(version,str) or not version or version in seen: raise TargetError(f"invalid or duplicate target version at index {i}") seen.add(version) - for key in ("system_version","iso_name","iso_url","sha256_url","middleware_ref","middleware_commit","foundry_profile","ha_apps_gate"): + for key in ("system_version","iso_name","iso_url","sha256_url","middleware_ref","middleware_commit","foundry_profile","ha_apps_gate","installer_rpc_path"): if not isinstance(t.get(key),str) or not t[key]: raise TargetError(f"{version}: missing {key}") if not SHA_RE.fullmatch(t["middleware_commit"]): raise TargetError(f"{version}: middleware_commit must be exact 40-hex") + if t["installer_rpc_path"] not in {"/", "/ws"}: + raise TargetError(f"{version}: unsupported installer_rpc_path") if any(token in t["iso_url"].lower() for token in ("latest","nightly","master+")): raise TargetError(f"{version}: supported target ISO must be exact, not floating") if t["sha256_url"] != t["iso_url"] + ".sha256": @@ -49,6 +51,7 @@ def shell(target): "MIDDLEWARE_COMMIT":target["middleware_commit"], "FOUNDRY_PROFILE":target["foundry_profile"], "HA_APPS_GATE":target["ha_apps_gate"], + "INSTALLER_RPC_PATH":target["installer_rpc_path"], "AUTHORITY_ISSUE":str(target.get("authority_issue","")), } return "\n".join(f"{k}={shlex.quote(v)}" for k,v in fields.items()) From 697bab4b49380447445097a4d6e6df4860d7f20b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:17:27 +0200 Subject: [PATCH 168/719] rdte: parameterize installer discovery WebSocket path --- scripts/truenas_installer_rpc_probe.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_installer_rpc_probe.py b/scripts/truenas_installer_rpc_probe.py index 9487ebfb3..599f25fd1 100644 --- a/scripts/truenas_installer_rpc_probe.py +++ b/scripts/truenas_installer_rpc_probe.py @@ -123,17 +123,18 @@ def main(): p = argparse.ArgumentParser() p.add_argument("--host", default="127.0.0.1") p.add_argument("--port", type=int, required=True) + p.add_argument("--path", default="/ws") p.add_argument("--out", required=True) p.add_argument("--timeout", type=float, default=6.0) a = p.parse_args() started = time.time() payload = { "schema": "truenas-installer-rpc-discovery/v1", - "endpoint": {"host": a.host, "port": a.port, "path": "/ws"}, + "endpoint": {"host": a.host, "port": a.port, "path": a.path}, "methods": {}, "oracleSatisfied": False, } - ws = WebSocket(a.host, a.port, timeout=a.timeout) + ws = WebSocket(a.host, a.port, path=a.path, timeout=a.timeout) try: adopted = ws.call("is_adopted", 1) payload["methods"]["is_adopted"] = adopted From d5222a66461ac8270b9bcc745f3c942e0e38e158 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:17:31 +0200 Subject: [PATCH 169/719] rdte: parameterize installer mutation WebSocket path --- scripts/truenas_installer_rpc_install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_installer_rpc_install.py b/scripts/truenas_installer_rpc_install.py index 220851c2f..6ceede879 100755 --- a/scripts/truenas_installer_rpc_install.py +++ b/scripts/truenas_installer_rpc_install.py @@ -33,6 +33,7 @@ def main(): p = argparse.ArgumentParser() p.add_argument("--host", default="127.0.0.1") p.add_argument("--port", type=int, required=True) + p.add_argument("--path", default="/ws") p.add_argument("--password-file", required=True) p.add_argument("--out", required=True) p.add_argument("--timeout", type=float, default=15.0) @@ -48,12 +49,13 @@ def main(): "classification": "ORACLE_FAILURE", "selected_disk": None, "selected_interface": None, + "installer_rpc_path": a.path, "progress": [], } started = time.time() ws = None try: - ws = WebSocket(a.host, a.port, timeout=a.timeout) + ws = WebSocket(a.host, a.port, path=a.path, timeout=a.timeout) adopted = rpc_call(ws, "is_adopted", 1) payload["is_adopted"] = adopted if adopted: From 59ef245e595c4c27a61e2a18960606de8fe3d15e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:20:10 +0200 Subject: [PATCH 170/719] rdte: select installer RPC endpoint from exact TrueNAS target --- scripts/gha_kvm_truenas_rdte.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index d052cca47..78c02bb76 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -106,7 +106,7 @@ write_receipt() { export R_TARGET_VERSION="$VERSION" R_EXPECTED_SYSTEM_VERSION="$EXPECTED_SYSTEM_VERSION" export R_ISO_NAME="$ISO_NAME" R_ISO_URL="$ISO_URL" R_SHA_URL="$SHA_URL" export R_MIDDLEWARE_REF="$MIDDLEWARE_REF" R_MIDDLEWARE_COMMIT="$MIDDLEWARE_COMMIT" - export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" + export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_INSTALLER_RPC_PATH="$INSTALLER_RPC_PATH" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" @@ -149,6 +149,7 @@ payload = { "middleware_commit": os.environ["R_MIDDLEWARE_COMMIT"], "foundry_profile": os.environ["R_FOUNDRY_PROFILE"], "ha_apps_gate": os.environ["R_HA_APPS_GATE"], + "installer_rpc_path": os.environ["R_INSTALLER_RPC_PATH"], "system_version_expected": os.environ["R_EXPECTED_SYSTEM_VERSION"], "authority_issue": int(os.environ["R_AUTHORITY_ISSUE"]) if os.environ.get("R_AUTHORITY_ISSUE") else None, }, @@ -177,7 +178,7 @@ payload = { "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ "T0 proves pinned vendor media integrity and installer-environment boot under the disposable virtual target profile.", - "T1 is read-only installer RPC discovery.", + "T1 is version-profiled read-only installer RPC discovery.", "T2 adds vendor installation plus installed middleware authentication/health.", "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", "T4 initializes Apps on that pool and runs one synthetic public-safe custom Compose app.", @@ -316,7 +317,7 @@ PY try_rpc_discovery() { local rpc_out="$STATE_DIR/rpc-discovery.json" rm -f "$rpc_out" - python3 "$SCRIPT_DIR/truenas_installer_rpc_probe.py" --host 127.0.0.1 --port "$RPC_PORT" --out "$rpc_out" --timeout 3 >/dev/null 2>&1 || true + python3 "$SCRIPT_DIR/truenas_installer_rpc_probe.py" --host 127.0.0.1 --port "$RPC_PORT" --path "$INSTALLER_RPC_PATH" --out "$rpc_out" --timeout 3 >/dev/null 2>&1 || true [[ -f "$rpc_out" ]] || return 1 RPC_DISCOVERY_JSON="$(cat "$rpc_out")" if python3 - "$rpc_out" <<'PY' @@ -411,7 +412,7 @@ PY INSTALL_OUT="$STATE_DIR/install-result.json" python3 "$SCRIPT_DIR/truenas_installer_rpc_install.py" \ - --host 127.0.0.1 --port "$RPC_PORT" \ + --host 127.0.0.1 --port "$RPC_PORT" --path "$INSTALLER_RPC_PATH" \ --password-file "$PASSWORD_FILE" \ --out "$INSTALL_OUT" --timeout 120 >/dev/null 2>&1 || true [[ -f "$INSTALL_OUT" ]] || fail_evidence HARNESS_FAILURE installer-install "installer mutation client did not emit a receipt" From 256d0b0cdff866ea741639a441cfd5fd15fbf53a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:20:22 +0200 Subject: [PATCH 171/719] rdte: expose exact 25.04.2.6 target --- .github/workflows/gha-kvm-system-rdte.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index d237d5d12..1abd1a82b 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -23,6 +23,7 @@ on: options: - "26.0.0-BETA.3" - "25.10.7" + - "25.04.2.6" - "25.04.1" truenas_rung: description: "TrueNAS system qualification rung" From abb3f84d0410a502ff9d7a40348766efabf146e4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:20:43 +0200 Subject: [PATCH 172/719] test: lock exact TrueNAS installer endpoint matrix --- tests/test_truenas_rdte_target.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 31fd49bd8..79742a6bd 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -11,9 +11,10 @@ class TargetRegistryTests(unittest.TestCase): def setUp(self): self.targets=MOD.load_registry(REG) def test_exact_supported_matrix(self): - self.assertEqual(set(self.targets), {"25.04.1","25.10.7","26.0.0-BETA.3"}) + self.assertEqual(set(self.targets), {"25.04.1","25.04.2.6","25.10.7","26.0.0-BETA.3"}) def test_exact_source_anchors(self): self.assertEqual(self.targets["25.04.1"]["middleware_commit"], "74ab5a2d373be4097dece257d00e1086376333ba") + self.assertEqual(self.targets["25.04.2.6"]["middleware_commit"], "244b717370fe35fb9eefc3096acfbc25f12b57b6") self.assertEqual(self.targets["25.10.7"]["middleware_commit"], "8ede398839710e56893d88ce85088139d8fab18e") self.assertEqual(self.targets["26.0.0-BETA.3"]["middleware_commit"], "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3") def test_vendor_media_is_exact_and_has_digest_sidecar(self): @@ -29,11 +30,19 @@ def test_exact_installed_version_identity_for_stable_target(self): def test_exact_installed_version_identity_for_legacy_target(self): self.assertEqual(self.targets["25.04.1"]["system_version"], "TrueNAS-25.04.1") + self.assertEqual(self.targets["25.04.2.6"]["system_version"], "TrueNAS-25.04.2.6") def test_apps_gate_deltas_are_not_normalized_away(self): self.assertEqual(self.targets["25.04.1"]["ha_apps_gate"], "system.license:JAILS") + self.assertEqual(self.targets["25.04.2.6"]["ha_apps_gate"], "system.license:JAILS") self.assertEqual(self.targets["25.10.7"]["ha_apps_gate"], "system.license:JAILS") self.assertEqual(self.targets["26.0.0-BETA.3"]["ha_apps_gate"], "system.feature_enabled:APPS") + def test_installer_rpc_path_is_exact_version_profiled(self): + self.assertEqual(self.targets["25.04.1"]["installer_rpc_path"], "/") + self.assertEqual(self.targets["25.04.2.6"]["installer_rpc_path"], "/ws") + self.assertEqual(self.targets["25.10.7"]["installer_rpc_path"], "/ws") + self.assertEqual(self.targets["26.0.0-BETA.3"]["installer_rpc_path"], "/ws") + def test_unknown_exact_version_fails_closed(self): with self.assertRaises(MOD.TargetError): targets=self.targets From 220efcf7948997ca8ed33cb277a438facb982703 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:21:13 +0200 Subject: [PATCH 173/719] test: prove installer endpoint is profile-selected --- tests/test_gha_kvm_system_rdte.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index fd67edf3d..596081358 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -192,6 +192,8 @@ def test_truenas_uses_exact_target_registry_and_vendor_digest_sidecar(self): self.assertIn("installer-boot", text) self.assertIn("installer-rpc", text) self.assertIn("truenas_installer_rpc_probe.py", text) + self.assertIn('INSTALLER_RPC_PATH', text) + self.assertIn('--path "$INSTALLER_RPC_PATH"', text) self.assertNotIn("xdotool", text) def test_no_literal_escaped_shell_parameter_expansions(self): @@ -205,6 +207,7 @@ def test_truenas_rpc_probe_is_dependency_free_and_read_only(self): for method in ("is_adopted", "system_info", "list_disks", "list_network_interfaces"): self.assertIn(method, text) self.assertNotIn('"install"', text) + self.assertIn('p.add_argument("--path", default="/ws")', text) self.assertNotIn("pip install", text) cp = subprocess.run( ["python3", "-m", "py_compile", str(TRUENAS_RPC)], @@ -240,6 +243,7 @@ def test_truenas_rpc_probe_protocol_round_trip(self): listener.listen(1) port = listener.getsockname()[1] errors = [] + request_lines = [] def read_exact(conn, count): parts = [] @@ -287,8 +291,10 @@ def server(): request = bytearray() while b"\r\n\r\n" not in request: request.extend(conn.recv(4096)) + decoded_request = request.decode() + request_lines.append(decoded_request.split("\r\n", 1)[0]) headers = {} - for line in request.decode().split("\r\n")[1:]: + for line in decoded_request.split("\r\n")[1:]: if ":" in line: key, value = line.split(":", 1) headers[key.lower().strip()] = value.strip() @@ -341,6 +347,8 @@ def server(): str(TRUENAS_RPC), "--port", str(port), + "--path", + "/", "--out", str(out), "--timeout", @@ -356,9 +364,11 @@ def server(): self.assertEqual(payload["classification"], "SUPPORTED") self.assertFalse(payload["methods"]["is_adopted"]) self.assertEqual(payload["methods"]["system_info"]["version"], "synthetic") + self.assertEqual(payload["endpoint"]["path"], "/") thread.join(timeout=5) self.assertFalse(thread.is_alive()) self.assertEqual(errors, []) + self.assertEqual(request_lines, ["GET / HTTP/1.1"]) def test_truenas_t1_requires_real_rpc_not_hostfwd_tcp(self): text = TRUENAS.read_text(encoding="utf-8") @@ -373,6 +383,8 @@ def test_future_truenas_mutating_clients_compile_and_refuse_ambiguity(self): middleware = TRUENAS_MIDDLEWARE.read_text(encoding="utf-8") self.assertIn("expected exactly one non-removable disk", install) self.assertIn("expected exactly one non-loopback interface", install) + self.assertIn('p.add_argument("--path", default="/ws")', install) + self.assertIn("path=a.path", install) self.assertIn('"truenas_admin"', install) self.assertIn('"auth.login_ex"', middleware) self.assertIn('"PASSWORD_PLAIN"', middleware) From 77c29e2eed823bfcef9d258266f35c4c06f89518 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:21:26 +0200 Subject: [PATCH 174/719] rdte: request 25.04.1 T5 with version-profiled installer endpoint --- config/truenas-rdte-run-request.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 2810b5764..c81684e3e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,8 +1,9 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t5-matrix-001", + "request_id": "tn-25.04.1-t5-matrix-002-versioned-installer-endpoint", "version": "25.04.1", "rung": "t5", "authority_issue": 411, - "purpose": "Exact deployed-legacy TrueNAS 25.04.1 T0-T5 qualification for the Foundry materialization and lifecycle matrix." + "consumer_authority_issue": 274, + "purpose": "Retry exact TrueNAS 25.04.1 T0-T5 after source-backed discovery that this release exposes installer JSON-RPC at / rather than the later /ws endpoint." } From 4bd5c6363d34b77aad9f77cb4082658de5a81b31 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:22:31 +0200 Subject: [PATCH 175/719] rdte: bind exact TrueNAS installer RPC path and guest port --- config/truenas-rdte-targets.json | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/config/truenas-rdte-targets.json b/config/truenas-rdte-targets.json index 45fd83ed5..028b12774 100644 --- a/config/truenas-rdte-targets.json +++ b/config/truenas-rdte-targets.json @@ -19,7 +19,10 @@ "foundry_profile": ".foundry/truenas-compatibility/25.04.1-materialization.json", "ha_apps_gate": "system.license:JAILS", "authority_issue": 411, - "installer_rpc_path": "/" + "installer_rpc_path": "/", + "installer_rpc_guest_port": 80, + "installer_source_ref": "release/25.04.1", + "installer_main_blob_sha": "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f" }, { "version": "25.04.2.6", @@ -32,8 +35,11 @@ "middleware_commit": "244b717370fe35fb9eefc3096acfbc25f12b57b6", "foundry_profile": ".foundry/truenas-compatibility/25.04.2.6-materialization.json", "ha_apps_gate": "system.license:JAILS", - "installer_rpc_path": "/ws", - "authority_issue": 423 + "installer_rpc_path": "/", + "authority_issue": 423, + "installer_rpc_guest_port": 80, + "installer_source_ref": "release/25.04.2.6", + "installer_main_blob_sha": "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f" }, { "version": "25.10.7", @@ -47,7 +53,10 @@ "foundry_profile": ".foundry/truenas-compatibility/25.10.7-materialization.json", "ha_apps_gate": "system.license:JAILS", "authority_issue": 410, - "installer_rpc_path": "/ws" + "installer_rpc_path": "/ws", + "installer_rpc_guest_port": 8080, + "installer_source_ref": "release/25.10.7", + "installer_main_blob_sha": "9e4f8b2bd9db497210fe6737c053080137be94d8" }, { "version": "26.0.0-BETA.3", @@ -61,7 +70,10 @@ "foundry_profile": ".foundry/truenas-compatibility/26.0.0-BETA.3-materialization.json", "ha_apps_gate": "system.feature_enabled:APPS", "authority_issue": 396, - "installer_rpc_path": "/ws" + "installer_rpc_path": "/ws", + "installer_rpc_guest_port": 8080, + "installer_source_ref": "release/26.0.0-BETA.3", + "installer_main_blob_sha": "9e4f8b2bd9db497210fe6737c053080137be94d8" } ] } From 4417d066a9e2d81cfa627deef40342891478d456 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:22:48 +0200 Subject: [PATCH 176/719] rdte: export installer source and guest-port profile facts --- scripts/truenas_rdte_target.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_rdte_target.py b/scripts/truenas_rdte_target.py index ce1a3b706..0e0ebe245 100644 --- a/scripts/truenas_rdte_target.py +++ b/scripts/truenas_rdte_target.py @@ -26,13 +26,17 @@ def load_registry(path: pathlib.Path): if not isinstance(version,str) or not version or version in seen: raise TargetError(f"invalid or duplicate target version at index {i}") seen.add(version) - for key in ("system_version","iso_name","iso_url","sha256_url","middleware_ref","middleware_commit","foundry_profile","ha_apps_gate","installer_rpc_path"): + for key in ("system_version","iso_name","iso_url","sha256_url","middleware_ref","middleware_commit","foundry_profile","ha_apps_gate","installer_rpc_path","installer_source_ref","installer_main_blob_sha"): if not isinstance(t.get(key),str) or not t[key]: raise TargetError(f"{version}: missing {key}") if not SHA_RE.fullmatch(t["middleware_commit"]): raise TargetError(f"{version}: middleware_commit must be exact 40-hex") if t["installer_rpc_path"] not in {"/", "/ws"}: raise TargetError(f"{version}: unsupported installer_rpc_path") + if not isinstance(t.get("installer_rpc_guest_port"), int) or not (1 <= t["installer_rpc_guest_port"] <= 65535): + raise TargetError(f"{version}: installer_rpc_guest_port must be an integer TCP port") + if not SHA_RE.fullmatch(t["installer_main_blob_sha"]): + raise TargetError(f"{version}: installer_main_blob_sha must be exact 40-hex") if any(token in t["iso_url"].lower() for token in ("latest","nightly","master+")): raise TargetError(f"{version}: supported target ISO must be exact, not floating") if t["sha256_url"] != t["iso_url"] + ".sha256": @@ -52,6 +56,9 @@ def shell(target): "FOUNDRY_PROFILE":target["foundry_profile"], "HA_APPS_GATE":target["ha_apps_gate"], "INSTALLER_RPC_PATH":target["installer_rpc_path"], + "INSTALLER_RPC_GUEST_PORT":str(target["installer_rpc_guest_port"]), + "INSTALLER_SOURCE_REF":target["installer_source_ref"], + "INSTALLER_MAIN_BLOB_SHA":target["installer_main_blob_sha"], "AUTHORITY_ISSUE":str(target.get("authority_issue","")), } return "\n".join(f"{k}={shlex.quote(v)}" for k,v in fields.items()) From 90da79aef14a51b5b0623c31ddf4e22226a5cd41 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:23:13 +0200 Subject: [PATCH 177/719] rdte: preserve installer handshake failure evidence --- scripts/truenas_installer_rpc_probe.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_installer_rpc_probe.py b/scripts/truenas_installer_rpc_probe.py index 599f25fd1..2da88925c 100644 --- a/scripts/truenas_installer_rpc_probe.py +++ b/scripts/truenas_installer_rpc_probe.py @@ -134,8 +134,11 @@ def main(): "methods": {}, "oracleSatisfied": False, } - ws = WebSocket(a.host, a.port, path=a.path, timeout=a.timeout) + ws = None + payload["stage"] = "connect-or-handshake" try: + ws = WebSocket(a.host, a.port, path=a.path, timeout=a.timeout) + payload["stage"] = "rpc-calls" adopted = ws.call("is_adopted", 1) payload["methods"]["is_adopted"] = adopted if adopted: @@ -147,11 +150,13 @@ def main(): payload["oracleSatisfied"] = True payload["classification"] = "SUPPORTED" payload["detail"] = "read-only TrueNAS installer JSON-RPC discovery methods answered" + payload["stage"] = "complete" except Exception as exc: payload["classification"] = "ORACLE_FAILURE" payload["detail"] = f"{type(exc).__name__}: {exc}" finally: - ws.close() + if ws is not None: + ws.close() payload["elapsed_seconds"] = round(time.time() - started, 3) pathlib.Path(a.out).write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n") print(json.dumps(payload, indent=2, sort_keys=True)) From 1654f63ebd516d024a116382e5c3e41a92ab276b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:23:43 +0200 Subject: [PATCH 178/719] rdte: route installer RPC to exact release guest port --- scripts/gha_kvm_truenas_rdte.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 78c02bb76..59616909d 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -106,7 +106,7 @@ write_receipt() { export R_TARGET_VERSION="$VERSION" R_EXPECTED_SYSTEM_VERSION="$EXPECTED_SYSTEM_VERSION" export R_ISO_NAME="$ISO_NAME" R_ISO_URL="$ISO_URL" R_SHA_URL="$SHA_URL" export R_MIDDLEWARE_REF="$MIDDLEWARE_REF" R_MIDDLEWARE_COMMIT="$MIDDLEWARE_COMMIT" - export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_INSTALLER_RPC_PATH="$INSTALLER_RPC_PATH" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" + export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_INSTALLER_RPC_PATH="$INSTALLER_RPC_PATH" R_INSTALLER_RPC_GUEST_PORT="$INSTALLER_RPC_GUEST_PORT" R_INSTALLER_SOURCE_REF="$INSTALLER_SOURCE_REF" R_INSTALLER_MAIN_BLOB_SHA="$INSTALLER_MAIN_BLOB_SHA" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" @@ -150,6 +150,9 @@ payload = { "foundry_profile": os.environ["R_FOUNDRY_PROFILE"], "ha_apps_gate": os.environ["R_HA_APPS_GATE"], "installer_rpc_path": os.environ["R_INSTALLER_RPC_PATH"], + "installer_rpc_guest_port": int(os.environ["R_INSTALLER_RPC_GUEST_PORT"]), + "installer_source_ref": os.environ["R_INSTALLER_SOURCE_REF"], + "installer_main_blob_sha": os.environ["R_INSTALLER_MAIN_BLOB_SHA"], "system_version_expected": os.environ["R_EXPECTED_SYSTEM_VERSION"], "authority_issue": int(os.environ["R_AUTHORITY_ISSUE"]) if os.environ.get("R_AUTHORITY_ISSUE") else None, }, @@ -346,7 +349,7 @@ sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ -drive "file=$STATE_DIR/boot.qcow2,if=virtio,format=qcow2" \ -cdrom "$SERIAL_ISO" -boot order=d \ - -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080" -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$RPC_PORT-:$INSTALLER_RPC_GUEST_PORT" -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ -daemonize -pidfile "$STATE_DIR/qemu.pid" || From 03c3834197293e84670db8e6b0d3742927237dcc Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:24:03 +0200 Subject: [PATCH 179/719] test: lock exact installer path port and source identity --- tests/test_truenas_rdte_target.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 79742a6bd..8e7bcec3f 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -37,11 +37,19 @@ def test_apps_gate_deltas_are_not_normalized_away(self): self.assertEqual(self.targets["25.04.2.6"]["ha_apps_gate"], "system.license:JAILS") self.assertEqual(self.targets["25.10.7"]["ha_apps_gate"], "system.license:JAILS") self.assertEqual(self.targets["26.0.0-BETA.3"]["ha_apps_gate"], "system.feature_enabled:APPS") - def test_installer_rpc_path_is_exact_version_profiled(self): - self.assertEqual(self.targets["25.04.1"]["installer_rpc_path"], "/") - self.assertEqual(self.targets["25.04.2.6"]["installer_rpc_path"], "/ws") - self.assertEqual(self.targets["25.10.7"]["installer_rpc_path"], "/ws") - self.assertEqual(self.targets["26.0.0-BETA.3"]["installer_rpc_path"], "/ws") + def test_installer_rpc_endpoint_is_exact_version_profiled(self): + expected = { + "25.04.1": ("/", 80, "release/25.04.1", "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f"), + "25.04.2.6": ("/", 80, "release/25.04.2.6", "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f"), + "25.10.7": ("/ws", 8080, "release/25.10.7", "9e4f8b2bd9db497210fe6737c053080137be94d8"), + "26.0.0-BETA.3": ("/ws", 8080, "release/26.0.0-BETA.3", "9e4f8b2bd9db497210fe6737c053080137be94d8"), + } + for version, (path, port, source_ref, main_blob) in expected.items(): + target = self.targets[version] + self.assertEqual(target["installer_rpc_path"], path) + self.assertEqual(target["installer_rpc_guest_port"], port) + self.assertEqual(target["installer_source_ref"], source_ref) + self.assertEqual(target["installer_main_blob_sha"], main_blob) def test_unknown_exact_version_fails_closed(self): with self.assertRaises(MOD.TargetError): From 0aaef541c749626093843b3359684d7a5cbd126d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:24:28 +0200 Subject: [PATCH 180/719] test: preserve installer transport failures and dynamic guest port --- tests/test_gha_kvm_system_rdte.py | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 596081358..f55916d97 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -193,7 +193,12 @@ def test_truenas_uses_exact_target_registry_and_vendor_digest_sidecar(self): self.assertIn("installer-rpc", text) self.assertIn("truenas_installer_rpc_probe.py", text) self.assertIn('INSTALLER_RPC_PATH', text) + self.assertIn('INSTALLER_RPC_GUEST_PORT', text) + self.assertIn('INSTALLER_SOURCE_REF', text) + self.assertIn('INSTALLER_MAIN_BLOB_SHA', text) self.assertIn('--path "$INSTALLER_RPC_PATH"', text) + self.assertIn('hostfwd=tcp:127.0.0.1:$RPC_PORT-:$INSTALLER_RPC_GUEST_PORT', text) + self.assertNotIn('hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080', text) self.assertNotIn("xdotool", text) def test_no_literal_escaped_shell_parameter_expansions(self): @@ -370,6 +375,39 @@ def server(): self.assertEqual(errors, []) self.assertEqual(request_lines, ["GET / HTTP/1.1"]) + def test_truenas_rpc_probe_preserves_connect_failure_receipt(self): + with tempfile.TemporaryDirectory() as td: + out = pathlib.Path(td) / "rpc-failure.json" + unused = socket.socket() + unused.bind(("127.0.0.1", 0)) + port = unused.getsockname()[1] + unused.close() + cp = subprocess.run( + [ + sys.executable, + str(TRUENAS_RPC), + "--port", + str(port), + "--path", + "/", + "--out", + str(out), + "--timeout", + "0.2", + ], + text=True, + capture_output=True, + timeout=10, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + self.assertTrue(out.is_file()) + payload = json.loads(out.read_text(encoding="utf-8")) + self.assertFalse(payload["oracleSatisfied"]) + self.assertEqual(payload["classification"], "ORACLE_FAILURE") + self.assertEqual(payload["stage"], "connect-or-handshake") + self.assertEqual(payload["endpoint"]["path"], "/") + self.assertIn("ConnectionRefusedError", payload["detail"]) + def test_truenas_t1_requires_real_rpc_not_hostfwd_tcp(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn("try_rpc_discovery", text) From 103b08afb3801f1574b17f334f1bee81381dc1f0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:37:51 +0200 Subject: [PATCH 181/719] rdte: make committed run request authoritative for push execution --- .github/workflows/gha-kvm-system-rdte.yml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 1abd1a82b..f64a2f3c8 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -87,7 +87,6 @@ jobs: proxmox=false truenas=false qemu_topology=false - run_request_changed=false truenas_version="${DISPATCH_TRUENAS_VERSION:-26.0.0-BETA.3}" truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" truenas_t6_product="${DISPATCH_TRUENAS_T6_PRODUCT:-litellm}" @@ -103,7 +102,6 @@ jobs: case "$path" in config/truenas-rdte-run-request.json) truenas=true - run_request_changed=true ;; scripts/gha_kvm_proxmox_rdte.sh) proxmox=true @@ -116,20 +114,20 @@ jobs: ;; esac done < <(git diff --name-only "$BEFORE_SHA" "$AFTER_SHA") - if [[ "$run_request_changed" == "true" ]]; then - truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" - truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2")' "$RUN_REQUEST_PATH")" - jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null - python3 scripts/truenas_rdte_target.py \ - --registry config/truenas-rdte-targets.json \ - --version "$truenas_version" >/dev/null - fi else proxmox=true truenas=true qemu_topology=true fi + if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then + jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null + truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" + truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2")' "$RUN_REQUEST_PATH")" + python3 scripts/truenas_rdte_target.py \ + --registry config/truenas-rdte-targets.json \ + --version "$truenas_version" >/dev/null + fi echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" echo "truenas=$truenas" >>"$GITHUB_OUTPUT" echo "qemu_topology=$qemu_topology" >>"$GITHUB_OUTPUT" From 3f43eaa333f601fb24e71a6fd604b2ca17b27e1a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:40:07 +0200 Subject: [PATCH 182/719] test: make committed TrueNAS run request authoritative on push --- tests/test_gha_kvm_system_rdte.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index f55916d97..35f8d1167 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -235,8 +235,20 @@ def test_workflow_scopes_heavy_targets(self): self.assertIn("needs.changes.outputs.truenas_version", text) self.assertIn("needs.changes.outputs.truenas_rung", text) self.assertIn("config/truenas-rdte-run-request.json", text) - self.assertIn("run_request_changed", text) + self.assertNotIn("run_request_changed", text) + self.assertIn( + 'if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then', + text, + ) self.assertIn("gha-kvm-truenas-run-request/v1", text) + self.assertIn( + 'truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")"', + text, + ) + self.assertIn( + 'truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")"', + text, + ) def test_truenas_t1_requires_local_rpc_probe(self): text = TRUENAS.read_text(encoding="utf-8") From 32b3bd3a7f97acf9652e62ee059381ebf77ef399 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:40:22 +0200 Subject: [PATCH 183/719] rdte: request corrected exact 25.04.1 T5 rep --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index c81684e3e..da3e5012c 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,9 +1,9 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t5-matrix-002-versioned-installer-endpoint", + "request_id": "tn-25.04.1-t5-matrix-003-exact-installer-port-profile", "version": "25.04.1", "rung": "t5", "authority_issue": 411, "consumer_authority_issue": 274, - "purpose": "Retry exact TrueNAS 25.04.1 T0-T5 after source-backed discovery that this release exposes installer JSON-RPC at / rather than the later /ws endpoint." + "purpose": "Retry exact TrueNAS 25.04.1 T0-T5 after exact upstream installer source proved WebSocket JSON-RPC at / on guest TCP port 80; target profile now binds path, guest port, source ref/blob, and preserves transport failure evidence." } From f63d5c7c7f6a26bd0a552c4d4606f8726c44e827 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:40:58 +0200 Subject: [PATCH 184/719] test: fix run-request authority assertion quoting --- tests/test_gha_kvm_system_rdte.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 35f8d1167..157829a83 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -241,14 +241,10 @@ def test_workflow_scopes_heavy_targets(self): text, ) self.assertIn("gha-kvm-truenas-run-request/v1", text) - self.assertIn( - 'truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")"', - text, - ) - self.assertIn( - 'truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")"', - text, - ) + self.assertIn("truenas_version=\"$(jq -er", text) + self.assertIn(".version | select(type == \"string\" and length > 0)", text) + self.assertIn("truenas_rung=\"$(jq -er", text) + self.assertIn('.rung | select(. == "t0" or . == "t1"', text) def test_truenas_t1_requires_local_rpc_probe(self): text = TRUENAS.read_text(encoding="utf-8") From 35aed912cb18119d08786953f82b909dcb6401b8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:15:34 +0200 Subject: [PATCH 185/719] RDTE: launch exact TrueNAS 25.04.2.6 T5 matrix rep Authority: SemperSupra/agent-dispatch-private#423 Consumer authority: SemperSupra/truenas-app-foundry-private#274 Uses the current source-bound installer /:80 profile and committed-run-request routing. --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index da3e5012c..e5509b0d6 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,9 +1,9 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t5-matrix-003-exact-installer-port-profile", - "version": "25.04.1", + "request_id": "tn-25.04.2.6-t5-matrix-002-current-harness", + "version": "25.04.2.6", "rung": "t5", - "authority_issue": 411, + "authority_issue": 423, "consumer_authority_issue": 274, - "purpose": "Retry exact TrueNAS 25.04.1 T0-T5 after exact upstream installer source proved WebSocket JSON-RPC at / on guest TCP port 80; target profile now binds path, guest port, source ref/blob, and preserves transport failure evidence." + "purpose": "Exact TrueNAS 25.04.2.6 T0-T5 qualification for the Foundry cross-version matrix using the current harness with source-bound installer /:80 profile and committed-run-request routing." } From 1415a24099258a5371e7b9729deac6779d433d1c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:55:40 +0200 Subject: [PATCH 186/719] RDTE: stage official-catalog T6 .github/workflows/gha-kvm-system-rdte.yml --- .github/workflows/gha-kvm-system-rdte.yml | 28 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index f64a2f3c8..114396254 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -48,6 +48,7 @@ on: - wow-sidecar - garm - garm-provider-g2 + - official-catalog permissions: contents: read @@ -106,7 +107,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -123,7 +124,7 @@ jobs: jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "official-catalog")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null @@ -144,7 +145,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -173,6 +174,16 @@ jobs: permissions: contents: read + foundry-official-catalog-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@d2ee19c6805299d0bc55d6ca03903548be026f87 + with: + foundry_ref: d2ee19c6805299d0bc55d6ca03903548be026f87 + control_id: ntfy + permissions: + contents: read + provider-g2-fixtures: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' @@ -263,7 +274,7 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, provider-g2-fixtures] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures] if: >- always() && needs.contract.result == 'success' && @@ -274,6 +285,7 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'litellm' && needs['foundry-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') ) runs-on: ubuntu-26.04 @@ -300,6 +312,12 @@ jobs: with: name: garm-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact official-catalog Foundry T6 control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: official-catalog-truenas-t6-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c @@ -334,6 +352,8 @@ jobs: foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then + foundry_commit="d2ee19c6805299d0bc55d6ca03903548be026f87" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From ec5c6740f207287d9c773c10e5df76e9be5a7f02 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:55:47 +0200 Subject: [PATCH 187/719] RDTE: stage official-catalog T6 config/truenas-rdte-run-request.json --- config/truenas-rdte-run-request.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e5509b0d6..d29851505 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,9 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.2.6-t5-matrix-002-current-harness", - "version": "25.04.2.6", - "rung": "t5", - "authority_issue": 423, - "consumer_authority_issue": 274, - "purpose": "Exact TrueNAS 25.04.2.6 T0-T5 qualification for the Foundry cross-version matrix using the current harness with source-bound installer /:80 profile and committed-run-request routing." + "request_id": "tn-25.04.1-t6-official-catalog-001", + "version": "25.04.1", + "rung": "t6", + "product": "official-catalog", + "authority_issue": 411, + "consumer_authority_issue": 281, + "purpose": "Exercise the exact Foundry-exported native official-catalog ntfy control on accepted TrueNAS 25.04.1 using observe/discover -> plan -> apply -> verify, lifecycle convergence, conditional native upgrade, retain-data delete/reinstall, and final cleanup." } From 6acae47ae301f15d60e6c9409dcda067a805df44 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:55:53 +0200 Subject: [PATCH 188/719] RDTE: stage official-catalog T6 scripts/gha_kvm_truenas_rdte.sh --- scripts/gha_kvm_truenas_rdte.sh | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 59616909d..4e4fb1f0c 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA]" } OUT="" @@ -50,8 +50,10 @@ TARGET_ENV="$(python3 "$SCRIPT_DIR/truenas_rdte_target.py" --registry "$TARGET_R eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } if [[ "$RUNG" == "t6" ]]; then - [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "T6 is currently admitted only for exact TrueNAS 26.0.0-BETA.3; use Foundry F1-F5 for other targets" >&2; exit 2; } - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + if [[ "$T6_PRODUCT" != "official-catalog" ]]; then + [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } + fi + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -133,6 +135,8 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm" else {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" + else {"name": "rdte-t6-catalog-ntfy", "catalog_app": "ntfy", "catalog_version": "1.1.21"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} @@ -222,7 +226,10 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T5 lifecycle client" fi if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then + if [[ "$T6_PRODUCT" == "official-catalog" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_official_catalog_t6_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 official-catalog control client" + elif [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_garm_provider_g2_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G2 client" command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" @@ -635,7 +642,15 @@ if [[ "$RUNG" == "t5" ]]; then fi FOUNDRY_OUT="$STATE_DIR/foundry-control.json" -if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then +if [[ "$T6_PRODUCT" == "official-catalog" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_official_catalog_t6_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_provider_g2_probe.py" \ --host 127.0.0.1 --http-port "$HTTP_PORT" --https-port "$HTTPS_PORT" \ --password-file "$PASSWORD_FILE" \ @@ -678,7 +693,9 @@ PY [[ "$FOUNDRY_OK" == "true" ]] || fail_evidence ORACLE_FAILURE foundry-materialization "exact Foundry-exported $T6_PRODUCT control did not realize and verify on TrueNAS" -if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then +if [[ "$T6_PRODUCT" == "official-catalog" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported native catalog control passed observe/discover-plan-apply-verify convergence, stop/start, config update/read-back, redeploy, conditional native-upgrade receipt, retain-data delete/reinstall, NOOP convergence, and final cleanup" +elif [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider passed verified WSS/API-key transport, valid local adoption, foreign exclusion, managed-drift fail-closed behavior, fresh-process readoption, and zero-residue cleanup; provider create/delete and GitHub/JIT intentionally not exercised" elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" From 17b53bdb3f272250779e329068e1cc1690ca14df Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:55:59 +0200 Subject: [PATCH 189/719] RDTE: add official-catalog T6 scripts/truenas_middleware_official_catalog_t6_probe.py --- ...as_middleware_official_catalog_t6_probe.py | 463 ++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 scripts/truenas_middleware_official_catalog_t6_probe.py diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py new file mode 100644 index 000000000..1f1f24f0e --- /dev/null +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -0,0 +1,463 @@ +#!/usr/bin/env python3 +"""Bounded native official-catalog lifecycle oracle for disposable TrueNAS targets.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import time +from typing import Any + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + +EXPECTED_SCHEMA = "semper-supra.official-catalog-truenas-t6-control/1" +EXPECTED_FOUNDRY_REF = "d2ee19c6805299d0bc55d6ca03903548be026f87" +EXPECTED_CATALOG_COMMIT = "8d93ac087336ab642492209cb707aa1f9556d324" +EXPECTED_CONTROL_ID = "ntfy" +EXPECTED_CATALOG_VERSION = "1.1.21" +EXPECTED_APP_VERSION = "v2.28.0" +EXPECTED_LIB_VERSION = "2.3.4" +EXPECTED_LIB_VERSION_HASH = "2e3a8847308fb2eb0da046018f287c73822c094b5950a10377c3235794ff1242" +REQUIRED_METHODS = { + "system.version", + "core.get_methods", + "app.query", + "app.config", + "app.create", + "app.update", + "app.redeploy", + "app.stop", + "app.start", + "app.delete", + "app.upgrade", +} + + +def canonical_sha256(value: Any) -> str: + raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode() + return hashlib.sha256(raw).hexdigest() + + +def load_control(root: pathlib.Path, foundry_commit: str) -> dict[str, Any]: + path = root / "control.json" + control = json.loads(path.read_text(encoding="utf-8")) + if control.get("schema") != EXPECTED_SCHEMA: + raise RuntimeError("official-catalog control schema drifted") + if foundry_commit != EXPECTED_FOUNDRY_REF or control.get("foundry_ref") != EXPECTED_FOUNDRY_REF: + raise RuntimeError("Foundry source identity drifted") + source = control.get("catalog_source") or {} + if source.get("commit") != EXPECTED_CATALOG_COMMIT: + raise RuntimeError("catalog source commit drifted") + item = control.get("control") or {} + expected = { + "id": EXPECTED_CONTROL_ID, + "catalog_version": EXPECTED_CATALOG_VERSION, + "app_version": EXPECTED_APP_VERSION, + "lib_version": EXPECTED_LIB_VERSION, + "lib_version_hash": EXPECTED_LIB_VERSION_HASH, + } + for key, value in expected.items(): + if item.get(key) != value: + raise RuntimeError(f"control {key} drifted") + if control.get("secrets_captured") is not False: + raise RuntimeError("control unexpectedly captures secrets") + if control.get("universal_qualified") is not False: + raise RuntimeError("unqualified control falsely claims universal qualification") + claimed = control.get("control_sha256") + unsigned = dict(control) + unsigned.pop("control_sha256", None) + if claimed != canonical_sha256(unsigned): + raise RuntimeError("control canonical identity mismatch") + runtime = control.get("runtime") or {} + create = runtime.get("create_payload") or {} + if create.get("custom_app") is not False or create.get("catalog_app") != EXPECTED_CONTROL_ID: + raise RuntimeError("control is not native-catalog create") + if create.get("version") != EXPECTED_CATALOG_VERSION or create.get("train") != "community": + raise RuntimeError("native-catalog create identity drifted") + if (create.get("values") or {}).get("TZ") != "Etc/UTC": + raise RuntimeError("initial config oracle drifted") + update = runtime.get("config_update_payload") or {} + if (update.get("values") or {}).get("TZ") != "Europe/Berlin": + raise RuntimeError("update config oracle drifted") + return control + + +def normalize_system_version(value: Any) -> str: + if not isinstance(value, str) or not value: + raise RuntimeError("system.version did not return a non-empty string") + return value.removeprefix("TrueNAS-") + + +def owned(app: dict[str, Any], control: dict[str, Any]) -> bool: + item = control["control"] + metadata = app.get("metadata") or {} + return ( + app.get("custom_app") is False + and app.get("version") == item["catalog_version"] + and metadata.get("name") == item["id"] + and metadata.get("app_version") == item["app_version"] + and metadata.get("lib_version") == item["lib_version"] + and metadata.get("lib_version_hash") == item["lib_version_hash"] + ) + + +def native_lineage(app: dict[str, Any], control: dict[str, Any]) -> bool: + metadata = app.get("metadata") or {} + return app.get("custom_app") is False and metadata.get("name") == control["control"]["id"] + + +def healthy_running(app: dict[str, Any], control: dict[str, Any]) -> bool: + if not owned(app, control) or app.get("state") != "RUNNING": + return False + workloads = app.get("active_workloads") or {} + details = workloads.get("container_details") or [] + containers = workloads.get("containers") + if not isinstance(containers, int) or containers < 1 or not details: + return False + return all(item.get("state") == "running" for item in details) + + +def plan( + app: dict[str, Any] | None, + config: dict[str, Any] | None, + control: dict[str, Any], + *, + desired_present: bool = True, + desired_running: bool = True, + desired_tz: str = "Etc/UTC", +) -> str: + if app is None: + return "CREATE" if desired_present else "NOOP" + if not owned(app, control): + return "BLOCK_FOREIGN" + if not desired_present: + return "DELETE" + state = app.get("state") + if desired_running and state == "STOPPED": + return "START" + if not desired_running and state == "RUNNING": + return "STOP" + if state not in {"RUNNING", "STOPPED"}: + return "BLOCK_AMBIGUOUS" + if (config or {}).get("TZ") != desired_tz: + return "UPDATE" + return "NOOP" + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--control-dir", type=pathlib.Path, required=True) + p.add_argument("--foundry-commit", required=True) + p.add_argument("--out", required=True) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + p.add_argument("--state-timeout", type=float, default=240.0) + a = p.parse_args() + + started = time.time() + payload: dict[str, Any] = { + "schema": "truenas-official-catalog-t6/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "foundry_commit": a.foundry_commit, + "catalog_commit": EXPECTED_CATALOG_COMMIT, + "control_id": EXPECTED_CONTROL_ID, + "jobs": {}, + "states": [], + "plans": [], + "upgrade": None, + "cleanup": {"attempted": False, "app_absent": False}, + "secret_values_captured": False, + } + ws = None + request_id = 1 + created = False + + try: + control = load_control(a.control_dir, a.foundry_commit) + app_name = control["runtime"]["app_name"] + payload["app_name"] = app_name + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError(f"DDP connection failed: {connected!r}") + + def call(method, params): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + def wait_job(job_id: int, label: str): + deadline = time.monotonic() + a.job_timeout + last = None + while time.monotonic() < deadline: + last = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last: + state = last.get("state") + if state == "SUCCESS": + payload["jobs"][label] = {"id": job_id, "state": state} + return + if state in {"FAILED", "ABORTED"}: + raise RuntimeError( + f"{label} job {state}: {last.get('error') or last.get('exception')}" + ) + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS") + + def query(get: bool = True): + rows = call("app.query", [[["id", "=", app_name]]]) + if not rows: + return None if get else [] + return rows[0] if get else rows + + def config(): + return call("app.config", [app_name]) + + def wait_running(label: str): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = query() + if last and healthy_running(last, control): + payload["states"].append({"label": label, "state": "RUNNING"}) + return last + if last and last.get("state") in {"CRASHED", "ERROR"}: + raise RuntimeError(f"{label} entered failure state: {last!r}") + time.sleep(1) + raise RuntimeError(f"{label} did not become healthy RUNNING: {last!r}") + + def wait_native_running(label: str): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = query() + workloads = (last or {}).get("active_workloads") or {} + details = workloads.get("container_details") or [] + if ( + last + and native_lineage(last, control) + and last.get("state") == "RUNNING" + and isinstance(workloads.get("containers"), int) + and workloads.get("containers") >= 1 + and details + and all(item.get("state") == "running" for item in details) + ): + payload["states"].append({"label": label, "state": "RUNNING"}) + return last + time.sleep(1) + raise RuntimeError(f"{label} did not become native-lineage RUNNING: {last!r}") + + def wait_state(expected: str, label: str): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = query() + if last and last.get("state") == expected and owned(last, control): + payload["states"].append({"label": label, "state": expected}) + return last + time.sleep(1) + raise RuntimeError(f"{label} did not reach {expected}: {last!r}") + + def submit(method: str, params: list[Any], label: str): + job_id = call(method, params) + if not isinstance(job_id, int): + raise RuntimeError(f"{method} did not return job id: {job_id!r}") + wait_job(job_id, label) + + def observe(desired_tz: str, **desired): + app = query() + cfg = config() if app else None + action = plan(app, cfg, control, desired_tz=desired_tz, **desired) + payload["plans"].append({ + "action": action, + "desired_present": desired.get("desired_present", True), + "desired_running": desired.get("desired_running", True), + "desired_tz": desired_tz, + "observed_state": app.get("state") if app else "ABSENT", + "observed_tz": cfg.get("TZ") if isinstance(cfg, dict) else None, + }) + return action, app, cfg + + auth = call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError("authentication did not return SUCCESS") + + system_version = call("system.version", []) + payload["system_version"] = system_version + target_version = normalize_system_version(system_version) + payload["target_version"] = target_version + if target_version not in control.get("target_versions", []): + raise RuntimeError( + f"target version {system_version!r} ({target_version!r}) is outside control matrix" + ) + + methods = call("core.get_methods", []) + method_names = set(methods) if isinstance(methods, dict) else set() + missing = sorted(REQUIRED_METHODS - method_names) + payload["required_methods_present"] = not missing + payload["missing_methods"] = missing + if missing: + raise RuntimeError(f"required native catalog methods missing: {missing}") + + if query() is not None: + raise RuntimeError("refusing adopted official-catalog control state") + + action, _, _ = observe("Etc/UTC") + if action != "CREATE": + raise RuntimeError(f"initial plan was not CREATE: {action}") + submit("app.create", [control["runtime"]["create_payload"]], "create_1") + created = True + first = wait_running("create_1") + first_cfg = config() + if first_cfg.get("TZ") != "Etc/UTC": + raise RuntimeError("initial catalog config did not read back TZ=Etc/UTC") + payload["catalog_identity"] = { + "catalog_version": first.get("version"), + "app_version": (first.get("metadata") or {}).get("app_version"), + "lib_version": (first.get("metadata") or {}).get("lib_version"), + "lib_version_hash": (first.get("metadata") or {}).get("lib_version_hash"), + } + action, _, _ = observe("Etc/UTC") + if action != "NOOP": + raise RuntimeError(f"post-create replan was not NOOP: {action}") + + action, _, _ = observe("Etc/UTC", desired_running=False) + if action != "STOP": + raise RuntimeError(f"stop plan was not STOP: {action}") + submit("app.stop", [app_name], "stop") + wait_state("STOPPED", "stop") + + action, _, _ = observe("Etc/UTC", desired_running=True) + if action != "START": + raise RuntimeError(f"start plan was not START: {action}") + submit("app.start", [app_name], "start") + wait_running("start") + + action, _, _ = observe("Europe/Berlin") + if action != "UPDATE": + raise RuntimeError(f"config plan was not UPDATE: {action}") + submit("app.update", [app_name, control["runtime"]["config_update_payload"]], "update") + wait_running("update") + updated_cfg = config() + if updated_cfg.get("TZ") != "Europe/Berlin": + raise RuntimeError("app.config did not read back TZ=Europe/Berlin") + action, _, _ = observe("Europe/Berlin") + if action != "NOOP": + raise RuntimeError(f"post-update replan was not NOOP: {action}") + + submit("app.redeploy", [app_name], "redeploy") + wait_running("redeploy") + + action, _, _ = observe("Europe/Berlin", desired_present=False) + if action != "DELETE": + raise RuntimeError(f"retain-data delete plan was not DELETE: {action}") + submit("app.delete", [app_name, control["runtime"]["delete_options"]], "delete_retain") + created = False + if query() is not None: + raise RuntimeError("app still exists after retain-data delete") + payload["retain_data_delete"] = {"app_absent": True, **control["runtime"]["delete_options"]} + + action, _, _ = observe("Etc/UTC") + if action != "CREATE": + raise RuntimeError(f"reinstall plan was not CREATE: {action}") + submit("app.create", [control["runtime"]["create_payload"]], "create_2") + created = True + wait_running("create_2") + re_cfg = config() + if re_cfg.get("TZ") != "Etc/UTC": + raise RuntimeError("reinstalled config did not read back initial TZ") + action, _, _ = observe("Etc/UTC") + if action != "NOOP": + raise RuntimeError(f"post-reinstall replan was not NOOP: {action}") + + before_upgrade = query() + if before_upgrade.get("upgrade_available") is True: + before_version = before_upgrade.get("version") + submit("app.upgrade", [app_name, {}], "upgrade") + after_upgrade = wait_native_running("upgrade") + payload["upgrade"] = { + "status": "EXECUTED", + "from_version": before_version, + "to_version": after_upgrade.get("version"), + "discovered_identity": { + "catalog_version": after_upgrade.get("version"), + "app_version": (after_upgrade.get("metadata") or {}).get("app_version"), + "lib_version": (after_upgrade.get("metadata") or {}).get("lib_version"), + "lib_version_hash": (after_upgrade.get("metadata") or {}).get("lib_version_hash"), + }, + } + if after_upgrade.get("version") == before_version: + raise RuntimeError("native app.upgrade did not change catalog version") + else: + payload["upgrade"] = { + "status": "NOT_APPLICABLE", + "observed_upgrade_available": before_upgrade.get("upgrade_available"), + "receipt_required": True, + } + + destructive = { + "remove_images": False, + "remove_ix_volumes": True, + "force_remove_custom_app": False, + } + submit("app.delete", [app_name, destructive], "cleanup_delete") + created = False + payload["cleanup"]["attempted"] = True + payload["cleanup"]["app_absent"] = query() is None + payload["cleanup"]["remove_ix_volumes"] = True + if not payload["cleanup"]["app_absent"]: + raise RuntimeError("official-catalog app remained after final cleanup") + + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "exact native catalog control converged CREATE/NOOP, stop/start, config UPDATE/read-back/NOOP, " + "conditional native upgrade receipt, redeploy, retain-data delete, reinstall/NOOP, and final cleanup" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + if ws is not None and created: + payload["cleanup"]["attempted"] = True + try: + rows = call("app.query", [[["id", "=", payload.get("app_name")]]]) + if rows: + job_id = call("app.delete", [payload["app_name"], { + "remove_images": False, + "remove_ix_volumes": True, + "force_remove_custom_app": False, + }]) + if isinstance(job_id, int): + wait_job(job_id, "failure_cleanup") + payload["cleanup"]["app_absent"] = not call( + "app.query", [[["id", "=", payload.get("app_name")]]] + ) + except Exception as cleanup_exc: + payload["cleanup"]["error"] = f"{type(cleanup_exc).__name__}: {cleanup_exc}" + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 09b5b82d71c54a37051bf8965f9bd77c04e262a5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:56:04 +0200 Subject: [PATCH 190/719] RDTE: add official-catalog T6 tests/test_truenas_t6_official_catalog_control.py --- ...est_truenas_t6_official_catalog_control.py | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 tests/test_truenas_t6_official_catalog_control.py diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py new file mode 100644 index 000000000..53b531b1b --- /dev/null +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +import copy +import importlib.util +import json +import pathlib +import sys +import tempfile +import unittest + +HERE = pathlib.Path(__file__).resolve().parents[1] +SCRIPTS = HERE / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) +SCRIPT = SCRIPTS / "truenas_middleware_official_catalog_t6_probe.py" +SPEC = importlib.util.spec_from_file_location("official_catalog_t6_probe", SCRIPT) +MOD = importlib.util.module_from_spec(SPEC) +assert SPEC.loader +SPEC.loader.exec_module(MOD) + + +def control(): + value = { + "schema": MOD.EXPECTED_SCHEMA, + "foundry_ref": MOD.EXPECTED_FOUNDRY_REF, + "catalog_source": { + "repository": "https://github.com/truenas/apps.git", + "commit": MOD.EXPECTED_CATALOG_COMMIT, + }, + "control": { + "id": MOD.EXPECTED_CONTROL_ID, + "role": "universal-candidate-primary", + "train": "community", + "catalog_version": MOD.EXPECTED_CATALOG_VERSION, + "app_version": MOD.EXPECTED_APP_VERSION, + "lib_version": MOD.EXPECTED_LIB_VERSION, + "lib_version_hash": MOD.EXPECTED_LIB_VERSION_HASH, + "source_path": "ix-dev/community/ntfy/app.yaml", + "source_blob_sha": "187931529f5171bb30f35076e97e16e11cb9fadf", + }, + "runtime": { + "app_name": "rdte-t6-catalog-ntfy", + "create_payload": { + "custom_app": False, + "catalog_app": "ntfy", + "app_name": "rdte-t6-catalog-ntfy", + "train": "community", + "version": "1.1.21", + "values": {"TZ": "Etc/UTC"}, + }, + "config_update_payload": {"values": {"TZ": "Europe/Berlin"}}, + "delete_options": {"remove_images": False, "remove_ix_volumes": False}, + "upgrade": {"not_applicable_requires_receipt": True}, + }, + "target_versions": ["25.04.1", "25.04.2.6", "25.10.7", "26.0.0-BETA.3"], + "runtime_qualified_targets": [], + "universal_qualified": False, + "secrets_captured": False, + } + value["control_sha256"] = MOD.canonical_sha256(value) + return value + + +def app(c, state="RUNNING", tz=None): + item = c["control"] + return { + "id": c["runtime"]["app_name"], + "state": state, + "custom_app": False, + "version": item["catalog_version"], + "metadata": { + "name": item["id"], + "app_version": item["app_version"], + "lib_version": item["lib_version"], + "lib_version_hash": item["lib_version_hash"], + }, + "active_workloads": { + "containers": 1, + "container_details": [{"state": "running", "service_name": "ntfy"}], + }, + } + + +class OfficialCatalogT6ContractTests(unittest.TestCase): + def test_system_version_normalization_matches_control_matrix_tokens(self): + self.assertEqual(MOD.normalize_system_version("TrueNAS-25.04.1"), "25.04.1") + self.assertEqual(MOD.normalize_system_version("TrueNAS-25.04.2.6"), "25.04.2.6") + self.assertEqual(MOD.normalize_system_version("TrueNAS-25.10.7"), "25.10.7") + self.assertEqual(MOD.normalize_system_version("TrueNAS-26.0.0-BETA.3"), "26.0.0-BETA.3") + with self.assertRaisesRegex(RuntimeError, "non-empty string"): + MOD.normalize_system_version(None) + + def test_control_loads_only_exact_native_catalog_identity(self): + c = control() + with tempfile.TemporaryDirectory() as td: + pathlib.Path(td, "control.json").write_text(json.dumps(c), encoding="utf-8") + got = MOD.load_control(pathlib.Path(td), MOD.EXPECTED_FOUNDRY_REF) + self.assertEqual(got["control"]["id"], "ntfy") + self.assertFalse(got["runtime"]["create_payload"]["custom_app"]) + + def test_control_rejects_catalog_fingerprint_drift(self): + c = control() + c["control"]["lib_version_hash"] = "0" * 64 + unsigned = dict(c) + unsigned.pop("control_sha256", None) + c["control_sha256"] = MOD.canonical_sha256(unsigned) + with tempfile.TemporaryDirectory() as td: + pathlib.Path(td, "control.json").write_text(json.dumps(c), encoding="utf-8") + with self.assertRaisesRegex(RuntimeError, "lib_version_hash drifted"): + MOD.load_control(pathlib.Path(td), MOD.EXPECTED_FOUNDRY_REF) + + def test_plan_is_observe_driven_and_converges_noop(self): + c = control() + self.assertEqual(MOD.plan(None, None, c), "CREATE") + a = app(c) + self.assertEqual(MOD.plan(a, {"TZ": "Etc/UTC"}, c), "NOOP") + self.assertEqual(MOD.plan(a, {"TZ": "Europe/Berlin"}, c), "UPDATE") + self.assertEqual(MOD.plan(a, {"TZ": "Etc/UTC"}, c, desired_running=False), "STOP") + a["state"] = "STOPPED" + self.assertEqual(MOD.plan(a, {"TZ": "Etc/UTC"}, c), "START") + self.assertEqual(MOD.plan(a, {"TZ": "Etc/UTC"}, c, desired_present=False), "DELETE") + + def test_foreign_catalog_identity_blocks_mutation(self): + c = control() + a = app(c) + a["metadata"]["lib_version_hash"] = "0" * 64 + self.assertEqual(MOD.plan(a, {"TZ": "Etc/UTC"}, c), "BLOCK_FOREIGN") + + def test_harness_binds_full_native_lifecycle_and_cleanup(self): + text = SCRIPT.read_text(encoding="utf-8") + for method in ( + '"app.create"', '"app.update"', '"app.stop"', '"app.start"', + '"app.redeploy"', '"app.delete"', '"app.upgrade"', + ): + self.assertIn(method, text) + self.assertIn('"NOOP"', text) + self.assertIn('"NOT_APPLICABLE"', text) + self.assertIn('"remove_ix_volumes": True', text) + self.assertNotIn("custom_compose_config", text) + + def test_workflow_and_harness_bind_exact_official_catalog_producer(self): + workflow = (HERE / ".github" / "workflows" / "gha-kvm-system-rdte.yml").read_text(encoding="utf-8") + harness = (SCRIPTS / "gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + self.assertIn("export-official-catalog-t6-control.yml@" + MOD.EXPECTED_FOUNDRY_REF, workflow) + self.assertIn("foundry_ref: " + MOD.EXPECTED_FOUNDRY_REF, workflow) + self.assertIn("name: official-catalog-truenas-t6-control", workflow) + self.assertIn("needs.changes.outputs.truenas_t6_product == 'official-catalog'", workflow) + self.assertIn('foundry_commit="' + MOD.EXPECTED_FOUNDRY_REF + '"', workflow) + self.assertIn('"official-catalog"', harness) + self.assertIn("truenas_middleware_official_catalog_t6_probe.py", harness) + self.assertIn('if [[ "$T6_PRODUCT" != "official-catalog" ]]', harness) + + def test_probe_requires_observed_methods_and_noop_convergence(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('"core.get_methods"', text) + self.assertIn("REQUIRED_METHODS - method_names", text) + self.assertGreaterEqual(text.count('"NOOP"'), 3) + self.assertIn('"BLOCK_FOREIGN"', text) + self.assertIn("refusing adopted official-catalog control state", text) + + +if __name__ == "__main__": + unittest.main() From 9722e7eeb3af7efa7808652547c9e968331def0c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:57:11 +0200 Subject: [PATCH 191/719] RDTE: admit official-catalog T6 selector in regression contract --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 547bb0348..63bf39f17 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "official-catalog"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From 137b9d4ab3a27eb55692bc61421bca094660afb2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:57:14 +0200 Subject: [PATCH 192/719] RDTE: admit official-catalog T6 selector in regression contract --- tests/test_truenas_rdte_target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 8e7bcec3f..4560c1636 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -66,7 +66,7 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","official-catalog"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": From 7047048f673779eba52ea9f5674c42698877ec20 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:55:06 +0200 Subject: [PATCH 193/719] RDTE: separate bootstrap probe from discovered method gate --- scripts/truenas_middleware_official_catalog_t6_probe.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py index 1f1f24f0e..ed8f91d75 100644 --- a/scripts/truenas_middleware_official_catalog_t6_probe.py +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -19,9 +19,10 @@ EXPECTED_APP_VERSION = "v2.28.0" EXPECTED_LIB_VERSION = "2.3.4" EXPECTED_LIB_VERSION_HASH = "2e3a8847308fb2eb0da046018f287c73822c094b5950a10377c3235794ff1242" -REQUIRED_METHODS = { +BOOTSTRAP_METHODS = {"core.get_methods"} + +REQUIRED_DISCOVERED_METHODS = { "system.version", - "core.get_methods", "app.query", "app.config", "app.create", @@ -305,8 +306,9 @@ def observe(desired_tz: str, **desired): ) methods = call("core.get_methods", []) + payload["bootstrap_probes"] = {"core.get_methods": True} method_names = set(methods) if isinstance(methods, dict) else set() - missing = sorted(REQUIRED_METHODS - method_names) + missing = sorted(REQUIRED_DISCOVERED_METHODS - method_names) payload["required_methods_present"] = not missing payload["missing_methods"] = missing if missing: From 063b2dc88b543abbfb92615000507bddc37fe0f8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:55:10 +0200 Subject: [PATCH 194/719] RDTE: regress core.get_methods self-listing falsifier --- tests/test_truenas_t6_official_catalog_control.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py index 53b531b1b..2576ff5cc 100644 --- a/tests/test_truenas_t6_official_catalog_control.py +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -151,8 +151,11 @@ def test_workflow_and_harness_bind_exact_official_catalog_producer(self): def test_probe_requires_observed_methods_and_noop_convergence(self): text = SCRIPT.read_text(encoding="utf-8") - self.assertIn('"core.get_methods"', text) - self.assertIn("REQUIRED_METHODS - method_names", text) + self.assertIn('BOOTSTRAP_METHODS = {"core.get_methods"}', text) + self.assertIn('call("core.get_methods", [])', text) + self.assertIn('payload["bootstrap_probes"] = {"core.get_methods": True}', text) + self.assertIn("REQUIRED_DISCOVERED_METHODS - method_names", text) + self.assertNotIn('"core.get_methods",\n "app.query"', text) self.assertGreaterEqual(text.count('"NOOP"'), 3) self.assertIn('"BLOCK_FOREIGN"', text) self.assertIn("refusing adopted official-catalog control state", text) From f2de58d299f9328d14affea60e73e3fbcc081477 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:57:03 +0200 Subject: [PATCH 195/719] RDTE: rebind official-catalog probe to corrected Foundry head --- scripts/truenas_middleware_official_catalog_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py index ed8f91d75..e3a1498d9 100644 --- a/scripts/truenas_middleware_official_catalog_t6_probe.py +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -12,7 +12,7 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for EXPECTED_SCHEMA = "semper-supra.official-catalog-truenas-t6-control/1" -EXPECTED_FOUNDRY_REF = "d2ee19c6805299d0bc55d6ca03903548be026f87" +EXPECTED_FOUNDRY_REF = "586c8a356392195e1f5783aeb1befc3ce4817844" EXPECTED_CATALOG_COMMIT = "8d93ac087336ab642492209cb707aa1f9556d324" EXPECTED_CONTROL_ID = "ntfy" EXPECTED_CATALOG_VERSION = "1.1.21" From f79aabc6d9e792ce057e484483d4638b6d2801a1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:57:08 +0200 Subject: [PATCH 196/719] RDTE: rebind official-catalog producer to corrected Foundry head --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 114396254..7c4f4e32a 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -177,9 +177,9 @@ jobs: foundry-official-catalog-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@d2ee19c6805299d0bc55d6ca03903548be026f87 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@586c8a356392195e1f5783aeb1befc3ce4817844 with: - foundry_ref: d2ee19c6805299d0bc55d6ca03903548be026f87 + foundry_ref: 586c8a356392195e1f5783aeb1befc3ce4817844 control_id: ntfy permissions: contents: read @@ -353,7 +353,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then - foundry_commit="d2ee19c6805299d0bc55d6ca03903548be026f87" + foundry_commit="586c8a356392195e1f5783aeb1befc3ce4817844" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 70773d7c174d5cc1d8161c87ed24038072d91824 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:57:29 +0200 Subject: [PATCH 197/719] RDTE: stage corrected 25.04.1 official-catalog T6 rerun --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index d29851505..3e9ccdae1 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-official-catalog-001", + "request_id": "tn-25.04.1-t6-official-catalog-002-bootstrap-probe-fix", "version": "25.04.1", "rung": "t6", "product": "official-catalog", "authority_issue": 411, "consumer_authority_issue": 281, - "purpose": "Exercise the exact Foundry-exported native official-catalog ntfy control on accepted TrueNAS 25.04.1 using observe/discover -> plan -> apply -> verify, lifecycle convergence, conditional native upgrade, retain-data delete/reinstall, and final cleanup." + "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after correcting the capability contract so core.get_methods is proven as a bootstrap call and only returned public adapter methods are self-listed/validated." } From 9e959c6cc6f97160c010abb4e791657c7c8c5dd7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:37:04 +0200 Subject: [PATCH 198/719] RDTE: bind corrected upstream-derived catalog fixture --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 7c4f4e32a..23b3ef21e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -177,9 +177,9 @@ jobs: foundry-official-catalog-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@586c8a356392195e1f5783aeb1befc3ce4817844 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@be7c2e7fd81785ee3fce2a26f75d226004bf52e5 with: - foundry_ref: 586c8a356392195e1f5783aeb1befc3ce4817844 + foundry_ref: be7c2e7fd81785ee3fce2a26f75d226004bf52e5 control_id: ntfy permissions: contents: read @@ -353,7 +353,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then - foundry_commit="586c8a356392195e1f5783aeb1befc3ce4817844" + foundry_commit="be7c2e7fd81785ee3fce2a26f75d226004bf52e5" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 2693727a3dc126b0557260552d8d61c64ef92c6b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 20:37:07 +0200 Subject: [PATCH 199/719] RDTE: rerun 25.04.1 catalog T6 with upstream fixture --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3e9ccdae1..a5f36d719 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-official-catalog-002-bootstrap-probe-fix", + "request_id": "tn-25.04.1-t6-official-catalog-003-upstream-runtime-fixture", "version": "25.04.1", "rung": "t6", "product": "official-catalog", "authority_issue": 411, "consumer_authority_issue": 281, - "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after correcting the capability contract so core.get_methods is proven as a bootstrap call and only returned public adapter methods are self-listed/validated." + "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 using the exact upstream-derived basic-values runtime fixture after the prior TZ-only fixture left ntfy.base_url empty and caused the native app to stop." } From 1e59366710988d0b6b5d5f386699ddb055853dc6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:02:42 +0200 Subject: [PATCH 200/719] RDTE: bind native-catalog probe to corrected Foundry producer --- scripts/truenas_middleware_official_catalog_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py index e3a1498d9..af85d9fc8 100644 --- a/scripts/truenas_middleware_official_catalog_t6_probe.py +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -12,7 +12,7 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for EXPECTED_SCHEMA = "semper-supra.official-catalog-truenas-t6-control/1" -EXPECTED_FOUNDRY_REF = "586c8a356392195e1f5783aeb1befc3ce4817844" +EXPECTED_FOUNDRY_REF = "be7c2e7fd81785ee3fce2a26f75d226004bf52e5" EXPECTED_CATALOG_COMMIT = "8d93ac087336ab642492209cb707aa1f9556d324" EXPECTED_CONTROL_ID = "ntfy" EXPECTED_CATALOG_VERSION = "1.1.21" From 037e33a21c64546a410f158fb88a61248ad731cf Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:18:12 +0200 Subject: [PATCH 201/719] RDTE: add version-adaptive container and VM materialization contracts * RDTE: add compute materialization contract (validate-compute-materialization-contract.yml) * RDTE: add compute materialization contract (compute-materialization-targets.json) * RDTE: add compute materialization contract (compute-materialization-contract.md) * RDTE: add compute materialization contract (compute_materialization_contract.py) * RDTE: add compute materialization contract (test_compute_materialization_contract.py) * RDTE: bind compute adapters to exact TrueNAS API schemas * RDTE: require exact API schema fingerprints * RDTE: test API schema fail-closed contract * RDTE: add version-adaptive TrueNAS container C0 probe * RDTE: test TrueNAS container C0 probe contract * RDTE: validate container C0 probe contract * RDTE: admit exact Proxmox discovery candidates * RDTE: fail closed on Proxmox discovery candidate state * RDTE: test discovery-only Proxmox candidates * RDTE: bind VM device schemas and qualification rungs * RDTE: add TrueNAS VM V0 materialization probe * RDTE: test VM V0 materialization contract * RDTE: validate VM V0 probe contract * RDTE: wait for legacy VM update job before readback --- ...idate-compute-materialization-contract.yml | 28 ++ config/compute-materialization-targets.json | 463 ++++++++++++++++++ docs/compute-materialization-contract.md | 36 ++ scripts/compute_materialization_contract.py | 218 +++++++++ scripts/truenas_compute_container_probe.py | 390 +++++++++++++++ scripts/truenas_compute_vm_probe.py | 309 ++++++++++++ .../test_compute_materialization_contract.py | 117 +++++ tests/test_truenas_compute_container_probe.py | 74 +++ tests/test_truenas_compute_vm_probe.py | 49 ++ 9 files changed, 1684 insertions(+) create mode 100644 .github/workflows/validate-compute-materialization-contract.yml create mode 100644 config/compute-materialization-targets.json create mode 100644 docs/compute-materialization-contract.md create mode 100644 scripts/compute_materialization_contract.py create mode 100644 scripts/truenas_compute_container_probe.py create mode 100644 scripts/truenas_compute_vm_probe.py create mode 100644 tests/test_compute_materialization_contract.py create mode 100644 tests/test_truenas_compute_container_probe.py create mode 100644 tests/test_truenas_compute_vm_probe.py diff --git a/.github/workflows/validate-compute-materialization-contract.yml b/.github/workflows/validate-compute-materialization-contract.yml new file mode 100644 index 000000000..5b265d29b --- /dev/null +++ b/.github/workflows/validate-compute-materialization-contract.yml @@ -0,0 +1,28 @@ +name: Validate compute materialization contract + +on: + pull_request: + paths: + - "config/compute-materialization-targets.json" + - "scripts/compute_materialization_contract.py" + - "tests/test_compute_materialization_contract.py" + - "scripts/truenas_compute_container_probe.py" + - "tests/test_truenas_compute_container_probe.py" + - "scripts/truenas_compute_vm_probe.py" + - "tests/test_truenas_compute_vm_probe.py" + - "docs/compute-materialization-contract.md" + - ".github/workflows/validate-compute-materialization-contract.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - name: Contract tests + run: python3 -m unittest -v tests/test_compute_materialization_contract.py tests/test_truenas_compute_container_probe.py tests/test_truenas_compute_vm_probe.py + - name: Validate exact target profiles + run: python3 scripts/compute_materialization_contract.py --validate-only diff --git a/config/compute-materialization-targets.json b/config/compute-materialization-targets.json new file mode 100644 index 000000000..5bd3b06f2 --- /dev/null +++ b/config/compute-materialization-targets.json @@ -0,0 +1,463 @@ +{ + "schema": "semper-supra.compute-materialization-targets/v1", + "authority": "SemperSupra/agent-dispatch-private#426", + "parent_system_authority": "SemperSupra/agent-dispatch-private#396", + "policy": { + "exact_target_required": true, + "observed_capabilities_required_before_apply": true, + "source_profile_required": true, + "cli_fallback_when_product_api_exists": false, + "ambiguous_operation_retry": "reobserve-first", + "workflow_green_not_acceptance": true + }, + "platforms": { + "truenas": { + "targets": [ + { + "version": "25.04.1", + "middleware_commit": "74ab5a2d373be4097dece257d00e1086376333ba", + "management_families": [ + "virt-incus" + ], + "source_fingerprint": { + "virt.instance": "87fc6bb365b3b09820331d90ed2c0138af41ce1b" + }, + "container_adapters": [ + { + "id": "truenas-virt-incus-container", + "instance_type": "CONTAINER", + "required_methods": [ + "virt.instance.query", + "virt.instance.create", + "virt.instance.update", + "virt.instance.delete", + "virt.instance.start", + "virt.instance.stop", + "virt.instance.restart" + ], + "guest_oracle": "virt.instance.get_shell-or-instance-network", + "restart_semantics": "native", + "source_api_family": "v25_04_0" + } + ], + "vm_adapters": [ + { + "id": "truenas-virt-incus-vm", + "instance_type": "VM", + "required_methods": [ + "virt.instance.query", + "virt.instance.create", + "virt.instance.update", + "virt.instance.delete", + "virt.instance.start", + "virt.instance.stop", + "virt.instance.restart" + ], + "guest_oracle": "network-or-vnc/serial-capability", + "restart_semantics": "native", + "hypervisor_family": "incus-qemu", + "source_api_family": "v25_04_0" + } + ], + "api_schema_fingerprint": { + "virt_instance": { + "path": "src/middlewared/middlewared/api/v25_04_0/virt_instance.py", + "blob_sha": "f5caf3f2f98f8ce43fafc19ecccbd1aee1c7c6e0" + } + } + }, + { + "version": "25.04.2.6", + "middleware_commit": "244b717370fe35fb9eefc3096acfbc25f12b57b6", + "management_families": [ + "virt-incus", + "vm-libvirt" + ], + "source_fingerprint": { + "virt.instance": "ac22db500d9d1c6932a4cc109716cd8a3bd46223", + "vm.vms": "993ed915c7058c8e8433d5d5592bb30f24cbfd1c", + "vm.lifecycle": "85c5049c8c90572746ea1acf0a357a88afc88cb5", + "vm.device": "b98466b2247ed2460436c7d9329780f2c88d16ec" + }, + "container_adapters": [ + { + "id": "truenas-virt-incus-container", + "instance_type": "CONTAINER", + "required_methods": [ + "virt.instance.query", + "virt.instance.create", + "virt.instance.update", + "virt.instance.delete", + "virt.instance.start", + "virt.instance.stop", + "virt.instance.restart" + ], + "guest_oracle": "virt.instance.get_shell-or-instance-network", + "restart_semantics": "native", + "source_api_family": "v25_04_2" + } + ], + "vm_adapters": [ + { + "id": "truenas-vm-libvirt", + "preferred": true, + "required_methods": [ + "vm.query", + "vm.create", + "vm.update", + "vm.delete", + "vm.status", + "vm.start", + "vm.stop", + "vm.restart" + ], + "guest_oracle": "network-or-console-capability", + "restart_semantics": "native", + "hypervisor_family": "libvirt-qemu", + "source_api_family": "v25_04_2", + "required_device_methods": [ + "vm.device.query", + "vm.device.create", + "vm.device.update", + "vm.device.delete" + ], + "v0_storage_ownership": "exact-owned-zvol-device" + }, + { + "id": "truenas-virt-incus-vm", + "preferred": false, + "instance_type": "VM", + "required_methods": [ + "virt.instance.query", + "virt.instance.create", + "virt.instance.update", + "virt.instance.delete", + "virt.instance.start", + "virt.instance.stop", + "virt.instance.restart" + ], + "guest_oracle": "network-or-vnc/serial-capability", + "restart_semantics": "native", + "hypervisor_family": "incus-qemu", + "source_api_family": "v25_04_2" + } + ], + "api_schema_fingerprint": { + "virt_instance": { + "path": "src/middlewared/middlewared/api/v25_04_2/virt_instance.py", + "blob_sha": "6076bd3cbe3fb9c1ad4766831e2ffe86e5a1b605" + }, + "vm": { + "path": "src/middlewared/middlewared/api/v25_04_2/vm.py", + "blob_sha": "201721f7e42e663d11f3e26ff6f59ef2bbce0c5f" + }, + "vm_device": { + "path": "src/middlewared/middlewared/api/v25_04_2/vm_device.py", + "blob_sha": "f85240d8a804b5d8347bc9a51d76c9c93c19a813" + } + } + }, + { + "version": "25.10.7", + "middleware_commit": "8ede398839710e56893d88ce85088139d8fab18e", + "management_families": [ + "virt-incus", + "vm-libvirt" + ], + "source_fingerprint": { + "virt.instance": "8f3f21996b4d14385ba17405676704cb9b921bea", + "vm.vms": "2532f53d54d845e983c76e8479027587dec81df7", + "vm.lifecycle": "85c5049c8c90572746ea1acf0a357a88afc88cb5", + "vm.device": "7d9b0098692190015fc178d25d8483927b90d757" + }, + "container_adapters": [ + { + "id": "truenas-virt-incus-container", + "instance_type": "CONTAINER", + "required_methods": [ + "virt.instance.query", + "virt.instance.create", + "virt.instance.update", + "virt.instance.delete", + "virt.instance.start", + "virt.instance.stop", + "virt.instance.restart" + ], + "guest_oracle": "virt.instance.get_shell-or-instance-network", + "restart_semantics": "native", + "source_api_family": "v25_10_0" + } + ], + "vm_adapters": [ + { + "id": "truenas-vm-libvirt", + "preferred": true, + "required_methods": [ + "vm.query", + "vm.create", + "vm.update", + "vm.delete", + "vm.status", + "vm.start", + "vm.stop", + "vm.restart" + ], + "guest_oracle": "network-or-console-capability", + "restart_semantics": "native", + "hypervisor_family": "libvirt-qemu", + "source_api_family": "v25_10_0", + "required_device_methods": [ + "vm.device.query", + "vm.device.create", + "vm.device.update", + "vm.device.delete" + ], + "v0_storage_ownership": "exact-owned-zvol-device" + }, + { + "id": "truenas-virt-incus-vm", + "preferred": false, + "instance_type": "VM", + "required_methods": [ + "virt.instance.query", + "virt.instance.create", + "virt.instance.update", + "virt.instance.delete", + "virt.instance.start", + "virt.instance.stop", + "virt.instance.restart" + ], + "guest_oracle": "network-or-vnc/serial-capability", + "restart_semantics": "native", + "hypervisor_family": "incus-qemu", + "source_api_family": "v25_10_0" + } + ], + "api_schema_fingerprint": { + "virt_instance": { + "path": "src/middlewared/middlewared/api/v25_10_0/virt_instance.py", + "blob_sha": "5a97786a1522de7fb6e84b516127b01b8fd439df" + }, + "vm": { + "path": "src/middlewared/middlewared/api/v25_10_0/vm.py", + "blob_sha": "2dad2a7e851d354bc9d6e1d201781b2809e77894" + }, + "vm_device": { + "path": "src/middlewared/middlewared/api/v25_10_0/vm_device.py", + "blob_sha": "a7b8d406e81618db21fb98f2b57b002a9f2d87b7" + } + } + }, + { + "version": "26.0.0-BETA.3", + "middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", + "management_families": [ + "container-lxc", + "vm-libvirt" + ], + "source_fingerprint": { + "container.container": "54111667692fa7beea67893b50149ef605a8ce22", + "container.lifecycle": "1e29c7c3143d9adbd0cb8e03806a3d5f5ebe29e0", + "container.nsenter": "90b6e245e186441a863a701c0c4714671b691184", + "vm.vms": "43ed4b532b40da02ad885039d954c99a93ea22ed", + "vm.lifecycle": "b313e2eeb0baa0179ab9244fc244eee9b02e02e9", + "vm.device": "b0f313b060ffec4cf08ec2e44c706bafc5121830" + }, + "container_adapters": [ + { + "id": "truenas-container-lxc", + "required_methods": [ + "container.query", + "container.create", + "container.update", + "container.delete", + "container.start", + "container.stop" + ], + "guest_oracle": "supported-shell/namespace-or-instance-network", + "restart_semantics": "compose-stop-start", + "implementation_family": "lxc", + "source_api_family": "v26_0_0" + } + ], + "vm_adapters": [ + { + "id": "truenas-vm-libvirt", + "preferred": true, + "required_methods": [ + "vm.query", + "vm.create", + "vm.update", + "vm.delete", + "vm.status", + "vm.start", + "vm.stop", + "vm.restart" + ], + "guest_oracle": "network-or-console-capability", + "restart_semantics": "native", + "hypervisor_family": "libvirt-qemu", + "source_api_family": "v26_0_0", + "required_device_methods": [ + "vm.device.query", + "vm.device.create", + "vm.device.update", + "vm.device.delete" + ], + "v0_storage_ownership": "exact-owned-zvol-device" + } + ], + "api_schema_fingerprint": { + "container": { + "path": "src/middlewared/middlewared/api/v26_0_0/container.py", + "blob_sha": "6c1d11b9d42d5bfc6dd242d77c017b7fbe1a26aa" + }, + "vm": { + "path": "src/middlewared/middlewared/api/v26_0_0/vm.py", + "blob_sha": "0944382fedc0a8cbcfc55fb8a653a9e5758e88fd" + }, + "vm_device": { + "path": "src/middlewared/middlewared/api/v26_0_0/vm_device.py", + "blob_sha": "67603b6e2048da6235b802f970559b627857efc9" + } + } + } + ] + }, + "proxmox": { + "targets": [ + { + "version": "9.2-1", + "management_families": [ + "pve-rest-lxc", + "pve-rest-qemu" + ], + "source_fingerprint": { + "iso_sha256": "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", + "pve_installer_version": "9.2.5", + "pve_installer_commit": "32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9", + "pve_container_commit": "5eb5574ee9158ac40a5230de2cf18d7d6345709f", + "pve_qemu_commit": "684796e835289dab11af8606fbf7358b93526dd6", + "pve_qemu_submodule_commit": "98b060da3a4f92b2a994ead5b16a87e783baf77c", + "expected_pve_container_package": "6.1.10", + "expected_pve_qemu_kvm_package": "11.0.0-3" + }, + "container_adapters": [ + { + "id": "proxmox-lxc-rest", + "required_api_templates": [ + "POST /nodes/{node}/lxc", + "GET /nodes/{node}/lxc/{vmid}/config", + "POST /nodes/{node}/lxc/{vmid}/status/start", + "POST /nodes/{node}/lxc/{vmid}/status/stop", + "DELETE /nodes/{node}/lxc/{vmid}" + ], + "guest_oracle": "agent-or-console/exec-boundary", + "implementation_family": "pve-lxc" + } + ], + "vm_adapters": [ + { + "id": "proxmox-vm-rest", + "required_api_templates": [ + "POST /nodes/{node}/qemu", + "GET /nodes/{node}/qemu/{vmid}/config", + "POST /nodes/{node}/qemu/{vmid}/status/start", + "POST /nodes/{node}/qemu/{vmid}/status/stop", + "POST /nodes/{node}/qemu/{vmid}/status/reboot", + "DELETE /nodes/{node}/qemu/{vmid}" + ], + "guest_oracle": "agent-or-network/console-boundary", + "hypervisor_family": "pve-qemu-kvm" + } + ], + "compute_materialization_qualified": false + } + ], + "candidate_targets": [ + { + "version": "9.1-1", + "iso_name": "proxmox-ve_9.1-1.iso", + "iso_sha256": "6d8f5afc78c0c66812d7272cde7c8b98be7eb54401ceb045400db05eb5ae6d22", + "vendor_index": "https://enterprise.proxmox.com/iso/", + "state": "source-profile-open", + "claim_boundary": "exact vendor ISO identity only; PVE API/package/source profile and runtime qualification remain open" + }, + { + "version": "9.0-1", + "iso_name": "proxmox-ve_9.0-1.iso", + "iso_sha256": "228f948ae696f2448460443f4b619157cab78ee69802acc0d06761ebd4f51c3e", + "vendor_index": "https://archive.proxmox.com/iso/", + "state": "source-profile-open", + "claim_boundary": "exact vendor ISO identity only; PVE API/package/source profile and runtime qualification remain open" + }, + { + "version": "8.4-1", + "iso_name": "proxmox-ve_8.4-1.iso", + "iso_sha256": "d237d70ca48a9f6eb47f95fd4fd337722c3f69f8106393844d027d28c26523d8", + "vendor_index": "https://enterprise.proxmox.com/iso/", + "state": "source-profile-open", + "claim_boundary": "exact vendor ISO identity only; PVE API/package/source profile and runtime qualification remain open" + } + ] + } + }, + "fixtures": { + "system_container": { + "semantic_id": "linux-system-container-nonce/v1", + "required_lifecycle": [ + "create", + "readback", + "start", + "guest-nonce", + "update-if-supported", + "restart", + "stop", + "delete", + "absence", + "noop" + ] + }, + "firecracker_vm": { + "semantic_id": "linux-firecracker-nested-vm/v1", + "required_lifecycle": [ + "create", + "readback", + "start", + "guest-nonce", + "nested-kvm-observe", + "firecracker-if-kvm", + "restart", + "delete", + "absence" + ], + "nested_kvm": "must-be-observed-not-assumed", + "qualification_rungs": { + "V0": "native VM + owned device CRUD/readback/cleanup; guest boot not claimed", + "V1": "boot minimal Linux guest + deterministic network/agent nonce", + "V2": "observe nested KVM; execute Firecracker microVM only if /dev/kvm is genuinely exposed" + } + }, + "windows11_vm": { + "semantic_id": "windows-11-evaluation-vm/v1", + "required_lifecycle": [ + "create", + "readback", + "start", + "unattended-install", + "guest-oracle", + "restart", + "stop", + "delete", + "absence" + ], + "source_policy": "legal-public-evaluation-or-user-provided-media; no embedded product key", + "firmware_policy": "observe UEFI/q35/TPM/Secure-Boot capabilities; fail closed where required", + "qualification_rungs": { + "V0": "native VM + owned device CRUD/readback/cleanup", + "W1": "Windows 11 evaluation boot/install + bounded guest oracle + restart/stop/delete" + } + } + }, + "claim_boundary": "Source/profile/planning contract only. No container/VM target or fixture is runtime-qualified by this registry." +} diff --git a/docs/compute-materialization-contract.md b/docs/compute-materialization-contract.md new file mode 100644 index 000000000..40a2d03c1 --- /dev/null +++ b/docs/compute-materialization-contract.md @@ -0,0 +1,36 @@ +# Cross-platform compute materialization contract + +Private authority: `SemperSupra/agent-dispatch-private#426`. + +This contract describes how Agent Dispatch will select a native product adapter +for system containers and VMs without pretending the API is stable across +TrueNAS releases or that one Proxmox release proves another. + +## TrueNAS source-discovered transition + +- 25.04.1: `virt.instance` is Incus-backed and owns both CONTAINER and VM instances. +- 25.04.2.6 / 25.10.7: the Incus `virt.instance` family remains, while a + separate `vm.*` libvirt/QEMU family is also present. +- 26.0.0-BETA.3: the legacy `virt` plugin is gone; first-class + `container.*` uses the new LXC-oriented implementation and `vm.*` + remains the VM surface. + +The planner therefore chooses by exact profile **and observed methods**. Version +alone never authorizes apply. + +## Proxmox + +The current real-system harness admits only PVE 9.2-1. The compute contract +defines REST-family LXC and QEMU adapters for that exact profile. Additional PVE +versions must be added as exact source/package/ISO profiles before execution. + +## Fixtures + +Three semantic fixtures are tracked: +1. small Linux system container with lifecycle + nonce; +2. Linux VM with nested-KVM observation and Firecracker only when KVM is + actually available inside the VM; +3. Windows 11 evaluation VM with firmware/security capability checks. + +The registry and planner are cheap/source-level contracts. They perform no +mutation and grant no runtime support claim. diff --git a/scripts/compute_materialization_contract.py b/scripts/compute_materialization_contract.py new file mode 100644 index 000000000..0ed40d3b6 --- /dev/null +++ b/scripts/compute_materialization_contract.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +from typing import Any + + +class ContractError(RuntimeError): + pass + + +def load(path: pathlib.Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise ContractError(f"cannot read {path}: {exc}") from exc + if not isinstance(value, dict): + raise ContractError(f"{path} must contain an object") + return value + + +def platform_targets(registry: dict[str, Any], platform: str) -> list[dict[str, Any]]: + if registry.get("schema") != "semper-supra.compute-materialization-targets/v1": + raise ContractError("unsupported registry schema") + platforms = registry.get("platforms") + if not isinstance(platforms, dict) or platform not in platforms: + raise ContractError(f"unsupported platform: {platform}") + targets = platforms[platform].get("targets") + if not isinstance(targets, list) or not targets: + raise ContractError(f"{platform}: target list missing") + versions = [x.get("version") for x in targets if isinstance(x, dict)] + if len(versions) != len(set(versions)) or any(not isinstance(v, str) or not v for v in versions): + raise ContractError(f"{platform}: target versions must be unique non-empty strings") + return targets + + +def target(registry: dict[str, Any], platform: str, version: str) -> dict[str, Any]: + rows = [x for x in platform_targets(registry, platform) if x["version"] == version] + if len(rows) != 1: + raise ContractError(f"exact target not admitted: {platform}/{version}") + return rows[0] + + +def validate_adapter(adapter: dict[str, Any], platform: str, kind: str) -> None: + if not isinstance(adapter.get("id"), str) or not adapter["id"]: + raise ContractError(f"{platform}/{kind}: adapter id missing") + if platform == "truenas": + methods = adapter.get("required_methods") + if not isinstance(methods, list) or not methods or any(not isinstance(x, str) or "." not in x for x in methods): + raise ContractError(f"{adapter['id']}: required_methods invalid") + elif platform == "proxmox": + templates = adapter.get("required_api_templates") + if not isinstance(templates, list) or not templates or any(not isinstance(x, str) or " /" not in x for x in templates): + raise ContractError(f"{adapter['id']}: required_api_templates invalid") + + +def validate(registry: dict[str, Any]) -> dict[str, Any]: + seen_adapters: set[tuple[str, str, str]] = set() + summary: dict[str, Any] = {} + for platform in ("truenas", "proxmox"): + rows = platform_targets(registry, platform) + ps = [] + for row in rows: + if not isinstance(row.get("source_fingerprint"), dict) or not row["source_fingerprint"]: + raise ContractError(f"{platform}/{row['version']}: source_fingerprint missing") + if platform == "truenas": + api_fp = row.get("api_schema_fingerprint") + if not isinstance(api_fp, dict) or not api_fp: + raise ContractError(f"{platform}/{row['version']}: api_schema_fingerprint missing") + for kind in ("container", "vm"): + adapters = row.get(f"{kind}_adapters") + if not isinstance(adapters, list) or not adapters: + raise ContractError(f"{platform}/{row['version']}: {kind}_adapters missing") + for adapter in adapters: + validate_adapter(adapter, platform, kind) + if platform == "truenas": + family = adapter.get("source_api_family") + if not isinstance(family, str) or not family.startswith("v"): + raise ContractError(f"{adapter['id']}: source_api_family missing") + key = (platform, row["version"], adapter["id"]) + if key in seen_adapters: + raise ContractError(f"duplicate adapter in target: {key}") + seen_adapters.add(key) + ps.append({ + "version": row["version"], + "management_families": row.get("management_families", []), + "container_adapters": [a["id"] for a in row["container_adapters"]], + "vm_adapters": [a["id"] for a in row["vm_adapters"]], + }) + if platform == "proxmox": + candidates = registry["platforms"][platform].get("candidate_targets", []) + if not isinstance(candidates, list): + raise ContractError("proxmox: candidate_targets must be a list") + admitted = {x["version"] for x in rows} + seen_candidates = set() + for candidate in candidates: + if not isinstance(candidate, dict): + raise ContractError("proxmox: candidate target must be an object") + version = candidate.get("version") + digest = candidate.get("iso_sha256") + if not isinstance(version, str) or not version or version in admitted or version in seen_candidates: + raise ContractError(f"proxmox: invalid or duplicate candidate version {version!r}") + if not isinstance(digest, str) or len(digest) != 64: + raise ContractError(f"proxmox/{version}: candidate ISO SHA-256 invalid") + if candidate.get("state") != "source-profile-open": + raise ContractError(f"proxmox/{version}: candidate must remain source-profile-open") + seen_candidates.add(version) + summary["proxmox_candidates"] = [ + {"version": x["version"], "state": x["state"]} for x in candidates + ] + summary[platform] = ps + return { + "schema": "semper-supra.compute-materialization-targets-validation/v1", + "status": "PASS", + "platforms": summary, + "claim_boundary": "profile contract only; no runtime qualification implied", + } + + +def choose_adapter( + registry: dict[str, Any], + platform: str, + version: str, + kind: str, + observed_methods: set[str] | None, +) -> dict[str, Any]: + if kind not in {"container", "vm"}: + raise ContractError(f"unsupported kind: {kind}") + row = target(registry, platform, version) + adapters = row[f"{kind}_adapters"] + + if platform == "proxmox": + if len(adapters) != 1: + raise ContractError(f"{platform}/{version}/{kind}: ambiguous adapter set") + return adapters[0] + + if observed_methods is None: + raise ContractError("TrueNAS adapter selection requires observed method set") + + candidates = [] + for adapter in adapters: + required = set(adapter["required_methods"]) + if required <= observed_methods: + candidates.append(adapter) + if not candidates: + missing = { + a["id"]: sorted(set(a["required_methods"]) - observed_methods) + for a in adapters + } + raise ContractError(f"no capability-satisfied adapter; missing={missing}") + + preferred = [a for a in candidates if a.get("preferred") is True] + if len(preferred) == 1: + return preferred[0] + if len(preferred) > 1: + raise ContractError("multiple preferred adapters satisfied") + if len(candidates) != 1: + raise ContractError(f"multiple capability-satisfied adapters without preference: {[a['id'] for a in candidates]}") + return candidates[0] + + +def semantic_plan(kind: str, adapter: dict[str, Any]) -> list[str]: + if kind == "container": + restart = "restart" if adapter.get("restart_semantics") == "native" else "stop+start" + return ["observe", "create", "readback", "start", "guest-oracle", "update-if-supported", restart, "stop", "delete", "absence", "noop"] + return ["observe", "create", "readback", "start", "guest-oracle", "nested-kvm-observe-if-firecracker", "restart", "stop", "delete", "absence", "noop"] + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--registry", type=pathlib.Path, default=pathlib.Path("config/compute-materialization-targets.json")) + p.add_argument("--platform", choices=["truenas", "proxmox"]) + p.add_argument("--version") + p.add_argument("--kind", choices=["container", "vm"]) + p.add_argument("--observed-methods", type=pathlib.Path) + p.add_argument("--validate-only", action="store_true") + args = p.parse_args() + + try: + registry = load(args.registry) + validation = validate(registry) + if args.validate_only: + print(json.dumps(validation, indent=2, sort_keys=True)) + return 0 + if not (args.platform and args.version and args.kind): + raise ContractError("--platform, --version and --kind are required unless --validate-only") + methods = None + if args.observed_methods: + raw = load(args.observed_methods) + values = raw.get("methods") + if not isinstance(values, list) or any(not isinstance(x, str) for x in values): + raise ContractError("observed methods file must contain string array 'methods'") + methods = set(values) + adapter = choose_adapter(registry, args.platform, args.version, args.kind, methods) + row = target(registry, args.platform, args.version) + result = { + "schema": "semper-supra.compute-materialization-plan/v1", + "platform": args.platform, + "version": args.version, + "kind": args.kind, + "adapter": adapter, + "source_fingerprint": row["source_fingerprint"], + "api_schema_fingerprint": row.get("api_schema_fingerprint"), + "semantic_plan": semantic_plan(args.kind, adapter), + "apply_authorized": False, + "claim_boundary": "plan only; runtime apply requires separate bounded executor/authority", + } + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + except ContractError as exc: + print(json.dumps({"status": "ERROR", "error": str(exc)}, sort_keys=True)) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/truenas_compute_container_probe.py b/scripts/truenas_compute_container_probe.py new file mode 100644 index 000000000..274869ad3 --- /dev/null +++ b/scripts/truenas_compute_container_probe.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import time +from typing import Any + +try: + from scripts.compute_materialization_contract import ContractError, choose_adapter, load, target, validate + from scripts.truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for +except ModuleNotFoundError: + from compute_materialization_contract import ContractError, choose_adapter, load, target, validate + from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +class ProbeError(RuntimeError): + pass + + +def normalize_system_version(value: Any) -> str: + if not isinstance(value, str) or not value: + raise ProbeError(f"invalid system.version: {value!r}") + return value.removeprefix("TrueNAS-") + + +def observed_state(row: dict[str, Any] | None) -> str: + if not row: + return "ABSENT" + value = row.get("status") + if isinstance(value, str): + return value + if isinstance(value, dict) and isinstance(value.get("state"), str): + return value["state"] + return "UNKNOWN" + + +def build_create_payload( + adapter_id: str, + name: str, + nonce: str, + *, + legacy_image: str | None = None, + image_name: str | None = None, + image_version: str | None = None, + pool: str | None = None, +) -> dict[str, Any]: + if adapter_id == "truenas-virt-incus-container": + if not legacy_image: + raise ProbeError("legacy Incus adapter requires --legacy-image") + payload: dict[str, Any] = { + "name": name, + "source_type": "IMAGE", + "image": legacy_image, + "remote": "LINUX_CONTAINERS", + "instance_type": "CONTAINER", + "autostart": False, + "environment": {"RDTE_NONCE": nonce, "RDTE_GENERATION": "1"}, + "memory": 256 * 1024 * 1024, + } + if pool: + payload["storage_pool"] = pool + return payload + if adapter_id == "truenas-container-lxc": + if not image_name or not image_version: + raise ProbeError("LXC adapter requires --image-name and --image-version") + payload = { + "name": name, + "description": "Agent Dispatch disposable C0 compute-materialization fixture", + "autostart": False, + "time": "UTC", + "initenv": {"RDTE_NONCE": nonce, "RDTE_GENERATION": "1"}, + "idmap": {"type": "DEFAULT"}, + "capabilities_policy": "DEFAULT", + "image": {"name": image_name, "version": image_version}, + } + if pool: + payload["pool"] = pool + return payload + raise ProbeError(f"unsupported container adapter: {adapter_id}") + + +def update_payload(adapter_id: str, nonce: str) -> dict[str, Any]: + if adapter_id == "truenas-virt-incus-container": + return {"environment": {"RDTE_NONCE": nonce, "RDTE_GENERATION": "2"}} + if adapter_id == "truenas-container-lxc": + return { + "description": "Agent Dispatch disposable C0 compute-materialization fixture generation 2", + "initenv": {"RDTE_NONCE": nonce, "RDTE_GENERATION": "2"}, + } + raise ProbeError(f"unsupported container adapter: {adapter_id}") + + +def method_shapes(adapter_id: str, name: str, row_id: Any | None = None) -> dict[str, tuple[str, list[Any], bool]]: + if adapter_id == "truenas-virt-incus-container": + return { + "query": ("virt.instance.query", [[["id", "=", name]]], False), + "create": ("virt.instance.create", [], True), + "update": ("virt.instance.update", [name], True), + "start": ("virt.instance.start", [name], True), + "stop": ("virt.instance.stop", [name, {"timeout": 30, "force": True}], True), + "restart": ("virt.instance.restart", [name, {"timeout": 30, "force": True}], True), + "delete": ("virt.instance.delete", [name], True), + } + if adapter_id == "truenas-container-lxc": + if row_id is None: + query = ("container.query", [[["name", "=", name]]], False) + return {"query": query} + return { + "query": ("container.query", [[["name", "=", name]]], False), + "create": ("container.create", [], True), + "update": ("container.update", [row_id], False), + "start": ("container.start", [row_id], False), + "stop": ("container.stop", [row_id, {"timeout": 30, "force_after_timeout": True}], True), + "delete": ("container.delete", [row_id], False), + } + raise ProbeError(f"unsupported container adapter: {adapter_id}") + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--registry", type=pathlib.Path, default=pathlib.Path("config/compute-materialization-targets.json")) + p.add_argument("--target-version", required=True) + p.add_argument("--name", default="rdte-compute-container-c0") + p.add_argument("--nonce", default="agent-dispatch-container-c0-v1") + p.add_argument("--legacy-image") + p.add_argument("--image-name") + p.add_argument("--image-version") + p.add_argument("--pool") + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int) + p.add_argument("--tls", action="store_true") + p.add_argument("--password-file", type=pathlib.Path) + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=600.0) + p.add_argument("--state-timeout", type=float, default=180.0) + p.add_argument("--apply", action="store_true") + p.add_argument("--out", type=pathlib.Path) + a = p.parse_args() + + receipt: dict[str, Any] = { + "schema": "truenas-compute-container-c0/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "c0_oracle_satisfied": False, + "c1_guest_oracle_satisfied": False, + "target_version_requested": a.target_version, + "name": a.name, + "nonce": a.nonce, + "states": [], + "jobs": {}, + "cleanup": {"attempted": False, "absent": False}, + "claim_boundary": "C0 native API lifecycle only; guest-level C1 nonce/service oracle remains separately required", + } + ws = None + call = None + adapter_id = None + row_id = None + + def emit() -> int: + payload = json.dumps(receipt, indent=2, sort_keys=True) + "\n" + if a.out: + a.out.write_text(payload, encoding="utf-8") + print(payload, end="") + return 0 if receipt.get("oracleSatisfied") else 2 + + try: + registry = load(a.registry) + validate(registry) + row = target(registry, "truenas", a.target_version) + receipt["source_fingerprint"] = row["source_fingerprint"] + receipt["api_schema_fingerprint"] = row["api_schema_fingerprint"] + + if not a.apply: + receipt.update({ + "classification": "SUPPORTED", + "oracleSatisfied": True, + "phase": "plan-only", + "detail": "C0 probe contract validated; apply was not requested", + "apply_authorized": False, + }) + return emit() + + if not a.port or not a.password_file: + raise ProbeError("--apply requires --port and --password-file") + password = a.password_file.read_text(encoding="utf-8").strip() + if not password: + raise ProbeError("password file was empty") + + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise ProbeError(f"DDP connection failed: {connected!r}") + request_id = 1 + + def _call(method: str, params: list[Any]): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + call = _call + + auth = call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise ProbeError("authentication did not return SUCCESS") + + system_version = call("system.version", []) + receipt["system_version"] = system_version + exact = normalize_system_version(system_version) + if exact != a.target_version: + raise ProbeError(f"target mismatch: expected {a.target_version}, observed {exact}") + + method_map = call("core.get_methods", []) + if not isinstance(method_map, dict): + raise ProbeError("core.get_methods did not return a method map") + adapter = choose_adapter(registry, "truenas", exact, "container", set(method_map)) + adapter_id = adapter["id"] + receipt["adapter"] = adapter + + create_payload = build_create_payload( + adapter_id, a.name, a.nonce, + legacy_image=a.legacy_image, image_name=a.image_name, + image_version=a.image_version, pool=a.pool, + ) + receipt["desired_create"] = create_payload + + def query_one(): + shapes = method_shapes(adapter_id, a.name, row_id) + method, params, _ = shapes["query"] + rows = call(method, params) + return rows[0] if isinstance(rows, list) and rows else None + + def wait_job(job_id: Any, label: str): + if isinstance(job_id, bool) or not isinstance(job_id, int): + raise ProbeError(f"{label} did not return job id: {job_id!r}") + deadline = time.monotonic() + a.job_timeout + while time.monotonic() < deadline: + job = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if job: + state = job.get("state") + if state == "SUCCESS": + receipt["jobs"][label] = {"id": job_id, "state": state} + return + if state in {"FAILED", "ABORTED"}: + raise ProbeError(f"{label} job {state}: {job.get('error') or job.get('exception')}") + time.sleep(1) + raise ProbeError(f"{label} job timeout") + + def invoke(label: str, params_tail: list[Any] | None = None): + shapes = method_shapes(adapter_id, a.name, row_id) + method, base, job_backed = shapes[label] + params = list(base) + if params_tail: + params.extend(params_tail) + result = call(method, params) + if job_backed: + wait_job(result, label) + return result + + def wait_state(expected: str, label: str): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = query_one() + if observed_state(last) == expected: + receipt["states"].append({"label": label, "state": expected}) + return last + time.sleep(1) + raise ProbeError(f"{label} did not reach {expected}: {last!r}") + + if query_one() is not None: + raise ProbeError(f"preexisting instance {a.name!r} blocks ownership-safe apply") + + shapes = method_shapes(adapter_id, a.name) + create_method = shapes.get("create", (adapter["required_methods"][1], [], True))[0] + create_result = call(create_method, [create_payload]) + wait_job(create_result, "create") + current = query_one() + if not current: + raise ProbeError("created container not observable") + if adapter_id == "truenas-container-lxc": + row_id = current.get("id") + if not isinstance(row_id, int): + raise ProbeError(f"26.x container id missing: {row_id!r}") + receipt["states"].append({"label": "created", "state": observed_state(current)}) + + invoke("start") + wait_state("RUNNING", "start") + + invoke("stop") + wait_state("STOPPED", "stop-before-update") + + invoke("update", [update_payload(adapter_id, a.nonce)]) + updated = query_one() + if adapter_id == "truenas-virt-incus-container": + if (updated or {}).get("environment", {}).get("RDTE_GENERATION") != "2": + raise ProbeError("legacy environment update did not read back generation 2") + else: + if (updated or {}).get("initenv", {}).get("RDTE_GENERATION") != "2": + raise ProbeError("LXC initenv update did not read back generation 2") + receipt["update_readback"] = "RDTE_GENERATION=2" + + invoke("start") + wait_state("RUNNING", "post-update-start") + + if adapter_id == "truenas-virt-incus-container": + invoke("restart") + wait_state("RUNNING", "restart") + else: + invoke("stop") + wait_state("STOPPED", "restart-stop") + invoke("start") + wait_state("RUNNING", "restart-start") + + invoke("stop") + wait_state("STOPPED", "final-stop") + invoke("delete") + deadline = time.monotonic() + a.state_timeout + while time.monotonic() < deadline and query_one() is not None: + time.sleep(1) + receipt["cleanup"] = {"attempted": True, "absent": query_one() is None} + if not receipt["cleanup"]["absent"]: + raise ProbeError("container remained after delete") + + receipt.update({ + "classification": "SUPPORTED", + "oracleSatisfied": True, + "c0_oracle_satisfied": True, + "phase": "c0-api-lifecycle", + "detail": "native TrueNAS container API lifecycle, mutation readback and zero-residue cleanup passed", + }) + return emit() + except (ProbeError, ContractError, OSError, RuntimeError, ValueError) as exc: + receipt["detail"] = f"{type(exc).__name__}: {exc}" + if a.apply and call is not None and adapter_id: + receipt["cleanup"]["attempted"] = True + try: + current = None + shapes = method_shapes(adapter_id, a.name, row_id) + qmethod, qparams, _ = shapes["query"] + rows = call(qmethod, qparams) + current = rows[0] if isinstance(rows, list) and rows else None + if current: + if observed_state(current) == "RUNNING": + method, params, job_backed = shapes["stop"] + result = call(method, params) + if job_backed and isinstance(result, int) and not isinstance(result, bool): + # Bounded cleanup does not wait forever; the main evidence already records failure. + deadline = time.monotonic() + min(a.job_timeout, 60) + while time.monotonic() < deadline: + job = call("core.get_jobs", [[["id", "=", result]], {"get": True}]) + if job and job.get("state") in {"SUCCESS", "FAILED", "ABORTED"}: + break + time.sleep(1) + rows = call(qmethod, qparams) + current = rows[0] if isinstance(rows, list) and rows else None + if current: + if adapter_id == "truenas-container-lxc": + row_id = current.get("id") + shapes = method_shapes(adapter_id, a.name, row_id) + method, params, job_backed = shapes["delete"] + result = call(method, params) + if job_backed and isinstance(result, int) and not isinstance(result, bool): + deadline = time.monotonic() + min(a.job_timeout, 60) + while time.monotonic() < deadline: + job = call("core.get_jobs", [[["id", "=", result]], {"get": True}]) + if job and job.get("state") in {"SUCCESS", "FAILED", "ABORTED"}: + break + time.sleep(1) + rows = call(qmethod, qparams) + receipt["cleanup"]["absent"] = not rows + except Exception as cleanup_exc: + receipt["cleanup"]["error"] = f"{type(cleanup_exc).__name__}: {cleanup_exc}" + return emit() + finally: + if ws is not None: + try: + ws.close() + except Exception: + pass + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/truenas_compute_vm_probe.py b/scripts/truenas_compute_vm_probe.py new file mode 100644 index 000000000..cd3a3d9ae --- /dev/null +++ b/scripts/truenas_compute_vm_probe.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import time +from typing import Any + +try: + from scripts.compute_materialization_contract import ContractError, choose_adapter, load, target, validate + from scripts.truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for +except ModuleNotFoundError: + from compute_materialization_contract import ContractError, choose_adapter, load, target, validate + from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +class ProbeError(RuntimeError): + pass + + +def normalize_system_version(value: Any) -> str: + if not isinstance(value, str) or not value: + raise ProbeError(f"invalid system.version: {value!r}") + return value.removeprefix("TrueNAS-") + + +def native_vm_create_payload(name: str, generation: int = 1) -> dict[str, Any]: + return { + "name": name, + "description": f"Agent Dispatch disposable VM V0 fixture generation {generation}", + "vcpus": 1, + "cores": 1, + "threads": 1, + "memory": 512, + "cpu_mode": "HOST-MODEL", + "autostart": False, + "ensure_display_device": False, + "time": "UTC", + "bootloader": "UEFI", + "trusted_platform_module": False, + "hyperv_enlightenments": False, + "enable_secure_boot": False, + "shutdown_timeout": 30, + } + + +def legacy_vm_create_payload(name: str, image: str, generation: int = 1) -> dict[str, Any]: + if not image: + raise ProbeError("legacy Incus VM adapter requires --legacy-image") + return { + "name": name, + "source_type": "IMAGE", + "image": image, + "remote": "LINUX_CONTAINERS", + "instance_type": "VM", + "autostart": False, + "environment": {"RDTE_GENERATION": str(generation)}, + "memory": 512 * 1024 * 1024, + "root_disk_size": 5, + "root_disk_io_bus": "VIRTIO-BLK", + "secure_boot": False, + "enable_vnc": False, + } + + +def owned_zvol_device(vm_id: int, zvol_name: str, size_bytes: int) -> dict[str, Any]: + if not zvol_name or "/" not in zvol_name: + raise ProbeError("owned ZVOL name must include a pool/dataset prefix") + return { + "vm": vm_id, + "attributes": { + "dtype": "DISK", + "path": None, + "type": "VIRTIO", + "create_zvol": True, + "zvol_name": zvol_name, + "zvol_volsize": size_bytes, + "boot": False, + }, + "order": 1000, + } + + +def main() -> int: + p=argparse.ArgumentParser() + p.add_argument("--registry",type=pathlib.Path,default=pathlib.Path("config/compute-materialization-targets.json")) + p.add_argument("--target-version",required=True) + p.add_argument("--name",default="rdte-compute-vm-v0") + p.add_argument("--legacy-image") + p.add_argument("--zvol-name") + p.add_argument("--zvol-size-bytes",type=int,default=2*1024*1024*1024) + p.add_argument("--host",default="127.0.0.1") + p.add_argument("--port",type=int) + p.add_argument("--tls",action="store_true") + p.add_argument("--password-file",type=pathlib.Path) + p.add_argument("--timeout",type=float,default=8.0) + p.add_argument("--job-timeout",type=float,default=600.0) + p.add_argument("--apply",action="store_true") + p.add_argument("--out",type=pathlib.Path) + a=p.parse_args() + + receipt={ + "schema":"truenas-compute-vm-v0/v1", + "classification":"ORACLE_FAILURE", + "oracleSatisfied":False, + "v0_oracle_satisfied":False, + "v1_guest_oracle_satisfied":False, + "v2_firecracker_oracle_satisfied":False, + "windows_w1_oracle_satisfied":False, + "target_version_requested":a.target_version, + "name":a.name, + "cleanup":{"attempted":False,"vm_absent":False,"device_absent":False}, + "claim_boundary":"V0 native VM/device lifecycle only; Linux guest, Firecracker and Windows 11 remain separate rungs", + } + ws=None + call=None + vm_id=None + device_id=None + adapter_id=None + + def emit()->int: + payload=json.dumps(receipt,indent=2,sort_keys=True)+"\n" + if a.out: + a.out.write_text(payload,encoding="utf-8") + print(payload,end="") + return 0 if receipt.get("oracleSatisfied") else 2 + + try: + registry=load(a.registry) + validate(registry) + row=target(registry,"truenas",a.target_version) + receipt["source_fingerprint"]=row["source_fingerprint"] + receipt["api_schema_fingerprint"]=row["api_schema_fingerprint"] + + if not a.apply: + receipt.update({ + "classification":"SUPPORTED", + "oracleSatisfied":True, + "phase":"plan-only", + "detail":"V0 probe contract validated; apply was not requested", + "apply_authorized":False, + }) + return emit() + + if not a.port or not a.password_file: + raise ProbeError("--apply requires --port and --password-file") + password=a.password_file.read_text(encoding="utf-8").strip() + if not password: + raise ProbeError("password file was empty") + + ws=WebSocket(a.host,a.port,timeout=a.timeout,tls=a.tls) + ws.send_json({"msg":"connect","version":"1","support":["1"]}) + connected=wait_for(ws,lambda m:m.get("msg") in {"connected","failed"}) + if connected.get("msg")!="connected": + raise ProbeError(f"DDP connection failed: {connected!r}") + request_id=1 + + def _call(method:str,params:list[Any]): + nonlocal request_id + result=ddp_call(ws,str(request_id),method,params) + request_id+=1 + return result + call=_call + + auth=call("auth.login_ex",[{ + "mechanism":"PASSWORD_PLAIN","username":"truenas_admin","password":password, + }]) + if not isinstance(auth,dict) or auth.get("response_type")!="SUCCESS": + raise ProbeError("authentication did not return SUCCESS") + + observed=normalize_system_version(call("system.version",[])) + receipt["target_version_observed"]=observed + if observed!=a.target_version: + raise ProbeError(f"target mismatch: expected {a.target_version}, observed {observed}") + + methods=call("core.get_methods",[]) + if not isinstance(methods,dict): + raise ProbeError("core.get_methods did not return method map") + adapter=choose_adapter(registry,"truenas",observed,"vm",set(methods)) + adapter_id=adapter["id"] + receipt["adapter"]=adapter + required_device=set(adapter.get("required_device_methods",[])) + missing_device=sorted(required_device-set(methods)) + if missing_device: + raise ProbeError(f"required VM device methods missing: {missing_device}") + + if adapter_id=="truenas-virt-incus-vm": + rows=call("virt.instance.query",[[["id","=",a.name]]]) + if rows: + raise ProbeError(f"preexisting VM {a.name!r} blocks ownership-safe apply") + payload=legacy_vm_create_payload(a.name,a.legacy_image,1) + receipt["desired_create"]=payload + job=call("virt.instance.create",[payload]) + if not isinstance(job,int) or isinstance(job,bool): + raise ProbeError(f"virt.instance.create did not return job id: {job!r}") + deadline=time.monotonic()+a.job_timeout + while time.monotonic() Date: Thu, 1 Oct 2026 21:44:50 +0200 Subject: [PATCH 202/719] RDTE: bind Windows 11 and Firecracker guest capability profiles * RDTE: add compute guest capability profiles * RDTE: validate compute guest profiles * RDTE: test guest profile fail-closed boundaries * RDTE: validate guest profile contract in CI --- ...alidate-compute-guest-profile-contract.yml | 18 +++ config/compute-guest-profiles.json | 150 ++++++++++++++++++ scripts/compute_guest_profile_contract.py | 83 ++++++++++ tests/test_compute_guest_profile_contract.py | 73 +++++++++ 4 files changed, 324 insertions(+) create mode 100644 .github/workflows/validate-compute-guest-profile-contract.yml create mode 100644 config/compute-guest-profiles.json create mode 100644 scripts/compute_guest_profile_contract.py create mode 100644 tests/test_compute_guest_profile_contract.py diff --git a/.github/workflows/validate-compute-guest-profile-contract.yml b/.github/workflows/validate-compute-guest-profile-contract.yml new file mode 100644 index 000000000..619c9429f --- /dev/null +++ b/.github/workflows/validate-compute-guest-profile-contract.yml @@ -0,0 +1,18 @@ +name: Validate compute guest profile contract +on: + pull_request: + paths: + - "config/compute-guest-profiles.json" + - "scripts/compute_guest_profile_contract.py" + - "tests/test_compute_guest_profile_contract.py" + - ".github/workflows/validate-compute-guest-profile-contract.yml" + workflow_dispatch: +permissions: + contents: read +jobs: + validate: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py + - run: python3 scripts/compute_guest_profile_contract.py diff --git a/config/compute-guest-profiles.json b/config/compute-guest-profiles.json new file mode 100644 index 000000000..8f6900053 --- /dev/null +++ b/config/compute-guest-profiles.json @@ -0,0 +1,150 @@ +{ + "schema": "semper-supra.compute-guest-profiles/v1", + "authority": "SemperSupra/agent-dispatch-private#426", + "dependencies": { + "firecracker": "SemperSupra/agent-dispatch-private#277", + "windows_embodiment": "mark-e-deyoung/windows-utilities#26" + }, + "policy": { + "source_capability_is_not_runtime_qualification": true, + "nested_kvm_must_be_observed_in_guest": true, + "private_hypervisor_escape_hatches_prohibited": true, + "windows_media_must_be_legal_public_evaluation_or_user_supplied": true, + "windows_product_key_must_not_be_embedded": true, + "guest_oracle_required_before_backend_admission": true + }, + "truenas": { + "25.04.1": { + "vm_adapter": "truenas-virt-incus-vm", + "source_api_family": "v25_04_0", + "windows11": { + "source_status": "CANDIDATE", + "capabilities": { + "secure_boot": { + "surface": "virt.instance.create.secure_boot", + "source_proven": true + }, + "tpm": { + "surface": "virt device dev_type=TPM", + "source_proven": true + }, + "uefi_q35": { + "surface": "incus VM firmware/machine implementation", + "source_proven": false + } + }, + "runtime_status": "OPEN" + }, + "firecracker": { + "source_status": "RUNTIME_OBSERVE_ONLY", + "cpu_passthrough_control": null, + "nested_kvm_requirement": "observe /dev/kvm inside guest; do not inject raw.qemu", + "runtime_status": "OPEN" + } + }, + "25.04.2.6": { + "vm_adapter": "truenas-vm-libvirt", + "source_api_family": "v25_04_2", + "windows11": { + "source_status": "CANDIDATE", + "capabilities": { + "secure_boot": { + "surface": "vm.create.enable_secure_boot", + "source_proven": true + }, + "tpm": { + "surface": "vm.create.trusted_platform_module", + "source_proven": true + }, + "uefi_q35": { + "surface": "vm.create bootloader=UEFI + machine_type=q35 validation", + "source_proven": true + } + }, + "runtime_status": "OPEN" + }, + "firecracker": { + "source_status": "CANDIDATE", + "cpu_passthrough_control": "vm.create.cpu_mode=HOST-PASSTHROUGH", + "nested_kvm_requirement": "observe /dev/kvm inside guest before invoking Firecracker", + "runtime_status": "OPEN" + } + }, + "25.10.7": { + "vm_adapter": "truenas-vm-libvirt", + "source_api_family": "v25_10_0", + "windows11": { + "source_status": "CANDIDATE", + "capabilities": { + "secure_boot": { + "surface": "vm.create.enable_secure_boot", + "source_proven": true + }, + "tpm": { + "surface": "vm.create.trusted_platform_module", + "source_proven": true + }, + "uefi_q35": { + "surface": "vm.create bootloader=UEFI + machine_type=q35 validation", + "source_proven": true + } + }, + "runtime_status": "OPEN" + }, + "firecracker": { + "source_status": "CANDIDATE", + "cpu_passthrough_control": "vm.create.cpu_mode=HOST-PASSTHROUGH", + "nested_kvm_requirement": "observe /dev/kvm inside guest before invoking Firecracker", + "runtime_status": "OPEN" + } + }, + "26.0.0-BETA.3": { + "vm_adapter": "truenas-vm-libvirt", + "source_api_family": "v26_0_0", + "windows11": { + "source_status": "CANDIDATE", + "capabilities": { + "secure_boot": { + "surface": "vm.create.enable_secure_boot", + "source_proven": true + }, + "tpm": { + "surface": "vm.create.trusted_platform_module", + "source_proven": true + }, + "uefi_q35": { + "surface": "vm.create bootloader=UEFI + machine_type=q35 validation", + "source_proven": true + } + }, + "runtime_status": "OPEN" + }, + "firecracker": { + "source_status": "CANDIDATE", + "cpu_passthrough_control": "vm.create.cpu_mode=HOST-PASSTHROUGH", + "nested_kvm_requirement": "observe /dev/kvm inside guest before invoking Firecracker", + "runtime_status": "OPEN" + } + } + }, + "proxmox": { + "9.2-1": { + "vm_adapter": "proxmox-vm-rest", + "windows11": { + "source_status": "PROFILE_PENDING_REST_PAYLOAD_BINDING", + "runtime_status": "OPEN" + }, + "firecracker": { + "source_status": "PROFILE_PENDING_REST_PAYLOAD_BINDING", + "runtime_status": "OPEN" + } + } + }, + "rungs": { + "V0": "native VM + exact-owned device CRUD/readback/cleanup", + "V1": "boot pinned minimal Linux guest + external deterministic guest nonce", + "V2": "observe nested KVM and, iff present, execute Firecracker portable contract from #277", + "W1": "Windows 11 evaluation/user media install + firmware/TPM/Secure Boot readback + bounded guest oracle + lifecycle/cleanup", + "WB": "WinBot backend candidacy only after W1; admission decision remains windows-utilities#26" + } +} diff --git a/scripts/compute_guest_profile_contract.py b/scripts/compute_guest_profile_contract.py new file mode 100644 index 000000000..03bd6b9d0 --- /dev/null +++ b/scripts/compute_guest_profile_contract.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +from __future__ import annotations +import argparse, json, pathlib +from typing import Any + +class ProfileError(RuntimeError): + pass + +def load(path:pathlib.Path)->dict[str,Any]: + value=json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value,dict): + raise ProfileError("profile must be object") + return value + +def validate(p:dict[str,Any])->dict[str,Any]: + if p.get("schema")!="semper-supra.compute-guest-profiles/v1": + raise ProfileError("unsupported schema") + if p.get("dependencies",{}).get("firecracker")!="SemperSupra/agent-dispatch-private#277": + raise ProfileError("Firecracker authority must remain #277") + if p.get("dependencies",{}).get("windows_embodiment")!="mark-e-deyoung/windows-utilities#26": + raise ProfileError("Windows embodiment authority must remain windows-utilities#26") + policy=p.get("policy") + if not isinstance(policy,dict) or any(policy.get(k) is not True for k in ( + "source_capability_is_not_runtime_qualification", + "nested_kvm_must_be_observed_in_guest", + "private_hypervisor_escape_hatches_prohibited", + "windows_media_must_be_legal_public_evaluation_or_user_supplied", + "windows_product_key_must_not_be_embedded", + "guest_oracle_required_before_backend_admission", + )): + raise ProfileError("safety/claim policy incomplete") + tn=p.get("truenas") + expected={"25.04.1","25.04.2.6","25.10.7","26.0.0-BETA.3"} + if not isinstance(tn,dict) or set(tn)!=expected: + raise ProfileError("TrueNAS guest profile set must cover exact admitted matrix") + for version,row in tn.items(): + if row.get("windows11",{}).get("runtime_status")!="OPEN": + raise ProfileError(f"{version}: Windows runtime must remain OPEN before receipt") + if row.get("firecracker",{}).get("runtime_status")!="OPEN": + raise ProfileError(f"{version}: Firecracker runtime must remain OPEN before receipt") + if version=="25.04.1": + fc=row["firecracker"] + if fc.get("source_status")!="RUNTIME_OBSERVE_ONLY" or fc.get("cpu_passthrough_control") is not None: + raise ProfileError("25.04.1 Firecracker must remain observe-only without private raw.qemu") + else: + if row["firecracker"].get("cpu_passthrough_control")!="vm.create.cpu_mode=HOST-PASSTHROUGH": + raise ProfileError(f"{version}: native Firecracker candidate must bind HOST-PASSTHROUGH") + win=row["windows11"] + if win.get("source_status")!="CANDIDATE": + raise ProfileError(f"{version}: Windows source status must remain CANDIDATE") + caps=win.get("capabilities") + if not isinstance(caps,dict) or set(caps)!={"secure_boot","tpm","uefi_q35"}: + raise ProfileError(f"{version}: Windows capability set incomplete") + prox=p.get("proxmox") + if not isinstance(prox,dict) or set(prox)!={"9.2-1"}: + raise ProfileError("only admitted Proxmox 9.2-1 may have guest profile") + for kind in ("windows11","firecracker"): + if prox["9.2-1"][kind].get("runtime_status")!="OPEN": + raise ProfileError(f"Proxmox {kind} runtime must remain OPEN") + rungs=p.get("rungs") + if not isinstance(rungs,dict) or list(rungs)!=["V0","V1","V2","W1","WB"]: + raise ProfileError("guest rung order/coverage invalid") + return { + "schema":"semper-supra.compute-guest-profiles-validation/v1", + "status":"PASS", + "truenas_versions":sorted(tn), + "proxmox_versions":sorted(prox), + "runtime_claims":"OPEN", + } + +def main()->int: + ap=argparse.ArgumentParser() + ap.add_argument("--profile",type=pathlib.Path,default=pathlib.Path("config/compute-guest-profiles.json")) + a=ap.parse_args() + try: + result=validate(load(a.profile)) + except (ProfileError,OSError,json.JSONDecodeError) as exc: + print(json.dumps({"status":"ERROR","error":str(exc)},sort_keys=True)) + return 2 + print(json.dumps(result,indent=2,sort_keys=True)) + return 0 +if __name__=="__main__": + raise SystemExit(main()) diff --git a/tests/test_compute_guest_profile_contract.py b/tests/test_compute_guest_profile_contract.py new file mode 100644 index 000000000..0b07e1e77 --- /dev/null +++ b/tests/test_compute_guest_profile_contract.py @@ -0,0 +1,73 @@ +import copy, unittest +from scripts.compute_guest_profile_contract import ProfileError, validate + +def fixture(): + return { + "schema":"semper-supra.compute-guest-profiles/v1", + "dependencies":{ + "firecracker":"SemperSupra/agent-dispatch-private#277", + "windows_embodiment":"mark-e-deyoung/windows-utilities#26", + }, + "policy":{ + "source_capability_is_not_runtime_qualification":True, + "nested_kvm_must_be_observed_in_guest":True, + "private_hypervisor_escape_hatches_prohibited":True, + "windows_media_must_be_legal_public_evaluation_or_user_supplied":True, + "windows_product_key_must_not_be_embedded":True, + "guest_oracle_required_before_backend_admission":True, + }, + "truenas":{}, + "proxmox":{ + "9.2-1":{ + "windows11":{"runtime_status":"OPEN"}, + "firecracker":{"runtime_status":"OPEN"}, + } + }, + "rungs":{"V0":"x","V1":"x","V2":"x","W1":"x","WB":"x"}, + } + +def tnrow(version): + legacy=version=="25.04.1" + return { + "windows11":{ + "source_status":"CANDIDATE", + "runtime_status":"OPEN", + "capabilities":{ + "secure_boot":{"source_proven":True}, + "tpm":{"source_proven":True}, + "uefi_q35":{"source_proven":not legacy}, + }, + }, + "firecracker":{ + "source_status":"RUNTIME_OBSERVE_ONLY" if legacy else "CANDIDATE", + "cpu_passthrough_control":None if legacy else "vm.create.cpu_mode=HOST-PASSTHROUGH", + "runtime_status":"OPEN", + }, + } + +class Tests(unittest.TestCase): + def profile(self): + p=fixture() + for v in ("25.04.1","25.04.2.6","25.10.7","26.0.0-BETA.3"): + p["truenas"][v]=tnrow(v) + return p + def test_contract(self): + self.assertEqual(validate(self.profile())["status"],"PASS") + def test_legacy_nested_kvm_cannot_be_source_claimed(self): + p=self.profile() + p["truenas"]["25.04.1"]["firecracker"]["cpu_passthrough_control"]="raw.qemu=-cpu host" + with self.assertRaises(ProfileError): + validate(p) + def test_runtime_claim_fails_closed(self): + p=self.profile() + p["truenas"]["25.10.7"]["windows11"]["runtime_status"]="PASS" + with self.assertRaises(ProfileError): + validate(p) + def test_target_growth_fails_closed(self): + p=self.profile() + p["truenas"]["27.0.0"]=tnrow("27.0.0") + with self.assertRaises(ProfileError): + validate(p) + +if __name__=="__main__": + unittest.main() From e581157d975317880fa1e5c25fac72136b1fb48f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:47:55 +0200 Subject: [PATCH 203/719] RDTE: rerun 25.04.1 native catalog with user-schema projection * RDTE: bind native catalog probe to schema-projected producer * RDTE: request 25.04.1 schema-projected native catalog rerun --- config/truenas-rdte-run-request.json | 4 ++-- scripts/truenas_middleware_official_catalog_t6_probe.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index a5f36d719..b1da92afb 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-official-catalog-003-upstream-runtime-fixture", + "request_id": "tn-25.04.1-t6-official-catalog-004-user-schema-projection", "version": "25.04.1", "rung": "t6", "product": "official-catalog", "authority_issue": 411, "consumer_authority_issue": 281, - "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 using the exact upstream-derived basic-values runtime fixture after the prior TZ-only fixture left ntfy.base_url empty and caused the native app to stop." + "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after projecting the pinned upstream renderer fixture through the pinned questions.yaml user schema, excluding renderer-only ix_volumes and create_host_path fields." } diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py index af85d9fc8..eb7c3063a 100644 --- a/scripts/truenas_middleware_official_catalog_t6_probe.py +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -12,7 +12,7 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for EXPECTED_SCHEMA = "semper-supra.official-catalog-truenas-t6-control/1" -EXPECTED_FOUNDRY_REF = "be7c2e7fd81785ee3fce2a26f75d226004bf52e5" +EXPECTED_FOUNDRY_REF = "6a924585dc17c4b90cb942282ac3a642532d0b85" EXPECTED_CATALOG_COMMIT = "8d93ac087336ab642492209cb707aa1f9556d324" EXPECTED_CONTROL_ID = "ntfy" EXPECTED_CATALOG_VERSION = "1.1.21" From 7492c75930ccd5f39df70b6199ece44fc72946b5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:50:06 +0200 Subject: [PATCH 204/719] RDTE: align official-catalog workflow producer ref --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 23b3ef21e..05e652f3e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -177,9 +177,9 @@ jobs: foundry-official-catalog-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@be7c2e7fd81785ee3fce2a26f75d226004bf52e5 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@6a924585dc17c4b90cb942282ac3a642532d0b85 with: - foundry_ref: be7c2e7fd81785ee3fce2a26f75d226004bf52e5 + foundry_ref: 6a924585dc17c4b90cb942282ac3a642532d0b85 control_id: ntfy permissions: contents: read @@ -353,7 +353,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then - foundry_commit="be7c2e7fd81785ee3fce2a26f75d226004bf52e5" + foundry_commit="6a924585dc17c4b90cb942282ac3a642532d0b85" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From b041a7aebabde571d41290af19776dc90784d8cb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:50:34 +0200 Subject: [PATCH 205/719] RDTE: reconcile official-catalog workflow producer ref * RDTE: bind workflow to schema-projected official catalog producer * RDTE: request producer-ref reconciled 25.04.1 rerun --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index b1da92afb..3e59e28e9 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-official-catalog-004-user-schema-projection", + "request_id": "tn-25.04.1-t6-official-catalog-005-workflow-ref-reconcile", "version": "25.04.1", "rung": "t6", "product": "official-catalog", "authority_issue": 411, "consumer_authority_issue": 281, - "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after projecting the pinned upstream renderer fixture through the pinned questions.yaml user schema, excluding renderer-only ix_volumes and create_host_path fields." + "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after reconciling the workflow reusable-exporter ref, harness foundry_commit argument, and probe EXPECTED_FOUNDRY_REF to schema-projected producer 6a924585dc17c4b90cb942282ac3a642532d0b85." } From 16190e4bb402677750c0600c7c40424a5d353960 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:54:16 +0200 Subject: [PATCH 206/719] RDTE: correct native catalog runtime health oracle * RDTE: accept completed native helper containers in health oracle * RDTE: test primary-service native health semantics * RDTE: request 25.04.1 primary-service-health rep --- config/truenas-rdte-run-request.json | 4 +-- ...as_middleware_official_catalog_t6_probe.py | 28 +++++++-------- ...est_truenas_t6_official_catalog_control.py | 34 +++++++++++++++++++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3e59e28e9..ae1cc9118 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-official-catalog-005-workflow-ref-reconcile", + "request_id": "tn-25.04.1-t6-official-catalog-006-primary-service-health", "version": "25.04.1", "rung": "t6", "product": "official-catalog", "authority_issue": 411, "consumer_authority_issue": 281, - "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after reconciling the workflow reusable-exporter ref, harness foundry_commit argument, and probe EXPECTED_FOUNDRY_REF to schema-projected producer 6a924585dc17c4b90cb942282ac3a642532d0b85." + "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after correcting the runtime-health oracle to require the owned primary service running while allowing completed one-shot helper containers such as permissions." } diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py index eb7c3063a..bda00fd2b 100644 --- a/scripts/truenas_middleware_official_catalog_t6_probe.py +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -108,15 +108,25 @@ def native_lineage(app: dict[str, Any], control: dict[str, Any]) -> bool: return app.get("custom_app") is False and metadata.get("name") == control["control"]["id"] -def healthy_running(app: dict[str, Any], control: dict[str, Any]) -> bool: - if not owned(app, control) or app.get("state") != "RUNNING": +def native_runtime_healthy(app: dict[str, Any], control: dict[str, Any], *, exact_identity: bool = True) -> bool: + if (owned(app, control) if exact_identity else native_lineage(app, control)) is False: + return False + if app.get("state") != "RUNNING": return False workloads = app.get("active_workloads") or {} details = workloads.get("container_details") or [] containers = workloads.get("containers") if not isinstance(containers, int) or containers < 1 or not details: return False - return all(item.get("state") == "running" for item in details) + primary = control["control"]["id"] + return any( + item.get("service_name") == primary and item.get("state") == "running" + for item in details + ) + + +def healthy_running(app: dict[str, Any], control: dict[str, Any]) -> bool: + return native_runtime_healthy(app, control, exact_identity=True) def plan( @@ -241,17 +251,7 @@ def wait_native_running(label: str): last = None while time.monotonic() < deadline: last = query() - workloads = (last or {}).get("active_workloads") or {} - details = workloads.get("container_details") or [] - if ( - last - and native_lineage(last, control) - and last.get("state") == "RUNNING" - and isinstance(workloads.get("containers"), int) - and workloads.get("containers") >= 1 - and details - and all(item.get("state") == "running" for item in details) - ): + if last and native_runtime_healthy(last, control, exact_identity=False): payload["states"].append({"label": label, "state": "RUNNING"}) return last time.sleep(1) diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py index 2576ff5cc..45d1c3e01 100644 --- a/tests/test_truenas_t6_official_catalog_control.py +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -119,6 +119,40 @@ def test_plan_is_observe_driven_and_converges_noop(self): self.assertEqual(MOD.plan(a, {"TZ": "Etc/UTC"}, c), "START") self.assertEqual(MOD.plan(a, {"TZ": "Etc/UTC"}, c, desired_present=False), "DELETE") + def test_health_allows_completed_permissions_helper(self): + c = control() + a = app(c) + a["active_workloads"] = { + "containers": 2, + "container_details": [ + {"state": "running", "service_name": "ntfy"}, + {"state": "exited", "service_name": "permissions"}, + ], + } + self.assertTrue(MOD.healthy_running(a, c)) + self.assertTrue(MOD.native_runtime_healthy(a, c, exact_identity=False)) + + def test_health_requires_primary_service_running(self): + c = control() + a = app(c) + a["active_workloads"] = { + "containers": 2, + "container_details": [ + {"state": "exited", "service_name": "ntfy"}, + {"state": "running", "service_name": "permissions"}, + ], + } + self.assertFalse(MOD.healthy_running(a, c)) + + def test_health_rejects_missing_primary_service(self): + c = control() + a = app(c) + a["active_workloads"] = { + "containers": 1, + "container_details": [{"state": "running", "service_name": "permissions"}], + } + self.assertFalse(MOD.healthy_running(a, c)) + def test_foreign_catalog_identity_blocks_mutation(self): c = control() a = app(c) From 14188ed71a4fe3cefbdeeb564a406c133c27afde Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:24:04 +0200 Subject: [PATCH 207/719] RDTE: harden TrueNAS pool probe against slow WSS reads * RDTE: phase pool probe and tolerate slower WSS reads * RDTE: raise bounded pool probe read timeout * RDTE: test pool WSS timeout budget and phase receipts * RDTE: request 25.04.1 rep 007 after pool WSS timeout --- config/truenas-rdte-run-request.json | 4 ++-- scripts/gha_kvm_truenas_rdte.sh | 2 +- scripts/truenas_middleware_pool_probe.py | 11 +++++++++++ tests/test_gha_kvm_system_rdte.py | 15 +++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index ae1cc9118..3b92c7918 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-official-catalog-006-primary-service-health", + "request_id": "tn-25.04.1-t6-official-catalog-007-pool-wss-budget", "version": "25.04.1", "rung": "t6", "product": "official-catalog", "authority_issue": 411, "consumer_authority_issue": 281, - "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after correcting the runtime-health oracle to require the owned primary service running while allowing completed one-shot helper containers such as permissions." + "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after hardening the generic pool probe for slower WSS read latency; app producer and primary-service health oracle are unchanged from rep 006." } diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 4e4fb1f0c..74167bf7e 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -575,7 +575,7 @@ python3 "$SCRIPT_DIR/truenas_middleware_pool_probe.py" \ --password-file "$PASSWORD_FILE" \ --out "$POOL_OUT" --pool-name "$DATA_POOL_NAME" \ --expected-data-disks "$DATA_DISK_COUNT" --data-serial-prefix "$DATA_SERIAL_PREFIX" \ - --timeout 6 --job-timeout 180 >/dev/null 2>&1 || true + --timeout 15 --job-timeout 180 >/dev/null 2>&1 || true [[ -f "$POOL_OUT" ]] || fail_evidence HARNESS_FAILURE data-pool "T3/T4 pool client did not emit a receipt" POOL_RESULT_JSON="$(cat "$POOL_OUT")" POOL_OK="$(python3 - "$POOL_OUT" <<'PY' diff --git a/scripts/truenas_middleware_pool_probe.py b/scripts/truenas_middleware_pool_probe.py index ad69c4c2f..51a2c4393 100755 --- a/scripts/truenas_middleware_pool_probe.py +++ b/scripts/truenas_middleware_pool_probe.py @@ -41,12 +41,14 @@ def main(): started = time.time() ws = None try: + payload["phase"] = "connect" ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) if connected.get("msg") != "connected": raise RuntimeError(f"DDP connection failed: {connected!r}") + payload["phase"] = "authenticate" auth = ddp_call( ws, "1", "auth.login_ex", [{ @@ -59,9 +61,12 @@ def main(): if payload["auth_response_type"] != "SUCCESS": raise RuntimeError(f"authentication did not return SUCCESS: {auth!r}") + payload["phase"] = "discover-system" payload["system_version"] = ddp_call(ws, "2", "system.version", []) + payload["phase"] = "discover-boot-disks" boot_disks = ddp_call(ws, "3", "boot.get_disks", []) payload["boot_disks"] = sorted(boot_disks) + payload["phase"] = "discover-disk-details" disk_details = ddp_call(ws, "4", "disk.details", []) payload["disk_details"] = disk_details if not isinstance(disk_details, dict) or not isinstance(disk_details.get("unused"), list): @@ -92,6 +97,7 @@ def main(): raise RuntimeError("refusing pool mutation: an owned data disk overlaps the boot pool") payload["selected_data_disks"] = sorted(selected) + payload["phase"] = "precondition-pool-absence" existing = ddp_call(ws, "5", "pool.query", [[["name", "=", a.pool_name]]]) if existing: raise RuntimeError(f"refusing pool mutation: pool {a.pool_name!r} already exists") @@ -113,11 +119,13 @@ def main(): "allow_duplicate_serials": False, "topology": {"data": [{"type": "MIRROR", "disks": sorted(selected)}]}, } + payload["phase"] = "pool-create-submit" job_id = ddp_call(ws, "6", "pool.create", [create]) if not isinstance(job_id, int): raise RuntimeError(f"pool.create did not return a legacy DDP job id: {job_id!r}") payload["job_id"] = job_id + payload["phase"] = "pool-create-wait" deadline = time.monotonic() + a.job_timeout job = None poll = 7 @@ -145,6 +153,7 @@ def main(): if not job or job.get("state") != "SUCCESS": raise RuntimeError(f"pool.create did not reach SUCCESS: {job!r}") + payload["phase"] = "verify-pool" pool = ddp_call( ws, str(poll), "pool.query", [[["name", "=", a.pool_name]], {"get": True}], @@ -158,6 +167,7 @@ def main(): if pool.get("healthy") is not True: raise RuntimeError(f"created pool is not healthy: {pool.get('healthy')!r}") + payload["phase"] = "verify-pool-disks" pool_disks = ddp_call(ws, str(poll), "pool.get_disks", [pool["id"]]) payload["pool_disks"] = sorted(pool_disks) if set(pool_disks) != set(selected): @@ -165,6 +175,7 @@ def main(): f"pool disk membership mismatch: selected={sorted(selected)!r} pool={sorted(pool_disks)!r}" ) + payload["phase"] = "complete" payload["oracleSatisfied"] = True payload["classification"] = "SUPPORTED" payload["detail"] = "real two-disk mirror pool is ONLINE, healthy, and owns exactly the selected disposable disks" diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 157829a83..c4780c787 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -613,5 +613,20 @@ def test_t4_uses_exact_target_profile_metadata(self): self.assertIn('"ha_apps_gate": a.ha_apps_gate', text) + +class PoolTransportBudgetTests(unittest.TestCase): + def test_pool_probe_has_wss_tolerant_read_budget_and_phase_receipts(self): + root = pathlib.Path(__file__).resolve().parents[1] + harness = (root / "scripts" / "gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + probe = (root / "scripts" / "truenas_middleware_pool_probe.py").read_text(encoding="utf-8") + pool_call = harness.split('POOL_OUT="$STATE_DIR/data-pool.json"', 1)[1].split('[[ -f "$POOL_OUT" ]]', 1)[0] + self.assertIn("--timeout 15 --job-timeout 180", pool_call) + for phase in ( + "connect", "authenticate", "discover-system", "discover-boot-disks", + "discover-disk-details", "precondition-pool-absence", + "pool-create-submit", "pool-create-wait", "verify-pool", "verify-pool-disks", "complete", + ): + self.assertIn(f'payload["phase"] = "{phase}"', probe) + if __name__ == "__main__": unittest.main() From 90fa7c10ee1e35c6b5f2d083d912179e2c6f0004 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:33:13 +0200 Subject: [PATCH 208/719] RDTE: rebase G3 provider route onto current execution branch --- .github/workflows/gha-kvm-system-rdte.yml | 33 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 05e652f3e..701f423d6 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -48,6 +48,7 @@ on: - wow-sidecar - garm - garm-provider-g2 + - garm-provider-g3 - official-catalog permissions: @@ -107,7 +108,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -124,7 +125,7 @@ jobs: jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "official-catalog")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "official-catalog")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null @@ -145,7 +146,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -193,6 +194,15 @@ jobs: permissions: contents: read + provider-g3-fixture: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' + uses: SemperSupra/garm-provider-truenas/.github/workflows/export-g3-nested-fixture.yml@e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab + with: + provider_ref: e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab + permissions: + contents: read + qemu-t3-topology: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' @@ -274,7 +284,7 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures, provider-g3-fixture] if: >- always() && needs.contract.result == 'success' && @@ -286,7 +296,8 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || - (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') ) runs-on: ubuntu-26.04 timeout-minutes: 50 @@ -324,6 +335,12 @@ jobs: with: name: garm-provider-truenas-g2-fixtures path: ${{ runner.temp }}/gha-kvm-system-rdte/g2-fixtures + - name: Download exact GARM provider G3 fixture + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-provider-truenas-g3-fixture + path: ${{ runner.temp }}/gha-kvm-system-rdte/g3-fixture - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -347,6 +364,12 @@ jobs: --g2-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g2-fixtures" --g2-fixture-producer "1667aa73e514c278e221082ac33f2f10a4141eef" ) + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-provider-g3" ]]; then + args+=( + --t6-product "garm-provider-g3" + --g3-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g3-fixture" + --g3-fixture-producer "e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab" + ) else if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" From 7262a1eed2293914f12b6581e0af822f168b9db7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:33:16 +0200 Subject: [PATCH 209/719] RDTE: rebase G3 TrueNAS harness route --- scripts/gha_kvm_truenas_rdte.sh | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 74167bf7e..2381b50af 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA]" } OUT="" @@ -28,6 +28,8 @@ FOUNDRY_CONTROL_DIR="" FOUNDRY_COMMIT="" G2_FIXTURE_DIR="" G2_FIXTURE_PRODUCER="" +G3_FIXTURE_DIR="" +G3_FIXTURE_PRODUCER="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -39,6 +41,8 @@ while [[ $# -gt 0 ]]; do --foundry-commit) FOUNDRY_COMMIT="$2"; shift 2 ;; --g2-fixture-dir) G2_FIXTURE_DIR="$2"; shift 2 ;; --g2-fixture-producer) G2_FIXTURE_PRODUCER="$2"; shift 2 ;; + --g3-fixture-dir) G3_FIXTURE_DIR="$2"; shift 2 ;; + --g3-fixture-producer) G3_FIXTURE_PRODUCER="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -53,11 +57,15 @@ if [[ "$RUNG" == "t6" ]]; then if [[ "$T6_PRODUCT" != "official-catalog" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } G2_FIXTURE_DIR="$(realpath "$G2_FIXTURE_DIR")" + elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then + [[ -n "$G3_FIXTURE_DIR" && -d "$G3_FIXTURE_DIR" ]] || { echo "garm-provider-g3 requires --g3-fixture-dir" >&2; exit 2; } + [[ "$G3_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g3 requires exact --g3-fixture-producer SHA" >&2; exit 2; } + G3_FIXTURE_DIR="$(realpath "$G3_FIXTURE_DIR")" else [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } @@ -131,6 +139,8 @@ payload = { "app": ( {"name": "garm-provider-g2-fixtures", "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g2" + else {"name": "garm-provider-g3-runner", "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g3" else {"name": "rdte-t6-garm", "image": "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm" else {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} @@ -234,6 +244,11 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G2 client" command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" + elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_garm_provider_g3_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G3 client" + command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" + command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" elif [[ "$T6_PRODUCT" == "garm" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 GARM control client" @@ -657,6 +672,13 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then --fixture-dir "$G2_FIXTURE_DIR" \ --fixture-producer-commit "$G2_FIXTURE_PRODUCER" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_garm_provider_g3_probe.py" \ + --host 127.0.0.1 --http-port "$HTTP_PORT" --https-port "$HTTPS_PORT" \ + --password-file "$PASSWORD_FILE" \ + --fixture-dir "$G3_FIXTURE_DIR" \ + --fixture-producer-commit "$G3_FIXTURE_PRODUCER" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "garm" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$GARM_HOST_PORT" \ @@ -697,6 +719,8 @@ if [[ "$T6_PRODUCT" == "official-catalog" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported native catalog control passed observe/discover-plan-apply-verify convergence, stop/start, config update/read-back, redeploy, conditional native-upgrade receipt, retain-data delete/reinstall, NOOP convergence, and final cleanup" elif [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider passed verified WSS/API-key transport, valid local adoption, foreign exclusion, managed-drift fail-closed behavior, fresh-process readoption, and zero-residue cleanup; provider create/delete and GitHub/JIT intentionally not exercised" +elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider CreateInstance realized the source-derived fixed runner App profile, exact Compose read-back and provider Get/List reconciliation passed, supported middleware moved the experiment App inactive, provider DeleteInstance retired it, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then From 596dee7248e1dc3acc29e6a7d469a0426f59f9c0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:33:18 +0200 Subject: [PATCH 210/719] RDTE: restore bounded G3 provider boundary probe --- ...uenas_middleware_garm_provider_g3_probe.py | 594 ++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 scripts/truenas_middleware_garm_provider_g3_probe.py diff --git a/scripts/truenas_middleware_garm_provider_g3_probe.py b/scripts/truenas_middleware_garm_provider_g3_probe.py new file mode 100644 index 000000000..cff8ecbfb --- /dev/null +++ b/scripts/truenas_middleware_garm_provider_g3_probe.py @@ -0,0 +1,594 @@ +#!/usr/bin/env python3 +"""G3 oracle: exact provider-created runner App to the synthetic GitHub credential boundary.""" +from __future__ import annotations + +import argparse +import copy +import json +import os +import pathlib +import secrets +import subprocess +import tempfile +import time +import urllib.request + +import truenas_middleware_garm_provider_g2_probe as g2 + + +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_APPLIANCE = ( + "ghcr.io/sempersupra/garm-appliance@" + "sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9" +) +EXPECTED_PROVIDER_PRODUCT_SOURCE = "14535745dc3aa3c0b5466da7c704bca4d23dcec5" +EXPECTED_PROVIDER_BINARY_SHA256 = "911f076ba9421f7bb2a1ea72acc927327d799a5ad6265bffe2d869b2cbf5ecc2" +EXPECTED_FIXTURE_SCHEMA = "semper-supra.garm-provider-truenas-g3-create-fixture/1" +EXPECTED_CONTROLLER_ID = "g3-controller" +EXPECTED_POOL_ID = "g3-pool" +DEFAULT_CALLBACK_URL = "https://httpbin.org/anything/sempersupra-g3/status" +DEFAULT_METADATA_URL = "https://httpbin.org/anything/sempersupra-g3/metadata" +EXPECTED_SUBSTITUTIONS = [ + "bootstrap_template.callback-url", + "bootstrap_template.metadata-url", + "bootstrap_template.instance-token", + "expected_compose_template.services.runner.environment.GARM_CALLBACK_URL", + "expected_compose_template.services.runner.environment.GARM_METADATA_URL", + "expected_compose_template.services.runner.environment.GARM_INSTANCE_TOKEN", +] + + +class FixtureEnvironmentError(RuntimeError): + pass + + +def preflight_public_fixture(callback_url: str, metadata_url: str, timeout: float) -> None: + payload = b'{"status":"probe","message":"public synthetic G3 fixture preflight"}' + request = urllib.request.Request( + callback_url, + data=payload, + method="POST", + headers={ + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": "SemperSupra-G3-RDTE/1", + }, + ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + if not (200 <= response.status < 300): + raise FixtureEnvironmentError( + f"synthetic callback preflight returned HTTP {response.status}" + ) + response.read(4096) + + metadata_probe = metadata_url.rstrip("/") + "/credentials/runner" + request = urllib.request.Request( + metadata_probe, + method="GET", + headers={ + "Accept": "application/json", + "User-Agent": "SemperSupra-G3-RDTE/1", + }, + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + body = response.read(4096) + if not (200 <= response.status < 300) or not body: + raise FixtureEnvironmentError( + "synthetic metadata preflight did not return non-empty HTTP 2xx" + ) + except FixtureEnvironmentError: + raise + except Exception as exc: + raise FixtureEnvironmentError( + f"public synthetic callback/metadata fixture unavailable: {type(exc).__name__}: {exc}" + ) from exc + + + +def load_bundle(directory: pathlib.Path, expected_producer: str) -> dict: + path = directory / "garm-provider-g3-fixture.json" + bundle = json.loads(path.read_text(encoding="utf-8")) + if bundle.get("schema") != EXPECTED_FIXTURE_SCHEMA: + raise RuntimeError("unexpected G3 fixture schema") + if bundle.get("provider_product_source") != EXPECTED_PROVIDER_PRODUCT_SOURCE: + raise RuntimeError("G3 fixture product source drifted") + if bundle.get("producer_source") != expected_producer: + raise RuntimeError("G3 fixture producer source drifted") + if bundle.get("controller_id") != EXPECTED_CONTROLLER_ID: + raise RuntimeError("G3 fixture controller identity drifted") + if bundle.get("pool_id") != EXPECTED_POOL_ID: + raise RuntimeError("G3 fixture pool identity drifted") + if bundle.get("run_local_substitutions") != EXPECTED_SUBSTITUTIONS: + raise RuntimeError("G3 run-local substitution allowlist drifted") + if not isinstance(bundle.get("bootstrap_template"), dict): + raise RuntimeError("G3 fixture has no bootstrap template") + if not isinstance(bundle.get("expected_compose_template"), dict): + raise RuntimeError("G3 fixture has no expected Compose template") + if not isinstance(bundle.get("expected_app_name"), str) or not bundle["expected_app_name"]: + raise RuntimeError("G3 fixture has no expected App name") + return bundle + + +def set_path(root: dict, dotted: str, value: str) -> None: + parts = dotted.split(".") + cur = root + for part in parts[:-1]: + nxt = cur.get(part) + if not isinstance(nxt, dict): + raise RuntimeError(f"G3 substitution path is not an object: {dotted}") + cur = nxt + if parts[-1] not in cur: + raise RuntimeError(f"G3 substitution target absent: {dotted}") + cur[parts[-1]] = value + + +def lower_fixture(bundle: dict, callback_url: str, metadata_url: str, token: str) -> tuple[dict, dict]: + bootstrap = copy.deepcopy(bundle["bootstrap_template"]) + expected = copy.deepcopy(bundle["expected_compose_template"]) + values = { + "bootstrap_template.callback-url": callback_url, + "bootstrap_template.metadata-url": metadata_url, + "bootstrap_template.instance-token": token, + "expected_compose_template.services.runner.environment.GARM_CALLBACK_URL": callback_url, + "expected_compose_template.services.runner.environment.GARM_METADATA_URL": metadata_url, + "expected_compose_template.services.runner.environment.GARM_INSTANCE_TOKEN": token, + } + for dotted in EXPECTED_SUBSTITUTIONS: + target = bootstrap if dotted.startswith("bootstrap_template.") else expected + relative = dotted.split(".", 1)[1] + set_path(target, relative, values[dotted]) + return bootstrap, expected + + +def provider_env( + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + command: str, + instance_id: str | None = None, +) -> dict[str, str]: + env = os.environ.copy() + env.update({ + "GARM_INTERFACE_VERSION": "v0.1.1", + "GARM_COMMAND": command, + "GARM_CONTROLLER_ID": EXPECTED_CONTROLLER_ID, + "GARM_POOL_ID": EXPECTED_POOL_ID, + "GARM_PROVIDER_CONFIG_FILE": str(config_path), + "TRUENAS_API_KEY": api_key, + "SSL_CERT_FILE": str(ca_path), + }) + if instance_id: + env["GARM_INSTANCE_ID"] = instance_id + else: + env.pop("GARM_INSTANCE_ID", None) + return env + + +def invoke_provider( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + command: str, + *, + instance_id: str | None = None, + stdin_object: dict | None = None, +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(provider_binary)], + env=provider_env(config_path, api_key, ca_path, command, instance_id), + input=(json.dumps(stdin_object) if stdin_object is not None else None), + text=True, + capture_output=True, + check=False, + timeout=120, + ) + + +def parse_provider_json(cp: subprocess.CompletedProcess[str], label: str, token: str): + if cp.returncode != 0: + detail = (cp.stderr or cp.stdout or "")[-1200:].replace(token, "") + raise RuntimeError(f"{label} failed rc={cp.returncode}: {detail}") + text = cp.stdout.strip() + if not text: + raise RuntimeError(f"{label} returned empty stdout") + try: + return json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"{label} stdout was not JSON") from exc + + +def query_app(session: g2.AdminSession, name: str) -> dict | None: + matches = session.call("app.query", [[["id", "=", name]]]) + if not matches: + return None + return matches[0] + + +def ensure_inactive(session: g2.AdminSession, name: str, job_timeout: float) -> str: + app = query_app(session, name) + if app is None: + raise RuntimeError("provider-created App disappeared before retirement") + state = str(app.get("state") or "") + if state in {"STOPPED", "CRASHED"}: + return state + stop_id = session.call("app.stop", [name]) + if not isinstance(stop_id, int): + raise RuntimeError("app.stop did not return a job id") + session.wait_job(stop_id, f"app.stop {name}", job_timeout) + app = query_app(session, name) + if app is None: + raise RuntimeError("provider-created App disappeared after stop") + state = str(app.get("state") or "") + if state not in {"STOPPED", "CRASHED"}: + raise RuntimeError(f"provider-created App did not become inactive: {state}") + return state + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--http-port", type=int, required=True) + p.add_argument("--https-port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--fixture-dir", type=pathlib.Path, required=True) + p.add_argument("--fixture-producer-commit", required=True) + p.add_argument("--callback-url", default=DEFAULT_CALLBACK_URL) + p.add_argument("--metadata-url", default=DEFAULT_METADATA_URL) + p.add_argument("--out", required=True) + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-garm-provider-g3/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected_version": EXPECTED_VERSION, + "provider_product_source": EXPECTED_PROVIDER_PRODUCT_SOURCE, + "provider_binary_sha256": EXPECTED_PROVIDER_BINARY_SHA256, + "appliance": EXPECTED_APPLIANCE, + "controller_id": EXPECTED_CONTROLLER_ID, + "pool_id": EXPECTED_POOL_ID, + "provider_create_exercised": False, + "provider_delete_exercised": False, + "github_credentials_present": False, + "github_jit_registration_exercised": False, + "private_repository_execution": False, + "physical_truenas_mutation": False, + "capacity_promotion": False, + "secret_values_recorded": False, + "synthetic_token_recorded": False, + "callback_fixture": { + "callback_url": a.callback_url, + "metadata_url": a.metadata_url, + "authority": "public-synthetic-non-github", + }, + } + + if g2.EXPECTED_APPLIANCE != EXPECTED_APPLIANCE: + raise RuntimeError("G2 helper appliance identity drifted") + if g2.EXPECTED_PROVIDER_BINARY_SHA256 != EXPECTED_PROVIDER_BINARY_SHA256: + raise RuntimeError("G2 helper provider binary identity drifted") + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + session = g2.AdminSession(a.host, a.http_port, password, a.timeout) + api_key_id: int | None = None + api_key_value: str | None = None + imported_cert_id: int | None = None + original_ui_cert_id: int | None = None + app_name: str | None = None + provider_delete_succeeded = False + fallback_cleanup_used = False + cleanup_errors: list[str] = [] + + with tempfile.TemporaryDirectory(prefix="garm-provider-g3-") as td: + root = pathlib.Path(td) + token = f"g3-public-{secrets.token_hex(12)}" + try: + bundle = load_bundle(a.fixture_dir, a.fixture_producer_commit) + payload["fixture_producer_source"] = bundle["producer_source"] + preflight_public_fixture(a.callback_url, a.metadata_url, a.timeout) + payload["synthetic_fixture_preflight"] = True + payload["fixture_bundle_sha256"] = g2.sha256_file( + a.fixture_dir / "garm-provider-g3-fixture.json" + ) + payload["expected_app_name"] = bundle["expected_app_name"] + payload["run_local_substitutions"] = EXPECTED_SUBSTITUTIONS + + bootstrap, expected_compose = lower_fixture( + bundle, a.callback_url, a.metadata_url, token + ) + expected_compose_sha = g2.canonical_json_sha256(expected_compose) + + provider_binary = g2.extract_provider_binary(root) + payload["provider_binary_from_appliance"] = True + + cert_path, key_path = g2.generate_tls(root) + payload["transport"] = { + "scheme": "wss", + "host": a.host, + "port": a.https_port, + "insecure_skip_verify": False, + "certificate_sha256": g2.sha256_file(cert_path), + "private_key_recorded": False, + "api_key_recorded": False, + } + + session.connect() + version = session.call("system.version", []) + payload["system_version"] = version + if version != EXPECTED_VERSION: + raise RuntimeError(f"target version drifted: {version}") + + general = session.call("system.general.config", []) + original_ui_cert_id = g2.ui_cert_id(general.get("ui_certificate")) + + api_key = session.call("api_key.create", [{ + "name": f"g3-provider-{secrets.token_hex(5)}", + "username": "truenas_admin", + }]) + if not isinstance(api_key, dict) or not isinstance(api_key.get("id"), int): + raise RuntimeError("api_key.create did not return an id") + api_key_id = int(api_key["id"]) + api_key_value = api_key.get("key") + if not isinstance(api_key_value, str) or not api_key_value: + raise RuntimeError("api_key.create did not return a key") + + create_cert_id = session.call("certificate.create", [{ + "name": f"g3_provider_{secrets.token_hex(4)}", + "create_type": "CERTIFICATE_CREATE_IMPORTED", + "add_to_trusted_store": False, + "certificate": cert_path.read_text(encoding="utf-8"), + "privatekey": key_path.read_text(encoding="utf-8"), + }]) + if not isinstance(create_cert_id, int): + raise RuntimeError("certificate.create did not return a job id") + cert_job = session.wait_job(create_cert_id, "certificate.create", a.job_timeout) + cert_result = cert_job.get("result") + if not isinstance(cert_result, dict) or not isinstance(cert_result.get("id"), int): + raise RuntimeError("certificate.create job did not return certificate id") + imported_cert_id = int(cert_result["id"]) + + session.call("system.general.update", [{"ui_certificate": imported_cert_id}]) + try: + session.call("system.general.ui_restart", [0]) + except Exception: + pass + session.close() + g2.wait_verified_https(a.host, a.https_port, cert_path) + + provider_config = { + "mode": "truenas", + "truenas": { + "host": a.host, + "username": "truenas_admin", + "api_key_env": "TRUENAS_API_KEY", + "port": a.https_port, + "insecure_skip_verify": False, + "callback_host_gateway": False, + }, + } + config_path = root / "provider.json" + config_path.write_text( + json.dumps(provider_config, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + config_path.chmod(0o600) + + session.connect() + payload["provider_create_exercised"] = True + create_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "CreateInstance", stdin_object=bootstrap, + ) + created = parse_provider_json(create_cp, "CreateInstance", token) + if not isinstance(created, dict): + raise RuntimeError("CreateInstance did not return an object") + app_name = str(created.get("provider_id") or "") + if app_name != bundle["expected_app_name"]: + raise RuntimeError( + f"provider-created App identity drifted: {app_name!r}" + ) + + config = session.call("app.config", [app_name]) + if not isinstance(config, dict): + raise RuntimeError("app.config did not return an object") + realized_sha = g2.canonical_json_sha256(config) + if realized_sha != expected_compose_sha: + raise RuntimeError("provider-created exact Compose read-back drifted") + + get_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "GetInstance", instance_id=app_name, + ) + got = parse_provider_json(get_cp, "GetInstance created", token) + if not isinstance(got, dict) or got.get("provider_id") != app_name: + raise RuntimeError("GetInstance did not read back created provider identity") + + list_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "ListInstances", + ) + listed = parse_provider_json(list_cp, "ListInstances created", token) + if not isinstance(listed, list): + raise RuntimeError("ListInstances did not return an array") + ids = sorted( + str(item.get("provider_id")) + for item in listed + if isinstance(item, dict) + ) + if ids != [app_name]: + raise RuntimeError(f"ListInstances created inventory drifted: {ids}") + + inactive_state = ensure_inactive(session, app_name, a.job_timeout) + + payload["provider_delete_exercised"] = True + delete_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "DeleteInstance", instance_id=app_name, + ) + if delete_cp.returncode != 0: + detail = (delete_cp.stderr or delete_cp.stdout or "")[-1200:].replace( + token, "" + ) + raise RuntimeError( + f"DeleteInstance failed rc={delete_cp.returncode}: {detail}" + ) + provider_delete_succeeded = True + + if query_app(session, app_name) is not None: + raise RuntimeError("provider-created App remains after DeleteInstance") + + list_after_cp = invoke_provider( + provider_binary, config_path, api_key_value, cert_path, + "ListInstances", + ) + listed_after = parse_provider_json( + list_after_cp, "ListInstances post-delete", token + ) + if listed_after != []: + raise RuntimeError( + f"provider inventory not empty after delete: {listed_after!r}" + ) + + payload["oracles"] = { + "verified_tls_transport": True, + "api_key_auth_transport": True, + "create_instance_returned_exact_owned_identity": True, + "exact_realized_compose_readback": True, + "get_created_instance": True, + "list_created_instance_only": True, + "inactive_transition_via_supported_middleware": True, + "provider_delete_after_inactive": True, + "post_delete_app_absent": True, + "post_delete_provider_inventory_empty": True, + "github_registration_not_exercised": True, + } + payload["retirement_inactive_state"] = inactive_state + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "exact packaged provider CreateInstance realized the source-derived fixed " + "runner App profile on real nested TrueNAS; exact Compose was read back, " + "Get/List reconciled the created identity, the experiment App was moved " + "inactive through supported middleware, provider DeleteInstance retired it, " + "and no GitHub credential, JIT registration, private workload, physical " + "TrueNAS, or capacity promotion was exercised" + ) + except FixtureEnvironmentError as exc: + payload["classification"] = "ENVIRONMENT_FAILURE" + payload["detail"] = str(exc).replace(token, "") + except Exception as exc: + payload["detail"] = str(exc).replace(token, "") + finally: + try: + session.connect() + if app_name and query_app(session, app_name) is not None: + fallback_cleanup_used = True + try: + g2.delete_app_if_present(session, app_name, a.job_timeout) + except Exception as exc: + cleanup_errors.append( + f"fallback delete {app_name}: {type(exc).__name__}: {exc}" + ) + if app_name: + try: + if query_app(session, app_name) is not None: + cleanup_errors.append( + f"provider-created App remains after cleanup: {app_name}" + ) + except Exception as exc: + cleanup_errors.append( + f"verify App absence {app_name}: {type(exc).__name__}: {exc}" + ) + + if api_key_id is not None: + try: + deleted = session.call("api_key.delete", [api_key_id]) + if deleted is not True: + cleanup_errors.append("api_key.delete did not return true") + except Exception as exc: + cleanup_errors.append( + f"api_key.delete: {type(exc).__name__}: {exc}" + ) + api_key_value = None + + if original_ui_cert_id is not None and imported_cert_id is not None: + try: + session.call( + "system.general.update", + [{"ui_certificate": original_ui_cert_id}], + ) + try: + session.call("system.general.ui_restart", [0]) + except Exception: + pass + except Exception as exc: + cleanup_errors.append( + f"restore ui certificate: {type(exc).__name__}: {exc}" + ) + session.close() + time.sleep(5) + try: + session.connect() + except Exception as exc: + cleanup_errors.append( + f"reconnect after ui restore: {type(exc).__name__}: {exc}" + ) + + if imported_cert_id is not None and session.ws is not None: + try: + delete_result = session.call( + "certificate.delete", [imported_cert_id, False] + ) + if isinstance(delete_result, int): + session.wait_job( + delete_result, "certificate.delete", a.job_timeout + ) + elif delete_result is not True: + cleanup_errors.append( + f"certificate.delete unexpected result {delete_result!r}" + ) + except Exception as exc: + cleanup_errors.append( + f"certificate.delete: {type(exc).__name__}: {exc}" + ) + except Exception as exc: + cleanup_errors.append( + f"cleanup session: {type(exc).__name__}: {exc}" + ) + finally: + session.close() + + payload["cleanup"] = { + "attempted": True, + "provider_delete_succeeded": provider_delete_succeeded, + "fallback_cleanup_used": fallback_cleanup_used, + "errors": cleanup_errors, + "zero_unintended_residue": not cleanup_errors, + "secret_values_recorded": False, + "synthetic_token_recorded": False, + } + if cleanup_errors or fallback_cleanup_used: + payload["oracleSatisfied"] = False + payload["classification"] = "ORACLE_FAILURE" + reasons = [] + if fallback_cleanup_used: + reasons.append("provider retirement required fallback direct cleanup") + reasons.extend(cleanup_errors) + payload["detail"] = payload.get("detail", "") + "; cleanup: " + "; ".join(reasons) + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From dadf4cb373ef027e8e1e2a14827c377b881e317f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:33:20 +0200 Subject: [PATCH 211/719] test: restore G3 provider boundary falsifiers --- tests/test_truenas_garm_provider_g3.py | 146 +++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 tests/test_truenas_garm_provider_g3.py diff --git a/tests/test_truenas_garm_provider_g3.py b/tests/test_truenas_garm_provider_g3.py new file mode 100644 index 000000000..5fbef0046 --- /dev/null +++ b/tests/test_truenas_garm_provider_g3.py @@ -0,0 +1,146 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "truenas_middleware_garm_provider_g3_probe.py" +SPEC = importlib.util.spec_from_file_location("garm_provider_g3_probe", SCRIPT) +assert SPEC and SPEC.loader +MOD = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MOD) + + +class GarmProviderG3Tests(unittest.TestCase): + def fixture_bundle(self) -> dict: + return { + "schema": MOD.EXPECTED_FIXTURE_SCHEMA, + "provider_product_source": MOD.EXPECTED_PROVIDER_PRODUCT_SOURCE, + "producer_source": "a" * 40, + "controller_id": MOD.EXPECTED_CONTROLLER_ID, + "pool_id": MOD.EXPECTED_POOL_ID, + "bootstrap_template": { + "callback-url": "https://fixture.invalid/status", + "metadata-url": "https://fixture.invalid/metadata", + "instance-token": "placeholder", + }, + "expected_app_name": "garm-g3-controlle-g3-synthetic-runner", + "expected_compose_template": { + "services": { + "runner": { + "environment": { + "GARM_CALLBACK_URL": "https://fixture.invalid/status", + "GARM_METADATA_URL": "https://fixture.invalid/metadata", + "GARM_INSTANCE_TOKEN": "placeholder", + } + } + } + }, + "run_local_substitutions": MOD.EXPECTED_SUBSTITUTIONS, + } + + def test_fixture_bundle_binds_product_producer_and_substitution_allowlist(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + (root / "garm-provider-g3-fixture.json").write_text( + json.dumps(self.fixture_bundle()), encoding="utf-8" + ) + loaded = MOD.load_bundle(root, "a" * 40) + self.assertEqual( + loaded["provider_product_source"], + MOD.EXPECTED_PROVIDER_PRODUCT_SOURCE, + ) + + with self.assertRaisesRegex(RuntimeError, "producer source drifted"): + MOD.load_bundle(root, "b" * 40) + + bad = self.fixture_bundle() + bad["run_local_substitutions"] = bad["run_local_substitutions"][:-1] + (root / "garm-provider-g3-fixture.json").write_text( + json.dumps(bad), encoding="utf-8" + ) + with self.assertRaisesRegex(RuntimeError, "substitution allowlist drifted"): + MOD.load_bundle(root, "a" * 40) + + def test_lower_fixture_changes_only_callback_metadata_and_synthetic_token(self): + bundle = self.fixture_bundle() + callback = "https://example.invalid/anything/status" + metadata = "https://example.invalid/anything/metadata" + token = "synthetic-token" + bootstrap, expected = MOD.lower_fixture(bundle, callback, metadata, token) + + self.assertEqual(bootstrap["callback-url"], callback) + self.assertEqual(bootstrap["metadata-url"], metadata) + self.assertEqual(bootstrap["instance-token"], token) + env = expected["services"]["runner"]["environment"] + self.assertEqual(env["GARM_CALLBACK_URL"], callback) + self.assertEqual(env["GARM_METADATA_URL"], metadata) + self.assertEqual(env["GARM_INSTANCE_TOKEN"], token) + + original = self.fixture_bundle() + self.assertEqual( + original["bootstrap_template"]["callback-url"], + "https://fixture.invalid/status", + ) + + def test_boundary_is_create_readback_retire_not_github_execution(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + '"CreateInstance"', + '"GetInstance"', + '"ListInstances"', + '"DeleteInstance"', + '"provider_create_exercised": False', + '"provider_delete_exercised": False', + '"github_credentials_present": False', + '"github_jit_registration_exercised": False', + '"private_repository_execution": False', + '"physical_truenas_mutation": False', + '"capacity_promotion": False', + '"callback_host_gateway": False', + '"fallback_cleanup_used": fallback_cleanup_used', + '"provider_delete_succeeded": provider_delete_succeeded', + ): + self.assertIn(required, text) + self.assertNotIn("GITHUB_TOKEN", text) + self.assertNotIn("GH_TOKEN", text) + self.assertNotIn("PRIVATE_KEY", text) + + def test_exact_compose_is_compared_before_retirement(self): + text = SCRIPT.read_text(encoding="utf-8") + readback = text.index('session.call("app.config", [app_name])') + compare = text.index("provider-created exact Compose read-back drifted") + inactive = text.index("ensure_inactive(session, app_name") + delete = text.index('"DeleteInstance", instance_id=app_name') + self.assertLess(readback, compare) + self.assertLess(compare, inactive) + self.assertLess(inactive, delete) + + def test_synthetic_fixture_preflight_sends_no_provider_token(self): + text = SCRIPT.read_text(encoding="utf-8") + block = text.split("def preflight_public_fixture", 1)[1].split( + "def load_bundle", 1 + )[0] + self.assertIn('method="POST"', block) + self.assertIn('method="GET"', block) + self.assertNotIn("Authorization", block) + self.assertIn('"ENVIRONMENT_FAILURE"', text) + self.assertIn('"synthetic_fixture_preflight"', text) + + def test_transport_reuses_verified_g2_helper_tuple(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn("g2.wait_verified_https", text) + self.assertIn('"SSL_CERT_FILE": str(ca_path)', text) + self.assertIn('"TRUENAS_API_KEY": api_key', text) + self.assertIn("g2.EXPECTED_APPLIANCE != EXPECTED_APPLIANCE", text) + self.assertIn( + "g2.EXPECTED_PROVIDER_BINARY_SHA256 != EXPECTED_PROVIDER_BINARY_SHA256", + text, + ) + + +if __name__ == "__main__": + unittest.main() From 8f8b929f5d3a34e9bf1c5cd58271662dd61e8e32 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:33:22 +0200 Subject: [PATCH 212/719] test: admit G3 run-request authority on current base --- tests/test_truenas_rdte_target.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 4560c1636..da8441eee 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -66,15 +66,15 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","official-catalog"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","official-catalog"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": self.assertEqual(request.get("consumer_authority_issue"), 277) if request.get("product") == "garm-provider-g2": self.assertEqual(request.get("consumer_authority_issue"), 39) - if request.get("product") == "garm-provider-g2": - self.assertEqual(request.get("consumer_authority_issue"), 39) + if request.get("product") == "garm-provider-g3": + self.assertEqual(request.get("consumer_authority_issue"), 40) self.assertNotIn("latest", request["version"].lower()) self.assertNotIn("nightly", request["version"].lower()) From fd9117324d614b83e1f1d6030356c0b4036d1102 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:38:28 +0200 Subject: [PATCH 213/719] RDTE: stage exact BETA.3 GARM provider G3 rep --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3b92c7918..b1bd99965 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-official-catalog-007-pool-wss-budget", - "version": "25.04.1", + "request_id": "tn-26-beta3-t6-garm-provider-g3-001-create-delete-boundary", + "version": "26.0.0-BETA.3", "rung": "t6", - "product": "official-catalog", - "authority_issue": 411, - "consumer_authority_issue": 281, - "purpose": "Re-run exact TrueNAS 25.04.1 native official-catalog ntfy T6 after hardening the generic pool probe for slower WSS read latency; app producer and primary-service health oracle are unchanged from rep 006." + "product": "garm-provider-g3", + "authority_issue": 396, + "consumer_authority_issue": 40, + "purpose": "Exercise the exact packaged GARM TrueNAS provider CreateInstance boundary on disposable nested TrueNAS: exact provider-owned runner App realization and Compose read-back, Get/List reconciliation, supported inactive transition, provider DeleteInstance, post-delete absence/inventory, and zero residue. GitHub credential/JIT/private workload/physical/capacity claims remain unexercised." } From e02796a74eea5a33cb9d610db821fc09c294d60a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:39:13 +0200 Subject: [PATCH 214/719] test: admit G3 in shared T6 routing guardrail --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 63bf39f17..b1b6be9f1 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "official-catalog"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "official-catalog"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From 9b3a1079c3b6068026035be037169187985fe799 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:22:36 +0200 Subject: [PATCH 215/719] RDTE: qualify exact 25.04.2.6 native catalog * RDTE: bind official catalog to runtime-qualified producer * RDTE: expect runtime-qualified official catalog producer * RDTE: request exact 25.04.2.6 native catalog T6 --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- config/truenas-rdte-run-request.json | 12 ++++++------ .../truenas_middleware_official_catalog_t6_probe.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 701f423d6..e34f0fe58 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -178,9 +178,9 @@ jobs: foundry-official-catalog-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@6a924585dc17c4b90cb942282ac3a642532d0b85 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@546f4e07b06f77035dab28104883b52f75143bd6 with: - foundry_ref: 6a924585dc17c4b90cb942282ac3a642532d0b85 + foundry_ref: 546f4e07b06f77035dab28104883b52f75143bd6 control_id: ntfy permissions: contents: read @@ -376,7 +376,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then - foundry_commit="6a924585dc17c4b90cb942282ac3a642532d0b85" + foundry_commit="546f4e07b06f77035dab28104883b52f75143bd6" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index b1bd99965..e394c7a75 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g3-001-create-delete-boundary", - "version": "26.0.0-BETA.3", + "request_id": "tn-25.04.2.6-t6-official-catalog-002-runtime-qualified-producer", + "version": "25.04.2.6", "rung": "t6", - "product": "garm-provider-g3", - "authority_issue": 396, - "consumer_authority_issue": 40, - "purpose": "Exercise the exact packaged GARM TrueNAS provider CreateInstance boundary on disposable nested TrueNAS: exact provider-owned runner App realization and Compose read-back, Get/List reconciliation, supported inactive transition, provider DeleteInstance, post-delete absence/inventory, and zero residue. GitHub credential/JIT/private workload/physical/capacity claims remain unexercised." + "product": "official-catalog", + "authority_issue": 423, + "consumer_authority_issue": 281, + "purpose": "Qualify exact TrueNAS 25.04.2.6 native official-catalog ntfy T6 using producer 546f4e07b06f77035dab28104883b52f75143bd6, which records accepted 25.04.1 runtime evidence while preserving universal_qualified=false until all exact targets pass." } diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py index bda00fd2b..efeabf51d 100644 --- a/scripts/truenas_middleware_official_catalog_t6_probe.py +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -12,7 +12,7 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for EXPECTED_SCHEMA = "semper-supra.official-catalog-truenas-t6-control/1" -EXPECTED_FOUNDRY_REF = "6a924585dc17c4b90cb942282ac3a642532d0b85" +EXPECTED_FOUNDRY_REF = "546f4e07b06f77035dab28104883b52f75143bd6" EXPECTED_CATALOG_COMMIT = "8d93ac087336ab642492209cb707aa1f9556d324" EXPECTED_CONTROL_ID = "ntfy" EXPECTED_CATALOG_VERSION = "1.1.21" From ec84cd55cb7d55f56661eab73d1c4c6939c1f838 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:53:05 +0200 Subject: [PATCH 216/719] RDTE: qualify exact 25.10.7 native catalog * RDTE: bind 25.10.7 row to two-row producer * RDTE: expect two-row official catalog producer * RDTE: request exact 25.10.7 native catalog T6 --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- config/truenas-rdte-run-request.json | 8 ++++---- scripts/truenas_middleware_official_catalog_t6_probe.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index e34f0fe58..1ebac1a6c 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -178,9 +178,9 @@ jobs: foundry-official-catalog-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@546f4e07b06f77035dab28104883b52f75143bd6 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-official-catalog-t6-control.yml@60d56cc1752bc825cb232d72b397ff17fe6d47c4 with: - foundry_ref: 546f4e07b06f77035dab28104883b52f75143bd6 + foundry_ref: 60d56cc1752bc825cb232d72b397ff17fe6d47c4 control_id: ntfy permissions: contents: read @@ -376,7 +376,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then - foundry_commit="546f4e07b06f77035dab28104883b52f75143bd6" + foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e394c7a75..e7be040af 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.2.6-t6-official-catalog-002-runtime-qualified-producer", - "version": "25.04.2.6", + "request_id": "tn-25.10.7-t6-official-catalog-001-two-row-producer", + "version": "25.10.7", "rung": "t6", "product": "official-catalog", - "authority_issue": 423, + "authority_issue": 410, "consumer_authority_issue": 281, - "purpose": "Qualify exact TrueNAS 25.04.2.6 native official-catalog ntfy T6 using producer 546f4e07b06f77035dab28104883b52f75143bd6, which records accepted 25.04.1 runtime evidence while preserving universal_qualified=false until all exact targets pass." + "purpose": "Qualify exact TrueNAS 25.10.7 native official-catalog ntfy T6 using producer 60d56cc1752bc825cb232d72b397ff17fe6d47c4, which records accepted 25.04.1 and 25.04.2.6 runtime rows while universal_qualified remains false." } diff --git a/scripts/truenas_middleware_official_catalog_t6_probe.py b/scripts/truenas_middleware_official_catalog_t6_probe.py index efeabf51d..5653fb67d 100644 --- a/scripts/truenas_middleware_official_catalog_t6_probe.py +++ b/scripts/truenas_middleware_official_catalog_t6_probe.py @@ -12,7 +12,7 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for EXPECTED_SCHEMA = "semper-supra.official-catalog-truenas-t6-control/1" -EXPECTED_FOUNDRY_REF = "546f4e07b06f77035dab28104883b52f75143bd6" +EXPECTED_FOUNDRY_REF = "60d56cc1752bc825cb232d72b397ff17fe6d47c4" EXPECTED_CATALOG_COMMIT = "8d93ac087336ab642492209cb707aa1f9556d324" EXPECTED_CONTROL_ID = "ntfy" EXPECTED_CATALOG_VERSION = "1.1.21" From 686e942a1316d8378b480c54aa208163badfd4d4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:16:57 +0200 Subject: [PATCH 217/719] fix: hold G3 synthetic metadata at pre-JIT boundary --- ...uenas_middleware_garm_provider_g3_probe.py | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/scripts/truenas_middleware_garm_provider_g3_probe.py b/scripts/truenas_middleware_garm_provider_g3_probe.py index cff8ecbfb..ea5205c01 100644 --- a/scripts/truenas_middleware_garm_provider_g3_probe.py +++ b/scripts/truenas_middleware_garm_provider_g3_probe.py @@ -11,6 +11,7 @@ import subprocess import tempfile import time +import urllib.error import urllib.request import truenas_middleware_garm_provider_g2_probe as g2 @@ -27,7 +28,9 @@ EXPECTED_CONTROLLER_ID = "g3-controller" EXPECTED_POOL_ID = "g3-pool" DEFAULT_CALLBACK_URL = "https://httpbin.org/anything/sempersupra-g3/status" -DEFAULT_METADATA_URL = "https://httpbin.org/anything/sempersupra-g3/metadata" +DEFAULT_METADATA_URL = ( + "https://httpbin.org/drip?duration=1&numbytes=1&code=200&delay=60&path=" +) EXPECTED_SUBSTITUTIONS = [ "bootstrap_template.callback-url", "bootstrap_template.metadata-url", @@ -71,12 +74,18 @@ def preflight_public_fixture(callback_url: str, metadata_url: str, timeout: floa "User-Agent": "SemperSupra-G3-RDTE/1", }, ) - with urllib.request.urlopen(request, timeout=timeout) as response: - body = response.read(4096) - if not (200 <= response.status < 300) or not body: - raise FixtureEnvironmentError( - "synthetic metadata preflight did not return non-empty HTTP 2xx" - ) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + response.read(4096) + except TimeoutError: + pass + except urllib.error.URLError as exc: + if not isinstance(exc.reason, TimeoutError): + raise + else: + raise FixtureEnvironmentError( + "synthetic metadata hold-open returned before the preflight timeout" + ) except FixtureEnvironmentError: raise except Exception as exc: @@ -265,6 +274,7 @@ def main() -> int: "callback_url": a.callback_url, "metadata_url": a.metadata_url, "authority": "public-synthetic-non-github", + "metadata_behavior": "intentional-hold-open-no-jit-credentials", }, } @@ -297,6 +307,7 @@ def main() -> int: ) payload["expected_app_name"] = bundle["expected_app_name"] payload["run_local_substitutions"] = EXPECTED_SUBSTITUTIONS + app_name = bundle["expected_app_name"] bootstrap, expected_compose = lower_fixture( bundle, a.callback_url, a.metadata_url, token @@ -387,10 +398,10 @@ def main() -> int: created = parse_provider_json(create_cp, "CreateInstance", token) if not isinstance(created, dict): raise RuntimeError("CreateInstance did not return an object") - app_name = str(created.get("provider_id") or "") - if app_name != bundle["expected_app_name"]: + created_app_name = str(created.get("provider_id") or "") + if created_app_name != app_name: raise RuntimeError( - f"provider-created App identity drifted: {app_name!r}" + f"provider-created App identity drifted: {created_app_name!r}" ) config = session.call("app.config", [app_name]) From 90a7273befcc0006712da213cd6c8a3080903048 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:17:18 +0200 Subject: [PATCH 218/719] test: lock G3 hold-open pre-JIT fixture --- tests/test_truenas_garm_provider_g3.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_truenas_garm_provider_g3.py b/tests/test_truenas_garm_provider_g3.py index 5fbef0046..eee2a1fad 100644 --- a/tests/test_truenas_garm_provider_g3.py +++ b/tests/test_truenas_garm_provider_g3.py @@ -5,6 +5,7 @@ from pathlib import Path import tempfile import unittest +import urllib.parse ROOT = Path(__file__).resolve().parents[1] SCRIPT = ROOT / "scripts" / "truenas_middleware_garm_provider_g3_probe.py" @@ -126,10 +127,29 @@ def test_synthetic_fixture_preflight_sends_no_provider_token(self): )[0] self.assertIn('method="POST"', block) self.assertIn('method="GET"', block) + self.assertIn("except TimeoutError", block) + self.assertIn("synthetic metadata hold-open returned before the preflight timeout", block) self.assertNotIn("Authorization", block) self.assertIn('"ENVIRONMENT_FAILURE"', text) self.assertIn('"synthetic_fixture_preflight"', text) + def test_default_metadata_fixture_holds_open_before_jit_credentials(self): + self.assertTrue(MOD.DEFAULT_METADATA_URL.startswith("https://httpbin.org/drip?")) + self.assertIn("delay=60", MOD.DEFAULT_METADATA_URL) + self.assertIn("path=", MOD.DEFAULT_METADATA_URL) + expanded = MOD.DEFAULT_METADATA_URL.rstrip("/") + "/credentials/runner" + parsed = urllib.parse.urlsplit(expanded) + self.assertEqual(parsed.path, "/drip") + query = urllib.parse.parse_qs(parsed.query) + self.assertEqual(query.get("path"), ["/credentials/runner"]) + self.assertEqual(query.get("delay"), ["60"]) + + def test_expected_app_name_is_bound_before_provider_create_for_cleanup(self): + text = SCRIPT.read_text(encoding="utf-8") + bind = text.index('app_name = bundle["expected_app_name"]') + create = text.index('"CreateInstance", stdin_object=bootstrap') + self.assertLess(bind, create) + def test_transport_reuses_verified_g2_helper_tuple(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn("g2.wait_verified_https", text) From cf2d2358c524d9e6a6ea31eb9f79b3b5d5bbd697 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:17:29 +0200 Subject: [PATCH 219/719] rdte: stage corrected G3 pre-JIT boundary rep --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e7be040af..183ac37e0 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.10.7-t6-official-catalog-001-two-row-producer", - "version": "25.10.7", + "request_id": "tn-26-beta3-t6-garm-provider-g3-002-hold-open-metadata-boundary", + "version": "26.0.0-BETA.3", "rung": "t6", - "product": "official-catalog", - "authority_issue": 410, - "consumer_authority_issue": 281, - "purpose": "Qualify exact TrueNAS 25.10.7 native official-catalog ntfy T6 using producer 60d56cc1752bc825cb232d72b397ff17fe6d47c4, which records accepted 25.04.1 and 25.04.2.6 runtime rows while universal_qualified remains false." + "product": "garm-provider-g3", + "authority_issue": 396, + "consumer_authority_issue": 40, + "purpose": "Re-run GARM provider G3 on exact TrueNAS 26.0.0-BETA.3 with a public synthetic metadata endpoint that intentionally holds the pre-JIT fetch open long enough for provider CreateInstance/App startup observation, while returning no usable GitHub JIT credential material; then require exact Compose read-back, Get/List, supported inactive transition, provider DeleteInstance, absence, empty inventory, and zero residue." } From e455968e08ba99c000c26aa62dd5421e4cd4e747 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:24:28 +0200 Subject: [PATCH 220/719] diag: add G3 runner profile smoke gate --- .github/workflows/prep-rdte-static.yml | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index a8d54494c..4a3aa7d40 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -20,6 +20,7 @@ on: - 'tests/test_truenas_t6_wow_sidecar_control.py' - 'tests/test_truenas_t6_garm_control.py' - 'tests/test_truenas_garm_provider_g2.py' + - 'tests/test_truenas_garm_provider_g3.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_rdte_target.py' pull_request: @@ -37,6 +38,7 @@ on: - 'tests/test_truenas_t6_wow_sidecar_control.py' - 'tests/test_truenas_t6_garm_control.py' - 'tests/test_truenas_garm_provider_g2.py' + - 'tests/test_truenas_garm_provider_g3.py' - 'tests/test_truenas_rdte_target.py' permissions: @@ -78,7 +80,43 @@ jobs: if [[ -f tests/test_truenas_garm_provider_g2.py ]]; then tests+=(tests/test_truenas_garm_provider_g2.py) fi + if [[ -f tests/test_truenas_garm_provider_g3.py ]]; then + tests+=(tests/test_truenas_garm_provider_g3.py) + fi if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi python3 -m unittest -v "${tests[@]}" + + - name: G3 runner container profile smoke + if: ${{ hashFiles('scripts/truenas_middleware_garm_provider_g3_probe.py') != '' }} + run: | + set -euo pipefail + image='ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda' + docker pull "$image" + docker run --rm \ + --user 1001:1001 \ + --cpus 4 \ + --memory 8g \ + --cap-drop ALL \ + --security-opt no-new-privileges:true \ + --tmpfs /run/garm-jit:rw,nosuid,nodev,noexec,uid=1001,gid=1001,mode=0700 \ + --entrypoint /bin/sh \ + "$image" -c ' + set -eu + test "$(id -u)" = "1001" + test "$(id -g)" = "1001" + test -w /home/runner + test -w /run/garm-jit + command -v curl >/dev/null + command -v tar >/dev/null + command -v sha256sum >/dev/null + printf profile-smoke > /home/runner/g3-profile-smoke + rm -f /home/runner/g3-profile-smoke + printf jit-smoke > /run/garm-jit/g3-profile-smoke + rm -f /run/garm-jit/g3-profile-smoke + curl --fail --silent --show-error --location \ + --connect-timeout 5 --max-time 20 --range 0-0 \ + https://github.com/actions/runner/releases/download/v2.336.0/actions-runner-linux-x64-2.336.0.tar.gz \ + -o /dev/null + ' From 9fdd29b29ac980f83683616c5949b299b0af9f84 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:27:34 +0200 Subject: [PATCH 221/719] diag: capture sanitized G3 app.create job logs --- ...uenas_middleware_garm_provider_g3_probe.py | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_garm_provider_g3_probe.py b/scripts/truenas_middleware_garm_provider_g3_probe.py index ea5205c01..36f87fc23 100644 --- a/scripts/truenas_middleware_garm_provider_g3_probe.py +++ b/scripts/truenas_middleware_garm_provider_g3_probe.py @@ -208,6 +208,51 @@ def parse_provider_json(cp: subprocess.CompletedProcess[str], label: str, token: raise RuntimeError(f"{label} stdout was not JSON") from exc +def capture_create_failure_job( + session: g2.AdminSession, app_name: str, token: str +) -> dict: + jobs = session.call( + "core.get_jobs", + [[["method", "=", "app.create"]], {"order_by": ["-id"], "limit": 20}], + ) + if not isinstance(jobs, list): + raise RuntimeError("core.get_jobs did not return an array") + selected = None + for job in jobs: + if not isinstance(job, dict): + continue + arguments = job.get("arguments") + try: + rendered = json.dumps(arguments, sort_keys=True) + except TypeError: + rendered = repr(arguments) + if app_name in rendered: + selected = job + break + if selected is None: + return {"found": False} + + def scrub(value): + if isinstance(value, str): + return value.replace(token, "") + if isinstance(value, dict): + return {str(k): scrub(v) for k, v in value.items()} + if isinstance(value, list): + return [scrub(v) for v in value] + return value + + return { + "found": True, + "id": selected.get("id"), + "state": selected.get("state"), + "error": scrub(selected.get("error")), + "exception": scrub(selected.get("exception")), + "progress": scrub(selected.get("progress")), + "logs_excerpt": scrub(selected.get("logs_excerpt")), + "logs_available": bool(selected.get("logs_path")), + } + + def query_app(session: g2.AdminSession, name: str) -> dict | None: matches = session.call("app.query", [[["id", "=", name]]]) if not matches: @@ -395,7 +440,21 @@ def main() -> int: provider_binary, config_path, api_key_value, cert_path, "CreateInstance", stdin_object=bootstrap, ) - created = parse_provider_json(create_cp, "CreateInstance", token) + try: + created = parse_provider_json(create_cp, "CreateInstance", token) + except Exception: + try: + payload["create_failure_job"] = capture_create_failure_job( + session, app_name, token + ) + except Exception as capture_exc: + payload["create_failure_job"] = { + "found": False, + "capture_error": ( + f"{type(capture_exc).__name__}: {capture_exc}" + ).replace(token, ""), + } + raise if not isinstance(created, dict): raise RuntimeError("CreateInstance did not return an object") created_app_name = str(created.get("provider_id") or "") From ae6f73a9a3de44a1cb25950d969f2f58af2acf5f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:27:48 +0200 Subject: [PATCH 222/719] test: lock sanitized G3 create failure evidence --- tests/test_truenas_garm_provider_g3.py | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/test_truenas_garm_provider_g3.py b/tests/test_truenas_garm_provider_g3.py index eee2a1fad..d7306041d 100644 --- a/tests/test_truenas_garm_provider_g3.py +++ b/tests/test_truenas_garm_provider_g3.py @@ -150,6 +150,50 @@ def test_expected_app_name_is_bound_before_provider_create_for_cleanup(self): create = text.index('"CreateInstance", stdin_object=bootstrap') self.assertLess(bind, create) + def test_create_failure_job_capture_is_sanitized_and_excludes_arguments(self): + token = "g3-secret-synthetic-token" + + class FakeSession: + def call(self, method, params): + self.method = method + self.params = params + return [ + { + "id": 77, + "method": "app.create", + "state": "FAILED", + "arguments": [{ + "app_name": "garm-g3-controlle-g3-synthetic-runner", + "custom_compose_config": { + "services": { + "runner": { + "environment": { + "GARM_INSTANCE_TOKEN": token, + } + } + } + }, + }], + "error": f"failed with {token}", + "exception": f"trace {token}", + "progress": {"description": f"cleanup {token}", "percent": 80}, + "logs_excerpt": f"container stderr {token}", + "logs_path": "/var/log/jobs/77.log", + } + ] + + session = FakeSession() + got = MOD.capture_create_failure_job( + session, "garm-g3-controlle-g3-synthetic-runner", token + ) + self.assertEqual(session.method, "core.get_jobs") + self.assertTrue(got["found"]) + self.assertEqual(got["id"], 77) + self.assertTrue(got["logs_available"]) + self.assertNotIn("arguments", got) + self.assertNotIn(token, json.dumps(got, sort_keys=True)) + self.assertIn("", got["logs_excerpt"]) + def test_transport_reuses_verified_g2_helper_tuple(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn("g2.wait_verified_https", text) From ade65ba989a8ce2b99ff0c734316f786212ffd0b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:28:08 +0200 Subject: [PATCH 223/719] rdte: stage G3 create-log diagnostic rep --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 183ac37e0..f51f7908a 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g3-002-hold-open-metadata-boundary", + "request_id": "tn-26-beta3-t6-garm-provider-g3-003-capture-create-job-logs", "version": "26.0.0-BETA.3", "rung": "t6", "product": "garm-provider-g3", "authority_issue": 396, "consumer_authority_issue": 40, - "purpose": "Re-run GARM provider G3 on exact TrueNAS 26.0.0-BETA.3 with a public synthetic metadata endpoint that intentionally holds the pre-JIT fetch open long enough for provider CreateInstance/App startup observation, while returning no usable GitHub JIT credential material; then require exact Compose read-back, Get/List, supported inactive transition, provider DeleteInstance, absence, empty inventory, and zero residue." + "purpose": "Re-run exact TrueNAS 26.0.0-BETA.3 GARM provider G3 with unchanged hold-open pre-JIT fixture, adding sanitized core.get_jobs app.create failure evidence (error/exception/progress/logs_excerpt only, never arguments/token) so the exact TrueNAS Compose up failure can be causally diagnosed while preserving cleanup and all G3 non-claims." } From 9dadbe73bd8f79de4e0829ef543b48a564ee479d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:04:09 +0200 Subject: [PATCH 224/719] diag: capture sanitized TrueNAS app lifecycle log --- ...uenas_middleware_garm_provider_g3_probe.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/scripts/truenas_middleware_garm_provider_g3_probe.py b/scripts/truenas_middleware_garm_provider_g3_probe.py index 36f87fc23..1746ffc7c 100644 --- a/scripts/truenas_middleware_garm_provider_g3_probe.py +++ b/scripts/truenas_middleware_garm_provider_g3_probe.py @@ -8,6 +8,7 @@ import os import pathlib import secrets +import ssl import subprocess import tempfile import time @@ -253,6 +254,84 @@ def scrub(value): } +def select_sanitized_log_excerpt( + text: str, + app_name: str, + sensitive_values: list[str], + *, + context_lines: int = 80, + max_chars: int = 24000, +) -> dict: + scrubbed = text + for value in sensitive_values: + if value: + scrubbed = scrubbed.replace(value, "") + lines = scrubbed.splitlines() + hits = [i for i, line in enumerate(lines) if app_name in line] + if hits: + center = hits[-1] + start = max(0, center - context_lines) + end = min(len(lines), center + context_lines + 1) + else: + start = max(0, len(lines) - (context_lines * 2 + 1)) + end = len(lines) + excerpt = "\n".join(lines[start:end]) + if len(excerpt) > max_chars: + excerpt = excerpt[-max_chars:] + return { + "app_name_observed": bool(hits), + "line_count": len(lines), + "excerpt": excerpt, + } + + +def capture_app_lifecycle_log( + session: g2.AdminSession, + host: str, + https_port: int, + ca_path: pathlib.Path, + app_name: str, + sensitive_values: list[str], + timeout: float, +) -> dict: + result = session.call( + "core.download", + ["filesystem.get", ["/var/log/app_lifecycle.log"], "app_lifecycle.log", True], + ) + if not isinstance(result, (list, tuple)) or len(result) != 2: + raise RuntimeError(f"core.download returned unexpected result: {type(result).__name__}") + job_id, relative_url = result + if not isinstance(job_id, int) or not isinstance(relative_url, str): + raise RuntimeError("core.download did not return job id and URL") + if not relative_url.startswith("/_download/"): + raise RuntimeError("core.download returned unexpected download path") + + context = ssl.create_default_context(cafile=str(ca_path)) + request = urllib.request.Request( + f"https://{host}:{https_port}{relative_url}", + method="GET", + headers={"User-Agent": "SemperSupra-G3-RDTE/1"}, + ) + max_bytes = 2 * 1024 * 1024 + with urllib.request.urlopen( + request, timeout=max(timeout, 30.0), context=context + ) as response: + raw = response.read(max_bytes + 1) + truncated = len(raw) > max_bytes + raw = raw[:max_bytes] + text = raw.decode("utf-8", errors="replace") + selected = select_sanitized_log_excerpt( + text, app_name, sensitive_values + ) + selected.update({ + "download_job_id": job_id, + "bytes_read": len(raw), + "truncated": truncated, + "source": "/var/log/app_lifecycle.log", + }) + return selected + + def query_app(session: g2.AdminSession, name: str) -> dict | None: matches = session.call("app.query", [[["id", "=", name]]]) if not matches: @@ -454,6 +533,27 @@ def main() -> int: f"{type(capture_exc).__name__}: {capture_exc}" ).replace(token, ""), } + try: + payload["app_lifecycle_log"] = capture_app_lifecycle_log( + session, + a.host, + a.https_port, + cert_path, + app_name, + [token, api_key_value or ""], + a.timeout, + ) + except Exception as capture_exc: + detail = ( + f"{type(capture_exc).__name__}: {capture_exc}" + ).replace(token, "") + if api_key_value: + detail = detail.replace(api_key_value, "") + payload["app_lifecycle_log"] = { + "captured": False, + "capture_error": detail, + "source": "/var/log/app_lifecycle.log", + } raise if not isinstance(created, dict): raise RuntimeError("CreateInstance did not return an object") From 216d4f9bb62b7b6441321b081642fcf835fe439a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:04:31 +0200 Subject: [PATCH 225/719] test: bound and sanitize G3 lifecycle log evidence --- tests/test_truenas_garm_provider_g3.py | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_truenas_garm_provider_g3.py b/tests/test_truenas_garm_provider_g3.py index d7306041d..08de374a4 100644 --- a/tests/test_truenas_garm_provider_g3.py +++ b/tests/test_truenas_garm_provider_g3.py @@ -194,6 +194,38 @@ def call(self, method, params): self.assertNotIn(token, json.dumps(got, sort_keys=True)) self.assertIn("", got["logs_excerpt"]) + def test_app_lifecycle_excerpt_is_bounded_and_redacts_sensitive_values(self): + app = "garm-g3-controlle-g3-synthetic-runner" + token = "g3-sensitive-token" + api_key = "api-sensitive-value" + lines = [f"noise-{i}" for i in range(220)] + lines += [ + f"compose up {app}", + f"stderr contains {token}", + f"environment accidentally contains {api_key}", + "fatal: exact compose failure", + ] + got = MOD.select_sanitized_log_excerpt( + "\n".join(lines), app, [token, api_key], context_lines=3, max_chars=1000 + ) + self.assertTrue(got["app_name_observed"]) + self.assertIn("fatal: exact compose failure", got["excerpt"]) + self.assertNotIn(token, got["excerpt"]) + self.assertNotIn(api_key, got["excerpt"]) + self.assertGreaterEqual(got["line_count"], 224) + + def test_appliance_log_capture_uses_single_specific_filesystem_source(self): + text = SCRIPT.read_text(encoding="utf-8") + block = text.split("def capture_app_lifecycle_log", 1)[1].split( + "def query_app", 1 + )[0] + self.assertIn('"core.download"', block) + self.assertIn('"filesystem.get"', block) + self.assertIn('"/var/log/app_lifecycle.log"', block) + self.assertIn("ssl.create_default_context", block) + self.assertNotIn("GITHUB_TOKEN", block) + self.assertNotIn("TRUENAS_API_KEY", block) + def test_transport_reuses_verified_g2_helper_tuple(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn("g2.wait_verified_https", text) From 26986a346151eaa0f616b2db7c2658e5e2d552a8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:04:40 +0200 Subject: [PATCH 226/719] rdte: stage G3 appliance lifecycle-log diagnostic --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index f51f7908a..7c27070ea 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g3-003-capture-create-job-logs", + "request_id": "tn-26-beta3-t6-garm-provider-g3-004-app-lifecycle-log", "version": "26.0.0-BETA.3", "rung": "t6", "product": "garm-provider-g3", "authority_issue": 396, "consumer_authority_issue": 40, - "purpose": "Re-run exact TrueNAS 26.0.0-BETA.3 GARM provider G3 with unchanged hold-open pre-JIT fixture, adding sanitized core.get_jobs app.create failure evidence (error/exception/progress/logs_excerpt only, never arguments/token) so the exact TrueNAS Compose up failure can be causally diagnosed while preserving cleanup and all G3 non-claims." + "purpose": "Re-run exact TrueNAS 26.0.0-BETA.3 GARM provider G3 with unchanged provider/runtime fixture while downloading only /var/log/app_lifecycle.log through authenticated core.download/filesystem.get after failed CreateInstance; retain a bounded token/API-key-redacted excerpt around the exact app name to causally identify the TrueNAS Compose up failure, preserving zero-residue cleanup and all G3 non-claims." } From b45335cc60a89c887976e98074cad3f713c4bc91 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:10:46 +0200 Subject: [PATCH 227/719] fix: give G3 nested TrueNAS four vCPUs --- scripts/gha_kvm_truenas_rdte.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 2381b50af..f54943d4e 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -73,6 +73,10 @@ if [[ "$RUNG" == "t6" ]]; then fi fi +if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "garm-provider-g3" ]]; then + VCPUS=4 +fi + if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" STATE_DIR="$(realpath "$STATE_DIR")" @@ -118,6 +122,7 @@ write_receipt() { export R_MIDDLEWARE_REF="$MIDDLEWARE_REF" R_MIDDLEWARE_COMMIT="$MIDDLEWARE_COMMIT" export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_INSTALLER_RPC_PATH="$INSTALLER_RPC_PATH" R_INSTALLER_RPC_GUEST_PORT="$INSTALLER_RPC_GUEST_PORT" R_INSTALLER_SOURCE_REF="$INSTALLER_SOURCE_REF" R_INSTALLER_MAIN_BLOB_SHA="$INSTALLER_MAIN_BLOB_SHA" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" + export R_VCPUS="$VCPUS" R_RAM_MIB="$RAM_MIB" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" python3 - <<'PY' @@ -130,8 +135,8 @@ payload = { "phase": os.environ["R_PHASE"], "detail": os.environ["R_DETAIL"], "requested_shape": { - "vcpus": 2, - "ram_mib": 8192, + "vcpus": int(os.environ["R_VCPUS"]), + "ram_mib": int(os.environ["R_RAM_MIB"]), "boot_disk": "24G", "data_disks": ["8G", "8G"] if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else [], "data_pool": {"name": "rdtepool", "topology": "MIRROR"} if os.environ.get("R_RUNG") in {"t3", "t4", "t5", "t6"} else None, @@ -270,6 +275,8 @@ sudo -n test -r /dev/kvm && sudo -n test -w /dev/kvm || fail_evidence ENVIRONMEN MEM_AVAIL_KIB="$(awk '/MemAvailable:/ {print $2}' /proc/meminfo)" FREE_KIB="$(df -Pk "$STATE_DIR" | awk 'NR==2 {print $4}')" +HOST_CPUS="$(nproc)" +(( HOST_CPUS >= VCPUS )) || fail_evidence SKIPPED_GUARDRAIL preflight "host CPU count $HOST_CPUS below requested guest vCPU count $VCPUS" (( MEM_AVAIL_KIB >= MIN_HOST_MEM_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host memory headroom below 11 GiB required before allocating 8 GiB guest" (( FREE_KIB >= MIN_HOST_FREE_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host disk headroom below 28 GiB" From 5c43617a32df598e6d8edb9c8365fec3d26dcf22 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:11:05 +0200 Subject: [PATCH 228/719] test: lock G3 four-vCPU RDTE embodiment --- tests/test_gha_kvm_system_rdte.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index c4780c787..1d1c6528a 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -201,6 +201,20 @@ def test_truenas_uses_exact_target_registry_and_vendor_digest_sidecar(self): self.assertNotIn('hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080', text) self.assertNotIn("xdotool", text) + def test_g3_nested_guest_meets_fixed_four_cpu_runner_profile(self): + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn('VCPUS=2', text) + self.assertIn( + 'if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "garm-provider-g3" ]]; then', + text, + ) + self.assertIn('VCPUS=4', text) + self.assertIn('HOST_CPUS="$(nproc)"', text) + self.assertIn('(( HOST_CPUS >= VCPUS ))', text) + self.assertIn('R_VCPUS="$VCPUS"', text) + self.assertIn('"vcpus": int(os.environ["R_VCPUS"])', text) + self.assertIn('"ram_mib": int(os.environ["R_RAM_MIB"])', text) + def test_no_literal_escaped_shell_parameter_expansions(self): needle = chr(92) + "$" + "{" for path in (PVE, TRUENAS): From eb3dba84ca66a0173f2c7c6748fc4bdf3956bec8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:48:08 +0200 Subject: [PATCH 229/719] rdte: stage four-vCPU G3 corrective rep --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 7c27070ea..a489f277e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g3-004-app-lifecycle-log", + "request_id": "tn-26-beta3-t6-garm-provider-g3-005-four-vcpu-corrective", "version": "26.0.0-BETA.3", "rung": "t6", "product": "garm-provider-g3", "authority_issue": 396, "consumer_authority_issue": 40, - "purpose": "Re-run exact TrueNAS 26.0.0-BETA.3 GARM provider G3 with unchanged provider/runtime fixture while downloading only /var/log/app_lifecycle.log through authenticated core.download/filesystem.get after failed CreateInstance; retain a bounded token/API-key-redacted excerpt around the exact app name to causally identify the TrueNAS Compose up failure, preserving zero-residue cleanup and all G3 non-claims." + "purpose": "Re-run exact TrueNAS 26.0.0-BETA.3 GARM provider G3 after correcting only the nested RDTE embodiment from 2 to 4 vCPUs so Docker can admit the fixed provider-exported cpus=4 runner profile; require the unchanged G3 CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absent -> empty provider inventory -> zero-residue oracle, with GitHub/JIT/private workload/physical mutation/capacity promotion still unexercised." } From 05e28a4ff99ccef8418b1667282204532c349f22 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:32:28 +0200 Subject: [PATCH 230/719] rdte: add nested G4 capacity-two provider probe --- ...uenas_middleware_garm_provider_g4_probe.py | 801 ++++++++++++++++++ 1 file changed, 801 insertions(+) create mode 100644 scripts/truenas_middleware_garm_provider_g4_probe.py diff --git a/scripts/truenas_middleware_garm_provider_g4_probe.py b/scripts/truenas_middleware_garm_provider_g4_probe.py new file mode 100644 index 000000000..afafeb23a --- /dev/null +++ b/scripts/truenas_middleware_garm_provider_g4_probe.py @@ -0,0 +1,801 @@ +#!/usr/bin/env python3 +"""G4 oracle: exact provider capacity-two concurrency/reconnect/retirement on nested TrueNAS.""" +from __future__ import annotations + +import argparse +import concurrent.futures +import copy +import json +import os +import pathlib +import secrets +import subprocess +import tempfile +import time + +import truenas_middleware_garm_provider_g2_probe as g2 +import truenas_middleware_garm_provider_g3_probe as g3 + + +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_APPLIANCE = g3.EXPECTED_APPLIANCE +EXPECTED_PROVIDER_PRODUCT_SOURCE = g3.EXPECTED_PROVIDER_PRODUCT_SOURCE +EXPECTED_PROVIDER_BINARY_SHA256 = g3.EXPECTED_PROVIDER_BINARY_SHA256 +EXPECTED_FIXTURE_SCHEMA = "semper-supra.garm-provider-truenas-g4-capacity-two-fixture/1" +EXPECTED_CONTROLLER_ID = "g4-controller" +EXPECTED_POOL_ID = "g4-pool" +EXPECTED_RUNNER_IMAGE = ( + "ghcr.io/actions/actions-runner:2.336.0@" + "sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda" +) +EXPECTED_CPU = 4 +EXPECTED_MEMORY_BYTES = 8 * 1024 * 1024 * 1024 +IX_APPS_PATH = "/mnt/.ix-apps" +SLOTS = ("alpha", "beta") +EXPECTED_SUBSTITUTIONS = [ + "runners[0].bootstrap_template.callback-url", + "runners[0].bootstrap_template.metadata-url", + "runners[0].bootstrap_template.instance-token", + "runners[0].expected_compose_template.services.runner.environment.GARM_CALLBACK_URL", + "runners[0].expected_compose_template.services.runner.environment.GARM_METADATA_URL", + "runners[0].expected_compose_template.services.runner.environment.GARM_INSTANCE_TOKEN", + "runners[1].bootstrap_template.callback-url", + "runners[1].bootstrap_template.metadata-url", + "runners[1].bootstrap_template.instance-token", + "runners[1].expected_compose_template.services.runner.environment.GARM_CALLBACK_URL", + "runners[1].expected_compose_template.services.runner.environment.GARM_METADATA_URL", + "runners[1].expected_compose_template.services.runner.environment.GARM_INSTANCE_TOKEN", +] + + +def fixture_urls(slot: str) -> tuple[str, str]: + callback = f"https://httpbin.org/anything/sempersupra-g4/{slot}/status" + metadata = ( + "https://httpbin.org/drip?duration=1&numbytes=1&code=200&delay=60&path=" + + slot + ) + return callback, metadata + + +def load_bundle(directory: pathlib.Path, expected_producer: str) -> dict: + path = directory / "garm-provider-g4-fixture.json" + bundle = json.loads(path.read_text(encoding="utf-8")) + if bundle.get("schema") != EXPECTED_FIXTURE_SCHEMA: + raise RuntimeError("unexpected G4 fixture schema") + if bundle.get("provider_product_source") != EXPECTED_PROVIDER_PRODUCT_SOURCE: + raise RuntimeError("G4 fixture product source drifted") + if bundle.get("producer_source") != expected_producer: + raise RuntimeError("G4 fixture producer source drifted") + if bundle.get("controller_id") != EXPECTED_CONTROLLER_ID: + raise RuntimeError("G4 fixture controller identity drifted") + if bundle.get("pool_id") != EXPECTED_POOL_ID: + raise RuntimeError("G4 fixture pool identity drifted") + if bundle.get("run_local_substitutions") != EXPECTED_SUBSTITUTIONS: + raise RuntimeError("G4 run-local substitution allowlist drifted") + runners = bundle.get("runners") + if not isinstance(runners, list) or len(runners) != 2: + raise RuntimeError("G4 fixture must contain exactly two runners") + if [r.get("slot") for r in runners if isinstance(r, dict)] != list(SLOTS): + raise RuntimeError("G4 fixture slot identities drifted") + names = [r.get("expected_app_name") for r in runners] + if not all(isinstance(name, str) and name for name in names) or len(set(names)) != 2: + raise RuntimeError("G4 fixture App identities are absent or aliased") + runner = bundle.get("runner") + if not isinstance(runner, dict): + raise RuntimeError("G4 fixture runner contract absent") + if runner.get("image") != EXPECTED_RUNNER_IMAGE: + raise RuntimeError("G4 runner image drifted") + if runner.get("cpu") != EXPECTED_CPU or runner.get("memory_bytes") != EXPECTED_MEMORY_BYTES: + raise RuntimeError("G4 fixed resource profile drifted") + if bundle.get("retirement_orders") != [["alpha", "beta"], ["beta", "alpha"]]: + raise RuntimeError("G4 retirement-order contract drifted") + return bundle + + +def lower_runner(runner: dict, callback: str, metadata: str, token: str) -> tuple[dict, dict]: + bootstrap = copy.deepcopy(runner["bootstrap_template"]) + expected = copy.deepcopy(runner["expected_compose_template"]) + bootstrap["callback-url"] = callback + bootstrap["metadata-url"] = metadata + bootstrap["instance-token"] = token + env = expected["services"]["runner"]["environment"] + env["GARM_CALLBACK_URL"] = callback + env["GARM_METADATA_URL"] = metadata + env["GARM_INSTANCE_TOKEN"] = token + return bootstrap, expected + + +def provider_env( + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + command: str, + instance_id: str | None = None, +) -> dict[str, str]: + env = os.environ.copy() + env.update({ + "GARM_INTERFACE_VERSION": "v0.1.1", + "GARM_COMMAND": command, + "GARM_CONTROLLER_ID": EXPECTED_CONTROLLER_ID, + "GARM_POOL_ID": EXPECTED_POOL_ID, + "GARM_PROVIDER_CONFIG_FILE": str(config_path), + "TRUENAS_API_KEY": api_key, + "SSL_CERT_FILE": str(ca_path), + }) + if instance_id: + env["GARM_INSTANCE_ID"] = instance_id + else: + env.pop("GARM_INSTANCE_ID", None) + return env + + +def invoke_provider( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + command: str, + *, + instance_id: str | None = None, + stdin_object: dict | None = None, +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(provider_binary)], + env=provider_env(config_path, api_key, ca_path, command, instance_id), + input=(json.dumps(stdin_object) if stdin_object is not None else None), + text=True, + capture_output=True, + check=False, + timeout=180, + ) + + +def scrub_text(text: str, sensitive: list[str]) -> str: + out = text + for value in sensitive: + if value: + out = out.replace(value, "") + return out + + +def parse_provider_json( + cp: subprocess.CompletedProcess[str], + label: str, + sensitive: list[str], +): + if cp.returncode != 0: + detail = scrub_text((cp.stderr or cp.stdout or "")[-1600:], sensitive) + raise RuntimeError(f"{label} failed rc={cp.returncode}: {detail}") + text = cp.stdout.strip() + if not text: + raise RuntimeError(f"{label} returned empty stdout") + try: + return json.loads(text) + except json.JSONDecodeError as exc: + raise RuntimeError(f"{label} stdout was not JSON") from exc + + +def list_ids( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + sensitive: list[str], +) -> list[str]: + cp = invoke_provider( + provider_binary, config_path, api_key, ca_path, "ListInstances" + ) + listed = parse_provider_json(cp, "ListInstances", sensitive) + if not isinstance(listed, list): + raise RuntimeError("ListInstances did not return an array") + return sorted( + str(item.get("provider_id")) + for item in listed + if isinstance(item, dict) + ) + + +def get_instance( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + app_name: str, + sensitive: list[str], +) -> dict: + cp = invoke_provider( + provider_binary, + config_path, + api_key, + ca_path, + "GetInstance", + instance_id=app_name, + ) + got = parse_provider_json(cp, f"GetInstance {app_name}", sensitive) + if not isinstance(got, dict) or got.get("provider_id") != app_name: + raise RuntimeError(f"GetInstance identity drifted for {app_name}") + return got + + +def delete_instance( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + app_name: str, + sensitive: list[str], +) -> None: + cp = invoke_provider( + provider_binary, + config_path, + api_key, + ca_path, + "DeleteInstance", + instance_id=app_name, + ) + if cp.returncode != 0: + detail = scrub_text((cp.stderr or cp.stdout or "")[-1600:], sensitive) + raise RuntimeError(f"DeleteInstance {app_name} failed rc={cp.returncode}: {detail}") + + +def require_active_delete_refusal( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + ca_path: pathlib.Path, + app_name: str, + sensitive: list[str], + session: g2.AdminSession, +) -> None: + cp = invoke_provider( + provider_binary, + config_path, + api_key, + ca_path, + "DeleteInstance", + instance_id=app_name, + ) + if cp.returncode == 0: + raise RuntimeError("provider unexpectedly deleted an active runner") + if g3.query_app(session, app_name) is None: + raise RuntimeError("active-delete refusal lost the target App") + + +def statfs_available(session: g2.AdminSession) -> int: + data = session.call("filesystem.statfs", [IX_APPS_PATH]) + if not isinstance(data, dict) or not isinstance(data.get("avail_bytes"), int): + raise RuntimeError("filesystem.statfs did not return avail_bytes") + return int(data["avail_bytes"]) + + +def verify_realized_pair( + session: g2.AdminSession, + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + cert_path: pathlib.Path, + lowered: dict[str, dict], + sensitive: list[str], +) -> None: + expected_ids = sorted(v["app_name"] for v in lowered.values()) + for slot in SLOTS: + item = lowered[slot] + config = session.call("app.config", [item["app_name"]]) + if not isinstance(config, dict): + raise RuntimeError(f"app.config did not return an object for {slot}") + realized_sha = g2.canonical_json_sha256(config) + expected_sha = g2.canonical_json_sha256(item["expected_compose"]) + if realized_sha != expected_sha: + raise RuntimeError(f"exact Compose read-back drifted for {slot}") + get_instance( + provider_binary, + config_path, + api_key, + cert_path, + item["app_name"], + sensitive, + ) + ids = list_ids( + provider_binary, config_path, api_key, cert_path, sensitive + ) + if ids != expected_ids: + raise RuntimeError(f"provider pair inventory drifted: {ids} != {expected_ids}") + + +def create_pair( + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + cert_path: pathlib.Path, + lowered: dict[str, dict], + sensitive: list[str], +) -> None: + def create(slot: str): + item = lowered[slot] + cp = invoke_provider( + provider_binary, + config_path, + api_key, + cert_path, + "CreateInstance", + stdin_object=item["bootstrap"], + ) + return slot, parse_provider_json(cp, f"CreateInstance {slot}", sensitive) + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(create, SLOTS)) + for slot, created in results: + if not isinstance(created, dict): + raise RuntimeError(f"CreateInstance {slot} did not return an object") + if created.get("provider_id") != lowered[slot]["app_name"]: + raise RuntimeError(f"CreateInstance identity drifted for {slot}") + + +def retire_order( + order: tuple[str, str], + session: g2.AdminSession, + provider_binary: pathlib.Path, + config_path: pathlib.Path, + api_key: str, + cert_path: pathlib.Path, + lowered: dict[str, dict], + sensitive: list[str], +) -> dict: + first, second = order + first_name = lowered[first]["app_name"] + second_name = lowered[second]["app_name"] + + first_state = g3.ensure_inactive(session, first_name, 300.0) + delete_instance( + provider_binary, config_path, api_key, cert_path, first_name, sensitive + ) + if g3.query_app(session, first_name) is not None: + raise RuntimeError(f"{first} App remains after provider DeleteInstance") + peer = g3.query_app(session, second_name) + if peer is None: + raise RuntimeError(f"retiring {first} corrupted peer {second}") + peer_state = str(peer.get("state") or "") + ids = list_ids( + provider_binary, config_path, api_key, cert_path, sensitive + ) + if ids != [second_name]: + raise RuntimeError(f"peer inventory drifted after retiring {first}: {ids}") + + second_state = g3.ensure_inactive(session, second_name, 300.0) + delete_instance( + provider_binary, config_path, api_key, cert_path, second_name, sensitive + ) + if g3.query_app(session, second_name) is not None: + raise RuntimeError(f"{second} App remains after provider DeleteInstance") + if list_ids(provider_binary, config_path, api_key, cert_path, sensitive) != []: + raise RuntimeError("provider inventory not empty after pair retirement") + return { + "order": [first, second], + "first_inactive_state": first_state, + "peer_state_after_first_retirement": peer_state, + "second_inactive_state": second_state, + } + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--http-port", type=int, required=True) + p.add_argument("--https-port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--fixture-dir", type=pathlib.Path, required=True) + p.add_argument("--fixture-producer-commit", required=True) + p.add_argument("--out", required=True) + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-garm-provider-g4/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "expected_version": EXPECTED_VERSION, + "provider_product_source": EXPECTED_PROVIDER_PRODUCT_SOURCE, + "provider_binary_sha256": EXPECTED_PROVIDER_BINARY_SHA256, + "appliance": EXPECTED_APPLIANCE, + "controller_id": EXPECTED_CONTROLLER_ID, + "pool_id": EXPECTED_POOL_ID, + "runner_count": 2, + "github_credentials_present": False, + "github_jit_registration_exercised": False, + "private_repository_execution": False, + "physical_truenas_mutation": False, + "capacity_promotion": False, + "secret_values_recorded": False, + "synthetic_tokens_recorded": False, + } + + if g2.EXPECTED_APPLIANCE != EXPECTED_APPLIANCE: + raise RuntimeError("G2 helper appliance identity drifted") + if g2.EXPECTED_PROVIDER_BINARY_SHA256 != EXPECTED_PROVIDER_BINARY_SHA256: + raise RuntimeError("G2 helper provider binary identity drifted") + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + session = g2.AdminSession(a.host, a.http_port, password, a.timeout) + api_key_id: int | None = None + api_key_value: str | None = None + imported_cert_id: int | None = None + original_ui_cert_id: int | None = None + app_names: list[str] = [] + fallback_cleanup_used = False + cleanup_errors: list[str] = [] + sensitive: list[str] = [] + + with tempfile.TemporaryDirectory(prefix="garm-provider-g4-") as td: + root = pathlib.Path(td) + try: + bundle = load_bundle(a.fixture_dir, a.fixture_producer_commit) + payload["fixture_producer_source"] = bundle["producer_source"] + payload["fixture_bundle_sha256"] = g2.sha256_file( + a.fixture_dir / "garm-provider-g4-fixture.json" + ) + payload["source_oracles"] = bundle.get("source_oracles", {}) + payload["retirement_orders"] = bundle["retirement_orders"] + payload["configured_resource_envelope"] = { + "per_runner_cpu_limit": EXPECTED_CPU, + "per_runner_memory_bytes": EXPECTED_MEMORY_BYTES, + "runner_count": 2, + "aggregate_cpu_limit_units": EXPECTED_CPU * 2, + "aggregate_memory_limit_bytes": EXPECTED_MEMORY_BYTES * 2, + "limits_are_not_reservations": True, + } + + lowered: dict[str, dict] = {} + for runner in bundle["runners"]: + slot = runner["slot"] + callback, metadata = fixture_urls(slot) + g3.preflight_public_fixture(callback, metadata, a.timeout) + token = f"g4-{slot}-{secrets.token_hex(12)}" + sensitive.append(token) + bootstrap, expected = lower_runner( + runner, callback, metadata, token + ) + lowered[slot] = { + "app_name": runner["expected_app_name"], + "callback_url": callback, + "metadata_url": metadata, + "bootstrap": bootstrap, + "expected_compose": expected, + } + app_names.append(runner["expected_app_name"]) + payload["synthetic_fixture_preflight"] = True + payload["expected_app_names"] = sorted(app_names) + payload["callback_metadata_isolation"] = { + slot: { + "callback_url": lowered[slot]["callback_url"], + "metadata_url": lowered[slot]["metadata_url"], + } + for slot in SLOTS + } + + provider_binary = g2.extract_provider_binary(root) + payload["provider_binary_from_appliance"] = True + + cert_path, key_path = g2.generate_tls(root) + payload["transport"] = { + "scheme": "wss", + "host": a.host, + "port": a.https_port, + "insecure_skip_verify": False, + "certificate_sha256": g2.sha256_file(cert_path), + "private_key_recorded": False, + "api_key_recorded": False, + } + + session.connect() + version = session.call("system.version", []) + payload["system_version"] = version + if version != EXPECTED_VERSION: + raise RuntimeError(f"target version drifted: {version}") + + general = session.call("system.general.config", []) + original_ui_cert_id = g2.ui_cert_id(general.get("ui_certificate")) + api_key = session.call("api_key.create", [{ + "name": f"g4-provider-{secrets.token_hex(5)}", + "username": "truenas_admin", + }]) + if not isinstance(api_key, dict) or not isinstance(api_key.get("id"), int): + raise RuntimeError("api_key.create did not return an id") + api_key_id = int(api_key["id"]) + api_key_value = api_key.get("key") + if not isinstance(api_key_value, str) or not api_key_value: + raise RuntimeError("api_key.create did not return a key") + sensitive.append(api_key_value) + + create_cert_id = session.call("certificate.create", [{ + "name": f"g4_provider_{secrets.token_hex(4)}", + "create_type": "CERTIFICATE_CREATE_IMPORTED", + "add_to_trusted_store": False, + "certificate": cert_path.read_text(encoding="utf-8"), + "privatekey": key_path.read_text(encoding="utf-8"), + }]) + if not isinstance(create_cert_id, int): + raise RuntimeError("certificate.create did not return a job id") + cert_job = session.wait_job( + create_cert_id, "certificate.create", a.job_timeout + ) + cert_result = cert_job.get("result") + if not isinstance(cert_result, dict) or not isinstance(cert_result.get("id"), int): + raise RuntimeError("certificate.create job did not return certificate id") + imported_cert_id = int(cert_result["id"]) + + session.call("system.general.update", [{"ui_certificate": imported_cert_id}]) + try: + session.call("system.general.ui_restart", [0]) + except Exception: + pass + session.close() + g2.wait_verified_https(a.host, a.https_port, cert_path) + + provider_config = { + "mode": "truenas", + "truenas": { + "host": a.host, + "username": "truenas_admin", + "api_key_env": "TRUENAS_API_KEY", + "port": a.https_port, + "insecure_skip_verify": False, + "callback_host_gateway": False, + }, + } + config_path = root / "provider.json" + config_path.write_text( + json.dumps(provider_config, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + config_path.chmod(0o600) + + session.connect() + storage_before = statfs_available(session) + + cycle_receipts = [] + for cycle, order in enumerate((("alpha", "beta"), ("beta", "alpha")), start=1): + cycle_tokens = [] + cycle_lowered = {} + for slot in SLOTS: + callback, metadata = fixture_urls(slot) + token = f"g4-cycle{cycle}-{slot}-{secrets.token_hex(12)}" + sensitive.append(token) + cycle_tokens.append(token) + source_runner = next( + item for item in bundle["runners"] if item["slot"] == slot + ) + bootstrap, expected = lower_runner( + source_runner, callback, metadata, token + ) + cycle_lowered[slot] = { + "app_name": source_runner["expected_app_name"], + "bootstrap": bootstrap, + "expected_compose": expected, + } + + create_pair( + provider_binary, + config_path, + api_key_value, + cert_path, + cycle_lowered, + sensitive, + ) + verify_realized_pair( + session, + provider_binary, + config_path, + api_key_value, + cert_path, + cycle_lowered, + sensitive, + ) + storage_two_apps = statfs_available(session) + + # Each command is a fresh provider process. This List/Get pass is + # the explicit reconnect/adoption observation with both Apps present. + reconnect_ids = list_ids( + provider_binary, + config_path, + api_key_value, + cert_path, + sensitive, + ) + if reconnect_ids != sorted(app_names): + raise RuntimeError( + f"fresh-process reconnect inventory drifted: {reconnect_ids}" + ) + for name in app_names: + get_instance( + provider_binary, + config_path, + api_key_value, + cert_path, + name, + sensitive, + ) + + if cycle == 1: + require_active_delete_refusal( + provider_binary, + config_path, + api_key_value, + cert_path, + cycle_lowered["alpha"]["app_name"], + sensitive, + session, + ) + if g3.query_app(session, cycle_lowered["beta"]["app_name"]) is None: + raise RuntimeError("active-delete refusal corrupted peer App") + + retirement = retire_order( + order, + session, + provider_binary, + config_path, + api_key_value, + cert_path, + cycle_lowered, + sensitive, + ) + storage_after = statfs_available(session) + cycle_receipts.append({ + "cycle": cycle, + "retirement": retirement, + "storage": { + "ix_apps_avail_bytes_before_pair": storage_before, + "ix_apps_avail_bytes_with_two_apps": storage_two_apps, + "ix_apps_avail_bytes_after_retirement": storage_after, + "consumed_bytes_at_two_apps": max( + 0, storage_before - storage_two_apps + ), + "residual_consumed_bytes_after_retirement": max( + 0, storage_before - storage_after + ), + }, + }) + storage_before = storage_after + + payload["cycles"] = cycle_receipts + payload["oracles"] = { + "verified_tls_transport": True, + "api_key_auth_transport": True, + "two_concurrent_create_calls_returned_distinct_owned_identities": True, + "exact_realized_compose_readback_both": True, + "fresh_process_get_list_exact_pair": True, + "callback_metadata_config_isolated": True, + "active_delete_refused_without_peer_corruption": True, + "retirement_alpha_then_beta": True, + "retirement_beta_then_alpha": True, + "post_retirement_apps_absent": True, + "post_retirement_provider_inventory_empty": True, + "storage_delta_measured": True, + "github_registration_not_exercised": True, + } + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + "exact packaged provider realized two concurrent source-derived runner Apps " + "on nested TrueNAS; both exact Compose profiles, isolated bootstrap configuration, " + "fresh-process Get/List adoption, active-delete refusal, both retirement orders, " + "empty final inventory, App absence, and bounded ix-apps storage deltas were " + "verified without GitHub/JIT/private/physical/capacity authority" + ) + except g3.FixtureEnvironmentError as exc: + payload["classification"] = "ENVIRONMENT_FAILURE" + payload["detail"] = scrub_text(str(exc), sensitive) + except Exception as exc: + payload["detail"] = scrub_text( + f"{type(exc).__name__}: {exc}", sensitive + ) + finally: + try: + session.connect() + for name in sorted(set(app_names)): + if g3.query_app(session, name) is not None: + fallback_cleanup_used = True + try: + g2.delete_app_if_present(session, name, a.job_timeout) + except Exception as exc: + cleanup_errors.append( + f"fallback delete {name}: {type(exc).__name__}: {exc}" + ) + for name in sorted(set(app_names)): + try: + if g3.query_app(session, name) is not None: + cleanup_errors.append( + f"provider-created App remains after cleanup: {name}" + ) + except Exception as exc: + cleanup_errors.append( + f"verify App absence {name}: {type(exc).__name__}: {exc}" + ) + + if api_key_id is not None: + try: + deleted = session.call("api_key.delete", [api_key_id]) + if deleted is not True: + cleanup_errors.append("api_key.delete did not return true") + except Exception as exc: + cleanup_errors.append( + f"api_key.delete: {type(exc).__name__}: {exc}" + ) + api_key_value = None + + if original_ui_cert_id is not None and imported_cert_id is not None: + try: + session.call( + "system.general.update", + [{"ui_certificate": original_ui_cert_id}], + ) + try: + session.call("system.general.ui_restart", [0]) + except Exception: + pass + except Exception as exc: + cleanup_errors.append( + f"restore ui certificate: {type(exc).__name__}: {exc}" + ) + session.close() + time.sleep(5) + try: + session.connect() + except Exception as exc: + cleanup_errors.append( + f"reconnect after ui restore: {type(exc).__name__}: {exc}" + ) + + if imported_cert_id is not None and session.ws is not None: + try: + delete_result = session.call( + "certificate.delete", [imported_cert_id, False] + ) + if isinstance(delete_result, int): + session.wait_job( + delete_result, "certificate.delete", a.job_timeout + ) + elif delete_result is not True: + cleanup_errors.append( + f"certificate.delete unexpected result {delete_result!r}" + ) + except Exception as exc: + cleanup_errors.append( + f"certificate.delete: {type(exc).__name__}: {exc}" + ) + except Exception as exc: + cleanup_errors.append( + f"cleanup session: {type(exc).__name__}: {exc}" + ) + finally: + session.close() + + payload["cleanup"] = { + "attempted": True, + "fallback_cleanup_used": fallback_cleanup_used, + "errors": cleanup_errors, + "zero_unintended_residue": not cleanup_errors, + "secret_values_recorded": False, + "synthetic_tokens_recorded": False, + } + if cleanup_errors or fallback_cleanup_used: + payload["oracleSatisfied"] = False + payload["classification"] = "ORACLE_FAILURE" + reasons = [] + if fallback_cleanup_used: + reasons.append("provider retirement required fallback direct cleanup") + reasons.extend(cleanup_errors) + payload["detail"] = payload.get("detail", "") + "; cleanup: " + "; ".join(reasons) + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 2c0a6e80c37d1476e281583376a1a48e2b48cd8d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:32:55 +0200 Subject: [PATCH 231/719] test: lock nested G4 capacity-two contract --- tests/test_truenas_garm_provider_g4.py | 198 +++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 tests/test_truenas_garm_provider_g4.py diff --git a/tests/test_truenas_garm_provider_g4.py b/tests/test_truenas_garm_provider_g4.py new file mode 100644 index 000000000..f69521d4c --- /dev/null +++ b/tests/test_truenas_garm_provider_g4.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "truenas_middleware_garm_provider_g4_probe.py" +SPEC = importlib.util.spec_from_file_location("garm_provider_g4_probe", SCRIPT) +assert SPEC and SPEC.loader +MOD = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MOD) + + +class GarmProviderG4Tests(unittest.TestCase): + def fixture_bundle(self) -> dict: + def runner(slot: str, name: str) -> dict: + callback = f"https://fixture.invalid/{slot}/status" + metadata = f"https://fixture.invalid/{slot}/metadata" + token = f"{slot}-placeholder" + return { + "slot": slot, + "bootstrap_template": { + "callback-url": callback, + "metadata-url": metadata, + "instance-token": token, + }, + "expected_app_name": name, + "expected_compose_template": { + "services": { + "runner": { + "environment": { + "GARM_CALLBACK_URL": callback, + "GARM_METADATA_URL": metadata, + "GARM_INSTANCE_TOKEN": token, + } + } + } + }, + } + + return { + "schema": MOD.EXPECTED_FIXTURE_SCHEMA, + "provider_product_source": MOD.EXPECTED_PROVIDER_PRODUCT_SOURCE, + "producer_source": "a" * 40, + "controller_id": MOD.EXPECTED_CONTROLLER_ID, + "pool_id": MOD.EXPECTED_POOL_ID, + "runners": [ + runner("alpha", "garm-g4-controlle-g4-synthetic-c43b29d9"), + runner("beta", "garm-g4-controlle-g4-synthetic-9e19d328"), + ], + "runner": { + "image": MOD.EXPECTED_RUNNER_IMAGE, + "cpu": MOD.EXPECTED_CPU, + "memory_bytes": MOD.EXPECTED_MEMORY_BYTES, + }, + "retirement_orders": [["alpha", "beta"], ["beta", "alpha"]], + "run_local_substitutions": MOD.EXPECTED_SUBSTITUTIONS, + "source_oracles": { + "two_distinct_provider_owned_names": True, + "fresh_manager_adopts_exact_pair": True, + }, + } + + def test_bundle_binds_exact_source_pair_and_substitutions(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + (root / "garm-provider-g4-fixture.json").write_text( + json.dumps(self.fixture_bundle()), encoding="utf-8" + ) + loaded = MOD.load_bundle(root, "a" * 40) + self.assertEqual(len(loaded["runners"]), 2) + self.assertNotEqual( + loaded["runners"][0]["expected_app_name"], + loaded["runners"][1]["expected_app_name"], + ) + + with self.assertRaisesRegex(RuntimeError, "producer source drifted"): + MOD.load_bundle(root, "b" * 40) + + bad = self.fixture_bundle() + bad["run_local_substitutions"] = bad["run_local_substitutions"][:-1] + (root / "garm-provider-g4-fixture.json").write_text( + json.dumps(bad), encoding="utf-8" + ) + with self.assertRaisesRegex(RuntimeError, "substitution allowlist drifted"): + MOD.load_bundle(root, "a" * 40) + + def test_lower_runner_changes_only_three_run_local_values(self): + runner = self.fixture_bundle()["runners"][0] + callback = "https://example.invalid/alpha/status" + metadata = "https://example.invalid/alpha/metadata" + token = "alpha-token" + bootstrap, expected = MOD.lower_runner(runner, callback, metadata, token) + self.assertEqual(bootstrap["callback-url"], callback) + self.assertEqual(bootstrap["metadata-url"], metadata) + self.assertEqual(bootstrap["instance-token"], token) + env = expected["services"]["runner"]["environment"] + self.assertEqual(env["GARM_CALLBACK_URL"], callback) + self.assertEqual(env["GARM_METADATA_URL"], metadata) + self.assertEqual(env["GARM_INSTANCE_TOKEN"], token) + + def test_fixture_urls_are_distinct_public_non_github_boundaries(self): + alpha = MOD.fixture_urls("alpha") + beta = MOD.fixture_urls("beta") + self.assertNotEqual(alpha, beta) + for callback, metadata in (alpha, beta): + self.assertTrue(callback.startswith("https://httpbin.org/")) + self.assertTrue(metadata.startswith("https://httpbin.org/drip?")) + self.assertIn("delay=60", metadata) + + def test_boundary_includes_concurrent_create_reconnect_and_both_retirements(self): + text = SCRIPT.read_text(encoding="utf-8") + required = ( + "ThreadPoolExecutor(max_workers=2)", + '"CreateInstance"', + '"GetInstance"', + '"ListInstances"', + '"DeleteInstance"', + "require_active_delete_refusal", + '("alpha", "beta")', + '("beta", "alpha")', + '"fresh_process_get_list_exact_pair": True', + '"active_delete_refused_without_peer_corruption": True', + '"retirement_alpha_then_beta": True', + '"retirement_beta_then_alpha": True', + '"post_retirement_provider_inventory_empty": True', + ) + for item in required: + self.assertIn(item, text) + + def test_public_boundary_and_cleanup_fail_closed(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + '"github_credentials_present": False', + '"github_jit_registration_exercised": False', + '"private_repository_execution": False', + '"physical_truenas_mutation": False', + '"capacity_promotion": False', + '"fallback_cleanup_used": fallback_cleanup_used', + ): + self.assertIn(required, text) + self.assertNotIn("GITHUB_TOKEN", text) + self.assertNotIn("GH_TOKEN", text) + self.assertNotIn("PRIVATE_KEY", text) + + def test_resource_observation_uses_exact_compose_and_ix_apps_statfs(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('"aggregate_cpu_limit_units": EXPECTED_CPU * 2', text) + self.assertIn('"aggregate_memory_limit_bytes": EXPECTED_MEMORY_BYTES * 2', text) + self.assertIn('"filesystem.statfs"', text) + self.assertIn('IX_APPS_PATH = "/mnt/.ix-apps"', text) + self.assertIn('"storage_delta_measured": True', text) + + def test_active_delete_refusal_requires_target_remain_present(self): + class FakeSession: + pass + + original_invoke = MOD.invoke_provider + original_query = MOD.g3.query_app + try: + class CP: + returncode = 1 + stdout = "" + stderr = "active" + + MOD.invoke_provider = lambda *args, **kwargs: CP() + MOD.g3.query_app = lambda session, name: {"id": name, "state": "RUNNING"} + MOD.require_active_delete_refusal( + Path("/provider"), + Path("/config"), + "api-key", + Path("/ca"), + "garm-alpha", + ["token"], + FakeSession(), + ) + + MOD.g3.query_app = lambda session, name: None + with self.assertRaisesRegex(RuntimeError, "lost the target App"): + MOD.require_active_delete_refusal( + Path("/provider"), + Path("/config"), + "api-key", + Path("/ca"), + "garm-alpha", + ["token"], + FakeSession(), + ) + finally: + MOD.invoke_provider = original_invoke + MOD.g3.query_app = original_query + + +if __name__ == "__main__": + unittest.main() From 97fe71748805b4c4229845d5c51e86e5dd2f63a1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:33:34 +0200 Subject: [PATCH 232/719] rdte: route nested G4 provider capacity-two probe --- scripts/gha_kvm_truenas_rdte.sh | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index f54943d4e..d0f2a6b5f 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA]" } OUT="" @@ -30,6 +30,8 @@ G2_FIXTURE_DIR="" G2_FIXTURE_PRODUCER="" G3_FIXTURE_DIR="" G3_FIXTURE_PRODUCER="" +G4_FIXTURE_DIR="" +G4_FIXTURE_PRODUCER="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -43,6 +45,8 @@ while [[ $# -gt 0 ]]; do --g2-fixture-producer) G2_FIXTURE_PRODUCER="$2"; shift 2 ;; --g3-fixture-dir) G3_FIXTURE_DIR="$2"; shift 2 ;; --g3-fixture-producer) G3_FIXTURE_PRODUCER="$2"; shift 2 ;; + --g4-fixture-dir) G4_FIXTURE_DIR="$2"; shift 2 ;; + --g4-fixture-producer) G4_FIXTURE_PRODUCER="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -57,7 +61,7 @@ if [[ "$RUNG" == "t6" ]]; then if [[ "$T6_PRODUCT" != "official-catalog" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -66,6 +70,10 @@ if [[ "$RUNG" == "t6" ]]; then [[ -n "$G3_FIXTURE_DIR" && -d "$G3_FIXTURE_DIR" ]] || { echo "garm-provider-g3 requires --g3-fixture-dir" >&2; exit 2; } [[ "$G3_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g3 requires exact --g3-fixture-producer SHA" >&2; exit 2; } G3_FIXTURE_DIR="$(realpath "$G3_FIXTURE_DIR")" + elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then + [[ -n "$G4_FIXTURE_DIR" && -d "$G4_FIXTURE_DIR" ]] || { echo "garm-provider-g4 requires --g4-fixture-dir" >&2; exit 2; } + [[ "$G4_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g4 requires exact --g4-fixture-producer SHA" >&2; exit 2; } + G4_FIXTURE_DIR="$(realpath "$G4_FIXTURE_DIR")" else [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } @@ -73,7 +81,7 @@ if [[ "$RUNG" == "t6" ]]; then fi fi -if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "garm-provider-g3" ]]; then +if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" ) ]]; then VCPUS=4 fi @@ -146,6 +154,8 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g2" else {"name": "garm-provider-g3-runner", "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g3" + else {"name": "garm-provider-g4-pair", "count": 2, "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g4" else {"name": "rdte-t6-garm", "image": "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm" else {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} @@ -254,6 +264,11 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G3 client" command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" + elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_garm_provider_g4_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G4 client" + command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" + command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" elif [[ "$T6_PRODUCT" == "garm" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 GARM control client" @@ -686,6 +701,13 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then --fixture-dir "$G3_FIXTURE_DIR" \ --fixture-producer-commit "$G3_FIXTURE_PRODUCER" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_garm_provider_g4_probe.py" \ + --host 127.0.0.1 --http-port "$HTTP_PORT" --https-port "$HTTPS_PORT" \ + --password-file "$PASSWORD_FILE" \ + --fixture-dir "$G4_FIXTURE_DIR" \ + --fixture-producer-commit "$G4_FIXTURE_PRODUCER" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "garm" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$GARM_HOST_PORT" \ @@ -728,6 +750,8 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider passed verified WSS/API-key transport, valid local adoption, foreign exclusion, managed-drift fail-closed behavior, fresh-process readoption, and zero-residue cleanup; provider create/delete and GitHub/JIT intentionally not exercised" elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider CreateInstance realized the source-derived fixed runner App profile, exact Compose read-back and provider Get/List reconciliation passed, supported middleware moved the experiment App inactive, provider DeleteInstance retired it, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" +elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider realized two source-derived runner Apps concurrently, exact Compose and fresh-process Get/List reconciliation passed for both, active deletion failed closed, both retirement orders reached empty inventory with zero residue, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then From 59be7f8dd74050968643f6a53b538a164199e5a0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:34:31 +0200 Subject: [PATCH 233/719] rdte: route source-exact G4 fixture and probe --- .github/workflows/gha-kvm-system-rdte.yml | 33 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 1ebac1a6c..6344dabab 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -49,6 +49,7 @@ on: - garm - garm-provider-g2 - garm-provider-g3 + - garm-provider-g4 - official-catalog permissions: @@ -108,7 +109,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_garm_provider_g4_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -125,7 +126,7 @@ jobs: jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "official-catalog")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "official-catalog")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null @@ -146,7 +147,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -203,6 +204,15 @@ jobs: permissions: contents: read + provider-g4-fixture: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' + uses: SemperSupra/garm-provider-truenas/.github/workflows/export-g4-nested-fixture.yml@e5fc7c50780de54f7579f64990c87b2e16482613 + with: + provider_ref: e5fc7c50780de54f7579f64990c87b2e16482613 + permissions: + contents: read + qemu-t3-topology: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' @@ -284,7 +294,7 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures, provider-g3-fixture] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture] if: >- always() && needs.contract.result == 'success' && @@ -297,7 +307,8 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || - (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') ) runs-on: ubuntu-26.04 timeout-minutes: 50 @@ -341,6 +352,12 @@ jobs: with: name: garm-provider-truenas-g3-fixture path: ${{ runner.temp }}/gha-kvm-system-rdte/g3-fixture + - name: Download exact GARM provider G4 fixture + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-provider-truenas-g4-fixture + path: ${{ runner.temp }}/gha-kvm-system-rdte/g4-fixture - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -370,6 +387,12 @@ jobs: --g3-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g3-fixture" --g3-fixture-producer "e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab" ) + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-provider-g4" ]]; then + args+=( + --t6-product "garm-provider-g4" + --g4-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g4-fixture" + --g4-fixture-producer "e5fc7c50780de54f7579f64990c87b2e16482613" + ) else if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" From 1304c159188f83edaf30c8daee3422e714779936 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:35:17 +0200 Subject: [PATCH 234/719] rdte: gate exact G4 pair on public Docker --- .github/workflows/prep-rdte-static.yml | 79 +++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 4a3aa7d40..ffc5ea25e 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -21,6 +21,7 @@ on: - 'tests/test_truenas_t6_garm_control.py' - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_garm_provider_g3.py' + - 'tests/test_truenas_garm_provider_g4.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_rdte_target.py' pull_request: @@ -39,6 +40,7 @@ on: - 'tests/test_truenas_t6_garm_control.py' - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_garm_provider_g3.py' + - 'tests/test_truenas_garm_provider_g4.py' - 'tests/test_truenas_rdte_target.py' permissions: @@ -47,7 +49,7 @@ permissions: jobs: static: runs-on: ubuntu-24.04 - timeout-minutes: 8 + timeout-minutes: 12 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -83,6 +85,9 @@ jobs: if [[ -f tests/test_truenas_garm_provider_g3.py ]]; then tests+=(tests/test_truenas_garm_provider_g3.py) fi + if [[ -f tests/test_truenas_garm_provider_g4.py ]]; then + tests+=(tests/test_truenas_garm_provider_g4.py) + fi if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi @@ -120,3 +125,75 @@ jobs: https://github.com/actions/runner/releases/download/v2.336.0/actions-runner-linux-x64-2.336.0.tar.gz \ -o /dev/null ' + + - name: G4 exact exported pair pre-JIT concurrency smoke + if: ${{ hashFiles('scripts/truenas_middleware_garm_provider_g4_probe.py') != '' }} + run: | + set -euo pipefail + provider_ref='e5fc7c50780de54f7579f64990c87b2e16482613' + rm -rf /tmp/g4-provider /tmp/g4-fixture.json /tmp/g4-alpha.json /tmp/g4-beta.json + git init -q /tmp/g4-provider + git -C /tmp/g4-provider fetch -q --depth=1 \ + https://github.com/SemperSupra/garm-provider-truenas.git "$provider_ref" + git -C /tmp/g4-provider checkout -q --detach FETCH_HEAD + [[ "$(git -C /tmp/g4-provider rev-parse HEAD)" == "$provider_ref" ]] + + ( + cd /tmp/g4-provider + G4_FIXTURE_OUT=/tmp/g4-fixture.json \ + G4_PRODUCER_SOURCE="$provider_ref" \ + go test ./internal/truenasstore \ + -run '^TestExportNestedG4CapacityTwoFixtureBundle$' -count=1 + ) + test -s /tmp/g4-fixture.json + + for slot in alpha beta; do + callback="https://httpbin.org/anything/sempersupra-g4/$slot/status" + metadata="https://httpbin.org/drip?duration=1&numbytes=1&code=200&delay=60&path=$slot" + token="g4-public-static-$slot-token" + jq --arg slot "$slot" --arg callback "$callback" --arg metadata "$metadata" --arg token "$token" ' + first(.runners[] | select(.slot == $slot) | .expected_compose_template) + | .services.runner.environment.GARM_CALLBACK_URL = $callback + | .services.runner.environment.GARM_METADATA_URL = $metadata + | .services.runner.environment.GARM_INSTANCE_TOKEN = $token + ' /tmp/g4-fixture.json > "/tmp/g4-$slot.json" + done + + cleanup() { + docker compose -p g4-smoke-alpha -f /tmp/g4-alpha.json down -v --remove-orphans >/dev/null 2>&1 || true + docker compose -p g4-smoke-beta -f /tmp/g4-beta.json down -v --remove-orphans >/dev/null 2>&1 || true + } + trap cleanup EXIT + + docker compose -p g4-smoke-alpha -f /tmp/g4-alpha.json up -d & + alpha_up=$! + docker compose -p g4-smoke-beta -f /tmp/g4-beta.json up -d & + beta_up=$! + wait "$alpha_up" + wait "$beta_up" + + deadline=$((SECONDS + 140)) + while (( SECONDS < deadline )); do + ready=0 + for slot in alpha beta; do + cid="$(docker compose -p "g4-smoke-$slot" -f "/tmp/g4-$slot.json" ps -aq runner || true)" + [[ -n "$cid" ]] || continue + if [[ "$(docker inspect -f '{{.State.Running}}' "$cid")" != "true" ]]; then + docker inspect "$cid" || true + docker logs "$cid" || true + exit 1 + fi + if docker exec "$cid" test -x /home/runner/actions-runner/run.sh; then + ready=$((ready + 1)) + fi + done + [[ "$ready" == "2" ]] && break + sleep 2 + done + + for slot in alpha beta; do + cid="$(docker compose -p "g4-smoke-$slot" -f "/tmp/g4-$slot.json" ps -aq runner)" + [[ "$(docker inspect -f '{{.State.Running}}' "$cid")" == "true" ]] + docker exec "$cid" test -x /home/runner/actions-runner/bin/Runner.Listener + docker exec "$cid" test -w /run/garm-jit + done From 2c2868704084dababeeab466a72f9ad592932e8b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:35:34 +0200 Subject: [PATCH 235/719] test: lock G4 routing and four-vCPU embodiment --- tests/test_gha_kvm_system_rdte.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 1d1c6528a..a42201234 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -201,11 +201,11 @@ def test_truenas_uses_exact_target_registry_and_vendor_digest_sidecar(self): self.assertNotIn('hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080', text) self.assertNotIn("xdotool", text) - def test_g3_nested_guest_meets_fixed_four_cpu_runner_profile(self): + def test_g3_and_g4_nested_guest_meet_fixed_four_cpu_runner_profile(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn('VCPUS=2', text) self.assertIn( - 'if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "garm-provider-g3" ]]; then', + 'if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" ) ]]; then', text, ) self.assertIn('VCPUS=4', text) @@ -214,6 +214,26 @@ def test_g3_nested_guest_meets_fixed_four_cpu_runner_profile(self): self.assertIn('R_VCPUS="$VCPUS"', text) self.assertIn('"vcpus": int(os.environ["R_VCPUS"])', text) self.assertIn('"ram_mib": int(os.environ["R_RAM_MIB"])', text) + self.assertIn('"garm-provider-g4-pair"', text) + self.assertIn('"count": 2', text) + + def test_g4_routing_is_source_exact_and_public_safe(self): + workflow = (ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml").read_text( + encoding="utf-8" + ) + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn("garm-provider-g4", workflow) + self.assertIn( + "export-g4-nested-fixture.yml@e5fc7c50780de54f7579f64990c87b2e16482613", + workflow, + ) + self.assertIn( + '--g4-fixture-producer "e5fc7c50780de54f7579f64990c87b2e16482613"', + workflow, + ) + self.assertIn("truenas_middleware_garm_provider_g4_probe.py", text) + self.assertIn('G4_FIXTURE_PRODUCER', text) + self.assertNotIn("GITHUB_TOKEN", (ROOT / "scripts" / "truenas_middleware_garm_provider_g4_probe.py").read_text(encoding="utf-8")) def test_no_literal_escaped_shell_parameter_expansions(self): needle = chr(92) + "$" + "{" From d8f45a78e6d582ac339c327953f40676e2f31c12 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:38:26 +0200 Subject: [PATCH 236/719] rdte: stage nested G4 capacity-two qualification --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index a489f277e..88cf3c0ae 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g3-005-four-vcpu-corrective", + "request_id": "tn-26-beta3-t6-garm-provider-g4-001-capacity-two-reconnect-retirement", "version": "26.0.0-BETA.3", "rung": "t6", - "product": "garm-provider-g3", + "product": "garm-provider-g4", "authority_issue": 396, - "consumer_authority_issue": 40, - "purpose": "Re-run exact TrueNAS 26.0.0-BETA.3 GARM provider G3 after correcting only the nested RDTE embodiment from 2 to 4 vCPUs so Docker can admit the fixed provider-exported cpus=4 runner profile; require the unchanged G3 CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absent -> empty provider inventory -> zero-residue oracle, with GitHub/JIT/private workload/physical mutation/capacity promotion still unexercised." + "consumer_authority_issue": 41, + "purpose": "Qualify exact TrueNAS 26.0.0-BETA.3 GARM provider G4 on the accepted four-vCPU nested RDTE embodiment: realize exactly two source-derived provider-owned runner Apps with concurrent packaged-provider CreateInstance calls, exact Compose read-back for both, isolated callback/metadata/token configuration, fresh-process Get/List adoption, active-delete fail-closed behavior, both alpha->beta and beta->alpha retirement orders, measured ix-apps storage deltas, final App absence, empty provider inventory, and zero unintended residue; GitHub JIT/private workload/physical TrueNAS/capacity promotion remain unexercised." } From 46372a6a1ebc818e207c68c376389691eac107a6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:39:22 +0200 Subject: [PATCH 237/719] test: admit G4 as explicit T6 product --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index b1b6be9f1..3c6522a0a 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "official-catalog"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "official-catalog"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From 817b343dda6a9e9f106d63eb8b29424aa5de5ef5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 00:39:25 +0200 Subject: [PATCH 238/719] test: register G4 run-request product --- tests/test_truenas_rdte_target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index da8441eee..afcd193d9 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -66,7 +66,7 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","official-catalog"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","garm-provider-g4","official-catalog"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": From e62d2e89789d88b4a532e070b2a2af729efb91a8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:23:04 +0200 Subject: [PATCH 239/719] rdte: parameterize G3 lifecycle oracle by exact TrueNAS version --- scripts/truenas_middleware_garm_provider_g3_probe.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_garm_provider_g3_probe.py b/scripts/truenas_middleware_garm_provider_g3_probe.py index 1746ffc7c..54b7f63a5 100644 --- a/scripts/truenas_middleware_garm_provider_g3_probe.py +++ b/scripts/truenas_middleware_garm_provider_g3_probe.py @@ -367,6 +367,7 @@ def main() -> int: p.add_argument("--password-file", required=True) p.add_argument("--fixture-dir", type=pathlib.Path, required=True) p.add_argument("--fixture-producer-commit", required=True) + p.add_argument("--expected-version", default=EXPECTED_VERSION) p.add_argument("--callback-url", default=DEFAULT_CALLBACK_URL) p.add_argument("--metadata-url", default=DEFAULT_METADATA_URL) p.add_argument("--out", required=True) @@ -379,7 +380,7 @@ def main() -> int: "schema": "truenas-garm-provider-g3/v1", "classification": "ORACLE_FAILURE", "oracleSatisfied": False, - "expected_version": EXPECTED_VERSION, + "expected_version": a.expected_version, "provider_product_source": EXPECTED_PROVIDER_PRODUCT_SOURCE, "provider_binary_sha256": EXPECTED_PROVIDER_BINARY_SHA256, "appliance": EXPECTED_APPLIANCE, @@ -455,7 +456,7 @@ def main() -> int: session.connect() version = session.call("system.version", []) payload["system_version"] = version - if version != EXPECTED_VERSION: + if version != a.expected_version: raise RuntimeError(f"target version drifted: {version}") general = session.call("system.general.config", []) From 4c5d9305aacf0074a418af6b76f6347e1cca4e15 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:23:51 +0200 Subject: [PATCH 240/719] rdte: add exact-version G5 provider row oracle --- ...uenas_middleware_garm_provider_g5_probe.py | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 scripts/truenas_middleware_garm_provider_g5_probe.py diff --git a/scripts/truenas_middleware_garm_provider_g5_probe.py b/scripts/truenas_middleware_garm_provider_g5_probe.py new file mode 100644 index 000000000..a4f13cb49 --- /dev/null +++ b/scripts/truenas_middleware_garm_provider_g5_probe.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""G5 oracle: bind one exact TrueNAS source-matrix row to the proven provider lifecycle oracle.""" +from __future__ import annotations + +import argparse +import json +import pathlib +import subprocess +import sys +import tempfile +import time + +import truenas_middleware_garm_provider_g3_probe as g3 + + +EXPECTED_MATRIX_SCHEMA = "semper-supra.garm-provider-truenas-g5-version-matrix/1" +EXPECTED_PROVIDER_PRODUCT_SOURCE = g3.EXPECTED_PROVIDER_PRODUCT_SOURCE + + +def load_matrix(directory: pathlib.Path, expected_producer: str) -> dict: + path = directory / "garm-provider-truenas-g5-version-matrix.json" + matrix = json.loads(path.read_text(encoding="utf-8")) + if matrix.get("schema") != EXPECTED_MATRIX_SCHEMA: + raise RuntimeError("unexpected G5 version-matrix schema") + if matrix.get("provider_product_source") != EXPECTED_PROVIDER_PRODUCT_SOURCE: + raise RuntimeError("G5 matrix provider product source drifted") + if matrix.get("producer_source") != expected_producer: + raise RuntimeError("G5 matrix producer source drifted") + if matrix.get("runtime_inheritance_allowed") is not False: + raise RuntimeError("G5 matrix must prohibit runtime support inheritance") + oracles = matrix.get("source_oracles") + if not isinstance(oracles, dict) or not oracles or not all(value is True for value in oracles.values()): + raise RuntimeError("G5 matrix source oracles are not all true") + targets = matrix.get("targets") + if not isinstance(targets, list) or not targets: + raise RuntimeError("G5 matrix has no targets") + return matrix + + +def select_row(matrix: dict, target_version: str, expected_middleware_commit: str) -> dict: + matches = [ + row for row in matrix["targets"] + if isinstance(row, dict) and row.get("version") == target_version + ] + if len(matches) != 1: + raise RuntimeError(f"G5 matrix target cardinality drifted for {target_version}: {len(matches)}") + row = matches[0] + expected_system_version = f"TrueNAS-{target_version}" + if row.get("system_version") != expected_system_version: + raise RuntimeError( + f"G5 matrix system_version drifted: {row.get('system_version')!r} != {expected_system_version!r}" + ) + if row.get("middleware_commit") != expected_middleware_commit: + raise RuntimeError( + "G5 matrix middleware commit drifted: " + f"{row.get('middleware_commit')!r} != {expected_middleware_commit!r}" + ) + blobs = row.get("source_blobs") + if not isinstance(blobs, dict) or len(blobs) < 7: + raise RuntimeError("G5 matrix row has incomplete source blob evidence") + if not isinstance(row.get("source_equivalence_group"), str) or not row["source_equivalence_group"]: + raise RuntimeError("G5 matrix row has no source equivalence group") + return row + + +def run_underlying_g3( + args: argparse.Namespace, + expected_system_version: str, + out: pathlib.Path, +) -> subprocess.CompletedProcess[str]: + script = pathlib.Path(__file__).with_name("truenas_middleware_garm_provider_g3_probe.py") + cmd = [ + sys.executable, + str(script), + "--host", args.host, + "--http-port", str(args.http_port), + "--https-port", str(args.https_port), + "--password-file", args.password_file, + "--fixture-dir", str(args.fixture_dir), + "--fixture-producer-commit", args.fixture_producer_commit, + "--expected-version", expected_system_version, + "--out", str(out), + "--timeout", str(args.timeout), + "--job-timeout", str(args.job_timeout), + ] + return subprocess.run(cmd, text=True, capture_output=True, check=False, timeout=900) + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--http-port", type=int, required=True) + p.add_argument("--https-port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--matrix-dir", type=pathlib.Path, required=True) + p.add_argument("--matrix-producer-commit", required=True) + p.add_argument("--fixture-dir", type=pathlib.Path, required=True) + p.add_argument("--fixture-producer-commit", required=True) + p.add_argument("--target-version", required=True) + p.add_argument("--expected-middleware-commit", required=True) + p.add_argument("--out", required=True) + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-garm-provider-g5-version-row/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "target_version": a.target_version, + "matrix_producer_source": a.matrix_producer_commit, + "fixture_producer_source": a.fixture_producer_commit, + "provider_product_source": EXPECTED_PROVIDER_PRODUCT_SOURCE, + "runtime_inheritance_allowed": False, + "github_credentials_present": False, + "github_jit_registration_exercised": False, + "private_repository_execution": False, + "physical_truenas_mutation": False, + "capacity_promotion": False, + } + + try: + matrix = load_matrix(a.matrix_dir, a.matrix_producer_commit) + row = select_row(matrix, a.target_version, a.expected_middleware_commit) + payload["matrix_row"] = row + payload["matrix_source_oracles"] = matrix["source_oracles"] + payload["source_equivalence_group"] = row["source_equivalence_group"] + payload["expected_system_version"] = row["system_version"] + + with tempfile.TemporaryDirectory(prefix="garm-provider-g5-") as td: + underlying_path = pathlib.Path(td) / "provider-lifecycle.json" + cp = run_underlying_g3(a, row["system_version"], underlying_path) + if not underlying_path.exists(): + detail = (cp.stderr or cp.stdout or "")[-1600:] + raise RuntimeError( + f"underlying provider lifecycle emitted no receipt rc={cp.returncode}: {detail}" + ) + underlying = json.loads(underlying_path.read_text(encoding="utf-8")) + + payload["underlying_provider_lifecycle"] = underlying + if underlying.get("provider_product_source") != EXPECTED_PROVIDER_PRODUCT_SOURCE: + raise RuntimeError("underlying provider product source drifted") + if underlying.get("provider_binary_sha256") != g3.EXPECTED_PROVIDER_BINARY_SHA256: + raise RuntimeError("underlying provider binary identity drifted") + if underlying.get("system_version") != row["system_version"]: + raise RuntimeError( + f"runtime system version drifted: {underlying.get('system_version')!r}" + ) + cleanup = underlying.get("cleanup") + if not isinstance(cleanup, dict): + raise RuntimeError("underlying lifecycle has no cleanup receipt") + if cleanup.get("fallback_cleanup_used") is not False: + raise RuntimeError("underlying lifecycle required fallback cleanup") + if cleanup.get("zero_unintended_residue") is not True: + raise RuntimeError("underlying lifecycle did not prove zero unintended residue") + if underlying.get("oracleSatisfied") is not True or underlying.get("classification") != "SUPPORTED": + raise RuntimeError( + "underlying exact-version provider lifecycle did not satisfy its oracle: " + + str(underlying.get("detail") or underlying.get("classification")) + ) + + payload["oracles"] = { + "exact_source_matrix_row_bound": True, + "exact_middleware_commit_bound": True, + "exact_runtime_system_version": True, + "exact_provider_identity": True, + "provider_lifecycle_supported": True, + "zero_unintended_residue": True, + "runtime_inheritance_prohibited": True, + "github_registration_not_exercised": True, + } + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = ( + f"exact TrueNAS {a.target_version} source-matrix row matched the RDTE target and " + "the fixed packaged provider completed CreateInstance, exact Compose read-back, " + "Get/List, supported inactive transition, DeleteInstance, App absence, empty " + "inventory, and zero-residue cleanup without GitHub/JIT/private/physical/capacity authority" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 28463f9f001bfc84f38fb9b2ab7eb82084eca418 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:24:22 +0200 Subject: [PATCH 241/719] test: lock exact-version G5 row contract --- tests/test_truenas_garm_provider_g5.py | 106 +++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/test_truenas_garm_provider_g5.py diff --git a/tests/test_truenas_garm_provider_g5.py b/tests/test_truenas_garm_provider_g5.py new file mode 100644 index 000000000..26bef9041 --- /dev/null +++ b/tests/test_truenas_garm_provider_g5.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "truenas_middleware_garm_provider_g5_probe.py" +G3_SCRIPT = ROOT / "scripts" / "truenas_middleware_garm_provider_g3_probe.py" +SPEC = importlib.util.spec_from_file_location("garm_provider_g5_probe", SCRIPT) +assert SPEC and SPEC.loader +MOD = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MOD) + + +class GarmProviderG5Tests(unittest.TestCase): + def matrix(self) -> dict: + return { + "schema": MOD.EXPECTED_MATRIX_SCHEMA, + "provider_product_source": MOD.EXPECTED_PROVIDER_PRODUCT_SOURCE, + "producer_source": "f" * 40, + "runtime_inheritance_allowed": False, + "source_oracles": { + "exact_middleware_commits": True, + "exact_git_blob_identities": True, + "provider_method_surface_scan": True, + "existing_25_04_1_contract_consistent": True, + "runtime_inheritance_prohibited": True, + }, + "targets": [ + { + "version": "25.10.7", + "system_version": "TrueNAS-25.10.7", + "middleware_ref": "TS-25.10.7", + "middleware_commit": "8ede398839710e56893d88ce85088139d8fab18e", + "source_equivalence_group": "goldeye-25.10.7-provider-control", + "source_blobs": {f"path-{i}": f"{i:040x}"[-40:] for i in range(7)}, + } + ], + } + + def test_matrix_binds_exact_row_and_prohibits_inheritance(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + path = root / "garm-provider-truenas-g5-version-matrix.json" + path.write_text(json.dumps(self.matrix()), encoding="utf-8") + loaded = MOD.load_matrix(root, "f" * 40) + row = MOD.select_row( + loaded, + "25.10.7", + "8ede398839710e56893d88ce85088139d8fab18e", + ) + self.assertEqual(row["system_version"], "TrueNAS-25.10.7") + + with self.assertRaisesRegex(RuntimeError, "middleware commit drifted"): + MOD.select_row(loaded, "25.10.7", "0" * 40) + + bad = self.matrix() + bad["runtime_inheritance_allowed"] = True + path.write_text(json.dumps(bad), encoding="utf-8") + with self.assertRaisesRegex(RuntimeError, "prohibit runtime support inheritance"): + MOD.load_matrix(root, "f" * 40) + + def test_matrix_rejects_false_source_oracle(self): + with tempfile.TemporaryDirectory() as td: + root = Path(td) + bad = self.matrix() + bad["source_oracles"]["exact_git_blob_identities"] = False + (root / "garm-provider-truenas-g5-version-matrix.json").write_text( + json.dumps(bad), encoding="utf-8" + ) + with self.assertRaisesRegex(RuntimeError, "source oracles are not all true"): + MOD.load_matrix(root, "f" * 40) + + def test_g3_lifecycle_is_version_parameterized_without_changing_default(self): + text = G3_SCRIPT.read_text(encoding="utf-8") + self.assertIn('p.add_argument("--expected-version", default=EXPECTED_VERSION)', text) + self.assertIn('"expected_version": a.expected_version', text) + self.assertIn("if version != a.expected_version:", text) + + def test_wrapper_passes_exact_system_version_to_underlying_lifecycle(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('"--expected-version", expected_system_version', text) + self.assertIn('"--fixture-producer-commit", args.fixture_producer_commit', text) + self.assertIn('"provider_lifecycle_supported": True', text) + self.assertIn('"runtime_inheritance_prohibited": True', text) + self.assertIn('"zero_unintended_residue": True', text) + + def test_public_boundary_remains_explicit(self): + text = SCRIPT.read_text(encoding="utf-8") + for required in ( + '"github_credentials_present": False', + '"github_jit_registration_exercised": False', + '"private_repository_execution": False', + '"physical_truenas_mutation": False', + '"capacity_promotion": False', + ): + self.assertIn(required, text) + self.assertNotIn("GITHUB_TOKEN", text) + self.assertNotIn("GH_TOKEN", text) + + +if __name__ == "__main__": + unittest.main() From d8954ae70fe83da904e10f2b5f6f16234bd106d8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:25:44 +0200 Subject: [PATCH 242/719] rdte: route exact-version G5 provider rows --- scripts/gha_kvm_truenas_rdte.sh | 39 +++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index d0f2a6b5f..a227300e7 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA]" } OUT="" @@ -32,6 +32,8 @@ G3_FIXTURE_DIR="" G3_FIXTURE_PRODUCER="" G4_FIXTURE_DIR="" G4_FIXTURE_PRODUCER="" +G5_MATRIX_DIR="" +G5_MATRIX_PRODUCER="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -47,6 +49,8 @@ while [[ $# -gt 0 ]]; do --g3-fixture-producer) G3_FIXTURE_PRODUCER="$2"; shift 2 ;; --g4-fixture-dir) G4_FIXTURE_DIR="$2"; shift 2 ;; --g4-fixture-producer) G4_FIXTURE_PRODUCER="$2"; shift 2 ;; + --g5-matrix-dir) G5_MATRIX_DIR="$2"; shift 2 ;; + --g5-matrix-producer) G5_MATRIX_PRODUCER="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -58,10 +62,10 @@ TARGET_ENV="$(python3 "$SCRIPT_DIR/truenas_rdte_target.py" --registry "$TARGET_R eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" != "official-catalog" ]]; then + if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -74,6 +78,13 @@ if [[ "$RUNG" == "t6" ]]; then [[ -n "$G4_FIXTURE_DIR" && -d "$G4_FIXTURE_DIR" ]] || { echo "garm-provider-g4 requires --g4-fixture-dir" >&2; exit 2; } [[ "$G4_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g4 requires exact --g4-fixture-producer SHA" >&2; exit 2; } G4_FIXTURE_DIR="$(realpath "$G4_FIXTURE_DIR")" + elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then + [[ -n "$G5_MATRIX_DIR" && -d "$G5_MATRIX_DIR" ]] || { echo "garm-provider-g5 requires --g5-matrix-dir" >&2; exit 2; } + [[ "$G5_MATRIX_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g5 requires exact --g5-matrix-producer SHA" >&2; exit 2; } + [[ -n "$G3_FIXTURE_DIR" && -d "$G3_FIXTURE_DIR" ]] || { echo "garm-provider-g5 requires --g3-fixture-dir" >&2; exit 2; } + [[ "$G3_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g5 requires exact --g3-fixture-producer SHA" >&2; exit 2; } + G5_MATRIX_DIR="$(realpath "$G5_MATRIX_DIR")" + G3_FIXTURE_DIR="$(realpath "$G3_FIXTURE_DIR")" else [[ -n "$FOUNDRY_CONTROL_DIR" && -d "$FOUNDRY_CONTROL_DIR" ]] || { echo "t6 requires --foundry-control-dir" >&2; exit 2; } [[ "$FOUNDRY_COMMIT" =~ ^[0-9a-f]{40}$ ]] || { echo "t6 requires exact --foundry-commit SHA" >&2; exit 2; } @@ -81,7 +92,7 @@ if [[ "$RUNG" == "t6" ]]; then fi fi -if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" ) ]]; then +if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" ) ]]; then VCPUS=4 fi @@ -156,6 +167,8 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g3" else {"name": "garm-provider-g4-pair", "count": 2, "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g4" + else {"name": "garm-provider-g5-version-row", "count": 1, "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g5" else {"name": "rdte-t6-garm", "image": "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm" else {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} @@ -269,6 +282,11 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G4 client" command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" + elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_garm_provider_g5_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G5 client" + command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" + command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" elif [[ "$T6_PRODUCT" == "garm" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 GARM control client" @@ -708,6 +726,17 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then --fixture-dir "$G4_FIXTURE_DIR" \ --fixture-producer-commit "$G4_FIXTURE_PRODUCER" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_garm_provider_g5_probe.py" \ + --host 127.0.0.1 --http-port "$HTTP_PORT" --https-port "$HTTPS_PORT" \ + --password-file "$PASSWORD_FILE" \ + --matrix-dir "$G5_MATRIX_DIR" \ + --matrix-producer-commit "$G5_MATRIX_PRODUCER" \ + --fixture-dir "$G3_FIXTURE_DIR" \ + --fixture-producer-commit "$G3_FIXTURE_PRODUCER" \ + --target-version "$VERSION" \ + --expected-middleware-commit "$MIDDLEWARE_COMMIT" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "garm" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$GARM_HOST_PORT" \ @@ -752,6 +781,8 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider CreateInstance realized the source-derived fixed runner App profile, exact Compose read-back and provider Get/List reconciliation passed, supported middleware moved the experiment App inactive, provider DeleteInstance retired it, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider realized two source-derived runner Apps concurrently, exact Compose and fresh-process Get/List reconciliation passed for both, active deletion failed closed, both retirement orders reached empty inventory with zero residue, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" +elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact G5 TrueNAS source-matrix row matched the RDTE target and the fixed packaged provider completed the capacity-one Create/read-back/Get/List/inactive/Delete/absence/empty-inventory/zero-residue lifecycle without runtime support inheritance or GitHub/JIT/private/physical/capacity claims" elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then From 1c873f1a3dfa3450435308c4225aacc45d0810b1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:26:52 +0200 Subject: [PATCH 243/719] rdte: route exact G5 matrix and runner fixture --- .github/workflows/gha-kvm-system-rdte.yml | 50 ++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 6344dabab..85d3bcd6f 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -50,6 +50,7 @@ on: - garm-provider-g2 - garm-provider-g3 - garm-provider-g4 + - garm-provider-g5 - official-catalog permissions: @@ -109,7 +110,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_garm_provider_g4_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_garm_provider_g4_probe.py|scripts/truenas_middleware_garm_provider_g5_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -126,7 +127,7 @@ jobs: jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "official-catalog")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null @@ -147,7 +148,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -213,6 +214,24 @@ jobs: permissions: contents: read + provider-g5-runner-fixture: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g5' + uses: SemperSupra/garm-provider-truenas/.github/workflows/export-g3-nested-fixture.yml@e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab + with: + provider_ref: e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab + permissions: + contents: read + + provider-g5-version-matrix: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g5' + uses: SemperSupra/garm-provider-truenas/.github/workflows/export-g5-version-matrix.yml@f83a712e095c6962741f93cf86bea69479ce4d4a + with: + provider_ref: f83a712e095c6962741f93cf86bea69479ce4d4a + permissions: + contents: read + qemu-t3-topology: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' @@ -294,7 +313,7 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] if: >- always() && needs.contract.result == 'success' && @@ -308,7 +327,8 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || - (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g5' && needs['provider-g5-runner-fixture'].result == 'success' && needs['provider-g5-version-matrix'].result == 'success') ) runs-on: ubuntu-26.04 timeout-minutes: 50 @@ -358,6 +378,18 @@ jobs: with: name: garm-provider-truenas-g4-fixture path: ${{ runner.temp }}/gha-kvm-system-rdte/g4-fixture + - name: Download exact GARM provider G5 runner fixture + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g5' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-provider-truenas-g3-fixture + path: ${{ runner.temp }}/gha-kvm-system-rdte/g3-fixture + - name: Download exact GARM provider G5 version matrix + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g5' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-provider-truenas-g5-version-matrix + path: ${{ runner.temp }}/gha-kvm-system-rdte/g5-matrix - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -393,6 +425,14 @@ jobs: --g4-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g4-fixture" --g4-fixture-producer "e5fc7c50780de54f7579f64990c87b2e16482613" ) + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-provider-g5" ]]; then + args+=( + --t6-product "garm-provider-g5" + --g5-matrix-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g5-matrix" + --g5-matrix-producer "f83a712e095c6962741f93cf86bea69479ce4d4a" + --g3-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g3-fixture" + --g3-fixture-producer "e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab" + ) else if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" From 0e79c20745aca08c28b7c981eef41a60e7486437 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:27:38 +0200 Subject: [PATCH 244/719] rdte: gate G5 source matrix in prep CI --- .github/workflows/prep-rdte-static.yml | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index ffc5ea25e..d1ecc5d6e 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -22,6 +22,7 @@ on: - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_garm_provider_g3.py' - 'tests/test_truenas_garm_provider_g4.py' + - 'tests/test_truenas_garm_provider_g5.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_rdte_target.py' pull_request: @@ -41,6 +42,7 @@ on: - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_garm_provider_g3.py' - 'tests/test_truenas_garm_provider_g4.py' + - 'tests/test_truenas_garm_provider_g5.py' - 'tests/test_truenas_rdte_target.py' permissions: @@ -88,6 +90,9 @@ jobs: if [[ -f tests/test_truenas_garm_provider_g4.py ]]; then tests+=(tests/test_truenas_garm_provider_g4.py) fi + if [[ -f tests/test_truenas_garm_provider_g5.py ]]; then + tests+=(tests/test_truenas_garm_provider_g5.py) + fi if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi @@ -197,3 +202,25 @@ jobs: docker exec "$cid" test -x /home/runner/actions-runner/bin/Runner.Listener docker exec "$cid" test -w /run/garm-jit done + + - name: G5 exact merged source matrix smoke + if: ${{ hashFiles('scripts/truenas_middleware_garm_provider_g5_probe.py') != '' }} + run: | + set -euo pipefail + provider_ref='f83a712e095c6962741f93cf86bea69479ce4d4a' + rm -rf /tmp/g5-provider + git init -q /tmp/g5-provider + git -C /tmp/g5-provider fetch -q --depth=1 \ + https://github.com/SemperSupra/garm-provider-truenas.git "$provider_ref" + git -C /tmp/g5-provider checkout -q --detach FETCH_HEAD + [[ "$(git -C /tmp/g5-provider rev-parse HEAD)" == "$provider_ref" ]] + ( + cd /tmp/g5-provider + go test ./internal/truenasstore -run '^TestG5VersionMatrixContract$' -count=1 + jq -e ' + .schema == "semper-supra.garm-provider-truenas-g5-version-matrix/1" and + .provider_product_source == "14535745dc3aa3c0b5466da7c704bca4d23dcec5" and + .runtime_inheritance_allowed == false and + ([.targets[].version] | sort) == (["25.04.1","25.04.2.6","25.10.7","26.0.0-BETA.3"] | sort) + ' internal/truenasstore/testdata/truenas-version-matrix.json >/dev/null + ) From a88e185683f37c542f13647ddf426b2886fa3c30 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:27:45 +0200 Subject: [PATCH 245/719] test: register G5 run-request product --- tests/test_truenas_rdte_target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index afcd193d9..83eebcdf1 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -66,7 +66,7 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","garm-provider-g4","official-catalog"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","official-catalog"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": From e6ce23710b6c22c77b2fb07dc5089eaf749a7352 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:27:52 +0200 Subject: [PATCH 246/719] test: admit G5 as explicit T6 product --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 3c6522a0a..ef53f381d 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "official-catalog"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "official-catalog"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From 79365655a2fb27a378a34e9d9e8a3bce5093e879 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:28:25 +0200 Subject: [PATCH 247/719] test: lock G5 cross-version routing and embodiment --- tests/test_gha_kvm_system_rdte.py | 39 +++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index a42201234..bb875b475 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -201,11 +201,11 @@ def test_truenas_uses_exact_target_registry_and_vendor_digest_sidecar(self): self.assertNotIn('hostfwd=tcp:127.0.0.1:$RPC_PORT-:8080', text) self.assertNotIn("xdotool", text) - def test_g3_and_g4_nested_guest_meet_fixed_four_cpu_runner_profile(self): + def test_g3_g4_and_g5_nested_guest_meet_fixed_four_cpu_runner_profile(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn('VCPUS=2', text) self.assertIn( - 'if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" ) ]]; then', + 'if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" ) ]]; then', text, ) self.assertIn('VCPUS=4', text) @@ -215,6 +215,7 @@ def test_g3_and_g4_nested_guest_meet_fixed_four_cpu_runner_profile(self): self.assertIn('"vcpus": int(os.environ["R_VCPUS"])', text) self.assertIn('"ram_mib": int(os.environ["R_RAM_MIB"])', text) self.assertIn('"garm-provider-g4-pair"', text) + self.assertIn('"garm-provider-g5-version-row"', text) self.assertIn('"count": 2', text) def test_g4_routing_is_source_exact_and_public_safe(self): @@ -235,6 +236,40 @@ def test_g4_routing_is_source_exact_and_public_safe(self): self.assertIn('G4_FIXTURE_PRODUCER', text) self.assertNotIn("GITHUB_TOKEN", (ROOT / "scripts" / "truenas_middleware_garm_provider_g4_probe.py").read_text(encoding="utf-8")) + def test_g5_routing_is_source_exact_cross_version_and_public_safe(self): + workflow = (ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml").read_text( + encoding="utf-8" + ) + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn("garm-provider-g5", workflow) + self.assertIn( + "export-g5-version-matrix.yml@f83a712e095c6962741f93cf86bea69479ce4d4a", + workflow, + ) + self.assertIn( + "export-g3-nested-fixture.yml@e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab", + workflow, + ) + self.assertIn( + '--g5-matrix-producer "f83a712e095c6962741f93cf86bea69479ce4d4a"', + workflow, + ) + self.assertIn( + '--g3-fixture-producer "e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab"', + workflow, + ) + self.assertIn("truenas_middleware_garm_provider_g5_probe.py", text) + self.assertIn('G5_MATRIX_PRODUCER', text) + self.assertIn( + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then', + text, + ) + g5_probe = (ROOT / "scripts" / "truenas_middleware_garm_provider_g5_probe.py").read_text( + encoding="utf-8" + ) + self.assertNotIn("GITHUB_TOKEN", g5_probe) + self.assertNotIn("GH_TOKEN", g5_probe) + def test_no_literal_escaped_shell_parameter_expansions(self): needle = chr(92) + "$" + "{" for path in (PVE, TRUENAS): From 35991c87648aaac9a774708a669d23fe55bf8664 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:30:27 +0200 Subject: [PATCH 248/719] rdte: stage G5 TrueNAS 25.10.7 provider row --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 88cf3c0ae..b105229f0 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-provider-g4-001-capacity-two-reconnect-retirement", - "version": "26.0.0-BETA.3", + "request_id": "tn-25.10.7-t6-garm-provider-g5-001-provider-row", + "version": "25.10.7", "rung": "t6", - "product": "garm-provider-g4", - "authority_issue": 396, - "consumer_authority_issue": 41, - "purpose": "Qualify exact TrueNAS 26.0.0-BETA.3 GARM provider G4 on the accepted four-vCPU nested RDTE embodiment: realize exactly two source-derived provider-owned runner Apps with concurrent packaged-provider CreateInstance calls, exact Compose read-back for both, isolated callback/metadata/token configuration, fresh-process Get/List adoption, active-delete fail-closed behavior, both alpha->beta and beta->alpha retirement orders, measured ix-apps storage deltas, final App absence, empty provider inventory, and zero unintended residue; GitHub JIT/private workload/physical TrueNAS/capacity promotion remain unexercised." + "product": "garm-provider-g5", + "authority_issue": 410, + "consumer_authority_issue": 42, + "purpose": "Qualify the exact TrueNAS 25.10.7 G5 provider matrix row using the fixed packaged provider and source-attributed matrix: exact system.version and middleware commit binding, verified WSS/API-key transport, CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue; runtime qualification must not be inherited from any neighboring release, and GitHub JIT/private workload/physical TrueNAS/capacity promotion remain unexercised." } From 72b8bb1f875b2b4b809a35c152a8865179b493e2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:34:34 +0200 Subject: [PATCH 249/719] test: preserve official-catalog guard with G5 exception --- tests/test_truenas_t6_official_catalog_control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py index 45d1c3e01..3fa8f04be 100644 --- a/tests/test_truenas_t6_official_catalog_control.py +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -181,7 +181,10 @@ def test_workflow_and_harness_bind_exact_official_catalog_producer(self): self.assertIn('foundry_commit="' + MOD.EXPECTED_FOUNDRY_REF + '"', workflow) self.assertIn('"official-catalog"', harness) self.assertIn("truenas_middleware_official_catalog_t6_probe.py", harness) - self.assertIn('if [[ "$T6_PRODUCT" != "official-catalog" ]]', harness) + self.assertIn( + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then', + harness, + ) def test_probe_requires_observed_methods_and_noop_convergence(self): text = SCRIPT.read_text(encoding="utf-8") From 642c41c92acc0cdcbbd5d74ccbc35719e048f990 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:35:49 +0200 Subject: [PATCH 250/719] ci: include official-catalog contract in prep gate --- .github/workflows/prep-rdte-static.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index d1ecc5d6e..a760e14ec 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -19,6 +19,7 @@ on: - 'tests/test_truenas_t6_litellm_control.py' - 'tests/test_truenas_t6_wow_sidecar_control.py' - 'tests/test_truenas_t6_garm_control.py' + - 'tests/test_truenas_t6_official_catalog_control.py' - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_garm_provider_g3.py' - 'tests/test_truenas_garm_provider_g4.py' @@ -39,6 +40,7 @@ on: - 'tests/test_truenas_t6_litellm_control.py' - 'tests/test_truenas_t6_wow_sidecar_control.py' - 'tests/test_truenas_t6_garm_control.py' + - 'tests/test_truenas_t6_official_catalog_control.py' - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_garm_provider_g3.py' - 'tests/test_truenas_garm_provider_g4.py' @@ -81,6 +83,9 @@ jobs: if [[ -f tests/test_truenas_t6_garm_control.py ]]; then tests+=(tests/test_truenas_t6_garm_control.py) fi + if [[ -f tests/test_truenas_t6_official_catalog_control.py ]]; then + tests+=(tests/test_truenas_t6_official_catalog_control.py) + fi if [[ -f tests/test_truenas_garm_provider_g2.py ]]; then tests+=(tests/test_truenas_garm_provider_g2.py) fi From 7435949f8e34aff13b9011963567963558f55148 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:39:13 +0200 Subject: [PATCH 251/719] rdte: relaunch G5 TrueNAS 25.10.7 provider row --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index b105229f0..033c1fa67 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.10.7-t6-garm-provider-g5-001-provider-row", + "request_id": "tn-25.10.7-t6-garm-provider-g5-002-contract-covered-relaunch", "version": "25.10.7", "rung": "t6", "product": "garm-provider-g5", "authority_issue": 410, "consumer_authority_issue": 42, - "purpose": "Qualify the exact TrueNAS 25.10.7 G5 provider matrix row using the fixed packaged provider and source-attributed matrix: exact system.version and middleware commit binding, verified WSS/API-key transport, CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue; runtime qualification must not be inherited from any neighboring release, and GitHub JIT/private workload/physical TrueNAS/capacity promotion remain unexercised." + "purpose": "Re-launch the unchanged exact TrueNAS 25.10.7 G5 provider matrix row after the launch-edge contract coverage repair: exact source-matrix/system-version/middleware binding, verified WSS/API-key transport, CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue; no runtime support inheritance, GitHub JIT/private workload/physical TrueNAS/capacity promotion." } From b960e9c7c6b81b0533c470a05d7837f6bcfd8a91 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 05:20:57 +0200 Subject: [PATCH 252/719] rdte: stage G5 TrueNAS 25.04.2.6 provider row --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 033c1fa67..654744442 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.10.7-t6-garm-provider-g5-002-contract-covered-relaunch", - "version": "25.10.7", + "request_id": "tn-25.04.2.6-t6-garm-provider-g5-003-maintenance-row", + "version": "25.04.2.6", "rung": "t6", "product": "garm-provider-g5", - "authority_issue": 410, + "authority_issue": 423, "consumer_authority_issue": 42, - "purpose": "Re-launch the unchanged exact TrueNAS 25.10.7 G5 provider matrix row after the launch-edge contract coverage repair: exact source-matrix/system-version/middleware binding, verified WSS/API-key transport, CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue; no runtime support inheritance, GitHub JIT/private workload/physical TrueNAS/capacity promotion." + "purpose": "Qualify the exact TrueNAS 25.04.2.6 G5 provider matrix row independently of 25.04.1 despite their shared Apps/API-key/filesystem source family: bind the exact source-matrix/system-version/middleware row, exercise verified WSS/API-key transport and the fixed packaged provider CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue lifecycle; prohibit runtime support inheritance and leave GitHub JIT/private workload/physical TrueNAS/capacity promotion unexercised." } From 017fdb9661d38f166fe06e76081f4892ec7fab83 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:00:36 +0200 Subject: [PATCH 253/719] rdte: stage final G5 TrueNAS 25.04.1 row --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 654744442..a3572ac8e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.2.6-t6-garm-provider-g5-003-maintenance-row", - "version": "25.04.2.6", + "request_id": "tn-25.04.1-t6-garm-provider-g5-004-final-matrix-row", + "version": "25.04.1", "rung": "t6", "product": "garm-provider-g5", - "authority_issue": 423, + "authority_issue": 411, "consumer_authority_issue": 42, - "purpose": "Qualify the exact TrueNAS 25.04.2.6 G5 provider matrix row independently of 25.04.1 despite their shared Apps/API-key/filesystem source family: bind the exact source-matrix/system-version/middleware row, exercise verified WSS/API-key transport and the fixed packaged provider CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue lifecycle; prohibit runtime support inheritance and leave GitHub JIT/private workload/physical TrueNAS/capacity promotion unexercised." + "purpose": "Qualify the final exact TrueNAS 25.04.1 G5 provider matrix row independently of the already-accepted 25.04.2.6 row: bind the exact source-matrix/system-version/middleware row, exercise verified WSS/API-key transport and the fixed packaged provider CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue lifecycle; prohibit runtime support inheritance and leave GitHub JIT/private workload/physical TrueNAS/capacity promotion unexercised." } From 7d64be4f14f7e604490f8d796ee087a31fe10942 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:30:57 +0200 Subject: [PATCH 254/719] rdte: add FolioRelay T6 producer routing --- .github/workflows/gha-kvm-system-rdte.yml | 27 +++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 85d3bcd6f..8585977db 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -52,6 +52,7 @@ on: - garm-provider-g4 - garm-provider-g5 - official-catalog + - foliorelay permissions: contents: read @@ -110,7 +111,7 @@ jobs: scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_garm_provider_g4_probe.py|scripts/truenas_middleware_garm_provider_g5_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py) + config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_garm_provider_g4_probe.py|scripts/truenas_middleware_garm_provider_g5_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py|scripts/truenas_middleware_foliorelay_t6_probe.py) truenas=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -127,7 +128,7 @@ jobs: jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null @@ -148,7 +149,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_t6_foliorelay_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -187,6 +188,15 @@ jobs: permissions: contents: read + foundry-foliorelay-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@fb41afd3d112f361b8c490aeb5915a978b956b20 + with: + foundry_ref: fb41afd3d112f361b8c490aeb5915a978b956b20 + permissions: + contents: read + provider-g2-fixtures: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' @@ -313,7 +323,7 @@ jobs: retention-days: 7 truenas-26-beta3: - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] if: >- always() && needs.contract.result == 'success' && @@ -325,6 +335,7 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'foliorelay' && needs['foundry-foliorelay-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') || @@ -360,6 +371,12 @@ jobs: with: name: official-catalog-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact FolioRelay Foundry T6 control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: foliorelay-t6-beta3-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c @@ -440,6 +457,8 @@ jobs: foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then + foundry_commit="fb41afd3d112f361b8c490aeb5915a978b956b20" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 69f97dd7244ce58e398045e9baa7dd441be24998 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:35:05 +0200 Subject: [PATCH 255/719] rdte: route FolioRelay through existing TrueNAS T6 harness --- scripts/gha_kvm_truenas_rdte.sh | 41 ++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index a227300e7..6f2a33002 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA]" } OUT="" @@ -65,7 +65,7 @@ if [[ "$RUNG" == "t6" ]]; then if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "official-catalog" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -175,6 +175,8 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" else {"name": "rdte-t6-catalog-ntfy", "catalog_app": "ntfy", "catalog_version": "1.1.21"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" + else {"name": "rdte-t6-foliorelay", "images": ["ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce", "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f"]} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} @@ -293,6 +295,11 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 WOW Sidecar control client" + elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 FolioRelay control client" + command -v ipptool >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: ipptool" + command -v cc >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: C compiler" else [[ -f "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 LiteLLM control client" @@ -532,6 +539,10 @@ LITELLM_HOST_PORT="" LITELLM_HOSTFWD="" GARM_HOST_PORT="" GARM_HOSTFWD="" +FOLIORELAY_CONTROL_HOST_PORT="" +FOLIORELAY_IPP_HOST_PORT="" +FOLIORELAY_OBSERVER_HOST_PORT="" +FOLIORELAY_HOSTFWD="" if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "litellm" ]]; then LITELLM_HOST_PORT="$(python3 - <<'PY' import socket @@ -548,6 +559,17 @@ PY )" GARM_HOSTFWD=",hostfwd=tcp:127.0.0.1:${GARM_HOST_PORT}-:30880" fi +if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "foliorelay" ]]; then + read -r FOLIORELAY_CONTROL_HOST_PORT FOLIORELAY_IPP_HOST_PORT FOLIORELAY_OBSERVER_HOST_PORT < <(python3 - <<'PY' +import socket +ports=[] +for _ in range(3): + s=socket.socket(); s.bind(("127.0.0.1",0)); ports.append(str(s.getsockname()[1])); s.close() +print(" ".join(ports)) +PY +) + FOLIORELAY_HOSTFWD=",hostfwd=tcp:127.0.0.1:${FOLIORELAY_CONTROL_HOST_PORT}-:18080,hostfwd=tcp:127.0.0.1:${FOLIORELAY_IPP_HOST_PORT}-:8634,hostfwd=tcp:127.0.0.1:${FOLIORELAY_OBSERVER_HOST_PORT}-:18081" +fi : >"$STATE_DIR/serial.log" sudo -n qemu-system-x86_64 \ -enable-kvm -cpu host -smp "$VCPUS" -m "$RAM_MIB" \ @@ -555,7 +577,7 @@ sudo -n qemu-system-x86_64 \ -device "virtio-blk-pci,drive=rdteboot,id=rdte-boot,addr=0x4,bootindex=1" \ "${DATA_DRIVE_ARGS[@]}" \ -boot strict=on \ - -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443${LITELLM_HOSTFWD}${GARM_HOSTFWD}" \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443${LITELLM_HOSTFWD}${GARM_HOSTFWD}${FOLIORELAY_HOSTFWD}" \ -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ @@ -753,6 +775,17 @@ elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then --control-dir "$FOUNDRY_CONTROL_DIR" \ --foundry-commit "$FOUNDRY_COMMIT" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + --control-port "$FOLIORELAY_CONTROL_HOST_PORT" \ + --ipp-port "$FOLIORELAY_IPP_HOST_PORT" \ + --observer-port "$FOLIORELAY_OBSERVER_HOST_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 300 >/dev/null 2>&1 || true else python3 "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$LITELLM_HOST_PORT" \ @@ -787,6 +820,8 @@ elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported WOW Sidecar control passed immutable image/config read-back, permissions/seed metadata, public-fixture worker runtime, restart persistence, and zero-residue cleanup" +elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported FolioRelay three-service control passed exact image/config read-back, portal and IPP reachability, canonical identity, PDF/URF exact-source Inbox preservation, independent in-guest DNS-SD observation, restart persistence, and zero-residue cleanup" else write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported LiteLLM control passed S1 projection, exact image/config read-back, health, restart persistence, and delete/absence" fi From 1e57c2d0104889258b132458ac6a6ffb3a6476f1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:35:18 +0200 Subject: [PATCH 256/719] rdte: install FolioRelay native client oracles --- .github/workflows/gha-kvm-system-rdte.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 8585977db..fc491658a 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -412,7 +412,7 @@ jobs: set -euo pipefail sudo apt-get update -qq sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - qemu-system-x86 qemu-utils xorriso + qemu-system-x86 qemu-utils xorriso cups-ipp-utils libcups2-dev build-essential - name: Exercise pinned TrueNAS installer target run: | set -euo pipefail From cc9db52c086312fbbd8818195251852e09575ec4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:39:02 +0200 Subject: [PATCH 257/719] rdte: add FolioRelay TrueNAS T6 product oracle --- .../truenas_middleware_foliorelay_t6_probe.py | 404 ++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 scripts/truenas_middleware_foliorelay_t6_probe.py diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py new file mode 100644 index 000000000..350b590e5 --- /dev/null +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -0,0 +1,404 @@ +#!/usr/bin/env python3 +"""TrueNAS T6 oracle for the exact Foundry-exported FolioRelay control.""" +from __future__ import annotations + +import argparse +import base64 +import hashlib +import http.client +import json +import pathlib +import re +import socket +import ssl +import struct +import subprocess +import tempfile +import time +import urllib.error +import urllib.request + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_FOUNDRY_REF = "fb41afd3d112f361b8c490aeb5915a978b956b20" +EXPECTED_SCHEMA = "semper-supra.foliorelay-truenas-t6-control/1" +EXPECTED_APP_NAME = "rdte-t6-foliorelay" +OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer" +EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" +EXPECTED_CUPS = "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" +OBSERVER_IMAGE = "ghcr.io/truenas/apps_validation@sha256:3f38cdaa6ed9c54e5c9c43ec15790aabc0028631dd3f0c28129832a69fd1f30c" +DATASET = "rdtepool/foliorelay-t6" +ROOT = "/mnt/rdtepool/foliorelay-t6" +TOKEN_PATH = ROOT + "/secrets/control.token" +OBSERVER_DIR = ROOT + "/observer" +TOKEN = b"foliorelay-t6-public-fixture-token-0001\n" +PUBLIC_URI = "ipp://foliorelay-t6.local:8634/printers/FolioRelay" + +def canonical_sha256(value): + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest() + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + +def load_json(path: pathlib.Path): + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise RuntimeError(f"{path.name} must be an object") + return value + +def load_control(root: pathlib.Path, foundry_ref: str): + control = load_json(root / "control.json") + compose = load_json(root / "compose.json") + if control.get("schema") != EXPECTED_SCHEMA: + raise RuntimeError("unexpected FolioRelay T6 control schema") + if foundry_ref != EXPECTED_FOUNDRY_REF or control.get("foundry_ref") != EXPECTED_FOUNDRY_REF: + raise RuntimeError("Foundry source ref drifted") + if control.get("secrets_captured") is not False: + raise RuntimeError("control bundle does not assert secrets_captured=false") + candidate = control.get("candidate") or {} + if candidate.get("truenas_version") != "26.0.0-BETA.3": + raise RuntimeError("TrueNAS target drifted") + if candidate.get("control_image") != EXPECTED_CONTROL: + raise RuntimeError("control image drifted") + if candidate.get("cups_image") != EXPECTED_CUPS: + raise RuntimeError("CUPS image drifted") + if canonical_sha256(compose) != (control.get("artifacts") or {}).get("compose_canonical_sha256"): + raise RuntimeError("Compose identity does not match control") + services = compose.get("services") or {} + if set(services) != {"control", "cups", "discovery"}: + raise RuntimeError("expected exact control/cups/discovery service set") + if services["control"].get("image") != EXPECTED_CONTROL or services["discovery"].get("image") != EXPECTED_CONTROL: + raise RuntimeError("control/discovery exact image identity drifted") + if services["cups"].get("image") != EXPECTED_CUPS: + raise RuntimeError("CUPS exact image identity drifted") + required = set(control.get("required_oracles") or []) + expected = { + "app-create-running", "config-readback-exact-compose", "portal-ready", + "ipp-get-printer-attributes", "canonical-uri-coherence", + "control-cups-dnssd-uuid-coherence", "dnssd-universal-visible", + "pdf-exact-source-inbox", "urf-exact-source-inbox", + "restart-preserves-identity-and-inbox", "delete-zero-residue", + } + if not expected.issubset(required): + raise RuntimeError("required FolioRelay oracle contract drifted") + return control, compose + +def multipart_upload(host, port, tls, username, password, remote_path, content, mode, timeout): + boundary = "----semper-supra-foliorelay-t6" + data = json.dumps({"method":"filesystem.put","params":[remote_path,{"append":False,"mode":mode}]}, separators=(",",":")) + chunks = [ + f"--{boundary}\r\n".encode(), b'Content-Disposition: form-data; name="data"\r\n\r\n', + data.encode(), b"\r\n", f"--{boundary}\r\n".encode(), + b'Content-Disposition: form-data; name="file"; filename="payload"\r\n', + b"Content-Type: application/octet-stream\r\n\r\n", content, b"\r\n", + f"--{boundary}--\r\n".encode(), + ] + body = b"".join(chunks) + token = base64.b64encode(f"{username}:{password}".encode()).decode() + headers = {"Authorization":f"Basic {token}","Content-Type":f"multipart/form-data; boundary={boundary}","Content-Length":str(len(body))} + if tls: + ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE + conn=http.client.HTTPSConnection(host,port,timeout=timeout,context=ctx) + else: + conn=http.client.HTTPConnection(host,port,timeout=timeout) + try: + conn.request("POST","/_upload/",body=body,headers=headers) + resp=conn.getresponse(); payload=resp.read() + finally: + conn.close() + if not (200 <= resp.status < 300): + raise RuntimeError(f"filesystem.put HTTP {resp.status}") + job_id=json.loads(payload.decode()).get("job_id") + if not isinstance(job_id,int): + raise RuntimeError("filesystem.put did not return job_id") + return job_id + +def http_bytes(host, port, path, token=None, method="GET", timeout=8.0): + req=urllib.request.Request(f"http://{host}:{port}{path}", method=method) + if token: + req.add_header("Authorization", f"Bearer {token}") + with urllib.request.urlopen(req, timeout=timeout) as r: + return r.read() + +def wait_http(host, port, path, timeout_s): + deadline=time.monotonic()+timeout_s + while time.monotonic()>endobj\n" + b"2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj\n" + b"3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 72 72] >>endobj\n" + b"trailer<< /Root 1 0 R >>\n%%EOF\n") + path.write_bytes(data) + +URF_C = r""" +#include +#include +#include +#include +#include +#include +int main(int argc,char **argv){ + cups_page_header2_t h; cups_raster_t *r; pwg_media_t *m; unsigned char *line; unsigned y; + if(argc!=2) return 64; m=pwgMediaForPWG("iso_a4_210x297mm"); if(!m) return 1; + if(!cupsRasterInitPWGHeader(&h,m,"sgray_8",300,300,"one-sided",NULL)) return 1; + h.cupsInteger[CUPS_RASTER_PWG_TotalPageCount]=1; + int fd=open(argv[1],O_CREAT|O_TRUNC|O_WRONLY,0600); if(fd<0) return 1; + r=cupsRasterOpen(fd,CUPS_RASTER_WRITE_APPLE); if(!r) return 1; + if(!cupsRasterWriteHeader2(r,&h)) return 1; + line=malloc(h.cupsBytesPerLine); if(!line) return 1; memset(line,0xff,h.cupsBytesPerLine); + for(y=0;y=len(pkt): raise ValueError("dns name overflow") + n=pkt[off] + if n==0: + if end is None: end=off+1 + return ".".join(labels),end + if n&0xC0==0xC0: + ptr=((n&0x3f)<<8)|pkt[off+1] + if ptr in seen: raise ValueError("dns pointer loop") + seen.add(ptr); tail,_=name_at(pkt,ptr,seen) + if tail: labels.append(tail) + if end is None: end=off+2 + return ".".join(labels),end + off+=1; labels.append(pkt[off:off+n].decode(errors="replace")); off+=n +def observe(expected,seconds=25): + qid=0; query=struct.pack("!HHHHHH",qid,0,1,0,0,0)+enc(Q)+struct.pack("!HH",12,1) + s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP) + s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) + try: s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEPORT,1) + except OSError: pass + s.bind(("",5353)) + mreq=socket.inet_aton("224.0.0.251")+socket.inet_aton("0.0.0.0") + s.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,mreq); s.settimeout(1) + s.sendto(query,("224.0.0.251",5353)) + deadline=time.monotonic()+seconds; packets=[] + while time.monotonic() Date: Sat, 3 Oct 2026 15:39:36 +0200 Subject: [PATCH 258/719] test: lock FolioRelay TrueNAS T6 consumer contract --- tests/test_truenas_t6_foliorelay_control.py | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/test_truenas_t6_foliorelay_control.py diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py new file mode 100644 index 000000000..a1fb2b3c8 --- /dev/null +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import importlib.util +import pathlib +import subprocess +import sys +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "truenas_middleware_foliorelay_t6_probe.py" +SPEC = importlib.util.spec_from_file_location("foliorelay_t6_probe", SCRIPT) +MOD = importlib.util.module_from_spec(SPEC) +assert SPEC.loader +SPEC.loader.exec_module(MOD) + +class FolioRelayT6ContractTests(unittest.TestCase): + def test_exact_product_and_foundry_identity_is_pinned(self): + self.assertEqual(MOD.EXPECTED_FOUNDRY_REF, "fb41afd3d112f361b8c490aeb5915a978b956b20") + self.assertIn("@sha256:", MOD.EXPECTED_CONTROL) + self.assertIn("@sha256:", MOD.EXPECTED_CUPS) + self.assertEqual(MOD.DATASET, "rdtepool/foliorelay-t6") + self.assertEqual(MOD.PUBLIC_URI, "ipp://foliorelay-t6.local:8634/printers/FolioRelay") + + def test_probe_preserves_product_oracles_and_zero_residue(self): + text = SCRIPT.read_text(encoding="utf-8") + for needle in ( + '"/api/v1/printer"', '"/api/v1/jobs"', 'application/pdf', 'image/urf', + 'ipptool', 'UNIRAST', '"pool.dataset.create"', '"pool.dataset.delete"', + '"app.create"', '"app.stop"', '"app.start"', '"app.delete"', + '"zero_residue":True', '_universal._sub._ipp._tcp.local', + 'dnssd_uuid_match', 'restart_preserved_identity_and_inbox', + ): + self.assertIn(needle, text) + self.assertIn(MOD.OBSERVER_IMAGE, text) + self.assertNotIn("avahi-publish-service", text) + self.assertNotIn("/run/dbus", text) + self.assertNotIn("/var/run/docker.sock", text) + + def test_observer_is_separate_host_network_app(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer"', text) + self.assertIn('"network_mode":"host"', text) + self.assertIn('"entrypoint":["python3","/observer/mdns_observer.py"]', text) + self.assertIn('"distinct_observer_context":True', text) + + def test_workflow_and_harness_route_exact_foliorelay_export(self): + workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") + harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + self.assertIn("truenas_t6_product == 'foliorelay'", workflow) + self.assertIn("export-foliorelay-t6-control.yml@" + MOD.EXPECTED_FOUNDRY_REF, workflow) + self.assertIn("name: foliorelay-t6-beta3-control", workflow) + self.assertIn('foundry_commit="' + MOD.EXPECTED_FOUNDRY_REF + '"', workflow) + self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) + self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) + self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) + self.assertIn('FOLIORELAY_OBSERVER_HOST_PORT', harness) + self.assertIn('--observer-port "$FOLIORELAY_OBSERVER_HOST_PORT"', harness) + + def test_probe_compiles(self): + cp=subprocess.run([sys.executable,"-m","py_compile",str(SCRIPT)],capture_output=True,text=True) + self.assertEqual(cp.returncode,0,cp.stderr) + +if __name__ == "__main__": + unittest.main() From 1032f0f496ee8fc929edd1cb1a846ccfaff56e8b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:39:50 +0200 Subject: [PATCH 259/719] ci: include FolioRelay T6 static contract --- .github/workflows/prep-rdte-static.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index a760e14ec..988e3e554 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -20,6 +20,8 @@ on: - 'tests/test_truenas_t6_wow_sidecar_control.py' - 'tests/test_truenas_t6_garm_control.py' - 'tests/test_truenas_t6_official_catalog_control.py' + - 'tests/test_truenas_t6_foliorelay_control.py' + - 'tests/test_truenas_t6_foliorelay_control.py' - 'tests/test_truenas_garm_provider_g2.py' - 'tests/test_truenas_garm_provider_g3.py' - 'tests/test_truenas_garm_provider_g4.py' @@ -86,6 +88,9 @@ jobs: if [[ -f tests/test_truenas_t6_official_catalog_control.py ]]; then tests+=(tests/test_truenas_t6_official_catalog_control.py) fi + if [[ -f tests/test_truenas_t6_foliorelay_control.py ]]; then + tests+=(tests/test_truenas_t6_foliorelay_control.py) + fi if [[ -f tests/test_truenas_garm_provider_g2.py ]]; then tests+=(tests/test_truenas_garm_provider_g2.py) fi From 876cb9b3929bd5fcf57212649eff3b3d39735c57 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:41:41 +0200 Subject: [PATCH 260/719] ci: qualify exact FolioRelay T6 producer-consumer seam --- .../workflows/prep-foliorelay-t6-consumer.yml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/prep-foliorelay-t6-consumer.yml diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml new file mode 100644 index 000000000..7226c78a3 --- /dev/null +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -0,0 +1,68 @@ +name: FolioRelay T6 consumer prep + +on: + pull_request: + branches: + - experiment/gha-kvm-system-rdte + paths: + - ".github/workflows/prep-foliorelay-t6-consumer.yml" + - ".github/workflows/gha-kvm-system-rdte.yml" + - "scripts/gha_kvm_truenas_rdte.sh" + - "scripts/truenas_middleware_foliorelay_t6_probe.py" + - "tests/test_truenas_t6_foliorelay_control.py" + push: + branches: + - "prep/truenas-t6-foliorelay-v1" + paths: + - ".github/workflows/prep-foliorelay-t6-consumer.yml" + - ".github/workflows/gha-kvm-system-rdte.yml" + - "scripts/gha_kvm_truenas_rdte.sh" + - "scripts/truenas_middleware_foliorelay_t6_probe.py" + - "tests/test_truenas_t6_foliorelay_control.py" + +permissions: + contents: read + +jobs: + foundry-control: + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@fb41afd3d112f361b8c490aeb5915a978b956b20 + with: + foundry_ref: fb41afd3d112f361b8c490aeb5915a978b956b20 + permissions: + contents: read + + consumer-contract: + needs: foundry-control + runs-on: ubuntu-24.04 + timeout-minutes: 8 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Download exact Foundry FolioRelay control + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: foliorelay-t6-beta3-control + path: ${{ runner.temp }}/foliorelay-control + - name: Verify producer-consumer binding + shell: bash + run: | + set -euo pipefail + root="$RUNNER_TEMP/foliorelay-control" + test -s "$root/control.json" + test -s "$root/compose.json" + jq -e ' + .schema == "semper-supra.foliorelay-truenas-t6-control/1" and + .foundry_ref == "fb41afd3d112f361b8c490aeb5915a978b956b20" and + .candidate.truenas_version == "26.0.0-BETA.3" and + .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" and + .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" and + .secrets_captured == false + ' "$root/control.json" >/dev/null + jq -e ' + (.services | keys | sort) == ["control","cups","discovery"] and + .services.discovery.network_mode == "host" + ' "$root/compose.json" >/dev/null + python3 -m unittest -v tests/test_truenas_t6_foliorelay_control.py + bash -n scripts/gha_kvm_truenas_rdte.sh + python3 -m py_compile scripts/truenas_middleware_foliorelay_t6_probe.py From 7ad6777a25609ed9d2ec2ea8ecae50eaf7c230e6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:42:24 +0200 Subject: [PATCH 261/719] test: import FolioRelay T6 probe from scripts path --- tests/test_truenas_t6_foliorelay_control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index a1fb2b3c8..64f4893a5 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -6,7 +6,10 @@ import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts" / "truenas_middleware_foliorelay_t6_probe.py" +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) +SCRIPT = SCRIPTS / "truenas_middleware_foliorelay_t6_probe.py" SPEC = importlib.util.spec_from_file_location("foliorelay_t6_probe", SCRIPT) MOD = importlib.util.module_from_spec(SPEC) assert SPEC.loader From cb407f747daf22adc35d6eeb37e19014cd6f3aca Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:43:36 +0200 Subject: [PATCH 262/719] rdte: prove FolioRelay replan NOOP and redeploy persistence --- .../truenas_middleware_foliorelay_t6_probe.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 350b590e5..0f43043c6 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -372,6 +372,22 @@ def wait_state(name,state): after_printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout)) after_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout)) if (after_printer.get("identity") or {}).get("printer_uuid")!=uuid or after_jobs!=before_jobs: raise RuntimeError("identity or Inbox drifted after restart") + # Re-plan from exact live read-back. No mutation is needed when desired and observed Compose identities match. + live_compose=call("app.config",[EXPECTED_APP_NAME]) + replan_action="NOOP" if canonical_sha256(live_compose)==canonical_sha256(compose) else "UPDATE" + if replan_action!="NOOP": raise RuntimeError("exact desired/live Compose re-plan was not NOOP") + # Exercise the public update/redeploy path separately from stop/start while preserving durable identity and Inbox. + uj=call("app.update",[EXPECTED_APP_NAME,{"custom_compose_config":compose}]) + if not isinstance(uj,int): raise RuntimeError("app.update did not return job") + wait_job(uj,"app.update"); wait_state(EXPECTED_APP_NAME,"RUNNING") + rj=call("app.redeploy",[EXPECTED_APP_NAME]) + if not isinstance(rj,int): raise RuntimeError("app.redeploy did not return job") + wait_job(rj,"app.redeploy"); wait_state(EXPECTED_APP_NAME,"RUNNING") + if not wait_http(a.host,a.control_port,"/readyz",a.state_timeout): raise RuntimeError("readyz failed after update/redeploy") + redeploy_printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout)) + redeploy_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout)) + if (redeploy_printer.get("identity") or {}).get("printer_uuid")!=uuid or redeploy_jobs!=before_jobs: + raise RuntimeError("identity or Inbox drifted after update/redeploy") od=call("app.delete",[OBSERVER_APP_NAME,{"remove_images":False,"remove_ix_volumes":False,"force_remove_custom_app":False}]); wait_job(od,"observer delete"); observer_created=False md=call("app.delete",[EXPECTED_APP_NAME,{"remove_images":False,"remove_ix_volumes":False,"force_remove_custom_app":False}]); wait_job(md,"app delete"); app_created=False if call("app.query",[[["id","in",[EXPECTED_APP_NAME,OBSERVER_APP_NAME]]]]): raise RuntimeError("app residue remains") @@ -385,7 +401,7 @@ def wait_state(name,state): payload.update({ "classification":"SUPPORTED","oracleSatisfied":True, "identity":{"printer_uuid":uuid,"public_uri":PUBLIC_URI,"control_cups_uuid_match":True,"dnssd_uuid_match":True}, - "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True}, + "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP"}, "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True}, "cleanup":{"apps_absent":True,"fixture_dataset_absent":True,"fixture_mountpoint_absent":True,"zero_residue":True}, "producer_gate":{"replan_noop_required":True,"runtime_does_not_reconstruct_foundry_control":True}, From 11e75cd70aeb4af03c94367c6c06f85740eab187 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:43:46 +0200 Subject: [PATCH 263/719] test: require FolioRelay replan and redeploy oracles --- tests/test_truenas_t6_foliorelay_control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 64f4893a5..befdc9257 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -31,6 +31,8 @@ def test_probe_preserves_product_oracles_and_zero_residue(self): '"app.create"', '"app.stop"', '"app.start"', '"app.delete"', '"zero_residue":True', '_universal._sub._ipp._tcp.local', 'dnssd_uuid_match', 'restart_preserved_identity_and_inbox', + 'app.update', 'app.redeploy', 'replan_action', '"NOOP"', + 'update_redeploy_preserved_identity_and_inbox', ): self.assertIn(needle, text) self.assertIn(MOD.OBSERVER_IMAGE, text) From 1847d33b36def08446e3bde4612fb3089899fb8e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:50:53 +0200 Subject: [PATCH 264/719] rdte: suppress stale run-request replay after FolioRelay consumer merge From a61166b3e2b8e0d55a3c11e7437212dcedf0875b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:54:49 +0200 Subject: [PATCH 265/719] test: admit FolioRelay in T6 product allowlist --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index ef53f381d..c47414c56 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "official-catalog"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "official-catalog", "foliorelay"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From d6ab708b7c7930d2aa8bcbc9a2e441d99221e96c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:54:53 +0200 Subject: [PATCH 266/719] test: bind FolioRelay T6 consumer authority --- tests/test_truenas_rdte_target.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 83eebcdf1..1754039bb 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -66,7 +66,7 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","official-catalog"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","official-catalog","foliorelay"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": @@ -75,6 +75,8 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request.get("consumer_authority_issue"), 39) if request.get("product") == "garm-provider-g3": self.assertEqual(request.get("consumer_authority_issue"), 40) + if request.get("product") == "foliorelay": + self.assertEqual(request.get("consumer_authority_issue"), 273) self.assertNotIn("latest", request["version"].lower()) self.assertNotIn("nightly", request["version"].lower()) From c90370a40d2502756702f8a68f9ebec7fc01a98a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:57:21 +0200 Subject: [PATCH 267/719] rdte: request FolioRelay BETA.3 T6 rep 001 --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index a3572ac8e..262f1bf35 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25.04.1-t6-garm-provider-g5-004-final-matrix-row", - "version": "25.04.1", + "request_id": "tn-26-beta3-t6-foliorelay-001-mvp", + "version": "26.0.0-BETA.3", "rung": "t6", - "product": "garm-provider-g5", - "authority_issue": 411, - "consumer_authority_issue": 42, - "purpose": "Qualify the final exact TrueNAS 25.04.1 G5 provider matrix row independently of the already-accepted 25.04.2.6 row: bind the exact source-matrix/system-version/middleware row, exercise verified WSS/API-key transport and the fixed packaged provider CreateInstance -> exact Compose read-back -> Get/List -> supported inactive transition -> provider DeleteInstance -> App absence -> empty provider inventory -> zero unintended residue lifecycle; prohibit runtime support inheritance and leave GitHub JIT/private workload/physical TrueNAS/capacity promotion unexercised." + "product": "foliorelay", + "authority_issue": 396, + "consumer_authority_issue": 273, + "purpose": "Qualify the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 using the pinned Foundry-exported three-service control: exact control/CUPS image read-back, canonical control/CUPS/DNS-SD identity, portal and native IPP, exact PDF and Apple Raster Inbox artifacts, independent in-guest _universal DNS-SD observation, stop/start plus update/redeploy persistence, desired/live re-plan NOOP, and zero-residue app/observer/dataset cleanup. This is pre-hardware software/control-plane qualification only." } From e9a59460265daca4dc14f3310e2c8bbf01f14644 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:28:36 +0200 Subject: [PATCH 268/719] rdte: add immutable TrueNAS session contract validator --- scripts/truenas_session_manifest.py | 285 ++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 scripts/truenas_session_manifest.py diff --git a/scripts/truenas_session_manifest.py b/scripts/truenas_session_manifest.py new file mode 100644 index 000000000..94f2201da --- /dev/null +++ b/scripts/truenas_session_manifest.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import re +import sys + +SCHEMA = "truenas-session/v1" +CAPSULE_SCHEMA = "truenas-capsule/v1" +ID_RE = re.compile(r"^[a-z0-9][a-z0-9._-]{2,79}$") +REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +KINDS = { + "substrate-sanity", "foundry-control", "official-catalog-control", + "foundry-product", "oci-custom-app-feature", "vm-management-plane", + "nested-virtualization", +} +RETRY_POLICIES = {"none", "read-only-reconcile"} +MAX_CAPSULES = 24 + +class SessionError(RuntimeError): + pass + +def _load_json(path: pathlib.Path) -> dict: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise SessionError(f"cannot read {path}: {exc}") from exc + if not isinstance(value, dict): + raise SessionError(f"{path}: top-level value must be an object") + return value + +def _positive_int(value, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value <= 0: + raise SessionError(f"{label} must be a positive integer") + return value + +def _string(value, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise SessionError(f"{label} must be a non-empty string") + return value + +def canonical_payload(doc: dict) -> bytes: + payload = dict(doc) + payload.pop("manifest_sha256", None) + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() + +def manifest_digest(doc: dict) -> str: + return hashlib.sha256(canonical_payload(doc)).hexdigest() + +def load_target_versions(path: pathlib.Path) -> set[str]: + registry = _load_json(path) + if registry.get("schema") != "gha-kvm-truenas-targets/v1": + raise SessionError("unsupported TrueNAS target registry schema") + targets = registry.get("targets") + if not isinstance(targets, list) or not targets: + raise SessionError("TrueNAS target registry has no targets") + versions = set() + for index, target in enumerate(targets): + if not isinstance(target, dict): + raise SessionError(f"targets[{index}] must be an object") + version = _string(target.get("version"), f"targets[{index}].version") + if version in versions: + raise SessionError(f"duplicate TrueNAS target version: {version}") + versions.add(version) + return versions + +def load_provider_ids(path: pathlib.Path) -> set[str]: + registry = _load_json(path) + if registry.get("schema") != "truenas-capsule-providers/v1": + raise SessionError("unsupported capsule provider registry schema") + providers = registry.get("providers") + if not isinstance(providers, list): + raise SessionError("capsule provider registry providers must be a list") + ids = set() + for index, provider in enumerate(providers): + if not isinstance(provider, dict): + raise SessionError(f"providers[{index}] must be an object") + provider_id = _string(provider.get("id"), f"providers[{index}].id") + if provider_id in ids: + raise SessionError(f"duplicate capsule provider id: {provider_id}") + ids.add(provider_id) + return ids + +def validate_capsule(capsule: dict, index: int, provider_ids: set[str]) -> dict: + prefix = f"capsules[{index}]" + if not isinstance(capsule, dict): + raise SessionError(f"{prefix} must be an object") + if capsule.get("schema") != CAPSULE_SCHEMA: + raise SessionError(f"{prefix}: unsupported capsule schema") + capsule_id = _string(capsule.get("id"), f"{prefix}.id") + if not ID_RE.fullmatch(capsule_id): + raise SessionError(f"{prefix}.id has invalid syntax") + kind = _string(capsule.get("kind"), f"{prefix}.kind") + if kind not in KINDS: + raise SessionError(f"{prefix}.kind is not admitted: {kind}") + provider = _string(capsule.get("provider"), f"{prefix}.provider") + if provider not in provider_ids: + raise SessionError(f"{prefix}.provider is not registered: {provider}") + authority = capsule.get("authority") + if not isinstance(authority, dict): + raise SessionError(f"{prefix}.authority must be an object") + repo = _string(authority.get("repository"), f"{prefix}.authority.repository") + if not REPO_RE.fullmatch(repo): + raise SessionError(f"{prefix}.authority.repository must be owner/name") + issue = _positive_int(authority.get("issue"), f"{prefix}.authority.issue") + exact = capsule.get("exact") + if not isinstance(exact, dict) or not exact: + raise SessionError(f"{prefix}.exact must be a non-empty object") + for key, value in exact.items(): + _string(key, f"{prefix}.exact key") + _string(value, f"{prefix}.exact.{key}") + namespace = _string(capsule.get("namespace"), f"{prefix}.namespace") + if not ID_RE.fullmatch(namespace): + raise SessionError(f"{prefix}.namespace has invalid syntax") + ports = capsule.get("ports", []) + if not isinstance(ports, list): + raise SessionError(f"{prefix}.ports must be a list") + normalized_ports = [] + for port in ports: + if not isinstance(port, int) or isinstance(port, bool) or not 1 <= port <= 65535: + raise SessionError(f"{prefix}.ports contains an invalid port") + normalized_ports.append(port) + if len(normalized_ports) != len(set(normalized_ports)): + raise SessionError(f"{prefix}.ports contains duplicates") + requirements = capsule.get("requirements", []) + if not isinstance(requirements, list) or any(not isinstance(x, str) or not x for x in requirements): + raise SessionError(f"{prefix}.requirements must be a list of non-empty strings") + resources = capsule.get("resources") + if not isinstance(resources, dict): + raise SessionError(f"{prefix}.resources must be an object") + memory_mib = _positive_int(resources.get("memory_mib"), f"{prefix}.resources.memory_mib") + download_mib = _positive_int(resources.get("download_mib"), f"{prefix}.resources.download_mib") + estimated_minutes = _positive_int(resources.get("estimated_minutes"), f"{prefix}.resources.estimated_minutes") + phases = capsule.get("phases") + if not isinstance(phases, dict): + raise SessionError(f"{prefix}.phases must be an object") + for phase in ("setup", "apply", "verify", "cleanup"): + if phases.get(phase) is not True: + raise SessionError(f"{prefix}.phases.{phase} must be true") + oracle = _string(capsule.get("oracle"), f"{prefix}.oracle") + receipt_contract = _string(capsule.get("receipt_contract"), f"{prefix}.receipt_contract") + retry_policy = _string(capsule.get("retry_policy"), f"{prefix}.retry_policy") + if retry_policy not in RETRY_POLICIES: + raise SessionError(f"{prefix}.retry_policy is not admitted: {retry_policy}") + mutating = capsule.get("mutating") + if not isinstance(mutating, bool): + raise SessionError(f"{prefix}.mutating must be boolean") + cleanup_required = capsule.get("cleanup_required") + if not isinstance(cleanup_required, bool): + raise SessionError(f"{prefix}.cleanup_required must be boolean") + if mutating and not cleanup_required: + raise SessionError(f"{prefix}: mutating capsules must require cleanup") + dependencies = capsule.get("dependencies", []) + if not isinstance(dependencies, list) or any(not isinstance(x, str) or not x for x in dependencies): + raise SessionError(f"{prefix}.dependencies must be a list of capsule ids") + return { + "id": capsule_id, "kind": kind, "provider": provider, + "authority": {"repository": repo, "issue": issue}, + "namespace": namespace, "ports": normalized_ports, + "resources": {"memory_mib": memory_mib, "download_mib": download_mib, "estimated_minutes": estimated_minutes}, + "oracle": oracle, "receipt_contract": receipt_contract, + "retry_policy": retry_policy, "mutating": mutating, + "cleanup_required": cleanup_required, "dependencies": dependencies, + } + +def validate_manifest(manifest_path: pathlib.Path, target_registry_path: pathlib.Path, provider_registry_path: pathlib.Path) -> dict: + doc = _load_json(manifest_path) + if doc.get("schema") != SCHEMA: + raise SessionError("unsupported TrueNAS session schema") + session_id = _string(doc.get("session_id"), "session_id") + if not ID_RE.fullmatch(session_id): + raise SessionError("session_id has invalid syntax") + version = _string(doc.get("version"), "version") + if version not in load_target_versions(target_registry_path): + raise SessionError(f"exact TrueNAS target is not registered: {version}") + authority = doc.get("authority") + if not isinstance(authority, dict): + raise SessionError("authority must be an object") + authority_repo = _string(authority.get("repository"), "authority.repository") + if not REPO_RE.fullmatch(authority_repo): + raise SessionError("authority.repository must be owner/name") + authority_issue = _positive_int(authority.get("issue"), "authority.issue") + budget = doc.get("budget") + if not isinstance(budget, dict): + raise SessionError("budget must be an object") + guest_memory_mib = _positive_int(budget.get("guest_memory_mib"), "budget.guest_memory_mib") + download_mib = _positive_int(budget.get("download_mib"), "budget.download_mib") + timeout_minutes = _positive_int(budget.get("timeout_minutes"), "budget.timeout_minutes") + if guest_memory_mib > 8192: + raise SessionError("budget.guest_memory_mib exceeds the current 8-GiB guardrail") + if timeout_minutes > 50: + raise SessionError("budget.timeout_minutes exceeds the current 50-minute guardrail") + pool = doc.get("pool") + if not isinstance(pool, dict): + raise SessionError("pool must be an object") + pool_name = _string(pool.get("name"), "pool.name") + if not ID_RE.fullmatch(pool_name): + raise SessionError("pool.name has invalid syntax") + if _positive_int(pool.get("data_disks"), "pool.data_disks") != 2: + raise SessionError("pool.data_disks must preserve the qualified two-disk topology") + capsules = doc.get("capsules") + if not isinstance(capsules, list) or not capsules: + raise SessionError("capsules must be a non-empty list") + if len(capsules) > MAX_CAPSULES: + raise SessionError(f"capsules exceeds bounded limit {MAX_CAPSULES}") + provider_ids = load_provider_ids(provider_registry_path) + normalized = [validate_capsule(c, i, provider_ids) for i, c in enumerate(capsules)] + ids = [c["id"] for c in normalized] + if len(ids) != len(set(ids)): + raise SessionError("capsule ids must be unique") + namespaces = [c["namespace"] for c in normalized] + if len(namespaces) != len(set(namespaces)): + raise SessionError("capsule namespaces must be unique") + all_ports = [p for c in normalized for p in c["ports"]] + if len(all_ports) != len(set(all_ports)): + raise SessionError("capsule ports must be globally unique within a session") + ids_set = set(ids) + depmap = {c["id"]: c["dependencies"] for c in normalized} + for c in normalized: + if c["id"] in c["dependencies"]: + raise SessionError(f"{c['id']}: self-dependency is forbidden") + missing = sorted(set(c["dependencies"]) - ids_set) + if missing: + raise SessionError(f"{c['id']}: unknown dependencies: {', '.join(missing)}") + visiting, visited = set(), set() + def visit(node: str) -> None: + if node in visited: + return + if node in visiting: + raise SessionError("capsule dependency cycle detected") + visiting.add(node) + for dep in depmap[node]: + visit(dep) + visiting.remove(node) + visited.add(node) + for node in ids: + visit(node) + max_memory = max(c["resources"]["memory_mib"] for c in normalized) + total_download = sum(c["resources"]["download_mib"] for c in normalized) + total_minutes = sum(c["resources"]["estimated_minutes"] for c in normalized) + if max_memory > guest_memory_mib: + raise SessionError("capsule memory ceiling exceeds session guest-memory budget") + if total_download > download_mib: + raise SessionError("capsule download budgets exceed session download budget") + if total_minutes > timeout_minutes: + raise SessionError("capsule estimated time exceeds session timeout budget") + computed = manifest_digest(doc) + declared = doc.get("manifest_sha256") + if not isinstance(declared, str) or not SHA256_RE.fullmatch(declared): + raise SessionError("manifest_sha256 must be exact lowercase sha256") + if declared != computed: + raise SessionError(f"manifest_sha256 mismatch: declared {declared}, computed {computed}") + return { + "schema": SCHEMA, "session_id": session_id, "version": version, + "authority": {"repository": authority_repo, "issue": authority_issue}, + "manifest_sha256": computed, "capsule_count": len(normalized), + "mutating_capsule_count": sum(1 for c in normalized if c["mutating"]), + "max_capsule_memory_mib": max_memory, + "total_download_mib": total_download, + "total_estimated_minutes": total_minutes, + } + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", type=pathlib.Path, required=True) + parser.add_argument("--targets", type=pathlib.Path, default=pathlib.Path("config/truenas-rdte-targets.json")) + parser.add_argument("--providers", type=pathlib.Path, default=pathlib.Path("config/truenas-capsule-providers.json")) + parser.add_argument("--digest-only", action="store_true") + args = parser.parse_args() + try: + if args.digest_only: + print(manifest_digest(_load_json(args.manifest))) + return 0 + print(json.dumps(validate_manifest(args.manifest, args.targets, args.providers), indent=2, sort_keys=True)) + return 0 + except SessionError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + +if __name__ == "__main__": + raise SystemExit(main()) From 6d24e23f57ae6856d24dd34875e35c850c607a13 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:28:46 +0200 Subject: [PATCH 269/719] rdte: register existing T6 probes as capsule providers --- config/truenas-capsule-providers.json | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 config/truenas-capsule-providers.json diff --git a/config/truenas-capsule-providers.json b/config/truenas-capsule-providers.json new file mode 100644 index 000000000..543841447 --- /dev/null +++ b/config/truenas-capsule-providers.json @@ -0,0 +1,49 @@ +{ + "schema": "truenas-capsule-providers/v1", + "authority": { + "repository": "SemperSupra/agent-dispatch-private", + "issue": 480 + }, + "providers": [ + { + "id": "litellm-t6", + "kind": "foundry-product", + "product": "litellm", + "probe": "scripts/truenas_middleware_litellm_t6_probe.py", + "control_artifact": "litellm-truenas-t6-control", + "existing_selector": "litellm" + }, + { + "id": "wow-sidecar-t6", + "kind": "foundry-product", + "product": "wow-sidecar", + "probe": "scripts/truenas_middleware_wow_sidecar_t6_probe.py", + "control_artifact": "wow-sidecar-truenas-t6-control", + "existing_selector": "wow-sidecar" + }, + { + "id": "garm-t6", + "kind": "foundry-product", + "product": "garm", + "probe": "scripts/truenas_middleware_garm_t6_probe.py", + "control_artifact": "garm-truenas-t6-control", + "existing_selector": "garm" + }, + { + "id": "official-catalog-t6", + "kind": "official-catalog-control", + "product": "official-catalog", + "probe": "scripts/truenas_middleware_official_catalog_t6_probe.py", + "control_artifact": "official-catalog-truenas-t6-control", + "existing_selector": "official-catalog" + }, + { + "id": "foliorelay-t6", + "kind": "foundry-product", + "product": "foliorelay", + "probe": "scripts/truenas_middleware_foliorelay_t6_probe.py", + "control_artifact": "foliorelay-t6-beta3-control", + "existing_selector": "foliorelay" + } + ] +} From f8adb6444fd852fe78da3c57a4c6ec62e40d0e8e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:29:02 +0200 Subject: [PATCH 270/719] rdte: test TrueNAS session and capsule contract --- tests/test_truenas_session_manifest.py | 167 +++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 tests/test_truenas_session_manifest.py diff --git a/tests/test_truenas_session_manifest.py b/tests/test_truenas_session_manifest.py new file mode 100644 index 000000000..f483c3b93 --- /dev/null +++ b/tests/test_truenas_session_manifest.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +import importlib.util +import json +import pathlib +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "truenas_session_manifest", ROOT / "scripts" / "truenas_session_manifest.py" +) +MOD = importlib.util.module_from_spec(SPEC) +assert SPEC.loader +SPEC.loader.exec_module(MOD) +TARGETS = ROOT / "config" / "truenas-rdte-targets.json" +PROVIDERS = ROOT / "config" / "truenas-capsule-providers.json" + +def base_manifest(): + doc = { + "schema": "truenas-session/v1", + "session_id": "tn-26-beta3-session-contract-001", + "version": "26.0.0-BETA.3", + "authority": { + "repository": "SemperSupra/agent-dispatch-private", + "issue": 480 + }, + "budget": { + "guest_memory_mib": 8192, + "download_mib": 1536, + "timeout_minutes": 45 + }, + "pool": {"name": "rdtepool", "data_disks": 2}, + "capsules": [{ + "schema": "truenas-capsule/v1", + "id": "official-catalog-control-001", + "kind": "official-catalog-control", + "provider": "official-catalog-t6", + "authority": { + "repository": "SemperSupra/truenas-app-foundry-private", + "issue": 281 + }, + "exact": { + "source_ref": "60d56cc1752bc825cb232d72b397ff17fe6d47c4", + "artifact": "official-catalog-truenas-t6-control" + }, + "namespace": "rdte-official-catalog-001", + "ports": [18080], + "requirements": ["apps", "zfs-pool", "native-catalog"], + "resources": { + "memory_mib": 1024, + "download_mib": 512, + "estimated_minutes": 10 + }, + "phases": { + "setup": True, "apply": True, "verify": True, "cleanup": True + }, + "oracle": "native catalog lifecycle readback plus zero-residue cleanup", + "receipt_contract": "truenas-capsule-receipt/v1", + "retry_policy": "read-only-reconcile", + "mutating": True, + "cleanup_required": True, + "dependencies": [] + }] + } + doc["manifest_sha256"] = MOD.manifest_digest(doc) + return doc + +class SessionManifestTests(unittest.TestCase): + def write(self, doc): + tmp = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) + with tmp: + json.dump(doc, tmp, indent=2, sort_keys=True) + tmp.write("\n") + path = pathlib.Path(tmp.name) + self.addCleanup(lambda: path.unlink(missing_ok=True)) + return path + + def validate(self, doc): + doc["manifest_sha256"] = MOD.manifest_digest(doc) + return MOD.validate_manifest(self.write(doc), TARGETS, PROVIDERS) + + def test_valid_single_capsule_manifest(self): + result = self.validate(base_manifest()) + self.assertEqual(result["capsule_count"], 1) + self.assertEqual(result["mutating_capsule_count"], 1) + self.assertEqual(result["version"], "26.0.0-BETA.3") + + def test_manifest_hash_fails_closed_on_drift(self): + doc = base_manifest() + path = self.write(doc) + loaded = json.loads(path.read_text()) + loaded["capsules"][0]["namespace"] = "rdte-mutated-after-lock" + path.write_text(json.dumps(loaded), encoding="utf-8") + with self.assertRaisesRegex(MOD.SessionError, "manifest_sha256 mismatch"): + MOD.validate_manifest(path, TARGETS, PROVIDERS) + + def test_unknown_target_fails_closed(self): + doc = base_manifest() + doc["version"] = "26.0.0-RC.1" + with self.assertRaisesRegex(MOD.SessionError, "not registered"): + self.validate(doc) + + def test_unknown_provider_fails_closed(self): + doc = base_manifest() + doc["capsules"][0]["provider"] = "invented-provider" + with self.assertRaisesRegex(MOD.SessionError, "not registered"): + self.validate(doc) + + def test_mutating_capsule_requires_cleanup(self): + doc = base_manifest() + doc["capsules"][0]["cleanup_required"] = False + with self.assertRaisesRegex(MOD.SessionError, "must require cleanup"): + self.validate(doc) + + def test_duplicate_namespace_rejected(self): + doc = base_manifest() + second = json.loads(json.dumps(doc["capsules"][0])) + second["id"] = "official-catalog-control-002" + second["ports"] = [18081] + doc["capsules"].append(second) + with self.assertRaisesRegex(MOD.SessionError, "namespaces must be unique"): + self.validate(doc) + + def test_global_port_collision_rejected(self): + doc = base_manifest() + second = json.loads(json.dumps(doc["capsules"][0])) + second["id"] = "official-catalog-control-002" + second["namespace"] = "rdte-official-catalog-002" + doc["capsules"].append(second) + with self.assertRaisesRegex(MOD.SessionError, "ports must be globally unique"): + self.validate(doc) + + def test_resource_budget_rejected_before_runtime(self): + doc = base_manifest() + doc["capsules"][0]["resources"]["estimated_minutes"] = 46 + with self.assertRaisesRegex(MOD.SessionError, "estimated time exceeds"): + self.validate(doc) + + def test_dependency_cycle_rejected(self): + doc = base_manifest() + first = doc["capsules"][0] + second = json.loads(json.dumps(first)) + second["id"] = "official-catalog-control-002" + second["namespace"] = "rdte-official-catalog-002" + second["ports"] = [18081] + first["dependencies"] = [second["id"]] + second["dependencies"] = [first["id"]] + doc["capsules"].append(second) + with self.assertRaisesRegex(MOD.SessionError, "dependency cycle"): + self.validate(doc) + + def test_provider_registry_wraps_existing_probes(self): + registry = json.loads(PROVIDERS.read_text(encoding="utf-8")) + expected = { + "litellm-t6": "scripts/truenas_middleware_litellm_t6_probe.py", + "wow-sidecar-t6": "scripts/truenas_middleware_wow_sidecar_t6_probe.py", + "garm-t6": "scripts/truenas_middleware_garm_t6_probe.py", + "official-catalog-t6": "scripts/truenas_middleware_official_catalog_t6_probe.py", + "foliorelay-t6": "scripts/truenas_middleware_foliorelay_t6_probe.py" + } + actual = {item["id"]: item["probe"] for item in registry["providers"]} + self.assertEqual(actual, expected) + for probe in actual.values(): + self.assertTrue((ROOT / probe).is_file(), probe) + +if __name__ == "__main__": + unittest.main() From fc8969ddc107dbc9b50834c3c1ef7cd765d8985b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:29:11 +0200 Subject: [PATCH 271/719] docs: define TrueNAS qualification session membrane --- docs/truenas-qualification-sessions.md | 68 ++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 docs/truenas-qualification-sessions.md diff --git a/docs/truenas-qualification-sessions.md b/docs/truenas-qualification-sessions.md new file mode 100644 index 000000000..aa48586c0 --- /dev/null +++ b/docs/truenas-qualification-sessions.md @@ -0,0 +1,68 @@ +# TrueNAS exact-version qualification sessions + +Authority: `SemperSupra/agent-dispatch-private#480`. + +This layer amortizes a single expensive disposable TrueNAS installation across multiple +independently authoritative test capsules. It does **not** turn a session into a product +verdict and does not move product authority into Agent Dispatch. + +## Contracts + +- `truenas-session/v1` binds one exact TrueNAS target, one immutable manifest digest, + one disposable pool topology, one session authority, and one bounded resource budget. +- `truenas-capsule/v1` binds one independently owned test capsule to an exact provider, + producer/artifact identities, namespace/ports, resource ceiling, oracle, cleanup + policy, receipt contract, and retry policy. +- `config/truenas-capsule-providers.json` is only an execution adapter registry. It + points at existing probes; it is not a product inventory or support matrix. + +Foundry and product repositories remain authoritative for what should be tested and for +the meaning of product receipts. + +## Isolation membrane + +Every mutating capsule must eventually execute under this membrane: + +``` +observe owned namespace + -> prove clean precondition + -> setup/apply + -> independent verify + -> cleanup + -> prove zero unintended residue + -> re-observe platform health +``` + +A product/oracle failure may be followed by another independent capsule only if cleanup +and platform-health reconciliation both succeed. Cleanup ambiguity must stop subsequent +mutation and classify the session as contaminated. + +## Immutable launch + +The session manifest contains `manifest_sha256`, computed over canonical JSON with the +hash field excluded. A runtime request must bind that digest before the heavyweight job +starts. Editing both payload and digest creates a different proposed session; it cannot +change the semantic identity of an already-launched session. + +The validator also fails closed on: + +- unknown exact TrueNAS versions; +- unknown capsule providers; +- duplicate capsule ids, namespaces, or ports; +- dependency cycles or missing dependencies; +- mutating capsules without mandatory cleanup; +- resource/time budget overflow; +- the current 8-GiB guest and 50-minute heavyweight guardrails. + +## Migration sequence + +1. Prove this static contract. +2. Wrap current T6 probes without rewriting them. +3. Prove one-capsule receipt equivalence with the existing one-product execution path. +4. Add the session runner and cleanup/platform-health membrane. +5. Prove an intentionally failing multi-capsule BETA.3 session. +6. Only then migrate ordinary product requests to immutable session manifests. +7. Retain one-capsule manifests for high-risk/new experiments. + +The active FolioRelay BETA.3 run that predates this contract is intentionally not +reinterpreted as a session. From ef77672f6ace84f5eda3d8a448899931b4a5a820 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:29:25 +0200 Subject: [PATCH 272/719] ci: admit TrueNAS session contract to cheap RDTE gate --- .github/workflows/prep-rdte-static.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 988e3e554..681bbb22e 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -9,8 +9,10 @@ on: - '.github/workflows/gha-kvm-system-rdte.yml' - 'config/truenas-rdte-targets.json' - 'config/truenas-rdte-run-request.json' + - 'config/truenas-capsule-providers.json' - 'scripts/gha_kvm_truenas_rdte.sh' - 'scripts/truenas_rdte_target.py' + - 'scripts/truenas_session_manifest.py' - 'scripts/truenas_rdte_target.py' - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_middleware_*.py' @@ -27,6 +29,7 @@ on: - 'tests/test_truenas_garm_provider_g4.py' - 'tests/test_truenas_garm_provider_g5.py' - 'tests/test_truenas_rdte_target.py' + - 'tests/test_truenas_session_manifest.py' - 'tests/test_truenas_rdte_target.py' pull_request: paths: @@ -106,6 +109,9 @@ jobs: if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi + if [[ -f tests/test_truenas_session_manifest.py ]]; then + tests+=(tests/test_truenas_session_manifest.py) + fi python3 -m unittest -v "${tests[@]}" - name: G3 runner container profile smoke From d29c9eeefaf043cb031ab0c733c314ee1c227102 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:31:59 +0200 Subject: [PATCH 273/719] rdte: preserve full FolioRelay CUPS UUID in oracle --- scripts/truenas_middleware_foliorelay_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 0f43043c6..56fec343c 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -337,7 +337,7 @@ def wait_state(name,state): with tempfile.TemporaryDirectory() as td: root=pathlib.Path(td) attrs=ipptool_attrs(a.ipp_port,root) - m=re.search(r"printer-uuid[^\n]*= urn:uuid:([^\s]+)",attrs) + m=re.search(r"printer-uuid[^\n]*= (urn:uuid:[^\s]+)",attrs) cups_uuid=m.group(1) if m else None if cups_uuid!=uuid: raise RuntimeError("CUPS UUID does not match control") if PUBLIC_URI not in attrs: raise RuntimeError("CUPS public URI does not match control") From 52c0b1ca1a4922b94c6ac5a146fdbeda26ec9f8b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:32:09 +0200 Subject: [PATCH 274/719] test: lock canonical FolioRelay UUID comparison --- tests/test_truenas_t6_foliorelay_control.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index befdc9257..f78522346 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -60,6 +60,11 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): self.assertIn('FOLIORELAY_OBSERVER_HOST_PORT', harness) self.assertIn('--observer-port "$FOLIORELAY_OBSERVER_HOST_PORT"', harness) + def test_cups_uuid_oracle_preserves_canonical_urn_prefix(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn(r'(urn:uuid:[^\\s]+)', text) + self.assertNotIn(r'= urn:uuid:([^\\s]+)', text) + def test_probe_compiles(self): cp=subprocess.run([sys.executable,"-m","py_compile",str(SCRIPT)],capture_output=True,text=True) self.assertEqual(cp.returncode,0,cp.stderr) From ec25a18cbd04503f5e9005c8946466c31dbb57f9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:32:17 +0200 Subject: [PATCH 275/719] rdte: request corrected FolioRelay BETA.3 rep 002 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 262f1bf35..45ffe3664 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-001-mvp", + "request_id": "tn-26-beta3-t6-foliorelay-002-uuid-oracle-fix", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Qualify the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 using the pinned Foundry-exported three-service control: exact control/CUPS image read-back, canonical control/CUPS/DNS-SD identity, portal and native IPP, exact PDF and Apple Raster Inbox artifacts, independent in-guest _universal DNS-SD observation, stop/start plus update/redeploy persistence, desired/live re-plan NOOP, and zero-residue app/observer/dataset cleanup. This is pre-hardware software/control-plane qualification only." + "purpose": "Re-run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after correcting only the external Agent Dispatch UUID parser/oracle demonstrated by rep 001. Preserve the pinned Foundry three-service control and require the same product oracles: full canonical urn:uuid identity coherence, portal and native IPP, exact PDF and Apple Raster Inbox artifacts, independent in-guest _universal DNS-SD observation, stop/start plus update/redeploy persistence, desired/live re-plan NOOP, and zero-residue app/observer/dataset cleanup." } From 901d14a7fa5afd2b27d8fbb191c1b985df8341fa Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:33:08 +0200 Subject: [PATCH 276/719] test: correct source-regex assertion escaping From 355b57da5a4e07280dcde83bc0a46c5f42900d81 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:35:03 +0200 Subject: [PATCH 277/719] rdte: make canonical CUPS UUID parsing directly testable --- scripts/truenas_middleware_foliorelay_t6_probe.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 56fec343c..7a81add42 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -179,6 +179,10 @@ def ipptool_attrs(port:int, root:pathlib.Path): if out.returncode: raise RuntimeError("IPP Get-Printer-Attributes failed") return out.stdout+"\n"+out.stderr +def extract_printer_uuid(attrs: str): + match = re.search(r"printer-uuid[^\n]*= (urn:uuid:[^\s]+)", attrs) + return match.group(1) if match else None + def ipptool_print(port:int, media:str, source:pathlib.Path, label:str, root:pathlib.Path): t=root/f"print-{label}.test" t.write_text("""{ @@ -337,8 +341,7 @@ def wait_state(name,state): with tempfile.TemporaryDirectory() as td: root=pathlib.Path(td) attrs=ipptool_attrs(a.ipp_port,root) - m=re.search(r"printer-uuid[^\n]*= (urn:uuid:[^\s]+)",attrs) - cups_uuid=m.group(1) if m else None + cups_uuid=extract_printer_uuid(attrs) if cups_uuid!=uuid: raise RuntimeError("CUPS UUID does not match control") if PUBLIC_URI not in attrs: raise RuntimeError("CUPS public URI does not match control") if "application/pdf" not in attrs or "image/urf" not in attrs: raise RuntimeError("CUPS document formats drifted") From 0d339394a5442acceee1cb60c806da2f68951462 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:35:12 +0200 Subject: [PATCH 278/719] test: exercise FolioRelay UUID parser behavior --- tests/test_truenas_t6_foliorelay_control.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index f78522346..bccb4714e 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -61,9 +61,15 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): self.assertIn('--observer-port "$FOLIORELAY_OBSERVER_HOST_PORT"', harness) def test_cups_uuid_oracle_preserves_canonical_urn_prefix(self): - text = SCRIPT.read_text(encoding="utf-8") - self.assertIn(r'(urn:uuid:[^\\s]+)', text) - self.assertNotIn(r'= urn:uuid:([^\\s]+)', text) + attrs = """ + printer-uuid (uri) = urn:uuid:01234567-89ab-4def-8123-456789abcdef + printer-uri-supported (uri) = ipp://foliorelay-t6.local:8634/printers/FolioRelay + """ + self.assertEqual( + MOD.extract_printer_uuid(attrs), + "urn:uuid:01234567-89ab-4def-8123-456789abcdef", + ) + self.assertIsNone(MOD.extract_printer_uuid("printer-name = FolioRelay")) def test_probe_compiles(self): cp=subprocess.run([sys.executable,"-m","py_compile",str(SCRIPT)],capture_output=True,text=True) From 4bf0199d9a5e5d8afdd5ee8730149b54a4711f83 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:37:10 +0200 Subject: [PATCH 279/719] rdte: lower one session capsule to existing T6 selector --- scripts/truenas_single_capsule_adapter.py | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 scripts/truenas_single_capsule_adapter.py diff --git a/scripts/truenas_single_capsule_adapter.py b/scripts/truenas_single_capsule_adapter.py new file mode 100644 index 000000000..7eb087ee8 --- /dev/null +++ b/scripts/truenas_single_capsule_adapter.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import sys + +from truenas_session_manifest import SessionError, validate_manifest + +def load_json(path: pathlib.Path): + value=json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value,dict): + raise SessionError(f"{path}: top-level value must be an object") + return value + +def provider_map(path: pathlib.Path): + doc=load_json(path) + if doc.get("schema")!="truenas-capsule-providers/v1": + raise SessionError("unsupported capsule provider registry schema") + return {x["id"]:x for x in doc.get("providers",[])} + +def lower_single(manifest_path, targets_path, providers_path): + validated=validate_manifest(manifest_path,targets_path,providers_path) + manifest=load_json(manifest_path) + capsules=manifest["capsules"] + if len(capsules)!=1: + raise SessionError("single-capsule equivalence adapter requires exactly one capsule") + capsule=capsules[0] + provider=provider_map(providers_path).get(capsule["provider"]) + if not provider: + raise SessionError(f"unregistered provider: {capsule['provider']}") + selector=provider.get("existing_selector") + artifact=provider.get("control_artifact") + probe=provider.get("probe") + if not all(isinstance(x,str) and x for x in (selector,artifact,probe)): + raise SessionError("provider lacks existing execution binding") + return { + "schema":"truenas-single-capsule-lowering/v1", + "session_id":validated["session_id"], + "manifest_sha256":validated["manifest_sha256"], + "version":validated["version"], + "capsule_id":capsule["id"], + "provider":capsule["provider"], + "existing_selector":selector, + "control_artifact":artifact, + "probe":probe, + "authority":capsule["authority"], + "runtime_semantics_changed":False, + } + +def main(): + p=argparse.ArgumentParser() + p.add_argument("--manifest",type=pathlib.Path,required=True) + p.add_argument("--targets",type=pathlib.Path,default=pathlib.Path("config/truenas-rdte-targets.json")) + p.add_argument("--providers",type=pathlib.Path,default=pathlib.Path("config/truenas-capsule-providers.json")) + a=p.parse_args() + try: + print(json.dumps(lower_single(a.manifest,a.targets,a.providers),indent=2,sort_keys=True)) + return 0 + except (SessionError,OSError,json.JSONDecodeError) as exc: + print(f"ERROR: {exc}",file=sys.stderr) + return 2 + +if __name__=="__main__": + raise SystemExit(main()) From 0586743397c283bb1eee5b2dbfc9025d7d281e36 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:37:27 +0200 Subject: [PATCH 280/719] test: prove session capsule lowers to existing T6 execution --- tests/test_truenas_single_capsule_adapter.py | 84 ++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_truenas_single_capsule_adapter.py diff --git a/tests/test_truenas_single_capsule_adapter.py b/tests/test_truenas_single_capsule_adapter.py new file mode 100644 index 000000000..58e6cb3bd --- /dev/null +++ b/tests/test_truenas_single_capsule_adapter.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +import importlib.util +import json +import pathlib +import tempfile +import unittest + +ROOT=pathlib.Path(__file__).resolve().parents[1] +SCRIPTS=ROOT/"scripts" +import sys +sys.path.insert(0,str(SCRIPTS)) +import truenas_session_manifest as session +import truenas_single_capsule_adapter as adapter + +TARGETS=ROOT/"config"/"truenas-rdte-targets.json" +PROVIDERS=ROOT/"config"/"truenas-capsule-providers.json" + +EXPECTED={ + "litellm-t6":("litellm","litellm-truenas-t6-control","scripts/truenas_middleware_litellm_t6_probe.py"), + "wow-sidecar-t6":("wow-sidecar","wow-sidecar-truenas-t6-control","scripts/truenas_middleware_wow_sidecar_t6_probe.py"), + "garm-t6":("garm","garm-truenas-t6-control","scripts/truenas_middleware_garm_t6_probe.py"), + "official-catalog-t6":("official-catalog","official-catalog-truenas-t6-control","scripts/truenas_middleware_official_catalog_t6_probe.py"), + "foliorelay-t6":("foliorelay","foliorelay-t6-beta3-control","scripts/truenas_middleware_foliorelay_t6_probe.py"), +} + +def manifest(provider): + doc={ + "schema":"truenas-session/v1", + "session_id":"tn-26-beta3-single-equivalence-001", + "version":"26.0.0-BETA.3", + "authority":{"repository":"SemperSupra/agent-dispatch-private","issue":480}, + "budget":{"guest_memory_mib":8192,"download_mib":1024,"timeout_minutes":30}, + "pool":{"name":"rdtepool","data_disks":2}, + "capsules":[{ + "schema":"truenas-capsule/v1","id":"equivalence-capsule-001", + "kind":"official-catalog-control" if provider=="official-catalog-t6" else "foundry-product", + "provider":provider, + "authority":{"repository":"SemperSupra/truenas-app-foundry-private","issue":282}, + "exact":{"source_ref":"0123456789012345678901234567890123456789","artifact":"fixture"}, + "namespace":"rdte-equivalence-001","ports":[18080], + "requirements":["apps","zfs-pool"], + "resources":{"memory_mib":1024,"download_mib":256,"estimated_minutes":10}, + "phases":{"setup":True,"apply":True,"verify":True,"cleanup":True}, + "oracle":"existing product oracle unchanged", + "receipt_contract":"existing-product-receipt/unchanged", + "retry_policy":"read-only-reconcile","mutating":True,"cleanup_required":True, + "dependencies":[] + }] + } + doc["manifest_sha256"]=session.manifest_digest(doc) + return doc + +class SingleCapsuleEquivalenceTests(unittest.TestCase): + def write(self,doc): + f=tempfile.NamedTemporaryFile("w",suffix=".json",delete=False) + with f: + json.dump(doc,f,indent=2,sort_keys=True); f.write("\n") + p=pathlib.Path(f.name) + self.addCleanup(lambda:p.unlink(missing_ok=True)) + return p + + def test_every_existing_t6_provider_lowers_without_runtime_semantic_change(self): + for provider,(selector,artifact,probe) in EXPECTED.items(): + with self.subTest(provider=provider): + lowered=adapter.lower_single(self.write(manifest(provider)),TARGETS,PROVIDERS) + self.assertEqual(lowered["existing_selector"],selector) + self.assertEqual(lowered["control_artifact"],artifact) + self.assertEqual(lowered["probe"],probe) + self.assertFalse(lowered["runtime_semantics_changed"]) + self.assertEqual(lowered["manifest_sha256"],session.manifest_digest(manifest(provider))) + + def test_multi_capsule_manifest_is_rejected_by_equivalence_adapter(self): + doc=manifest("foliorelay-t6") + second=json.loads(json.dumps(doc["capsules"][0])) + second["id"]="equivalence-capsule-002" + second["namespace"]="rdte-equivalence-002" + second["ports"]=[18081] + doc["capsules"].append(second) + doc["manifest_sha256"]=session.manifest_digest(doc) + with self.assertRaisesRegex(session.SessionError,"exactly one capsule"): + adapter.lower_single(self.write(doc),TARGETS,PROVIDERS) + +if __name__=="__main__": + unittest.main() From 55e8154d3e64fe33f0dfddaf0a031de26284f60e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:37:37 +0200 Subject: [PATCH 281/719] ci: gate single-capsule equivalence adapter --- .github/workflows/prep-rdte-static.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 681bbb22e..d174da023 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -13,6 +13,7 @@ on: - 'scripts/gha_kvm_truenas_rdte.sh' - 'scripts/truenas_rdte_target.py' - 'scripts/truenas_session_manifest.py' + - 'scripts/truenas_single_capsule_adapter.py' - 'scripts/truenas_rdte_target.py' - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_middleware_*.py' @@ -30,6 +31,7 @@ on: - 'tests/test_truenas_garm_provider_g5.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_session_manifest.py' + - 'tests/test_truenas_single_capsule_adapter.py' - 'tests/test_truenas_rdte_target.py' pull_request: paths: @@ -112,6 +114,9 @@ jobs: if [[ -f tests/test_truenas_session_manifest.py ]]; then tests+=(tests/test_truenas_session_manifest.py) fi + if [[ -f tests/test_truenas_single_capsule_adapter.py ]]; then + tests+=(tests/test_truenas_single_capsule_adapter.py) + fi python3 -m unittest -v "${tests[@]}" - name: G3 runner container profile smoke From 9057b95d56a28ad65833ef444f029a6843df5f0c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:40:09 +0200 Subject: [PATCH 282/719] rdte: codify TrueNAS session contamination state machine --- scripts/truenas_session_state.py | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 scripts/truenas_session_state.py diff --git a/scripts/truenas_session_state.py b/scripts/truenas_session_state.py new file mode 100644 index 000000000..22f2a0120 --- /dev/null +++ b/scripts/truenas_session_state.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable + +TERMINAL_SESSION = { + "SESSION_CONTAMINATED", + "PLATFORM_UNHEALTHY", + "RESOURCE_GUARDRAIL", + "AUTHORITY_MISMATCH", +} +CAPSULE_VERDICTS = { + "SUPPORTED", + "ORACLE_FAILURE", + "HARNESS_FAILURE", + "ENVIRONMENT_FAILURE", + "UNSUPPORTED", + "DOCUMENTED_NEGATIVE", + "NOT_EXECUTED", +} + +@dataclass(frozen=True) +class CapsuleOutcome: + capsule_id: str + verdict: str + mutating: bool + cleanup_required: bool + cleanup_satisfied: bool + platform_healthy: bool + authority_satisfied: bool = True + resource_guardrail_satisfied: bool = True + +@dataclass(frozen=True) +class SessionDecision: + continue_mutation: bool + session_classification: str + next_state: str + reason: str + +def decide_after_capsule(outcome: CapsuleOutcome) -> SessionDecision: + if outcome.verdict not in CAPSULE_VERDICTS: + return SessionDecision(False, "HARNESS_FAILURE", "STOPPED", "unknown capsule verdict") + if not outcome.authority_satisfied: + return SessionDecision(False, "AUTHORITY_MISMATCH", "STOPPED", "capsule authority identity changed") + if not outcome.resource_guardrail_satisfied: + return SessionDecision(False, "RESOURCE_GUARDRAIL", "STOPPED", "resource floor/ceiling violated") + if outcome.mutating and outcome.cleanup_required and not outcome.cleanup_satisfied: + return SessionDecision(False, "SESSION_CONTAMINATED", "STOPPED", "mutating capsule cleanup was not proven") + if not outcome.platform_healthy: + return SessionDecision(False, "PLATFORM_UNHEALTHY", "STOPPED", "post-capsule platform health was not proven") + return SessionDecision(True, "IN_PROGRESS", "READY", "capsule failure domain reconciled; next independent capsule may run") + +def fold_session(outcomes: Iterable[CapsuleOutcome]) -> SessionDecision: + last=SessionDecision(True,"IN_PROGRESS","READY","no capsules executed") + for outcome in outcomes: + last=decide_after_capsule(outcome) + if not last.continue_mutation: + return last + return last + +def finalize_session(outcomes: Iterable[CapsuleOutcome]) -> str: + rows=list(outcomes) + decision=fold_session(rows) + if not decision.continue_mutation: + return decision.session_classification + if not rows: + return "NO_CAPSULES" + # A session can be structurally clean even when an individual product oracle failed. + # Per-capsule verdicts remain authoritative and must not be collapsed into product PASS. + return "SESSION_CLEAN" From 87bfc7c3d2133b3f754c5fa8b3de5c2cfefa0460 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:40:22 +0200 Subject: [PATCH 283/719] test: falsify TrueNAS session contamination semantics --- tests/test_truenas_session_state.py | 73 +++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/test_truenas_session_state.py diff --git a/tests/test_truenas_session_state.py b/tests/test_truenas_session_state.py new file mode 100644 index 000000000..357bcb843 --- /dev/null +++ b/tests/test_truenas_session_state.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import pathlib +import sys +import unittest + +ROOT=pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/"scripts")) +from truenas_session_state import CapsuleOutcome, decide_after_capsule, finalize_session + +def outcome(**overrides): + base=dict( + capsule_id="capsule-001", + verdict="SUPPORTED", + mutating=True, + cleanup_required=True, + cleanup_satisfied=True, + platform_healthy=True, + authority_satisfied=True, + resource_guardrail_satisfied=True, + ) + base.update(overrides) + return CapsuleOutcome(**base) + +class SessionStateTests(unittest.TestCase): + def test_supported_clean_capsule_allows_next(self): + d=decide_after_capsule(outcome()) + self.assertTrue(d.continue_mutation) + self.assertEqual(d.session_classification,"IN_PROGRESS") + + def test_product_oracle_failure_with_clean_membrane_allows_independent_next(self): + d=decide_after_capsule(outcome(verdict="ORACLE_FAILURE")) + self.assertTrue(d.continue_mutation) + self.assertEqual(d.next_state,"READY") + self.assertEqual(finalize_session([outcome(verdict="ORACLE_FAILURE")]),"SESSION_CLEAN") + + def test_cleanup_failure_contaminates_and_stops(self): + d=decide_after_capsule(outcome(verdict="ORACLE_FAILURE",cleanup_satisfied=False)) + self.assertFalse(d.continue_mutation) + self.assertEqual(d.session_classification,"SESSION_CONTAMINATED") + + def test_platform_health_failure_stops(self): + d=decide_after_capsule(outcome(platform_healthy=False)) + self.assertFalse(d.continue_mutation) + self.assertEqual(d.session_classification,"PLATFORM_UNHEALTHY") + + def test_authority_drift_stops_before_later_mutation(self): + d=decide_after_capsule(outcome(authority_satisfied=False)) + self.assertFalse(d.continue_mutation) + self.assertEqual(d.session_classification,"AUTHORITY_MISMATCH") + + def test_resource_guardrail_stops(self): + d=decide_after_capsule(outcome(resource_guardrail_satisfied=False)) + self.assertFalse(d.continue_mutation) + self.assertEqual(d.session_classification,"RESOURCE_GUARDRAIL") + + def test_read_only_failure_does_not_require_cleanup_if_platform_is_healthy(self): + d=decide_after_capsule(outcome( + verdict="ORACLE_FAILURE",mutating=False,cleanup_required=False,cleanup_satisfied=False + )) + self.assertTrue(d.continue_mutation) + + def test_unknown_verdict_fails_closed(self): + d=decide_after_capsule(outcome(verdict="MAGIC_PASS")) + self.assertFalse(d.continue_mutation) + self.assertEqual(d.session_classification,"HARNESS_FAILURE") + + def test_session_clean_does_not_mean_all_capsules_supported(self): + rows=[outcome(),outcome(capsule_id="capsule-002",verdict="DOCUMENTED_NEGATIVE")] + self.assertEqual(finalize_session(rows),"SESSION_CLEAN") + self.assertEqual(rows[1].verdict,"DOCUMENTED_NEGATIVE") + +if __name__=="__main__": + unittest.main() From 5ee5c57c2259e544d409435c0fd50afa105c6cdb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:40:31 +0200 Subject: [PATCH 284/719] ci: gate session contamination state machine --- .github/workflows/prep-rdte-static.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index d174da023..8926c849b 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -14,6 +14,7 @@ on: - 'scripts/truenas_rdte_target.py' - 'scripts/truenas_session_manifest.py' - 'scripts/truenas_single_capsule_adapter.py' + - 'scripts/truenas_session_state.py' - 'scripts/truenas_rdte_target.py' - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_middleware_*.py' @@ -32,6 +33,7 @@ on: - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_session_manifest.py' - 'tests/test_truenas_single_capsule_adapter.py' + - 'tests/test_truenas_session_state.py' - 'tests/test_truenas_rdte_target.py' pull_request: paths: @@ -117,6 +119,9 @@ jobs: if [[ -f tests/test_truenas_single_capsule_adapter.py ]]; then tests+=(tests/test_truenas_single_capsule_adapter.py) fi + if [[ -f tests/test_truenas_session_state.py ]]; then + tests+=(tests/test_truenas_session_state.py) + fi python3 -m unittest -v "${tests[@]}" - name: G3 runner container profile smoke From d3fe1eb84d16d18ea6d9d6c976ff1a1ebd3bd390 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:45:30 +0200 Subject: [PATCH 285/719] rdte: require fresh request edge for heavyweight TrueNAS routing --- .github/workflows/gha-kvm-system-rdte.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index fc491658a..ccfd53045 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -106,14 +106,13 @@ jobs: while IFS= read -r path; do case "$path" in config/truenas-rdte-run-request.json) + # Heavy TrueNAS work is request-edge driven. Harness/probe/workflow + # changes alone must never replay the previously committed request. truenas=true ;; scripts/gha_kvm_proxmox_rdte.sh) proxmox=true ;; - config/truenas-rdte-targets.json|scripts/truenas_rdte_target.py|scripts/gha_kvm_truenas_rdte.sh|scripts/truenas_installer_rpc_probe.py|scripts/truenas_installer_rpc_install.py|scripts/truenas_middleware_ddp_probe.py|scripts/truenas_middleware_pool_probe.py|scripts/truenas_middleware_app_probe.py|scripts/truenas_middleware_app_lifecycle_probe.py|scripts/truenas_middleware_litellm_t6_probe.py|scripts/truenas_middleware_wow_sidecar_t6_probe.py|scripts/truenas_middleware_garm_t6_probe.py|scripts/truenas_middleware_garm_provider_g2_probe.py|scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_garm_provider_g4_probe.py|scripts/truenas_middleware_garm_provider_g5_probe.py|scripts/truenas_middleware_official_catalog_t6_probe.py|scripts/truenas_middleware_foliorelay_t6_probe.py) - truenas=true - ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) qemu_topology=true ;; @@ -126,12 +125,22 @@ jobs: fi if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null + current_request_id="$(jq -er '.request_id | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" + previous_request_id="" + if git cat-file -e "$BEFORE_SHA:$RUN_REQUEST_PATH" 2>/dev/null; then + previous_request_id="$(git show "$BEFORE_SHA:$RUN_REQUEST_PATH" | jq -er '.request_id | select(type == "string" and length > 0)')" + fi + if [[ -n "$previous_request_id" && "$current_request_id" == "$previous_request_id" ]]; then + echo "refusing stale TrueNAS request replay: request_id did not change ($current_request_id)" >&2 + exit 2 + fi truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null + printf 'fresh TrueNAS request edge: previous=%s current=%s\n' "${previous_request_id:-}" "$current_request_id" fi echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" echo "truenas=$truenas" >>"$GITHUB_OUTPUT" From 8fba2c4ec94119d61010ec20332ae600083e0dfd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:46:00 +0200 Subject: [PATCH 286/719] ci: bound prep RDTE cost to one PR gate and scoped smokes --- .github/workflows/prep-rdte-static.yml | 71 ++++++++++++-------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 8926c849b..80bbfec2d 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -1,9 +1,7 @@ name: Prep RDTE static gate on: - push: - branches: - - 'prep/**' + pull_request: paths: - '.github/workflows/prep-rdte-static.yml' - '.github/workflows/gha-kvm-system-rdte.yml' @@ -11,50 +9,21 @@ on: - 'config/truenas-rdte-run-request.json' - 'config/truenas-capsule-providers.json' - 'scripts/gha_kvm_truenas_rdte.sh' + - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_rdte_target.py' - 'scripts/truenas_session_manifest.py' - 'scripts/truenas_single_capsule_adapter.py' - 'scripts/truenas_session_state.py' - - 'scripts/truenas_rdte_target.py' - - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_middleware_*.py' - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' - - 'tests/test_truenas_t6_litellm_control.py' - - 'tests/test_truenas_t6_wow_sidecar_control.py' - - 'tests/test_truenas_t6_garm_control.py' - - 'tests/test_truenas_t6_official_catalog_control.py' - - 'tests/test_truenas_t6_foliorelay_control.py' - - 'tests/test_truenas_t6_foliorelay_control.py' - - 'tests/test_truenas_garm_provider_g2.py' - - 'tests/test_truenas_garm_provider_g3.py' - - 'tests/test_truenas_garm_provider_g4.py' - - 'tests/test_truenas_garm_provider_g5.py' + - 'tests/test_truenas_t6_*.py' + - 'tests/test_truenas_garm_provider_*.py' - 'tests/test_truenas_rdte_target.py' - 'tests/test_truenas_session_manifest.py' - 'tests/test_truenas_single_capsule_adapter.py' - 'tests/test_truenas_session_state.py' - - 'tests/test_truenas_rdte_target.py' - pull_request: - paths: - - '.github/workflows/prep-rdte-static.yml' - - '.github/workflows/gha-kvm-system-rdte.yml' - - 'config/truenas-rdte-targets.json' - - 'config/truenas-rdte-run-request.json' - - 'scripts/gha_kvm_truenas_rdte.sh' - - 'scripts/gha_kvm_proxmox_rdte.sh' - - 'scripts/truenas_middleware_*.py' - - 'tests/test_gha_kvm_system_rdte.py' - - 'tests/test_truenas_future_protocols.py' - - 'tests/test_truenas_t6_litellm_control.py' - - 'tests/test_truenas_t6_wow_sidecar_control.py' - - 'tests/test_truenas_t6_garm_control.py' - - 'tests/test_truenas_t6_official_catalog_control.py' - - 'tests/test_truenas_garm_provider_g2.py' - - 'tests/test_truenas_garm_provider_g3.py' - - 'tests/test_truenas_garm_provider_g4.py' - - 'tests/test_truenas_garm_provider_g5.py' - - 'tests/test_truenas_rdte_target.py' + workflow_dispatch: permissions: contents: read @@ -67,6 +36,7 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + fetch-depth: 0 - name: Shell syntax run: | set -euo pipefail @@ -124,8 +94,31 @@ jobs: fi python3 -m unittest -v "${tests[@]}" + - name: Scope expensive provider smokes + id: expensive_scope + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + provider_smoke=false + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + provider_smoke=true + else + while IFS= read -r path; do + case "$path" in + scripts/truenas_middleware_garm_provider_g3_probe.py|scripts/truenas_middleware_garm_provider_g4_probe.py|scripts/truenas_middleware_garm_provider_g5_probe.py|tests/test_truenas_garm_provider_g3.py|tests/test_truenas_garm_provider_g4.py|tests/test_truenas_garm_provider_g5.py) + provider_smoke=true + ;; + esac + done < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA") + fi + echo "provider_smoke=$provider_smoke" >>"$GITHUB_OUTPUT" + printf 'expensive provider smoke scope: %s\n' "$provider_smoke" + - name: G3 runner container profile smoke - if: ${{ hashFiles('scripts/truenas_middleware_garm_provider_g3_probe.py') != '' }} + if: ${{ steps.expensive_scope.outputs.provider_smoke == 'true' && hashFiles('scripts/truenas_middleware_garm_provider_g3_probe.py') != '' }} run: | set -euo pipefail image='ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda' @@ -158,7 +151,7 @@ jobs: ' - name: G4 exact exported pair pre-JIT concurrency smoke - if: ${{ hashFiles('scripts/truenas_middleware_garm_provider_g4_probe.py') != '' }} + if: ${{ steps.expensive_scope.outputs.provider_smoke == 'true' && hashFiles('scripts/truenas_middleware_garm_provider_g4_probe.py') != '' }} run: | set -euo pipefail provider_ref='e5fc7c50780de54f7579f64990c87b2e16482613' @@ -230,7 +223,7 @@ jobs: done - name: G5 exact merged source matrix smoke - if: ${{ hashFiles('scripts/truenas_middleware_garm_provider_g5_probe.py') != '' }} + if: ${{ steps.expensive_scope.outputs.provider_smoke == 'true' && hashFiles('scripts/truenas_middleware_garm_provider_g5_probe.py') != '' }} run: | set -euo pipefail provider_ref='f83a712e095c6962741f93cf86bea69479ce4d4a' From e1d83b1d0efa7af5d0454a92dc0193b89e988e29 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:46:16 +0200 Subject: [PATCH 287/719] test: lock fresh-request and bounded prep routing --- tests/test_gha_kvm_system_rdte.py | 34 +++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index bb875b475..e9bf527e5 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -15,6 +15,7 @@ TRUENAS = ROOT / "scripts" / "gha_kvm_truenas_rdte.sh" TRUENAS_RPC = ROOT / "scripts" / "truenas_installer_rpc_probe.py" WORKFLOW = ROOT / ".github" / "workflows" / "gha-kvm-system-rdte.yml" +PREP_WORKFLOW = ROOT / ".github" / "workflows" / "prep-rdte-static.yml" QEMU_TOPOLOGY = ROOT / "scripts" / "gha_qemu_t3_topology_probe.py" TRUENAS_INSTALL = ROOT / "scripts" / "truenas_installer_rpc_install.py" TRUENAS_MIDDLEWARE = ROOT / "scripts" / "truenas_middleware_ddp_probe.py" @@ -298,23 +299,52 @@ def test_workflow_scopes_heavy_targets(self): self.assertIn("DISPATCH_TARGET", text) self.assertIn("BEFORE_SHA:", text) self.assertIn('git diff --name-only "$BEFORE_SHA" "$AFTER_SHA"', text) - self.assertIn("scripts/truenas_installer_rpc_probe.py", text) self.assertIn("truenas_version:", text) self.assertIn("truenas_rung:", text) self.assertIn("needs.changes.outputs.truenas_version", text) self.assertIn("needs.changes.outputs.truenas_rung", text) self.assertIn("config/truenas-rdte-run-request.json", text) - self.assertNotIn("run_request_changed", text) self.assertIn( 'if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then', text, ) self.assertIn("gha-kvm-truenas-run-request/v1", text) + self.assertIn("current_request_id=", text) + self.assertIn("previous_request_id=", text) + self.assertIn("refusing stale TrueNAS request replay", text) + self.assertIn('git show "$BEFORE_SHA:$RUN_REQUEST_PATH"', text) self.assertIn("truenas_version=\"$(jq -er", text) self.assertIn(".version | select(type == \"string\" and length > 0)", text) self.assertIn("truenas_rung=\"$(jq -er", text) self.assertIn('.rung | select(. == "t0" or . == "t1"', text) + routing = text.split("while IFS= read -r path; do", 1)[1].split( + "done < <(git diff --name-only", 1 + )[0] + self.assertIn("config/truenas-rdte-run-request.json)", routing) + for stale_replay_source in ( + "scripts/gha_kvm_truenas_rdte.sh", + "scripts/truenas_installer_rpc_probe.py", + "scripts/truenas_middleware_foliorelay_t6_probe.py", + "config/truenas-rdte-targets.json", + ): + self.assertNotIn(stale_replay_source, routing) + + def test_prep_gate_avoids_duplicate_branch_and_pr_runs_and_scopes_expensive_smokes(self): + text = PREP_WORKFLOW.read_text(encoding="utf-8") + trigger = text.split("permissions:", 1)[0] + self.assertIn("pull_request:", trigger) + self.assertIn("workflow_dispatch:", trigger) + self.assertNotIn("\n push:", trigger) + self.assertIn("id: expensive_scope", text) + self.assertIn("provider_smoke=false", text) + self.assertIn("provider_smoke=true", text) + self.assertIn( + "steps.expensive_scope.outputs.provider_smoke == 'true'", + text, + ) + self.assertIn("fetch-depth: 0", text) + def test_truenas_t1_requires_local_rpc_probe(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn("missing TrueNAS installer RPC probe", text) From 8ce9d8ad7edb7620af4f0d2e9fb606711114a2fb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:46:36 +0200 Subject: [PATCH 288/719] docs: record bounded parallel TrueNAS execution plan --- docs/truenas-parallel-execution-plan.md | 83 +++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/truenas-parallel-execution-plan.md diff --git a/docs/truenas-parallel-execution-plan.md b/docs/truenas-parallel-execution-plan.md new file mode 100644 index 000000000..4d23f9287 --- /dev/null +++ b/docs/truenas-parallel-execution-plan.md @@ -0,0 +1,83 @@ +# TrueNAS parallel execution and resource-governance plan + +Authority: `SemperSupra/agent-dispatch-private#480`. + +This plan separates conceptual parallelism from runner parallelism. Many read-only, +design, source-audit, matrix, and code lanes may proceed independently. Expensive CI +and real-system execution remain deliberately bounded. + +## Resource policy + +### Heavyweight + +- At most one active Real-system TrueNAS run. +- No automatic retry. +- No speculative matrix sweep. +- Every push-launched TrueNAS run requires a fresh run-request edge with a changed + `request_id`, an exact-head cheap gate, and reconciliation of the heavyweight + ownership authority before dispatch. + +### Cheap GitHub Actions + +- Prefer one orchestration-surface pull request at a time when it runs the shared RDTE + contract suite. +- A second independent cheap lane is acceptable when it does not duplicate the same + heavyweight provider smoke/download work. +- Prep branches use the pull-request gate as the authoritative exact-head gate; they do + not also pay for an identical push gate. +- G3/G4/G5 provider container smokes run only when their provider surfaces changed, or + by explicit manual dispatch. + +### Read-only work + +Repository inspection, receipt classification, source/API audits, issue/matrix +reconciliation, documentation, and manifest design may proceed in parallel without a +TrueNAS VM. + +## Parallel lanes + +A. Session runner / WP2: session envelope, capsule receipts, cleanup/platform membrane. + +B. FolioRelay causal qualification: isolate the current public-URI mismatch and prepare +only the bounded successor proven by that evidence. + +C. Foundry matrices: keep product inventory/readiness in Foundry and project only READY +exact cells to session manifests. + +D. Synthetic OCI/custom-app controls: small orthogonal fixtures that can first be proven +as ordinary containers before admission to TrueNAS capsules. + +E. Orchestration/resource governance: fresh-request routing, prep-CI cost control, +cancellation/evidence handling, and stale-replay regression coverage. + +F. VM/API source audit: read-only management-plane capability census; nested KVM remains +a separate optional late capsule. + +## Sequential gates + +1. Session/capsule contracts before a session runner gains mutation authority. +2. Contamination semantics before multi-capsule runtime execution. +3. Single-capsule runtime equivalence before multi-capsule proving. +4. Multi-capsule clean-continuation proof before intentional contamination reps. +5. Product-specific causal fix before its next heavyweight rep. +6. `SUPPORTED` plus `oracleSatisfied=true` on a product/version before advancing that + product to its next exact target. +7. BETA.3 session acceptance before exact-version session rollout. + +## Red-team invariants + +- Harness/probe/workflow changes must not replay a stale committed TrueNAS request. +- Session PASS never substitutes for capsule/product PASS. +- Failed cleanup or platform-health reconciliation stops later mutation. +- Resource or authority drift fails closed. +- Parallel branches that modify shared orchestration files are serialized at merge time. +- Existing accepted evidence is reused when semantically equivalent instead of rerun. +- VM/nested-virtualization experiments are excluded from the first multiplexed proving + session. + +## Blue-team expected effect + +This preserves independent causal evidence while reducing repeated ISO install/pool +initialization and duplicate cheap CI. Parallel work prepares the next bounded edge while +the single heavyweight lane executes the current one, so runner utilization increases +without creating a thundering herd. From cc86dcc10265ec4912d09afe6d7b015151e24818 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:50:00 +0200 Subject: [PATCH 289/719] test: bind T3/T5 probes to harness not heavy router --- tests/test_gha_kvm_system_rdte.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index e9bf527e5..5b9f23521 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -583,7 +583,6 @@ def test_truenas_t3_real_harness_contract(self): self.assertIn('"data_pool"', text) self.assertIn("truenas_rung:", workflow) self.assertIn("needs.changes.outputs.truenas_rung", workflow) - self.assertIn("scripts/truenas_middleware_pool_probe.py", workflow) shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) py_check = subprocess.run(["python3", "-m", "py_compile", str(TRUENAS_POOL)], text=True, capture_output=True) @@ -637,7 +636,6 @@ def test_truenas_t5_lifecycle_harness_contract(self): self.assertIn('"expected_version": a.expected_version', lifecycle) self.assertIn('--expected-version "$EXPECTED_SYSTEM_VERSION"', text) self.assertIn('APP_IMAGE = "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"', lifecycle) - self.assertIn("scripts/truenas_middleware_app_lifecycle_probe.py", workflow) self.assertIn("truenas_rung:", workflow) self.assertIn("needs.changes.outputs.truenas_rung", workflow) shell_check = subprocess.run( From 60d99546dd5ee0503b2bc43d6c5ed28e16a38f2c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:52:51 +0200 Subject: [PATCH 290/719] rdte: prove FolioRelay public URI in guest DNS-SD context --- .../truenas_middleware_foliorelay_t6_probe.py | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 7a81add42..e12f7d00b 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -33,7 +33,10 @@ TOKEN_PATH = ROOT + "/secrets/control.token" OBSERVER_DIR = ROOT + "/observer" TOKEN = b"foliorelay-t6-public-fixture-token-0001\n" -PUBLIC_URI = "ipp://foliorelay-t6.local:8634/printers/FolioRelay" +PUBLIC_HOST = "foliorelay-t6.local" +PUBLIC_IPP_PORT = 8634 +PUBLIC_RESOURCE_PATH = "/printers/FolioRelay" +PUBLIC_URI = f"ipp://{PUBLIC_HOST}:{PUBLIC_IPP_PORT}{PUBLIC_RESOURCE_PATH}" def canonical_sha256(value): return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest() @@ -183,6 +186,17 @@ def extract_printer_uuid(attrs: str): match = re.search(r"printer-uuid[^\n]*= (urn:uuid:[^\s]+)", attrs) return match.group(1) if match else None +def extract_printer_uri(attrs: str): + match = re.search(r"printer-uri-supported[^\n]*= (ipp://[^\s]+)", attrs) + return match.group(1) if match else None + +def forwarded_ipp_uri_has_product_path(uri: str | None) -> bool: + if not uri or not uri.startswith("ipp://"): + return False + tail = uri.split("://", 1)[1] + slash = tail.find("/") + return slash >= 0 and tail[slash:] == PUBLIC_RESOURCE_PATH + def ipptool_print(port:int, media:str, source:pathlib.Path, label:str, root:pathlib.Path): t=root/f"print-{label}.test" t.write_text("""{ @@ -226,7 +240,7 @@ def name_at(pkt,off,seen=None): if end is None: end=off+2 return ".".join(labels),end off+=1; labels.append(pkt[off:off+n].decode(errors="replace")); off+=n -def observe(expected,seconds=25): +def observe(expected,expected_host,expected_ipp_port,seconds=25): qid=0; query=struct.pack("!HHHHHH",qid,0,1,0,0,0)+enc(Q)+struct.pack("!HH",12,1) s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP) s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) @@ -246,18 +260,25 @@ def observe(expected,seconds=25): _,_,qd,an,ns,ar=struct.unpack("!HHHHHH",pkt[:12]); off=12 for _ in range(qd): _,off=name_at(pkt,off); off+=4 - universal_ptr=False; txt=[] + universal_ptr=False; txt=[]; srv_target=None; srv_port=None for _ in range(an+ns+ar): nm,off=name_at(pkt,off); typ,cls,ttl,rdlen=struct.unpack("!HHIH",pkt[off:off+10]); off+=10 - r=pkt[off:off+rdlen]; off+=rdlen + rstart=off; r=pkt[off:off+rdlen]; off+=rdlen if nm.rstrip(".").lower()==Q.lower() and typ==12: universal_ptr=True if typ==16: j=0 while j=7: + _,_,srv_port=struct.unpack("!HHH",r[:6]) + srv_target,_=name_at(pkt,rstart+6) joined="\n".join(txt) - if universal_ptr and expected.lower() in joined.lower() and "rp=printers/FolioRelay" in joined and "pdl=application/pdf,image/urf" in joined: - return {"universal_ptr":True,"uuid":expected,"txt":txt} + if (universal_ptr and expected.lower() in joined.lower() + and "rp=printers/FolioRelay" in joined + and "pdl=application/pdf,image/urf" in joined + and (srv_target or "").rstrip(".").lower()==expected_host.rstrip(".").lower() + and srv_port==expected_ipp_port): + return {"universal_ptr":True,"uuid":expected,"txt":txt,"srv_target":srv_target,"srv_port":srv_port} except Exception: pass raise RuntimeError("no qualifying _universal FolioRelay mDNS response") class H(BaseHTTPRequestHandler): @@ -265,8 +286,8 @@ class H(BaseHTTPRequestHandler): def do_GET(self): body=json.dumps(H.result).encode(); self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body) def log_message(self,*args): pass -p=argparse.ArgumentParser(); p.add_argument("--uuid",required=True); p.add_argument("--port",type=int,default=18081); a=p.parse_args() -H.result=observe(a.uuid) +p=argparse.ArgumentParser(); p.add_argument("--uuid",required=True); p.add_argument("--expected-host",required=True); p.add_argument("--expected-ipp-port",type=int,required=True); p.add_argument("--port",type=int,default=18081); a=p.parse_args() +H.result=observe(a.uuid,a.expected_host,a.expected_ipp_port) HTTPServer(("0.0.0.0",a.port),H).serve_forever() ''' @@ -343,7 +364,9 @@ def wait_state(name,state): attrs=ipptool_attrs(a.ipp_port,root) cups_uuid=extract_printer_uuid(attrs) if cups_uuid!=uuid: raise RuntimeError("CUPS UUID does not match control") - if PUBLIC_URI not in attrs: raise RuntimeError("CUPS public URI does not match control") + cups_transport_uri=extract_printer_uri(attrs) + if not forwarded_ipp_uri_has_product_path(cups_transport_uri): + raise RuntimeError("CUPS forwarded IPP URI does not preserve product resource path") if "application/pdf" not in attrs or "image/urf" not in attrs: raise RuntimeError("CUPS document formats drifted") pdf=root/"probe.pdf"; write_pdf(pdf); urf=generate_urf(root) ipptool_print(a.ipp_port,"application/pdf",pdf,"pdf",root); ipptool_print(a.ipp_port,"image/urf",urf,"urf",root) @@ -361,13 +384,16 @@ def wait_state(name,state): blob=http_bytes(a.host,a.control_port,f"/api/v1/jobs/{matches[0]['job_id']}/artifact",tok,timeout=a.timeout) if sha256_bytes(blob)!=sha: raise RuntimeError(f"{media} downloaded artifact drifted") sj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,OBSERVER_DIR+"/mdns_observer.py",OBSERVER.encode(),0o555,a.timeout); wait_job(sj,"observer upload") - obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR,"target":"/observer","read_only":True}],"entrypoint":["python3","/observer/mdns_observer.py"],"command":["--uuid",uuid,"--port","18081"]}}} + obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR,"target":"/observer","read_only":True}],"entrypoint":["python3","/observer/mdns_observer.py"],"command":["--uuid",uuid,"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} oj=call("app.create",[{"app_name":OBSERVER_APP_NAME,"custom_app":True,"custom_compose_config":obs_compose}]) if not isinstance(oj,int): raise RuntimeError("observer app.create did not return job") wait_job(oj,"observer app.create"); observer_created=True; wait_state(OBSERVER_APP_NAME,"RUNNING") if not wait_http(a.host,a.observer_port,"/",60): raise RuntimeError("DNS-SD observer did not publish a result") observed=json.loads(http_bytes(a.host,a.observer_port,"/",timeout=a.timeout)) - if observed.get("universal_ptr") is not True or observed.get("uuid")!=uuid: raise RuntimeError("DNS-SD observer identity mismatch") + if (observed.get("universal_ptr") is not True or observed.get("uuid")!=uuid + or (observed.get("srv_target") or "").rstrip(".").lower()!=PUBLIC_HOST.lower() + or observed.get("srv_port")!=PUBLIC_IPP_PORT): + raise RuntimeError("DNS-SD observer public URI identity mismatch") before_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout)) stop=call("app.stop",[EXPECTED_APP_NAME]); wait_job(stop,"app.stop"); wait_state(EXPECTED_APP_NAME,"STOPPED") start=call("app.start",[EXPECTED_APP_NAME]); wait_job(start,"app.start"); wait_state(EXPECTED_APP_NAME,"RUNNING") @@ -403,9 +429,9 @@ def wait_state(name,state): if not absent: raise RuntimeError("fixture mountpoint remains") payload.update({ "classification":"SUPPORTED","oracleSatisfied":True, - "identity":{"printer_uuid":uuid,"public_uri":PUBLIC_URI,"control_cups_uuid_match":True,"dnssd_uuid_match":True}, - "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP"}, - "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True}, + "identity":{"printer_uuid":uuid,"public_uri":PUBLIC_URI,"control_cups_uuid_match":True,"dnssd_uuid_match":True,"dnssd_public_uri_match":True}, + "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP"}, + "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True,"srv_target":PUBLIC_HOST,"srv_port":PUBLIC_IPP_PORT,"resource_path":PUBLIC_RESOURCE_PATH}, "cleanup":{"apps_absent":True,"fixture_dataset_absent":True,"fixture_mountpoint_absent":True,"zero_residue":True}, "producer_gate":{"replan_noop_required":True,"runtime_does_not_reconstruct_foundry_control":True}, "detail":"exact Foundry-exported FolioRelay control realized on TrueNAS; exact three-service identity, portal/IPP, PDF+URF source preservation, independent in-guest DNS-SD observation, restart persistence, and zero-residue cleanup passed" From 0731c5257b305146a6705693203e099d7ee985e1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:52:53 +0200 Subject: [PATCH 291/719] test: separate forwarded IPP transport from public URI oracle --- tests/test_truenas_t6_foliorelay_control.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index bccb4714e..1ecc4e4e6 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -71,6 +71,25 @@ def test_cups_uuid_oracle_preserves_canonical_urn_prefix(self): ) self.assertIsNone(MOD.extract_printer_uuid("printer-name = FolioRelay")) + def test_forwarded_ipp_uri_preserves_product_resource_path_without_claiming_public_host(self): + attrs = """ + printer-uri-supported (uri) = ipp://127.0.0.1:48634/printers/FolioRelay + """ + uri = MOD.extract_printer_uri(attrs) + self.assertEqual(uri, "ipp://127.0.0.1:48634/printers/FolioRelay") + self.assertTrue(MOD.forwarded_ipp_uri_has_product_path(uri)) + self.assertFalse(MOD.forwarded_ipp_uri_has_product_path("ipp://127.0.0.1:48634/printers/Other")) + + def test_dnssd_observer_proves_public_host_port_and_resource_path(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('"--expected-host",PUBLIC_HOST', text) + self.assertIn('"--expected-ipp-port",str(PUBLIC_IPP_PORT)', text) + self.assertIn('srv_target', text) + self.assertIn('srv_port', text) + self.assertIn('"rp=printers/FolioRelay"', text) + self.assertIn('"dnssd_public_uri_match":True', text) + self.assertNotIn('if PUBLIC_URI not in attrs', text) + def test_probe_compiles(self): cp=subprocess.run([sys.executable,"-m","py_compile",str(SCRIPT)],capture_output=True,text=True) self.assertEqual(cp.returncode,0,cp.stderr) From 8854cf1f611d793f30d2a36375e7b969f49ee675 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:56:00 +0200 Subject: [PATCH 292/719] rdte: request FolioRelay BETA.3 rep 003 public URI oracle --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 45ffe3664..9448577d9 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-002-uuid-oracle-fix", + "request_id": "tn-26-beta3-t6-foliorelay-003-public-uri-dnssd-oracle", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Re-run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after correcting only the external Agent Dispatch UUID parser/oracle demonstrated by rep 001. Preserve the pinned Foundry three-service control and require the same product oracles: full canonical urn:uuid identity coherence, portal and native IPP, exact PDF and Apple Raster Inbox artifacts, independent in-guest _universal DNS-SD observation, stop/start plus update/redeploy persistence, desired/live re-plan NOOP, and zero-residue app/observer/dataset cleanup." + "purpose": "Re-run exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after the bounded rep-002 public-URI oracle correction. Preserve product/Foundry/image identities. Host-forwarded IPP must retain the exact FolioRelay resource path and functional PDF/Apple Raster behavior; canonical public URI remains exact in the control API and is independently proven in the guest/LAN context by DNS-SD SRV target foliorelay-t6.local, port 8634, rp=printers/FolioRelay, UUID and PDL. Retain stop/start, update/redeploy persistence, desired/live re-plan NOOP, and zero-residue cleanup." } From 7a50ba07b544c3d6a9ca9b4c5c6a3631d0921f42 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:00:51 +0200 Subject: [PATCH 293/719] rdte: serialize only heavyweight jobs without cancellation --- .github/workflows/gha-kvm-system-rdte.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index ccfd53045..cf4c5cfc2 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -57,10 +57,6 @@ on: permissions: contents: read -concurrency: - group: gha-kvm-system-rdte - cancel-in-progress: true - jobs: changes: runs-on: ubuntu-24.04 @@ -282,6 +278,9 @@ jobs: retention-days: 7 proxmox-9-2: + concurrency: + group: gha-kvm-system-rdte-heavy + cancel-in-progress: false needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.proxmox == 'true' runs-on: ubuntu-26.04 @@ -332,6 +331,9 @@ jobs: retention-days: 7 truenas-26-beta3: + concurrency: + group: gha-kvm-system-rdte-heavy + cancel-in-progress: false needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] if: >- always() && From af2fea767756cf3ff7ffa22f5f1652a247c5be86 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:01:06 +0200 Subject: [PATCH 294/719] test: prevent no-route pushes from cancelling heavyweight jobs --- tests/test_gha_kvm_system_rdte.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 5b9f23521..8cd8854d5 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -345,6 +345,17 @@ def test_prep_gate_avoids_duplicate_branch_and_pr_runs_and_scopes_expensive_smok ) self.assertIn("fetch-depth: 0", text) + def test_only_heavy_jobs_share_non_cancelling_concurrency_group(self): + text = WORKFLOW.read_text(encoding="utf-8") + header = text.split("jobs:", 1)[0] + self.assertNotIn("concurrency:", header) + for job in ("proxmox-9-2", "truenas-26-beta3"): + section = text.split(f" {job}:", 1)[1] + section = section.split("\n ", 1)[0] + self.assertIn("group: gha-kvm-system-rdte-heavy", section) + self.assertIn("cancel-in-progress: false", section) + self.assertNotIn("cancel-in-progress: true", text) + def test_truenas_t1_requires_local_rpc_probe(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn("missing TrueNAS installer RPC probe", text) From 98605ecf4a34e1e25e73c300a24571f37acaf572 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 01:02:11 +0200 Subject: [PATCH 295/719] test: assert heavyweight concurrency blocks directly --- tests/test_gha_kvm_system_rdte.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 8cd8854d5..036cfb1d6 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -350,10 +350,15 @@ def test_only_heavy_jobs_share_non_cancelling_concurrency_group(self): header = text.split("jobs:", 1)[0] self.assertNotIn("concurrency:", header) for job in ("proxmox-9-2", "truenas-26-beta3"): - section = text.split(f" {job}:", 1)[1] - section = section.split("\n ", 1)[0] - self.assertIn("group: gha-kvm-system-rdte-heavy", section) - self.assertIn("cancel-in-progress: false", section) + expected = ( + f" {job}:\n" + " concurrency:\n" + " group: gha-kvm-system-rdte-heavy\n" + " cancel-in-progress: false\n" + ) + self.assertIn(expected, text) + self.assertEqual(text.count("group: gha-kvm-system-rdte-heavy"), 2) + self.assertEqual(text.count("cancel-in-progress: false"), 2) self.assertNotIn("cancel-in-progress: true", text) def test_truenas_t1_requires_local_rpc_probe(self): From b0df730d2086a9eac2dcafe98f3288472b65d79c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:35:00 +0200 Subject: [PATCH 296/719] rdte: add fail-closed TrueNAS session receipt runner --- scripts/truenas_session_runner.py | 321 ++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 scripts/truenas_session_runner.py diff --git a/scripts/truenas_session_runner.py b/scripts/truenas_session_runner.py new file mode 100644 index 000000000..c2f95f616 --- /dev/null +++ b/scripts/truenas_session_runner.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import subprocess +import sys +import tempfile +from typing import Any + +from truenas_session_manifest import SessionError, validate_manifest +from truenas_session_state import ( + CAPSULE_VERDICTS, + CapsuleOutcome, + decide_after_capsule, + finalize_session, +) + +RECEIPT_SCHEMA = "truenas-capsule-execution/v1" +SESSION_SCHEMA = "truenas-session-execution/v1" + + +def load_json(path: pathlib.Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise SessionError(f"cannot read {path}: {exc}") from exc + if not isinstance(value, dict): + raise SessionError(f"{path}: top-level value must be an object") + return value + + +def provider_map(path: pathlib.Path) -> dict[str, dict[str, Any]]: + doc = load_json(path) + if doc.get("schema") != "truenas-capsule-providers/v1": + raise SessionError("unsupported capsule provider registry schema") + rows = doc.get("providers") + if not isinstance(rows, list): + raise SessionError("provider registry requires providers list") + result: dict[str, dict[str, Any]] = {} + for row in rows: + if not isinstance(row, dict) or not isinstance(row.get("id"), str): + raise SessionError("provider registry contains invalid row") + if row["id"] in result: + raise SessionError(f"duplicate provider: {row['id']}") + result[row["id"]] = row + return result + + +def stable_topological_capsules(capsules: list[dict[str, Any]]) -> list[dict[str, Any]]: + remaining = list(capsules) + emitted: list[dict[str, Any]] = [] + emitted_ids: set[str] = set() + while remaining: + found = None + for index, capsule in enumerate(remaining): + deps = set(capsule.get("dependencies") or []) + if deps.issubset(emitted_ids): + found = index + break + if found is None: + raise SessionError("capsule dependency graph cannot be scheduled") + capsule = remaining.pop(found) + emitted.append(capsule) + emitted_ids.add(capsule["id"]) + return emitted + + +def validate_capsule_receipt( + receipt: dict[str, Any], + capsule: dict[str, Any], + provider: dict[str, Any], + manifest_sha256: str, +) -> CapsuleOutcome: + if receipt.get("schema") != RECEIPT_SCHEMA: + raise SessionError(f"{capsule['id']}: unsupported capsule receipt schema") + if receipt.get("capsule_id") != capsule["id"]: + raise SessionError(f"{capsule['id']}: receipt capsule identity mismatch") + if receipt.get("provider") != capsule["provider"]: + raise SessionError(f"{capsule['id']}: receipt provider identity mismatch") + if receipt.get("manifest_sha256") != manifest_sha256: + raise SessionError(f"{capsule['id']}: receipt manifest identity mismatch") + verdict = receipt.get("verdict") + if verdict not in CAPSULE_VERDICTS: + raise SessionError(f"{capsule['id']}: invalid receipt verdict {verdict!r}") + if receipt.get("mutating") is not capsule.get("mutating"): + raise SessionError(f"{capsule['id']}: mutating contract drift") + if receipt.get("cleanup_required") is not capsule.get("cleanup_required"): + raise SessionError(f"{capsule['id']}: cleanup contract drift") + for field in ( + "cleanup_satisfied", + "platform_healthy", + "authority_satisfied", + "resource_guardrail_satisfied", + ): + if not isinstance(receipt.get(field), bool): + raise SessionError(f"{capsule['id']}: receipt missing boolean {field}") + if receipt.get("provider_probe") != provider.get("probe"): + raise SessionError(f"{capsule['id']}: provider probe identity drift") + return CapsuleOutcome( + capsule_id=capsule["id"], + verdict=verdict, + mutating=capsule["mutating"], + cleanup_required=capsule["cleanup_required"], + cleanup_satisfied=receipt["cleanup_satisfied"], + platform_healthy=receipt["platform_healthy"], + authority_satisfied=receipt["authority_satisfied"], + resource_guardrail_satisfied=receipt["resource_guardrail_satisfied"], + ) + + +def not_executed_receipt( + capsule: dict[str, Any], + provider: dict[str, Any], + manifest_sha256: str, + reason: str, +) -> dict[str, Any]: + return { + "schema": RECEIPT_SCHEMA, + "capsule_id": capsule["id"], + "provider": capsule["provider"], + "provider_probe": provider.get("probe"), + "manifest_sha256": manifest_sha256, + "verdict": "NOT_EXECUTED", + "mutating": capsule["mutating"], + "cleanup_required": capsule["cleanup_required"], + "cleanup_satisfied": True, + "platform_healthy": True, + "authority_satisfied": True, + "resource_guardrail_satisfied": True, + "reason": reason, + } + + +def harness_failure_receipt( + capsule: dict[str, Any], + provider: dict[str, Any], + manifest_sha256: str, + detail: str, +) -> dict[str, Any]: + return { + "schema": RECEIPT_SCHEMA, + "capsule_id": capsule["id"], + "provider": capsule["provider"], + "provider_probe": provider.get("probe"), + "manifest_sha256": manifest_sha256, + "verdict": "HARNESS_FAILURE", + "mutating": capsule["mutating"], + "cleanup_required": capsule["cleanup_required"], + "cleanup_satisfied": not capsule["mutating"], + "platform_healthy": False, + "authority_satisfied": True, + "resource_guardrail_satisfied": True, + "reason": detail, + } + + +def execute_capsule( + executor: pathlib.Path, + capsule: dict[str, Any], + provider: dict[str, Any], + manifest_sha256: str, + context: pathlib.Path, + receipt_path: pathlib.Path, +) -> dict[str, Any]: + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as handle: + descriptor_path = pathlib.Path(handle.name) + json.dump( + { + "schema": "truenas-capsule-dispatch/v1", + "manifest_sha256": manifest_sha256, + "capsule": capsule, + "provider": provider, + }, + handle, + indent=2, + sort_keys=True, + ) + handle.write("\n") + try: + cp = subprocess.run( + [ + str(executor), + "--capsule", + str(descriptor_path), + "--context", + str(context), + "--out", + str(receipt_path), + ], + text=True, + capture_output=True, + check=False, + ) + if receipt_path.is_file(): + receipt = load_json(receipt_path) + receipt.setdefault("executor_returncode", cp.returncode) + return receipt + return harness_failure_receipt( + capsule, + provider, + manifest_sha256, + f"executor exited {cp.returncode} without receipt", + ) + finally: + descriptor_path.unlink(missing_ok=True) + + +def run_session( + manifest_path: pathlib.Path, + targets_path: pathlib.Path, + providers_path: pathlib.Path, + executor: pathlib.Path, + context: pathlib.Path, + out_dir: pathlib.Path, +) -> dict[str, Any]: + validated = validate_manifest(manifest_path, targets_path, providers_path) + manifest = load_json(manifest_path) + providers = provider_map(providers_path) + ordered = stable_topological_capsules(manifest["capsules"]) + out_dir.mkdir(parents=True, exist_ok=True) + + receipts: list[dict[str, Any]] = [] + outcomes: dict[str, CapsuleOutcome] = {} + stop_reason: str | None = None + stop_classification: str | None = None + + for capsule in ordered: + provider = providers[capsule["provider"]] + receipt_path = out_dir / f"{capsule['id']}.json" + + if stop_reason is not None: + receipt = not_executed_receipt( + capsule, + provider, + validated["manifest_sha256"], + f"session stopped: {stop_classification}: {stop_reason}", + ) + receipt_path.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") + receipts.append(receipt) + continue + + failed_dependencies = [ + dep + for dep in capsule.get("dependencies") or [] + if dep not in outcomes or outcomes[dep].verdict != "SUPPORTED" + ] + if failed_dependencies: + receipt = not_executed_receipt( + capsule, + provider, + validated["manifest_sha256"], + "unsatisfied dependencies: " + ", ".join(failed_dependencies), + ) + receipt_path.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") + receipts.append(receipt) + outcomes[capsule["id"]] = validate_capsule_receipt( + receipt, capsule, provider, validated["manifest_sha256"] + ) + continue + + receipt = execute_capsule( + executor, + capsule, + provider, + validated["manifest_sha256"], + context, + receipt_path, + ) + receipt_path.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") + outcome = validate_capsule_receipt( + receipt, capsule, provider, validated["manifest_sha256"] + ) + receipts.append(receipt) + outcomes[capsule["id"]] = outcome + decision = decide_after_capsule(outcome) + if not decision.continue_mutation: + stop_reason = decision.reason + stop_classification = decision.session_classification + + executed_outcomes = [ + outcomes[c["id"]] + for c in ordered + if c["id"] in outcomes and outcomes[c["id"]].verdict != "NOT_EXECUTED" + ] + classification = stop_classification or finalize_session(executed_outcomes) + return { + "schema": SESSION_SCHEMA, + "session_id": validated["session_id"], + "version": validated["version"], + "manifest_sha256": validated["manifest_sha256"], + "classification": classification, + "session_clean": classification == "SESSION_CLEAN", + "capsules": receipts, + "product_acceptance_inferred": False, + } + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--manifest", type=pathlib.Path, required=True) + p.add_argument("--targets", type=pathlib.Path, default=pathlib.Path("config/truenas-rdte-targets.json")) + p.add_argument("--providers", type=pathlib.Path, default=pathlib.Path("config/truenas-capsule-providers.json")) + p.add_argument("--executor", type=pathlib.Path, required=True) + p.add_argument("--context", type=pathlib.Path, required=True) + p.add_argument("--out-dir", type=pathlib.Path, required=True) + p.add_argument("--out", type=pathlib.Path, required=True) + a = p.parse_args() + try: + result = run_session(a.manifest, a.targets, a.providers, a.executor, a.context, a.out_dir) + a.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + except (SessionError, OSError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From 96ee3eeb0440fa895d425941b3acb02773d9a10b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:35:03 +0200 Subject: [PATCH 297/719] test: falsify TrueNAS session runner continuation semantics --- tests/test_truenas_session_runner.py | 191 +++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 tests/test_truenas_session_runner.py diff --git a/tests/test_truenas_session_runner.py b/tests/test_truenas_session_runner.py new file mode 100644 index 000000000..bd72b0429 --- /dev/null +++ b/tests/test_truenas_session_runner.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +import json +import pathlib +import stat +import sys +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import truenas_session_manifest as manifest_mod +import truenas_session_runner as runner +from truenas_session_manifest import SessionError + +TARGETS = ROOT / "config" / "truenas-rdte-targets.json" +PROVIDERS = ROOT / "config" / "truenas-capsule-providers.json" + + +def capsule(cid, provider, port, dependencies=None): + return { + "schema": "truenas-capsule/v1", + "id": cid, + "kind": "official-catalog-control" if provider == "official-catalog-t6" else "foundry-product", + "provider": provider, + "authority": {"repository": "SemperSupra/truenas-app-foundry-private", "issue": 282}, + "exact": {"source_ref": "0123456789012345678901234567890123456789", "artifact": "fixture"}, + "namespace": f"rdte-{cid}", + "ports": [port], + "requirements": ["apps", "zfs-pool"], + "resources": {"memory_mib": 1024, "download_mib": 128, "estimated_minutes": 5}, + "phases": {"setup": True, "apply": True, "verify": True, "cleanup": True}, + "oracle": "existing provider oracle", + "receipt_contract": "truenas-capsule-execution/v1", + "retry_policy": "read-only-reconcile", + "mutating": True, + "cleanup_required": True, + "dependencies": dependencies or [], + } + + +def manifest(capsules): + doc = { + "schema": "truenas-session/v1", + "session_id": "tn-26-beta3-runner-static-001", + "version": "26.0.0-BETA.3", + "authority": {"repository": "SemperSupra/agent-dispatch-private", "issue": 480}, + "budget": {"guest_memory_mib": 8192, "download_mib": 1024, "timeout_minutes": 45}, + "pool": {"name": "rdtepool", "data_disks": 2}, + "capsules": capsules, + } + doc["manifest_sha256"] = manifest_mod.manifest_digest(doc) + return doc + + +FAKE_EXECUTOR = r"""#!/usr/bin/env python3 +import argparse, json, pathlib, sys +p=argparse.ArgumentParser() +p.add_argument("--capsule", required=True) +p.add_argument("--context", required=True) +p.add_argument("--out", required=True) +a=p.parse_args() +desc=json.loads(pathlib.Path(a.capsule).read_text()) +ctx=json.loads(pathlib.Path(a.context).read_text()) +cap=desc["capsule"]; provider=desc["provider"] +cfg=ctx[cap["id"]] +if cfg.get("emit") is False: + raise SystemExit(cfg.get("returncode", 3)) +receipt={ + "schema":"truenas-capsule-execution/v1", + "capsule_id":cap["id"], + "provider":cap["provider"], + "provider_probe":provider["probe"], + "manifest_sha256":desc["manifest_sha256"], + "verdict":cfg.get("verdict","SUPPORTED"), + "mutating":cap["mutating"], + "cleanup_required":cap["cleanup_required"], + "cleanup_satisfied":cfg.get("cleanup_satisfied",True), + "platform_healthy":cfg.get("platform_healthy",True), + "authority_satisfied":cfg.get("authority_satisfied",True), + "resource_guardrail_satisfied":cfg.get("resource_guardrail_satisfied",True) +} +if "manifest_sha256" in cfg: + receipt["manifest_sha256"]=cfg["manifest_sha256"] +pathlib.Path(a.out).write_text(json.dumps(receipt)) +raise SystemExit(cfg.get("returncode",0)) +""" + + +class SessionRunnerTests(unittest.TestCase): + def temp_json(self, value): + f = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) + with f: + json.dump(value, f, indent=2, sort_keys=True) + f.write("\n") + path = pathlib.Path(f.name) + self.addCleanup(lambda: path.unlink(missing_ok=True)) + return path + + def executor(self): + f = tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) + with f: + f.write(FAKE_EXECUTOR) + path = pathlib.Path(f.name) + path.chmod(path.stat().st_mode | stat.S_IXUSR) + self.addCleanup(lambda: path.unlink(missing_ok=True)) + return path + + def execute_session(self, doc, context): + root = pathlib.Path(tempfile.mkdtemp()) + self.addCleanup(lambda: __import__("shutil").rmtree(root, ignore_errors=True)) + return runner.run_session( + self.temp_json(doc), + TARGETS, + PROVIDERS, + self.executor(), + self.temp_json(context), + root / "receipts", + ) + + def test_clean_session_preserves_independent_product_failure(self): + doc = manifest([ + capsule("catalog-control", "official-catalog-t6", 18080), + capsule("foliorelay", "foliorelay-t6", 18081), + ]) + result = self.execute_session(doc, { + "catalog-control": {"verdict": "SUPPORTED"}, + "foliorelay": {"verdict": "ORACLE_FAILURE"}, + }) + self.assertEqual(result["classification"], "SESSION_CLEAN") + self.assertTrue(result["session_clean"]) + self.assertFalse(result["product_acceptance_inferred"]) + self.assertEqual([x["verdict"] for x in result["capsules"]], ["SUPPORTED", "ORACLE_FAILURE"]) + + def test_cleanup_failure_stops_later_mutation(self): + doc = manifest([ + capsule("first", "official-catalog-t6", 18080), + capsule("second", "foliorelay-t6", 18081), + ]) + result = self.execute_session(doc, { + "first": {"verdict": "ORACLE_FAILURE", "cleanup_satisfied": False}, + "second": {"verdict": "SUPPORTED"}, + }) + self.assertEqual(result["classification"], "SESSION_CONTAMINATED") + self.assertEqual(result["capsules"][1]["verdict"], "NOT_EXECUTED") + self.assertIn("SESSION_CONTAMINATED", result["capsules"][1]["reason"]) + + def test_failed_dependency_skips_only_dependent_capsule(self): + doc = manifest([ + capsule("first", "official-catalog-t6", 18080), + capsule("dependent", "foliorelay-t6", 18081, ["first"]), + capsule("independent", "garm-t6", 18082), + ]) + result = self.execute_session(doc, { + "first": {"verdict": "ORACLE_FAILURE"}, + "dependent": {"verdict": "SUPPORTED"}, + "independent": {"verdict": "SUPPORTED"}, + }) + verdicts = {x["capsule_id"]: x["verdict"] for x in result["capsules"]} + self.assertEqual(verdicts["first"], "ORACLE_FAILURE") + self.assertEqual(verdicts["dependent"], "NOT_EXECUTED") + self.assertEqual(verdicts["independent"], "SUPPORTED") + self.assertEqual(result["classification"], "SESSION_CLEAN") + + def test_executor_without_receipt_fails_closed(self): + doc = manifest([ + capsule("first", "official-catalog-t6", 18080), + capsule("second", "foliorelay-t6", 18081), + ]) + result = self.execute_session(doc, { + "first": {"emit": False, "returncode": 7}, + "second": {"verdict": "SUPPORTED"}, + }) + self.assertEqual(result["classification"], "SESSION_CONTAMINATED") + self.assertEqual(result["capsules"][0]["verdict"], "HARNESS_FAILURE") + self.assertFalse(result["capsules"][0]["platform_healthy"]) + self.assertEqual(result["capsules"][1]["verdict"], "NOT_EXECUTED") + + def test_receipt_manifest_drift_fails_closed(self): + doc = manifest([capsule("first", "official-catalog-t6", 18080)]) + with self.assertRaisesRegex(SessionError, "receipt manifest identity mismatch"): + self.execute_session(doc, { + "first": { + "verdict": "SUPPORTED", + "manifest_sha256": "0" * 64, + } + }) + + +if __name__ == "__main__": + unittest.main() From c64bbe2b9a4be230610d42df80e85ebf0404a491 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:35:05 +0200 Subject: [PATCH 298/719] ci: gate TrueNAS session receipt runner --- .github/workflows/prep-rdte-static.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 80bbfec2d..a2ff4fc29 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -14,6 +14,7 @@ on: - 'scripts/truenas_session_manifest.py' - 'scripts/truenas_single_capsule_adapter.py' - 'scripts/truenas_session_state.py' + - 'scripts/truenas_session_runner.py' - 'scripts/truenas_middleware_*.py' - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' @@ -23,6 +24,7 @@ on: - 'tests/test_truenas_session_manifest.py' - 'tests/test_truenas_single_capsule_adapter.py' - 'tests/test_truenas_session_state.py' + - 'tests/test_truenas_session_runner.py' workflow_dispatch: permissions: @@ -92,6 +94,9 @@ jobs: if [[ -f tests/test_truenas_session_state.py ]]; then tests+=(tests/test_truenas_session_state.py) fi + if [[ -f tests/test_truenas_session_runner.py ]]; then + tests+=(tests/test_truenas_session_runner.py) + fi python3 -m unittest -v "${tests[@]}" - name: Scope expensive provider smokes From 92f724499d9e8cd0700a4950d6b855f3c547d38c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:37:09 +0200 Subject: [PATCH 299/719] ci: reproduce FolioRelay URF fixture build on heavy-runner image --- .github/workflows/prep-foliorelay-t6-consumer.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 7226c78a3..1a8003c75 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -33,12 +33,18 @@ jobs: consumer-contract: needs: foundry-control - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 8 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + - name: Ensure fixture compiler dependencies + run: | + set -euo pipefail + sudo apt-get update -qq + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + cups-ipp-utils libcups2-dev build-essential - name: Download exact Foundry FolioRelay control uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: From 59ffb3eea337ba08ccf62d9bca8d372db12ea532 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:37:11 +0200 Subject: [PATCH 300/719] test: compile and emit synthetic Apple Raster fixture --- tests/test_truenas_t6_foliorelay_control.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 1ecc4e4e6..b53bb98d6 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -2,6 +2,7 @@ import importlib.util import pathlib import subprocess +import tempfile import sys import unittest @@ -90,6 +91,12 @@ def test_dnssd_observer_proves_public_host_port_and_resource_path(self): self.assertIn('"dnssd_public_uri_match":True', text) self.assertNotIn('if PUBLIC_URI not in attrs', text) + def test_synthetic_urf_generator_builds_and_emits_unirast(self): + with tempfile.TemporaryDirectory() as td: + out = MOD.generate_urf(pathlib.Path(td)) + self.assertTrue(out.is_file()) + self.assertEqual(out.read_bytes()[:7], b"UNIRAST") + def test_probe_compiles(self): cp=subprocess.run([sys.executable,"-m","py_compile",str(SCRIPT)],capture_output=True,text=True) self.assertEqual(cp.returncode,0,cp.stderr) From 2d694e973212754b796997737db15d62ecb822b8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:38:06 +0200 Subject: [PATCH 301/719] test: scope URF compile diagnostic to CUPS-dev environments --- tests/test_truenas_t6_foliorelay_control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index b53bb98d6..a1cfeb086 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -91,6 +91,7 @@ def test_dnssd_observer_proves_public_host_port_and_resource_path(self): self.assertIn('"dnssd_public_uri_match":True', text) self.assertNotIn('if PUBLIC_URI not in attrs', text) + @unittest.skipUnless(pathlib.Path("/usr/include/cups/raster.h").is_file(), "CUPS development headers not installed") def test_synthetic_urf_generator_builds_and_emits_unirast(self): with tempfile.TemporaryDirectory() as td: out = MOD.generate_urf(pathlib.Path(td)) From b5f5b740e36a1c23c3868e2b6dff4837984f5a1f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:39:23 +0200 Subject: [PATCH 302/719] rdte: fix and diagnose synthetic URF fixture compilation --- .../truenas_middleware_foliorelay_t6_probe.py | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index e12f7d00b..eff28f00d 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -149,23 +149,47 @@ def write_pdf(path: pathlib.Path): #include #include int main(int argc,char **argv){ - cups_page_header2_t h; cups_raster_t *r; pwg_media_t *m; unsigned char *line; unsigned y; - if(argc!=2) return 64; m=pwgMediaForPWG("iso_a4_210x297mm"); if(!m) return 1; + cups_page_header2_t h; + cups_raster_t *r; + pwg_media_t *m; + unsigned char *line; + unsigned y; + int fd; + + if(argc!=2) return 64; + m=pwgMediaForPWG("iso_a4_210x297mm"); + if(!m) return 1; if(!cupsRasterInitPWGHeader(&h,m,"sgray_8",300,300,"one-sided",NULL)) return 1; h.cupsInteger[CUPS_RASTER_PWG_TotalPageCount]=1; - int fd=open(argv[1],O_CREAT|O_TRUNC|O_WRONLY,0600); if(fd<0) return 1; - r=cupsRasterOpen(fd,CUPS_RASTER_WRITE_APPLE); if(!r) return 1; + + fd=open(argv[1],O_CREAT|O_TRUNC|O_WRONLY,0600); + if(fd<0) return 1; + r=cupsRasterOpen(fd,CUPS_RASTER_WRITE_APPLE); + if(!r) return 1; if(!cupsRasterWriteHeader2(r,&h)) return 1; - line=malloc(h.cupsBytesPerLine); if(!line) return 1; memset(line,0xff,h.cupsBytesPerLine); - for(y=0;y Date: Sun, 4 Oct 2026 02:40:39 +0200 Subject: [PATCH 303/719] rdte: request FolioRelay BETA.3 rep 004 after URF fixture fix --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 9448577d9..268b4253b 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-003-public-uri-dnssd-oracle", + "request_id": "tn-26-beta3-t6-foliorelay-004-urf-fixture-compile-fix", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Re-run exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after the bounded rep-002 public-URI oracle correction. Preserve product/Foundry/image identities. Host-forwarded IPP must retain the exact FolioRelay resource path and functional PDF/Apple Raster behavior; canonical public URI remains exact in the control API and is independently proven in the guest/LAN context by DNS-SD SRV target foliorelay-t6.local, port 8634, rp=printers/FolioRelay, UUID and PDL. Retain stop/start, update/redeploy persistence, desired/live re-plan NOOP, and zero-residue cleanup." + "purpose": "Re-run exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-003 isolated and cheap-qualified the external synthetic Apple Raster fixture compile defect. Preserve product/Foundry/image identities and the rep-003 public-URI/DNS-SD oracle correction. Require exact three-service readback, portal/IPP reachability, canonical control/DNS-SD identity, PDF and generated Apple Raster exact-source preservation, restart persistence, update/redeploy persistence, desired/live re-plan NOOP, and zero-residue cleanup." } From c098b239624c16abb0ae831b98056c6a7f4025e8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:43:31 +0200 Subject: [PATCH 304/719] rdte: stage existing-probe capsule executor --- ...truenas_existing_probe_capsule_executor.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 scripts/truenas_existing_probe_capsule_executor.py diff --git a/scripts/truenas_existing_probe_capsule_executor.py b/scripts/truenas_existing_probe_capsule_executor.py new file mode 100644 index 000000000..58a3710ed --- /dev/null +++ b/scripts/truenas_existing_probe_capsule_executor.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import subprocess +import sys +from typing import Any + +from truenas_session_manifest import SessionError + +SCHEMA = "truenas-capsule-context/v1" +DISPATCH_SCHEMA = "truenas-capsule-dispatch/v1" +RECEIPT_SCHEMA = "truenas-capsule-execution/v1" + +PROVIDER_PORT_ARGS = { + "litellm-t6": {"service": "--service-port"}, + "wow-sidecar-t6": {}, + "garm-t6": {"service": "--service-port"}, + "official-catalog-t6": {}, + "foliorelay-t6": { + "control": "--control-port", + "ipp": "--ipp-port", + "observer": "--observer-port", + }, +} + +STATE_TIMEOUT = { + "litellm-t6": 240, + "wow-sidecar-t6": 240, + "garm-t6": 240, + "official-catalog-t6": 240, + "foliorelay-t6": 300, +} + +KNOWN_FAILURES = { + "ORACLE_FAILURE", + "HARNESS_FAILURE", + "ENVIRONMENT_FAILURE", + "UNSUPPORTED", +} + + +def load_json(path: pathlib.Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise SessionError(f"cannot read {path}: {exc}") from exc + if not isinstance(value, dict): + raise SessionError(f"{path}: top-level value must be an object") + return value + + +def nonempty(value: Any, label: str) -> str: + if not isinstance(value, str) or not value: + raise SessionError(f"{label} must be a non-empty string") + return value + + +def positive_port(value: Any, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or not 1 <= value <= 65535: + raise SessionError(f"{label} must be a TCP port") + return value + + +def provider_context(context: dict[str, Any], provider_id: str) -> dict[str, Any]: + rows = context.get("providers") + if not isinstance(rows, dict): + raise SessionError("context.providers must be an object") + row = rows.get(provider_id) + if not isinstance(row, dict): + raise SessionError(f"context has no provider binding for {provider_id}") + return row + + +def build_probe_command( + descriptor: dict[str, Any], + context: dict[str, Any], + provider_receipt: pathlib.Path, +) -> list[str]: + if descriptor.get("schema") != DISPATCH_SCHEMA: + raise SessionError("unsupported capsule dispatch schema") + capsule = descriptor.get("capsule") + provider = descriptor.get("provider") + if not isinstance(capsule, dict) or not isinstance(provider, dict): + raise SessionError("dispatch requires capsule and provider objects") + + provider_id = nonempty(capsule.get("provider"), "capsule.provider") + if provider_id != provider.get("id"): + raise SessionError("capsule/provider identity mismatch") + if provider_id not in PROVIDER_PORT_ARGS: + raise SessionError(f"provider is not admitted by existing-probe executor: {provider_id}") + + probe = pathlib.Path(nonempty(provider.get("probe"), "provider.probe")) + if not probe.is_file(): + raise SessionError(f"provider probe does not exist: {probe}") + + if context.get("schema") != SCHEMA: + raise SessionError("unsupported capsule context schema") + host = nonempty(context.get("host"), "context.host") + middleware_port = positive_port(context.get("middleware_port"), "context.middleware_port") + password_file = pathlib.Path(nonempty(context.get("password_file"), "context.password_file")) + if not password_file.is_file(): + raise SessionError("context.password_file does not exist") + tls = context.get("tls") + if not isinstance(tls, bool): + raise SessionError("context.tls must be boolean") + + binding = provider_context(context, provider_id) + control_dir = pathlib.Path(nonempty(binding.get("control_dir"), f"{provider_id}.control_dir")) + if not control_dir.is_dir(): + raise SessionError(f"{provider_id}.control_dir does not exist") + foundry_commit = nonempty(binding.get("foundry_commit"), f"{provider_id}.foundry_commit") + if len(foundry_commit) != 40 or any(ch not in "0123456789abcdef" for ch in foundry_commit): + raise SessionError(f"{provider_id}.foundry_commit must be exact lowercase SHA") + + command = [ + sys.executable, + str(probe), + "--host", host, + "--port", str(middleware_port), + ] + if tls: + command.append("--tls") + command += [ + "--password-file", str(password_file), + "--control-dir", str(control_dir), + "--foundry-commit", foundry_commit, + ] + + ports = binding.get("ports", {}) + if not isinstance(ports, dict): + raise SessionError(f"{provider_id}.ports must be an object") + for name, flag in PROVIDER_PORT_ARGS[provider_id].items(): + command += [flag, str(positive_port(ports.get(name), f"{provider_id}.ports.{name}"))] + + command += [ + "--out", str(provider_receipt), + "--timeout", "8", + "--job-timeout", "300", + "--state-timeout", str(STATE_TIMEOUT[provider_id]), + ] + return command + + +def wrap_receipt( + descriptor: dict[str, Any], + provider_receipt: dict[str, Any] | None, + returncode: int, + stderr: str, +) -> dict[str, Any]: + capsule = descriptor["capsule"] + provider = descriptor["provider"] + supported = bool( + provider_receipt + and provider_receipt.get("classification") == "SUPPORTED" + and provider_receipt.get("oracleSatisfied") is True + ) + if supported: + verdict = "SUPPORTED" + cleanup_satisfied = True + platform_healthy = True + reason = "existing provider probe completed its accepted oracle and cleanup contract" + else: + observed = provider_receipt.get("classification") if provider_receipt else None + verdict = observed if observed in KNOWN_FAILURES else "HARNESS_FAILURE" + cleanup_satisfied = False + platform_healthy = False + reason = ( + (provider_receipt or {}).get("detail") + or stderr.strip() + or f"provider probe exited {returncode} without accepted receipt" + ) + return { + "schema": RECEIPT_SCHEMA, + "capsule_id": capsule["id"], + "provider": capsule["provider"], + "provider_probe": provider["probe"], + "manifest_sha256": descriptor["manifest_sha256"], + "verdict": verdict, + "mutating": capsule["mutating"], + "cleanup_required": capsule["cleanup_required"], + "cleanup_satisfied": cleanup_satisfied, + "platform_healthy": platform_healthy, + "authority_satisfied": True, + "resource_guardrail_satisfied": True, + "provider_returncode": returncode, + "provider_receipt": provider_receipt, + "reason": reason[:2000], + } + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--capsule", type=pathlib.Path, required=True) + p.add_argument("--context", type=pathlib.Path, required=True) + p.add_argument("--out", type=pathlib.Path, required=True) + p.add_argument("--dry-run", action="store_true") + a = p.parse_args() + try: + descriptor = load_json(a.capsule) + context = load_json(a.context) + provider_receipt = a.out.with_suffix(".provider.json") + command = build_probe_command(descriptor, context, provider_receipt) + if a.dry_run: + print(json.dumps({"schema":"truenas-capsule-command/v1","argv":command}, indent=2)) + return 0 + cp = subprocess.run(command, text=True, capture_output=True, check=False) + source = load_json(provider_receipt) if provider_receipt.is_file() else None + receipt = wrap_receipt(descriptor, source, cp.returncode, cp.stderr) + a.out.write_text(json.dumps(receipt, indent=2, sort_keys=True)+"\n", encoding="utf-8") + print(json.dumps(receipt, indent=2, sort_keys=True)) + return 0 + except (SessionError, OSError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From 057bbf4374963361c339cc5798dff290da3f3f81 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:43:57 +0200 Subject: [PATCH 305/719] test: stage capsule executor equivalence and fail-closed semantics --- ...truenas_existing_probe_capsule_executor.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/test_truenas_existing_probe_capsule_executor.py diff --git a/tests/test_truenas_existing_probe_capsule_executor.py b/tests/test_truenas_existing_probe_capsule_executor.py new file mode 100644 index 000000000..82fede1ca --- /dev/null +++ b/tests/test_truenas_existing_probe_capsule_executor.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +import json +import pathlib +import sys +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import truenas_existing_probe_capsule_executor as exe +from truenas_session_manifest import SessionError + + +class ExistingProbeExecutorTests(unittest.TestCase): + def temp_root(self): + td = tempfile.TemporaryDirectory() + self.addCleanup(td.cleanup) + return pathlib.Path(td.name) + + def fixture(self, provider_id): + root = self.temp_root() + probe = root / "probe.py" + probe.write_text("#!/usr/bin/env python3\n", encoding="utf-8") + password = root / "password" + password.write_text("test\n", encoding="utf-8") + control = root / "control" + control.mkdir() + ports = { + "litellm-t6": {"service": 18100}, + "wow-sidecar-t6": {}, + "garm-t6": {"service": 18101}, + "official-catalog-t6": {}, + "foliorelay-t6": {"control": 18102, "ipp": 18103, "observer": 18104}, + }[provider_id] + descriptor = { + "schema": "truenas-capsule-dispatch/v1", + "manifest_sha256": "a" * 64, + "capsule": { + "id": "capsule-one", + "provider": provider_id, + "mutating": True, + "cleanup_required": True, + }, + "provider": { + "id": provider_id, + "probe": str(probe), + }, + } + context = { + "schema": "truenas-capsule-context/v1", + "host": "127.0.0.1", + "middleware_port": 18000, + "password_file": str(password), + "tls": True, + "providers": { + provider_id: { + "control_dir": str(control), + "foundry_commit": "1" * 40, + "ports": ports, + } + }, + } + return root, descriptor, context + + def test_existing_provider_commands_preserve_current_probe_arguments(self): + expected = { + "litellm-t6": ["--service-port", "18100", "--state-timeout", "240"], + "wow-sidecar-t6": ["--state-timeout", "240"], + "garm-t6": ["--service-port", "18101", "--state-timeout", "240"], + "official-catalog-t6": ["--state-timeout", "240"], + "foliorelay-t6": [ + "--control-port", "18102", + "--ipp-port", "18103", + "--observer-port", "18104", + "--state-timeout", "300", + ], + } + for provider_id, required in expected.items(): + with self.subTest(provider=provider_id): + root, descriptor, context = self.fixture(provider_id) + command = exe.build_probe_command( + descriptor, context, root / "provider-receipt.json" + ) + self.assertEqual(command[0], sys.executable) + self.assertIn("--host", command) + self.assertIn("127.0.0.1", command) + self.assertIn("--port", command) + self.assertIn("18000", command) + self.assertIn("--tls", command) + self.assertIn("--password-file", command) + self.assertIn("--control-dir", command) + self.assertIn("--foundry-commit", command) + for token in required: + self.assertIn(token, command) + + def test_supported_provider_receipt_maps_to_clean_supported_capsule(self): + _, descriptor, _ = self.fixture("foliorelay-t6") + receipt = exe.wrap_receipt( + descriptor, + { + "classification": "SUPPORTED", + "oracleSatisfied": True, + "detail": "passed", + }, + 0, + "", + ) + self.assertEqual(receipt["verdict"], "SUPPORTED") + self.assertTrue(receipt["cleanup_satisfied"]) + self.assertTrue(receipt["platform_healthy"]) + self.assertTrue(receipt["authority_satisfied"]) + self.assertTrue(receipt["resource_guardrail_satisfied"]) + + def test_failed_provider_receipt_stops_fail_closed_until_membrane_exists(self): + _, descriptor, _ = self.fixture("foliorelay-t6") + receipt = exe.wrap_receipt( + descriptor, + { + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "detail": "product oracle failed", + }, + 0, + "", + ) + self.assertEqual(receipt["verdict"], "ORACLE_FAILURE") + self.assertFalse(receipt["cleanup_satisfied"]) + self.assertFalse(receipt["platform_healthy"]) + + def test_missing_provider_receipt_is_harness_failure(self): + _, descriptor, _ = self.fixture("official-catalog-t6") + receipt = exe.wrap_receipt(descriptor, None, 7, "crashed") + self.assertEqual(receipt["verdict"], "HARNESS_FAILURE") + self.assertFalse(receipt["cleanup_satisfied"]) + self.assertFalse(receipt["platform_healthy"]) + self.assertIn("crashed", receipt["reason"]) + + def test_context_requires_exact_provider_binding(self): + root, descriptor, context = self.fixture("garm-t6") + context["providers"] = {} + with self.assertRaisesRegex(SessionError, "no provider binding"): + exe.build_probe_command(descriptor, context, root / "receipt.json") + + +if __name__ == "__main__": + unittest.main() From c62be0ed35684dc2e6f714d400bcafbe2078eae1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:44:30 +0200 Subject: [PATCH 306/719] ci: gate existing-probe capsule executor --- .github/workflows/prep-rdte-static.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index a2ff4fc29..f15a1ab4a 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -15,6 +15,7 @@ on: - 'scripts/truenas_single_capsule_adapter.py' - 'scripts/truenas_session_state.py' - 'scripts/truenas_session_runner.py' + - 'scripts/truenas_existing_probe_capsule_executor.py' - 'scripts/truenas_middleware_*.py' - 'tests/test_gha_kvm_system_rdte.py' - 'tests/test_truenas_future_protocols.py' @@ -25,6 +26,7 @@ on: - 'tests/test_truenas_single_capsule_adapter.py' - 'tests/test_truenas_session_state.py' - 'tests/test_truenas_session_runner.py' + - 'tests/test_truenas_existing_probe_capsule_executor.py' workflow_dispatch: permissions: @@ -97,6 +99,9 @@ jobs: if [[ -f tests/test_truenas_session_runner.py ]]; then tests+=(tests/test_truenas_session_runner.py) fi + if [[ -f tests/test_truenas_existing_probe_capsule_executor.py ]]; then + tests+=(tests/test_truenas_existing_probe_capsule_executor.py) + fi python3 -m unittest -v "${tests[@]}" - name: Scope expensive provider smokes From c0e53c0cffd31ac470559016d46ea1ebc012598f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 02:46:12 +0200 Subject: [PATCH 307/719] test: prove single-capsule runner-to-existing-probe equivalence path --- ...truenas_existing_probe_capsule_executor.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/test_truenas_existing_probe_capsule_executor.py b/tests/test_truenas_existing_probe_capsule_executor.py index 82fede1ca..e863c92c1 100644 --- a/tests/test_truenas_existing_probe_capsule_executor.py +++ b/tests/test_truenas_existing_probe_capsule_executor.py @@ -2,6 +2,7 @@ import json import pathlib import sys +import stat import tempfile import unittest @@ -9,6 +10,8 @@ sys.path.insert(0, str(ROOT / "scripts")) import truenas_existing_probe_capsule_executor as exe +import truenas_session_manifest as manifest_mod +import truenas_session_runner as session_runner from truenas_session_manifest import SessionError @@ -136,6 +139,124 @@ def test_missing_provider_receipt_is_harness_failure(self): self.assertFalse(receipt["platform_healthy"]) self.assertIn("crashed", receipt["reason"]) + def test_session_runner_executes_one_existing_probe_capsule_end_to_end(self): + root = self.temp_root() + probe = root / "fake_probe.py" + probe.write_text( + """#!/usr/bin/env python3 +import argparse, json, pathlib +p=argparse.ArgumentParser() +p.add_argument("--out", required=True) +p.add_argument("--host"); p.add_argument("--port"); p.add_argument("--password-file") +p.add_argument("--control-dir"); p.add_argument("--foundry-commit") +p.add_argument("--control-port"); p.add_argument("--ipp-port"); p.add_argument("--observer-port") +p.add_argument("--timeout"); p.add_argument("--job-timeout"); p.add_argument("--state-timeout") +p.add_argument("--tls", action="store_true") +a=p.parse_args() +pathlib.Path(a.out).write_text(json.dumps({ + "classification":"SUPPORTED", + "oracleSatisfied":True, + "detail":"synthetic accepted existing-probe receipt" +})+"\\n") +""", + encoding="utf-8", + ) + password = root / "password" + password.write_text("test\n", encoding="utf-8") + control = root / "control" + control.mkdir() + + targets = root / "targets.json" + targets.write_text(json.dumps({ + "schema":"gha-kvm-truenas-targets/v1", + "targets":[{"version":"26.0.0-BETA.3"}], + })+"\n", encoding="utf-8") + + providers = root / "providers.json" + providers.write_text(json.dumps({ + "schema":"truenas-capsule-providers/v1", + "providers":[{ + "id":"foliorelay-t6", + "kind":"foundry-product", + "product":"foliorelay", + "probe":str(probe), + "control_artifact":"foliorelay-t6-beta3-control", + "existing_selector":"foliorelay", + }], + })+"\n", encoding="utf-8") + + capsule = { + "schema":"truenas-capsule/v1", + "id":"foliorelay-one", + "kind":"foundry-product", + "provider":"foliorelay-t6", + "authority":{"repository":"SemperSupra/folio-relay","issue":29}, + "exact":{"source_ref":"1"*40}, + "namespace":"rdte-foliorelay-one", + "ports":[18102,18103,18104], + "requirements":["apps","zfs-pool"], + "resources":{"memory_mib":1024,"download_mib":64,"estimated_minutes":2}, + "phases":{"setup":True,"apply":True,"verify":True,"cleanup":True}, + "oracle":"existing FolioRelay T6 oracle", + "receipt_contract":"truenas-capsule-execution/v1", + "retry_policy":"read-only-reconcile", + "mutating":True, + "cleanup_required":True, + "dependencies":[], + } + manifest = { + "schema":"truenas-session/v1", + "session_id":"tn-26-beta3-single-equivalence-001", + "version":"26.0.0-BETA.3", + "authority":{"repository":"SemperSupra/agent-dispatch-private","issue":480}, + "budget":{"guest_memory_mib":8192,"download_mib":128,"timeout_minutes":10}, + "pool":{"name":"rdtepool","data_disks":2}, + "capsules":[capsule], + } + manifest["manifest_sha256"] = manifest_mod.manifest_digest(manifest) + manifest_path = root / "manifest.json" + manifest_path.write_text(json.dumps(manifest)+"\n", encoding="utf-8") + + context = root / "context.json" + context.write_text(json.dumps({ + "schema":"truenas-capsule-context/v1", + "host":"127.0.0.1", + "middleware_port":18000, + "password_file":str(password), + "tls":False, + "providers":{ + "foliorelay-t6":{ + "control_dir":str(control), + "foundry_commit":"1"*40, + "ports":{"control":18102,"ipp":18103,"observer":18104}, + } + }, + })+"\n", encoding="utf-8") + + wrapper = root / "executor" + executor_script = ROOT / "scripts" / "truenas_existing_probe_capsule_executor.py" + wrapper.write_text( + f"#!/bin/sh\nexec {sys.executable} {executor_script} \"$@\"\n", + encoding="utf-8", + ) + wrapper.chmod(wrapper.stat().st_mode | stat.S_IXUSR) + + result = session_runner.run_session( + manifest_path, + targets, + providers, + wrapper, + context, + root / "receipts", + ) + self.assertEqual(result["classification"], "SESSION_CLEAN") + self.assertTrue(result["session_clean"]) + self.assertFalse(result["product_acceptance_inferred"]) + self.assertEqual(len(result["capsules"]), 1) + self.assertEqual(result["capsules"][0]["verdict"], "SUPPORTED") + self.assertTrue(result["capsules"][0]["cleanup_satisfied"]) + self.assertTrue(result["capsules"][0]["platform_healthy"]) + def test_context_requires_exact_provider_binding(self): root, descriptor, context = self.fixture("garm-t6") context["providers"] = {} From 4fd9fe7a9f37198a66ca766df774c7b8e5350199 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:06:44 +0200 Subject: [PATCH 308/719] rdte: separate DNS-SD observer lifecycle from oracle result --- .../truenas_middleware_foliorelay_t6_probe.py | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index eff28f00d..4eb468796 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -306,12 +306,18 @@ def observe(expected,expected_host,expected_ipp_port,seconds=25): except Exception: pass raise RuntimeError("no qualifying _universal FolioRelay mDNS response") class H(BaseHTTPRequestHandler): - result=None + result={"status":"pending"} def do_GET(self): body=json.dumps(H.result).encode(); self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body) def log_message(self,*args): pass -p=argparse.ArgumentParser(); p.add_argument("--uuid",required=True); p.add_argument("--expected-host",required=True); p.add_argument("--expected-ipp-port",type=int,required=True); p.add_argument("--port",type=int,default=18081); a=p.parse_args() -H.result=observe(a.uuid,a.expected_host,a.expected_ipp_port) +def run_observer(args): + try: + result=observe(args.uuid,args.expected_host,args.expected_ipp_port,args.seconds) + H.result={"status":"success",**result} + except Exception as exc: + H.result={"status":"error","error":f"{type(exc).__name__}: {exc}"} +p=argparse.ArgumentParser(); p.add_argument("--uuid",required=True); p.add_argument("--expected-host",required=True); p.add_argument("--expected-ipp-port",type=int,required=True); p.add_argument("--port",type=int,default=18081); p.add_argument("--seconds",type=float,default=25); a=p.parse_args() +threading.Thread(target=run_observer,args=(a,),daemon=True).start() HTTPServer(("0.0.0.0",a.port),H).serve_forever() ''' @@ -339,7 +345,9 @@ def wait_job(j,label): while time.monotonic() Date: Sun, 4 Oct 2026 03:07:28 +0200 Subject: [PATCH 309/719] test: separate observer lifecycle from mDNS oracle outcome --- tests/test_truenas_t6_foliorelay_control.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index a1cfeb086..47c1e9e08 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -48,6 +48,16 @@ def test_observer_is_separate_host_network_app(self): self.assertIn('"entrypoint":["python3","/observer/mdns_observer.py"]', text) self.assertIn('"distinct_observer_context":True', text) + def test_observer_lifecycle_is_independent_from_mdns_oracle_result(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('H.result={"status":"pending"}', text) + self.assertIn('threading.Thread(target=run_observer', text) + self.assertIn('{"status":"success",**result}', text) + self.assertIn('{"status":"error","error":', text) + self.assertIn('p.add_argument("--seconds",type=float,default=25)', text) + self.assertIn('DNS-SD observer oracle failed:', text) + self.assertIn('DNS-SD observer oracle remained pending', text) + def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") From d9716aaca81201b2a5f08d1de1cda14a3634ebf4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:07:31 +0200 Subject: [PATCH 310/719] ci: smoke exact FolioRelay DNS-SD observer container --- .../workflows/prep-foliorelay-t6-consumer.yml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 1a8003c75..161169496 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -50,6 +50,63 @@ jobs: with: name: foliorelay-t6-beta3-control path: ${{ runner.temp }}/foliorelay-control + - name: Smoke exact independent DNS-SD observer container + shell: bash + run: | + set -euo pipefail + image='ghcr.io/truenas/apps_validation@sha256:3f38cdaa6ed9c54e5c9c43ec15790aabc0028631dd3f0c28129832a69fd1f30c' + work="$RUNNER_TEMP/foliorelay-observer-smoke" + rm -rf "$work" + mkdir -p "$work" + python3 - "$work/mdns_observer.py" <<'PY' + import importlib.util, pathlib, sys + script=pathlib.Path("scripts/truenas_middleware_foliorelay_t6_probe.py") + spec=importlib.util.spec_from_file_location("foliorelay_probe",script) + mod=importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + pathlib.Path(sys.argv[1]).write_text(mod.OBSERVER,encoding="utf-8") + PY + port="$(python3 - <<'PY' + import socket + s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() + PY + )" + docker pull "$image" >/dev/null + docker rm -f foliorelay-observer-smoke >/dev/null 2>&1 || true + trap 'docker rm -f foliorelay-observer-smoke >/dev/null 2>&1 || true' EXIT + docker run -d --name foliorelay-observer-smoke \ + --network host \ + --read-only \ + --cap-drop ALL \ + --security-opt no-new-privileges:true \ + --mount "type=bind,src=$work,dst=/observer,readonly" \ + --entrypoint python3 \ + "$image" /observer/mdns_observer.py \ + --uuid urn:uuid:01234567-89ab-4def-8123-456789abcdef \ + --expected-host intentionally-absent.local \ + --expected-ipp-port 8634 \ + --port "$port" \ + --seconds 1 >/dev/null + python3 - "$port" <<'PY' + import json, sys, time, urllib.request + url=f"http://127.0.0.1:{sys.argv[1]}/" + deadline=time.monotonic()+15 + last=None + while time.monotonic() Date: Sun, 4 Oct 2026 03:09:19 +0200 Subject: [PATCH 311/719] test: assert observer pending class state accurately --- tests/test_truenas_t6_foliorelay_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 47c1e9e08..156f21466 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -50,7 +50,7 @@ def test_observer_is_separate_host_network_app(self): def test_observer_lifecycle_is_independent_from_mdns_oracle_result(self): text = SCRIPT.read_text(encoding="utf-8") - self.assertIn('H.result={"status":"pending"}', text) + self.assertIn('result={"status":"pending"}', text) self.assertIn('threading.Thread(target=run_observer', text) self.assertIn('{"status":"success",**result}', text) self.assertIn('{"status":"error","error":', text) From c10abf58a2a82a7b01aef9513901471e1df0fb2f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:11:31 +0200 Subject: [PATCH 312/719] ci: load FolioRelay observer fixture with local probe dependencies --- .github/workflows/prep-foliorelay-t6-consumer.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 161169496..9aa70fba5 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -60,7 +60,9 @@ jobs: mkdir -p "$work" python3 - "$work/mdns_observer.py" <<'PY' import importlib.util, pathlib, sys - script=pathlib.Path("scripts/truenas_middleware_foliorelay_t6_probe.py") + scripts=pathlib.Path("scripts").resolve() + sys.path.insert(0,str(scripts)) + script=scripts/"truenas_middleware_foliorelay_t6_probe.py" spec=importlib.util.spec_from_file_location("foliorelay_probe",script) mod=importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) From 06827dd692aa722561bbee1e517df71e1ab455cc Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:18:06 +0200 Subject: [PATCH 313/719] rdte: request FolioRelay BETA.3 rep 005 observer oracle separation --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 268b4253b..e82689aa2 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-004-urf-fixture-compile-fix", + "request_id": "tn-26-beta3-t6-foliorelay-005-observer-oracle-separation", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Re-run exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-003 isolated and cheap-qualified the external synthetic Apple Raster fixture compile defect. Preserve product/Foundry/image identities and the rep-003 public-URI/DNS-SD oracle correction. Require exact three-service readback, portal/IPP reachability, canonical control/DNS-SD identity, PDF and generated Apple Raster exact-source preservation, restart persistence, update/redeploy persistence, desired/live re-plan NOOP, and zero-residue cleanup." + "purpose": "Re-run exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-004 isolated the independent DNS-SD observer lifecycle/oracle conflation. Preserve all product/Foundry/image/Compose identities, the public-URI oracle correction, and the cheap-qualified URF fixture. Require the observer Custom App to realize independently, then require its explicit mDNS oracle result; accept only with exact three-service readback, portal/IPP reachability, canonical control/CUPS/DNS-SD identity, PDF and Apple Raster exact-source preservation, restart persistence, desired/live re-plan NOOP, update/redeploy persistence, and zero-residue cleanup." } From 9b588183e8bd631d8c09f79dfaa5daa1e8917e30 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:21:23 +0200 Subject: [PATCH 314/719] rdte: refresh prep-rdte-static.yml on rep-005 base --- .github/workflows/prep-rdte-static.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index f15a1ab4a..b4e01c8ee 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -8,6 +8,7 @@ on: - 'config/truenas-rdte-targets.json' - 'config/truenas-rdte-run-request.json' - 'config/truenas-capsule-providers.json' + - 'config/truenas-session-*.json' - 'scripts/gha_kvm_truenas_rdte.sh' - 'scripts/gha_kvm_proxmox_rdte.sh' - 'scripts/truenas_rdte_target.py' From a35ef6fde60fd0e3d4b0880ac6893818cd12bf91 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:21:29 +0200 Subject: [PATCH 315/719] rdte: refresh truenas-session-foliorelay-single-equivalence.json on rep-005 base --- ...session-foliorelay-single-equivalence.json | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 config/truenas-session-foliorelay-single-equivalence.json diff --git a/config/truenas-session-foliorelay-single-equivalence.json b/config/truenas-session-foliorelay-single-equivalence.json new file mode 100644 index 000000000..adacd9158 --- /dev/null +++ b/config/truenas-session-foliorelay-single-equivalence.json @@ -0,0 +1,67 @@ +{ + "authority": { + "issue": 480, + "repository": "SemperSupra/agent-dispatch-private" + }, + "budget": { + "download_mib": 512, + "guest_memory_mib": 8192, + "timeout_minutes": 20 + }, + "capsules": [ + { + "authority": { + "issue": 29, + "repository": "SemperSupra/folio-relay" + }, + "cleanup_required": true, + "dependencies": [], + "exact": { + "compose_sha256": "d4fc297eccdd884bf1882f3cceb5f3c97c1302e5537528cb06633fa2b65e9c78", + "control_image": "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce", + "cups_image": "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f", + "foundry_ref": "fb41afd3d112f361b8c490aeb5915a978b956b20", + "product_head": "88fd70c8891eaf7b4c886aa928b96e232ca041c6" + }, + "id": "foliorelay-beta3-single-equivalence", + "kind": "foundry-product", + "mutating": true, + "namespace": "rdte-foliorelay-equivalence", + "oracle": "existing FolioRelay T6 product oracle through session wrapper", + "phases": { + "apply": true, + "cleanup": true, + "setup": true, + "verify": true + }, + "ports": [ + 8634, + 18080, + 18081 + ], + "provider": "foliorelay-t6", + "receipt_contract": "truenas-capsule-execution/v1", + "requirements": [ + "apps", + "zfs-pool", + "host-network-dnssd", + "ipp" + ], + "resources": { + "download_mib": 256, + "estimated_minutes": 10, + "memory_mib": 2048 + }, + "retry_policy": "read-only-reconcile", + "schema": "truenas-capsule/v1" + } + ], + "manifest_sha256": "a59e7781ef64c5679426f1f0d1231d6b4a2e903db35129c4d3064da81543be8b", + "pool": { + "data_disks": 2, + "name": "rdtepool" + }, + "schema": "truenas-session/v1", + "session_id": "tn-26-beta3-foliorelay-single-equivalence-001", + "version": "26.0.0-BETA.3" +} From 53168ae574b05245566d8671433126b5c5098ebe Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:21:36 +0200 Subject: [PATCH 316/719] rdte: refresh gha_kvm_truenas_rdte.sh on rep-005 base --- scripts/gha_kvm_truenas_rdte.sh | 111 +++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 6f2a33002..90509d313 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" } OUT="" @@ -34,6 +34,7 @@ G4_FIXTURE_DIR="" G4_FIXTURE_PRODUCER="" G5_MATRIX_DIR="" G5_MATRIX_PRODUCER="" +SESSION_MANIFEST="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -51,6 +52,7 @@ while [[ $# -gt 0 ]]; do --g4-fixture-producer) G4_FIXTURE_PRODUCER="$2"; shift 2 ;; --g5-matrix-dir) G5_MATRIX_DIR="$2"; shift 2 ;; --g5-matrix-producer) G5_MATRIX_PRODUCER="$2"; shift 2 ;; + --session-manifest) SESSION_MANIFEST="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -96,6 +98,18 @@ if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" VCPUS=4 fi +if [[ -n "$SESSION_MANIFEST" ]]; then + [[ "$RUNG" == "t6" ]] || { echo "--session-manifest requires rung t6" >&2; exit 2; } + [[ "$T6_PRODUCT" != garm-provider-* ]] || { echo "session equivalence currently wraps Foundry/control providers only" >&2; exit 2; } + [[ -f "$SESSION_MANIFEST" ]] || { echo "session manifest does not exist: $SESSION_MANIFEST" >&2; exit 2; } + SESSION_MANIFEST="$(realpath "$SESSION_MANIFEST")" + SESSION_LOWERED="$(python3 "$SCRIPT_DIR/truenas_single_capsule_adapter.py" --manifest "$SESSION_MANIFEST" --targets "$TARGET_REGISTRY" --providers "$SCRIPT_DIR/../config/truenas-capsule-providers.json")" || exit 2 + SESSION_SELECTOR="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["existing_selector"])' <<<"$SESSION_LOWERED")" + SESSION_VERSION="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["version"])' <<<"$SESSION_LOWERED")" + [[ "$SESSION_SELECTOR" == "$T6_PRODUCT" ]] || { echo "session capsule selector $SESSION_SELECTOR does not match --t6-product $T6_PRODUCT" >&2; exit 2; } + [[ "$SESSION_VERSION" == "$VERSION" ]] || { echo "session exact version $SESSION_VERSION does not match target $VERSION" >&2; exit 2; } +fi + if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-truenas.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" STATE_DIR="$(realpath "$STATE_DIR")" @@ -117,6 +131,7 @@ POOL_RESULT_JSON="" APP_RESULT_JSON="" LIFECYCLE_RESULT_JSON="" FOUNDRY_RESULT_JSON="" +SESSION_RESULT_JSON="" cleanup() { set +e if [[ -n "$QEMU_PID" ]]; then @@ -143,7 +158,7 @@ write_receipt() { export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_VCPUS="$VCPUS" R_RAM_MIB="$RAM_MIB" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" - export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" + export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" R_SESSION_RESULT="$SESSION_RESULT_JSON" python3 - <<'PY' import json, os, pathlib payload = { @@ -221,6 +236,7 @@ payload = { "apps_runtime": json.loads(os.environ["R_APP_RESULT"]) if os.environ.get("R_APP_RESULT") else None, "app_lifecycle": json.loads(os.environ["R_LIFECYCLE_RESULT"]) if os.environ.get("R_LIFECYCLE_RESULT") else None, "foundry_materialization": json.loads(os.environ["R_FOUNDRY_RESULT"]) if os.environ.get("R_FOUNDRY_RESULT") else None, + "session_execution": json.loads(os.environ["R_SESSION_RESULT"]) if os.environ.get("R_SESSION_RESULT") else None, "qemu_alive_at_gate": os.environ.get("R_QEMU_ALIVE"), "serial_tail": os.environ.get("R_SERIAL", ""), "limitations": [ @@ -719,6 +735,96 @@ if [[ "$RUNG" == "t5" ]]; then fi FOUNDRY_OUT="$STATE_DIR/foundry-control.json" +if [[ -n "$SESSION_MANIFEST" ]]; then + SESSION_CONTEXT="$STATE_DIR/session-context.json" + SESSION_OUT="$STATE_DIR/session-execution.json" + SESSION_RECEIPTS="$STATE_DIR/session-capsules" + export SCTX_PRODUCT="$T6_PRODUCT" SCTX_MIDDLEWARE_PORT="$MIDDLEWARE_PORT" + export SCTX_PASSWORD_FILE="$PASSWORD_FILE" SCTX_CONTROL_DIR="$FOUNDRY_CONTROL_DIR" SCTX_FOUNDRY_COMMIT="$FOUNDRY_COMMIT" + export SCTX_LITELLM_PORT="$LITELLM_HOST_PORT" SCTX_GARM_PORT="$GARM_HOST_PORT" + export SCTX_FOLIO_CONTROL="$FOLIORELAY_CONTROL_HOST_PORT" SCTX_FOLIO_IPP="$FOLIORELAY_IPP_HOST_PORT" SCTX_FOLIO_OBSERVER="$FOLIORELAY_OBSERVER_HOST_PORT" + if (( ${#MIDDLEWARE_TLS_ARG[@]} )); then SCTX_TLS=true; else SCTX_TLS=false; fi + export SCTX_TLS + python3 - "$SESSION_CONTEXT" <<'PY' +import json, os, pathlib, sys +selector=os.environ["SCTX_PRODUCT"] +provider_id={ + "litellm":"litellm-t6", + "wow-sidecar":"wow-sidecar-t6", + "garm":"garm-t6", + "official-catalog":"official-catalog-t6", + "foliorelay":"foliorelay-t6", +}[selector] +ports={} +if selector=="litellm": ports["service"]=int(os.environ["SCTX_LITELLM_PORT"]) +elif selector=="garm": ports["service"]=int(os.environ["SCTX_GARM_PORT"]) +elif selector=="foliorelay": + ports={ + "control":int(os.environ["SCTX_FOLIO_CONTROL"]), + "ipp":int(os.environ["SCTX_FOLIO_IPP"]), + "observer":int(os.environ["SCTX_FOLIO_OBSERVER"]), + } +payload={ + "schema":"truenas-capsule-context/v1", + "host":"127.0.0.1", + "middleware_port":int(os.environ["SCTX_MIDDLEWARE_PORT"]), + "password_file":os.environ["SCTX_PASSWORD_FILE"], + "tls":os.environ["SCTX_TLS"].lower()=="true", + "providers":{ + provider_id:{ + "control_dir":os.environ["SCTX_CONTROL_DIR"], + "foundry_commit":os.environ["SCTX_FOUNDRY_COMMIT"], + "ports":ports, + } + }, +} +pathlib.Path(sys.argv[1]).write_text(json.dumps(payload,indent=2,sort_keys=True)+"\n",encoding="utf-8") +PY + python3 "$SCRIPT_DIR/truenas_session_runner.py" \ + --manifest "$SESSION_MANIFEST" \ + --targets "$TARGET_REGISTRY" \ + --providers "$SCRIPT_DIR/../config/truenas-capsule-providers.json" \ + --executor "$SCRIPT_DIR/truenas_existing_probe_capsule_executor.py" \ + --context "$SESSION_CONTEXT" \ + --out-dir "$SESSION_RECEIPTS" \ + --out "$SESSION_OUT" >/dev/null 2>&1 || true + [[ -f "$SESSION_OUT" ]] || fail_evidence HARNESS_FAILURE foundry-materialization "single-capsule session runner did not emit a receipt" + SESSION_RESULT_JSON="$(cat "$SESSION_OUT")" + SESSION_CAPSULE_VERDICT="$(python3 - "$SESSION_OUT" <<'PY' +import json, pathlib, sys +session=json.loads(pathlib.Path(sys.argv[1]).read_text()) +caps=session.get("capsules") or [] +print(caps[0].get("verdict","HARNESS_FAILURE") if len(caps)==1 else "HARNESS_FAILURE") +PY +)" + if [[ "$SESSION_CAPSULE_VERDICT" != "SUPPORTED" ]]; then + case "$SESSION_CAPSULE_VERDICT" in + ORACLE_FAILURE|HARNESS_FAILURE|ENVIRONMENT_FAILURE|UNSUPPORTED) + fail_evidence "$SESSION_CAPSULE_VERDICT" foundry-materialization "single-capsule session provider returned $SESSION_CAPSULE_VERDICT" + ;; + *) + fail_evidence HARNESS_FAILURE foundry-materialization "single-capsule session returned unexpected verdict $SESSION_CAPSULE_VERDICT" + ;; + esac + fi + SESSION_OK="$(python3 - "$SESSION_OUT" <<'PY' +import json, pathlib, sys +session=json.loads(pathlib.Path(sys.argv[1]).read_text()) +caps=session.get("capsules") or [] +ok=(session.get("classification")=="SESSION_CLEAN" and len(caps)==1 and caps[0].get("verdict")=="SUPPORTED") +print("true" if ok else "false") +PY +)" + [[ "$SESSION_OK" == "true" ]] || fail_evidence HARNESS_FAILURE foundry-materialization "single-capsule session envelope was not clean" + python3 - "$SESSION_OUT" "$FOUNDRY_OUT" <<'PY' +import json, pathlib, sys +session=json.loads(pathlib.Path(sys.argv[1]).read_text()) +provider=session["capsules"][0].get("provider_receipt") +if not isinstance(provider,dict) or provider.get("classification")!="SUPPORTED" or provider.get("oracleSatisfied") is not True: + raise SystemExit("accepted capsule did not retain accepted provider receipt") +pathlib.Path(sys.argv[2]).write_text(json.dumps(provider,indent=2,sort_keys=True)+"\n",encoding="utf-8") +PY +else if [[ "$T6_PRODUCT" == "official-catalog" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_official_catalog_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ @@ -795,6 +901,7 @@ else --foundry-commit "$FOUNDRY_COMMIT" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true fi +fi [[ -f "$FOUNDRY_OUT" ]] || fail_evidence HARNESS_FAILURE foundry-materialization "T6 Foundry control client did not emit a receipt" FOUNDRY_RESULT_JSON="$(cat "$FOUNDRY_OUT")" FOUNDRY_OK="$(python3 - "$FOUNDRY_OUT" <<'PY' From 56f8320326cf2448d647208d9e4eaa0badebb1d9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:21:45 +0200 Subject: [PATCH 317/719] rdte: refresh test_gha_kvm_system_rdte.py on rep-005 base --- tests/test_gha_kvm_system_rdte.py | 43 +++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 036cfb1d6..e4bcae10c 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -22,6 +22,7 @@ TRUENAS_POOL = ROOT / "scripts" / "truenas_middleware_pool_probe.py" TRUENAS_APP = ROOT / "scripts" / "truenas_middleware_app_probe.py" TRUENAS_LIFECYCLE = ROOT / "scripts" / "truenas_middleware_app_lifecycle_probe.py" +FOLIORELAY_SESSION_EQUIV = ROOT / "config" / "truenas-session-foliorelay-single-equivalence.json" class SystemRdteContractTests(unittest.TestCase): @@ -361,6 +362,48 @@ def test_only_heavy_jobs_share_non_cancelling_concurrency_group(self): self.assertEqual(text.count("cancel-in-progress: false"), 2) self.assertNotIn("cancel-in-progress: true", text) + def test_single_capsule_session_mode_is_opt_in_and_selector_bound(self): + text = TRUENAS.read_text(encoding="utf-8") + self.assertIn('--session-manifest', text) + self.assertIn('SESSION_MANIFEST=""', text) + self.assertIn("truenas_single_capsule_adapter.py", text) + self.assertIn('SESSION_SELECTOR', text) + self.assertIn('does not match --t6-product', text) + self.assertIn('truenas_session_runner.py', text) + self.assertIn('truenas_existing_probe_capsule_executor.py', text) + self.assertIn('"session_execution":', text) + self.assertIn('SESSION_CAPSULE_VERDICT=', text) + self.assertIn('fail_evidence "$SESSION_CAPSULE_VERDICT"', text) + self.assertIn('SESSION_OK=', text) + self.assertIn('session.get("classification")=="SESSION_CLEAN"', text) + self.assertIn('caps[0].get("verdict")=="SUPPORTED"', text) + self.assertIn('provider.get("classification")!="SUPPORTED"', text) + self.assertIn('provider.get("oracleSatisfied") is not True', text) + + def test_foliorelay_single_capsule_equivalence_manifest_is_exact_and_valid(self): + doc = json.loads(FOLIORELAY_SESSION_EQUIV.read_text(encoding="utf-8")) + self.assertEqual(doc["version"], "26.0.0-BETA.3") + self.assertEqual(len(doc["capsules"]), 1) + cap = doc["capsules"][0] + self.assertEqual(cap["provider"], "foliorelay-t6") + self.assertEqual(cap["authority"], {"repository":"SemperSupra/folio-relay","issue":29}) + self.assertEqual(cap["exact"]["foundry_ref"], "fb41afd3d112f361b8c490aeb5915a978b956b20") + self.assertEqual(cap["exact"]["product_head"], "88fd70c8891eaf7b4c886aa928b96e232ca041c6") + self.assertIn("@sha256:", cap["exact"]["control_image"]) + self.assertIn("@sha256:", cap["exact"]["cups_image"]) + cp = subprocess.run( + [ + sys.executable, + str(ROOT / "scripts" / "truenas_session_manifest.py"), + "--manifest", str(FOLIORELAY_SESSION_EQUIV), + "--targets", str(ROOT / "config" / "truenas-rdte-targets.json"), + "--providers", str(ROOT / "config" / "truenas-capsule-providers.json"), + ], + text=True, + capture_output=True, + ) + self.assertEqual(cp.returncode, 0, cp.stderr) + def test_truenas_t1_requires_local_rpc_probe(self): text = TRUENAS.read_text(encoding="utf-8") self.assertIn("missing TrueNAS installer RPC probe", text) From ecaea0eccfc01e555df045463102200547b26c71 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:38:34 +0200 Subject: [PATCH 318/719] RDTE: retain observer failed-create diagnostics and cleanup --- .../truenas_middleware_foliorelay_t6_probe.py | 161 +++++++++++++++++- 1 file changed, 155 insertions(+), 6 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 4eb468796..ed3df74c9 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -37,6 +37,9 @@ PUBLIC_IPP_PORT = 8634 PUBLIC_RESOURCE_PATH = "/printers/FolioRelay" PUBLIC_URI = f"ipp://{PUBLIC_HOST}:{PUBLIC_IPP_PORT}{PUBLIC_RESOURCE_PATH}" +OBSERVER_LOG_TAIL_LINES = 200 +OBSERVER_LOG_MAX_EVENTS = 200 +OBSERVER_LOG_CAPTURE_SECONDS = 2.0 def canonical_sha256(value): return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest() @@ -321,6 +324,96 @@ def run_observer(args): HTTPServer(("0.0.0.0",a.port),H).serve_forever() ''' + +class JobFailure(RuntimeError): + def __init__(self, label: str, job: dict): + self.label = label + self.job = job + detail = job.get("error") or job.get("exception") or job.get("exc_info") or "no job diagnostic" + super().__init__(f"{label} job {job.get('state')}: {detail}") + +def bounded_text(value, limit=8000): + if value is None: + return None + text = str(value) + return text if len(text) <= limit else text[:limit] + "..." + +def bounded_job_snapshot(job): + if not isinstance(job, dict): + return None + return { + key: (bounded_text(job.get(key)) if key in {"error", "exception", "exc_info"} else job.get(key)) + for key in ("id", "state", "progress", "error", "exception", "exc_info") + if key in job + } + +def bounded_app_snapshot(app): + if not isinstance(app, dict): + return None + workloads = app.get("active_workloads") or {} + details = workloads.get("container_details") or [] + containers = [] + for item in details[:8]: + if not isinstance(item, dict): + continue + containers.append({ + key: item.get(key) + for key in ("id", "container_id", "service_name", "image", "state", "health") + if item.get(key) is not None + }) + return { + "id": app.get("id"), + "name": app.get("name"), + "state": app.get("state"), + "error_reason": bounded_text(app.get("error_reason")), + "containers": containers, + } + +def capture_container_log_tail(ws, app_name, container_id, tail_lines=OBSERVER_LOG_TAIL_LINES): + params = {"app_name": app_name, "container_id": container_id, "tail_lines": tail_lines} + subscription = "app.container_log_follow:" + json.dumps(params, sort_keys=True, separators=(",", ":")) + sub_id = "observer-log-" + hashlib.sha256(container_id.encode()).hexdigest()[:12] + old_timeout = ws.sock.gettimeout() + events = [] + try: + ws.sock.settimeout(min(float(old_timeout or 2.0), OBSERVER_LOG_CAPTURE_SECONDS)) + ws.send_json({"msg": "sub", "id": sub_id, "name": subscription, "params": []}) + deadline = time.monotonic() + OBSERVER_LOG_CAPTURE_SECONDS + while len(events) < OBSERVER_LOG_MAX_EVENTS and time.monotonic() < deadline: + try: + message = ws.recv_json() + except (socket.timeout, TimeoutError): + break + if message.get("msg") == "ping": + pong = {"msg": "pong"} + if "id" in message: + pong["id"] = message["id"] + ws.send_json(pong) + continue + if message.get("msg") == "added": + fields = message.get("fields") or {} + data = fields.get("data") + if data is not None: + events.append({ + "timestamp": fields.get("timestamp"), + "data": bounded_text(data, 4000), + }) + if message.get("msg") == "nosub" and message.get("id") == sub_id: + break + finally: + try: + ws.send_json({"msg": "unsub", "id": sub_id}) + except Exception: + pass + ws.sock.settimeout(old_timeout) + return { + "app_name": app_name, + "container_id": container_id, + "tail_lines": tail_lines, + "events": events, + } + + def main(): p=argparse.ArgumentParser() p.add_argument("--host",default="127.0.0.1"); p.add_argument("--port",type=int,required=True) @@ -328,7 +421,7 @@ def main(): p.add_argument("--password-file",required=True); p.add_argument("--control-dir",type=pathlib.Path,required=True); p.add_argument("--foundry-commit",required=True) p.add_argument("--out",required=True); p.add_argument("--tls",action="store_true"); p.add_argument("--timeout",type=float,default=8); p.add_argument("--job-timeout",type=float,default=300); p.add_argument("--state-timeout",type=float,default=300) a=p.parse_args() - started=time.time(); ws=None; app_created=False; observer_created=False + started=time.time(); ws=None; dataset_owned=False; app_created=False; observer_created=False payload={"schema":"truenas-foliorelay-foundry-t6/v1","classification":"ORACLE_FAILURE","oracleSatisfied":False,"expected_version":EXPECTED_VERSION,"foundry_commit":a.foundry_commit,"app_name":EXPECTED_APP_NAME,"secret_values_captured":False} try: control,compose=load_control(a.control_dir,a.foundry_commit) @@ -346,8 +439,7 @@ def wait_job(j,label): x=call("core.get_jobs",[[["id","=",j]],{"get":True}]) if x and x.get("state")=="SUCCESS": return x if x and x.get("state") in {"FAILED","ABORTED"}: - detail=x.get("error") or x.get("exception") or x.get("exc_info") or "no job diagnostic" - raise RuntimeError(f"{label} job {x.get('state')}: {detail}") + raise JobFailure(label, x) time.sleep(1) raise RuntimeError(f"{label} job timeout") def wait_state(name,state): @@ -365,6 +457,7 @@ def wait_state(name,state): if call("pool.dataset.query",[[["id","=",DATASET]]]): raise RuntimeError("refusing adopted FolioRelay dataset") ds=call("pool.dataset.create",[{"name":DATASET,"type":"FILESYSTEM","share_type":"GENERIC","comments":"SemperSupra disposable FolioRelay T6 fixture"}]) if not isinstance(ds,dict) or ds.get("id")!=DATASET: raise RuntimeError("dataset identity mismatch") + dataset_owned=True dirs=[ (ROOT+"/control","700",10001),(ROOT+"/artifacts","700",10001),(ROOT+"/cups-state","755",10001),(ROOT+"/cups-spool","755",10001), (ROOT+"/secrets","700",10001),(OBSERVER_DIR,"755",0), @@ -379,7 +472,8 @@ def wait_state(name,state): if isinstance(j,int): wait_job(j,"token setperm") create=call("app.create",[{"app_name":EXPECTED_APP_NAME,"custom_app":True,"custom_compose_config":compose}]) if not isinstance(create,int): raise RuntimeError("app.create did not return job") - wait_job(create,"app.create"); app_created=True; app=wait_state(EXPECTED_APP_NAME,"RUNNING") + app_created=True + wait_job(create,"app.create"); app=wait_state(EXPECTED_APP_NAME,"RUNNING") details=(app.get("active_workloads") or {}).get("container_details") or [] exact={(x.get("service_name"),x.get("image"),x.get("state")) for x in details} for needed in [("control",EXPECTED_CONTROL,"running"),("cups",EXPECTED_CUPS,"running"),("discovery",EXPECTED_CONTROL,"running")]: @@ -419,7 +513,30 @@ def wait_state(name,state): obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR,"target":"/observer","read_only":True}],"entrypoint":["python3","/observer/mdns_observer.py"],"command":["--uuid",uuid,"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} oj=call("app.create",[{"app_name":OBSERVER_APP_NAME,"custom_app":True,"custom_compose_config":obs_compose}]) if not isinstance(oj,int): raise RuntimeError("observer app.create did not return job") - wait_job(oj,"observer app.create"); observer_created=True; wait_state(OBSERVER_APP_NAME,"RUNNING") + observer_created=True + try: + wait_job(oj,"observer app.create"); wait_state(OBSERVER_APP_NAME,"RUNNING") + except Exception as observer_exc: + diagnostic={"detail":f"{type(observer_exc).__name__}: {observer_exc}"} + if isinstance(observer_exc,JobFailure): diagnostic["job"]=bounded_job_snapshot(observer_exc.job) + try: + observer_app=call("app.query",[[["id","=",OBSERVER_APP_NAME]],{"get":True}]) + diagnostic["app"]=bounded_app_snapshot(observer_app) + state=observer_app.get("state") if isinstance(observer_app,dict) else None + if state in {"RUNNING","CRASHED","DEPLOYING"}: + log_tails=[] + for detail in ((observer_app.get("active_workloads") or {}).get("container_details") or [])[:4]: + container_id=detail.get("id") or detail.get("container_id") + if not container_id: continue + try: + log_tails.append(capture_container_log_tail(ws,OBSERVER_APP_NAME,str(container_id))) + except Exception as log_exc: + log_tails.append({"container_id":str(container_id),"capture_error":f"{type(log_exc).__name__}: {log_exc}"}) + diagnostic["container_log_tails"]=log_tails + except Exception as diagnostic_exc: + diagnostic["diagnostic_error"]=f"{type(diagnostic_exc).__name__}: {diagnostic_exc}" + payload["observer_create_failure"]=diagnostic + raise if not wait_http(a.host,a.observer_port,"/",60): raise RuntimeError("DNS-SD observer HTTP witness did not become reachable") deadline=time.monotonic()+45; observed=None while time.monotonic() Date: Sun, 4 Oct 2026 06:38:36 +0200 Subject: [PATCH 319/719] test: lock observer failed-create evidence contract --- tests/test_truenas_t6_foliorelay_control.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 156f21466..7826e34b9 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -58,6 +58,24 @@ def test_observer_lifecycle_is_independent_from_mdns_oracle_result(self): self.assertIn('DNS-SD observer oracle failed:', text) self.assertIn('DNS-SD observer oracle remained pending', text) + + def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn("class JobFailure", text) + self.assertIn('observer_created=True', text) + self.assertLess( + text.index('observer_created=True'), + text.index('wait_job(oj,"observer app.create")'), + ) + self.assertIn('payload["observer_create_failure"]', text) + self.assertIn('"app.container_log_follow:"', text) + self.assertIn('"tail_lines": tail_lines', text) + self.assertIn('"container_log_tails"', text) + self.assertIn('bounded_job_snapshot(observer_exc.job)', text) + self.assertIn('f"{name} cleanup delete"', text) + self.assertIn('"zero_residue"', text) + self.assertIn('payload["cleanup_needed"]', text) + def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") From b9791d86fdddc74de5f8b6d868432a79f2b29c30 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:39:43 +0200 Subject: [PATCH 320/719] test: require failed custom-app force cleanup --- tests/test_truenas_t6_foliorelay_control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 7826e34b9..426be316d 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -73,6 +73,7 @@ def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(sel self.assertIn('"container_log_tails"', text) self.assertIn('bounded_job_snapshot(observer_exc.job)', text) self.assertIn('f"{name} cleanup delete"', text) + self.assertIn('"force_remove_custom_app":True', text) self.assertIn('"zero_residue"', text) self.assertIn('payload["cleanup_needed"]', text) From 8b1748ef3563454556b6debc094e899a002f9016 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:40:05 +0200 Subject: [PATCH 321/719] test: restore non-forced failed-app cleanup contract --- tests/test_truenas_t6_foliorelay_control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 426be316d..7826e34b9 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -73,7 +73,6 @@ def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(sel self.assertIn('"container_log_tails"', text) self.assertIn('bounded_job_snapshot(observer_exc.job)', text) self.assertIn('f"{name} cleanup delete"', text) - self.assertIn('"force_remove_custom_app":True', text) self.assertIn('"zero_residue"', text) self.assertIn('payload["cleanup_needed"]', text) From 8b8086a5891d91fe904d611cdcca832d53336212 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:48:14 +0200 Subject: [PATCH 322/719] RDTE: request FolioRelay BETA.3 T6 rep 006 observer diagnostics --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e82689aa2..129066666 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-005-observer-oracle-separation", + "request_id": "tn-26-beta3-t6-foliorelay-006-observer-failed-create-diagnostics", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Re-run exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-004 isolated the independent DNS-SD observer lifecycle/oracle conflation. Preserve all product/Foundry/image/Compose identities, the public-URI oracle correction, and the cheap-qualified URF fixture. Require the observer Custom App to realize independently, then require its explicit mDNS oracle result; accept only with exact three-service readback, portal/IPP reachability, canonical control/CUPS/DNS-SD identity, PDF and Apple Raster exact-source preservation, restart persistence, desired/live re-plan NOOP, update/redeploy persistence, and zero-residue cleanup." + "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-005 repeated the independent observer Custom App up failure. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, and DNS-SD oracle semantics. The only causal harness delta is bounded failed-create evidence and cleanup accounting from PR #227: if observer app.create fails, retain job/app/container diagnostics and prove cleanup/zero residue; if the observer realizes, continue the existing DNS-SD oracle unchanged. PR #220 remains HOLD unless this rep actually reaches the TXT UUID comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." } From 58a96e06f5e3e6793c78583db2447a2e7b9ad009 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:55:29 +0200 Subject: [PATCH 323/719] RDTE: retain failed app-create job log excerpt --- scripts/truenas_middleware_foliorelay_t6_probe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index ed3df74c9..15e50beee 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -342,8 +342,8 @@ def bounded_job_snapshot(job): if not isinstance(job, dict): return None return { - key: (bounded_text(job.get(key)) if key in {"error", "exception", "exc_info"} else job.get(key)) - for key in ("id", "state", "progress", "error", "exception", "exc_info") + key: (bounded_text(job.get(key)) if key in {"error", "exception", "exc_info", "logs_excerpt"} else job.get(key)) + for key in ("id", "state", "progress", "error", "exception", "exc_info", "logs_excerpt") if key in job } From a6ef4a76e8bb80357067b9b19c6c2b3d09ebc28e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:55:32 +0200 Subject: [PATCH 324/719] test: require failed app-create log excerpt evidence --- tests/test_truenas_t6_foliorelay_control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 7826e34b9..1b8a7d9fc 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -72,6 +72,7 @@ def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(sel self.assertIn('"tail_lines": tail_lines', text) self.assertIn('"container_log_tails"', text) self.assertIn('bounded_job_snapshot(observer_exc.job)', text) + self.assertIn('"logs_excerpt"', text) self.assertIn('f"{name} cleanup delete"', text) self.assertIn('"zero_residue"', text) self.assertIn('payload["cleanup_needed"]', text) From ea2635a3a04b01ae4cd609bf71438bf764a4351a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:58:26 +0200 Subject: [PATCH 325/719] RDTE: treat absent failed resources as clean state --- .../truenas_middleware_foliorelay_t6_probe.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 15e50beee..be498dc0b 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -433,6 +433,13 @@ def main(): def call(method,params): nonlocal rid v=ddp_call(ws,str(rid),method,params); rid+=1; return v + def query_optional(method,filters): + rows=call(method,[filters]) + if not isinstance(rows,list): + raise RuntimeError(f"{method} optional query did not return list") + if len(rows)>1: + raise RuntimeError(f"{method} optional query returned multiple rows") + return rows[0] if rows else None def wait_job(j,label): deadline=time.monotonic()+a.job_timeout while time.monotonic() Date: Sun, 4 Oct 2026 23:58:29 +0200 Subject: [PATCH 326/719] test: lock optional cleanup query semantics --- tests/test_truenas_t6_foliorelay_control.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 1b8a7d9fc..114c4a5df 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -73,6 +73,12 @@ def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(sel self.assertIn('"container_log_tails"', text) self.assertIn('bounded_job_snapshot(observer_exc.job)', text) self.assertIn('"logs_excerpt"', text) + self.assertIn('def query_optional(method,filters):', text) + self.assertIn('observer_app=query_optional("app.query"', text) + self.assertIn('before=query_optional("app.query"', text) + self.assertIn('query_optional("app.query",[["id","=",name]]) is None', text) + self.assertIn('before=query_optional("pool.dataset.query"', text) + self.assertIn('query_optional("pool.dataset.query",[["id","=",DATASET]]) is None', text) self.assertIn('f"{name} cleanup delete"', text) self.assertIn('"zero_residue"', text) self.assertIn('payload["cleanup_needed"]', text) From 15139e2a3cbf1ff4ab85c6ac9327bc07f7ac7ac8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:27:51 +0200 Subject: [PATCH 327/719] RDTE: request FolioRelay BETA.3 T6 rep 007 job log evidence --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 129066666..222f254ff 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-006-observer-failed-create-diagnostics", + "request_id": "tn-26-beta3-t6-foliorelay-007-failed-create-job-log-excerpt", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-005 repeated the independent observer Custom App up failure. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, and DNS-SD oracle semantics. The only causal harness delta is bounded failed-create evidence and cleanup accounting from PR #227: if observer app.create fails, retain job/app/container diagnostics and prove cleanup/zero residue; if the observer realizes, continue the existing DNS-SD oracle unchanged. PR #220 remains HOLD unless this rep actually reaches the TXT UUID comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." + "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-006 proved that the observer Custom App still fails during app.create and that TrueNAS removes the failed App before post-failure inspection. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, and DNS-SD oracle semantics. The only causal harness delta is PR #233 evidence/accounting: retain the exact failed app.create job logs_excerpt and treat zero-row post-failure App/dataset queries as clean absence while failing closed on malformed or multiple results. Use the job-scoped excerpt to identify the Compose up failure. PR #220 remains HOLD unless this rep reaches the TXT UUID comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." } From ba056fa384b8d37bfc4c6fbe1d601109e400c411 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:32:16 +0200 Subject: [PATCH 328/719] RDTE: capture bounded observer app lifecycle evidence --- .../truenas_middleware_foliorelay_t6_probe.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index be498dc0b..ecc943bb0 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -127,6 +127,21 @@ def http_bytes(host, port, path, token=None, method="GET", timeout=8.0): with urllib.request.urlopen(req, timeout=timeout) as r: return r.read() +def middleware_http_bytes(host, port, tls, path, timeout=8.0): + if tls: + ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE + conn=http.client.HTTPSConnection(host,port,timeout=timeout,context=ctx) + else: + conn=http.client.HTTPConnection(host,port,timeout=timeout) + try: + conn.request("GET",path) + resp=conn.getresponse(); payload=resp.read() + finally: + conn.close() + if not (200 <= resp.status < 300): + raise RuntimeError(f"middleware download HTTP {resp.status}") + return payload + def wait_http(host, port, path, timeout_s): deadline=time.monotonic()+timeout_s while time.monotonic()" +def bounded_observer_lifecycle_excerpt(value, app_name=OBSERVER_APP_NAME, before=2, after=24, limit=8000): + if value is None: + return None + lines=str(value).splitlines() + needles=(app_name, f"ix-{app_name}") + hits=[i for i,line in enumerate(lines) if any(needle in line for needle in needles)] + if not hits: + return None + selected=set() + for i in hits: + selected.update(range(max(0,i-before), min(len(lines),i+after+1))) + excerpt="\n".join(lines[i] for i in sorted(selected)) + excerpt=re.sub(r"(auth_token=)[^&\\s]+", r"\1", excerpt) + return bounded_text(excerpt,limit) + def bounded_job_snapshot(job): if not isinstance(job, dict): return None @@ -526,6 +556,15 @@ def wait_state(name,state): except Exception as observer_exc: diagnostic={"detail":f"{type(observer_exc).__name__}: {observer_exc}"} if isinstance(observer_exc,JobFailure): diagnostic["job"]=bounded_job_snapshot(observer_exc.job) + try: + download=call("core.download",["filesystem.get",["/var/log/app_lifecycle.log"],"app_lifecycle.log",True]) + if not (isinstance(download,list) and len(download)==2 and isinstance(download[0],int) and isinstance(download[1],str)): + raise RuntimeError("core.download app_lifecycle contract drifted") + wait_job(download[0],"app lifecycle log download") + lifecycle=middleware_http_bytes(a.host,a.port,a.tls,download[1],timeout=a.timeout).decode("utf-8","replace") + diagnostic["app_lifecycle_excerpt"]=bounded_observer_lifecycle_excerpt(lifecycle) + except Exception as lifecycle_exc: + diagnostic["app_lifecycle_capture_error"]=f"{type(lifecycle_exc).__name__}: {lifecycle_exc}" try: observer_app=query_optional("app.query",[["id","=",OBSERVER_APP_NAME]]) diagnostic["app"]=bounded_app_snapshot(observer_app) From f6ef45813aaaeac6a66dff9f096c704557801539 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:32:20 +0200 Subject: [PATCH 329/719] test: bound observer app lifecycle diagnostics --- tests/test_truenas_t6_foliorelay_control.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 114c4a5df..a6dbd7ade 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -83,6 +83,26 @@ def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(sel self.assertIn('"zero_residue"', text) self.assertIn('payload["cleanup_needed"]', text) + self.assertIn('call("core.download",["filesystem.get",["/var/log/app_lifecycle.log"]', text) + self.assertIn('"app_lifecycle_excerpt"', text) + self.assertIn('"app_lifecycle_capture_error"', text) + + def test_observer_lifecycle_excerpt_is_bounded_scoped_and_redacted(self): + sample = "\n".join([ + "unrelated-before", + "2026 app_lifecycle Failed 'up' action for 'rdte-t6-foliorelay-observer' app auth_token=secret-value", + "compose detail: failed to start container", + "compose detail: permission denied", + "unrelated-after", + ]) + excerpt = MOD.bounded_observer_lifecycle_excerpt(sample, before=0, after=2, limit=1000) + self.assertIn("rdte-t6-foliorelay-observer", excerpt) + self.assertIn("failed to start container", excerpt) + self.assertIn("permission denied", excerpt) + self.assertNotIn("unrelated-before", excerpt) + self.assertNotIn("secret-value", excerpt) + self.assertIn("auth_token=", excerpt) + def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") From a312ca61cdb11084f46073eee64365e6c002afe9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:34:01 +0200 Subject: [PATCH 330/719] fix: correct lifecycle evidence redaction regex --- scripts/truenas_middleware_foliorelay_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index ecc943bb0..392cf8ff6 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -365,7 +365,7 @@ def bounded_observer_lifecycle_excerpt(value, app_name=OBSERVER_APP_NAME, before for i in hits: selected.update(range(max(0,i-before), min(len(lines),i+after+1))) excerpt="\n".join(lines[i] for i in sorted(selected)) - excerpt=re.sub(r"(auth_token=)[^&\\s]+", r"\1", excerpt) + excerpt=re.sub(r"(auth_token=)[^&\s]+", r"\1", excerpt) return bounded_text(excerpt,limit) def bounded_job_snapshot(job): From 2a4aff5f5d30b6eceab93a9a620729fffb8894d1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 00:51:25 +0200 Subject: [PATCH 331/719] RDTE: request FolioRelay BETA.3 T6 rep 008 lifecycle evidence --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 222f254ff..9fa108305 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-007-failed-create-job-log-excerpt", + "request_id": "tn-26-beta3-t6-foliorelay-008-app-lifecycle-evidence", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-006 proved that the observer Custom App still fails during app.create and that TrueNAS removes the failed App before post-failure inspection. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, and DNS-SD oracle semantics. The only causal harness delta is PR #233 evidence/accounting: retain the exact failed app.create job logs_excerpt and treat zero-row post-failure App/dataset queries as clean absence while failing closed on malformed or multiple results. Use the job-scoped excerpt to identify the Compose up failure. PR #220 remains HOLD unless this rep reaches the TXT UUID comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." + "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-007 proved that failed app.create job logs contain no Docker Compose stderr even though cleanup is clean. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, and DNS-SD oracle semantics. The only causal harness delta is PR #235 evidence capture: after observer app.create fails, retrieve the appliance app lifecycle log through authenticated middleware and retain only a bounded observer-scoped excerpt, while preserving the primary failure and zero-residue accounting. Use that excerpt to identify the TrueNAS appliance/runtime Compose failure. PR #220 remains HOLD unless this rep reaches the TXT UUID comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." } From f32e865a14672d8e93d558250d1ac09604c954d3 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:37:20 +0200 Subject: [PATCH 332/719] RDTE: preserve lifecycle stderr head and terminal tail --- .../truenas_middleware_foliorelay_t6_probe.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 392cf8ff6..ad70ac69f 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -353,6 +353,20 @@ def bounded_text(value, limit=8000): text = str(value) return text if len(text) <= limit else text[:limit] + "..." +def bounded_head_tail_text(value, limit=8000): + if value is None: + return None + text = str(value) + if len(text) <= limit: + return text + marker = "\n......\n" + if limit <= len(marker): + return text[-limit:] + available = limit - len(marker) + head = max(1, available // 3) + tail = available - head + return text[:head] + marker + text[-tail:] + def bounded_observer_lifecycle_excerpt(value, app_name=OBSERVER_APP_NAME, before=2, after=24, limit=8000): if value is None: return None @@ -366,7 +380,7 @@ def bounded_observer_lifecycle_excerpt(value, app_name=OBSERVER_APP_NAME, before selected.update(range(max(0,i-before), min(len(lines),i+after+1))) excerpt="\n".join(lines[i] for i in sorted(selected)) excerpt=re.sub(r"(auth_token=)[^&\s]+", r"\1", excerpt) - return bounded_text(excerpt,limit) + return bounded_head_tail_text(excerpt,limit) def bounded_job_snapshot(job): if not isinstance(job, dict): From cd2e8f17f069c981197a0dd8678a3883effeb5c8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:37:23 +0200 Subject: [PATCH 333/719] test: require terminal lifecycle stderr preservation --- tests/test_truenas_t6_foliorelay_control.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index a6dbd7ade..0887f633f 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -103,6 +103,19 @@ def test_observer_lifecycle_excerpt_is_bounded_scoped_and_redacted(self): self.assertNotIn("secret-value", excerpt) self.assertIn("auth_token=", excerpt) + def test_observer_lifecycle_excerpt_preserves_terminal_error_when_long_line_is_bounded(self): + prefix = "pull progress " * 900 + terminal = "failed to register layer: write /var/lib/docker/overlay2: no space left on device" + sample = ( + "2026 app_lifecycle Failed 'up' action for 'rdte-t6-foliorelay-observer' app " + + prefix + terminal + ) + excerpt = MOD.bounded_observer_lifecycle_excerpt(sample, before=0, after=0, limit=1200) + self.assertLessEqual(len(excerpt), 1200) + self.assertIn("rdte-t6-foliorelay-observer", excerpt) + self.assertIn("......", excerpt) + self.assertIn(terminal, excerpt) + def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") From 15fed384d08e0b921c2de737095cf7c88935049e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:42:16 +0200 Subject: [PATCH 334/719] RDTE: request FolioRelay BETA.3 T6 rep 009 terminal pull error --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 9fa108305..e023d199b 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-008-app-lifecycle-evidence", + "request_id": "tn-26-beta3-t6-foliorelay-009-terminal-pull-error", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep-007 proved that failed app.create job logs contain no Docker Compose stderr even though cleanup is clean. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, and DNS-SD oracle semantics. The only causal harness delta is PR #235 evidence capture: after observer app.create fails, retrieve the appliance app lifecycle log through authenticated middleware and retain only a bounded observer-scoped excerpt, while preserving the primary failure and zero-residue accounting. Use that excerpt to identify the TrueNAS appliance/runtime Compose failure. PR #220 remains HOLD unless this rep reaches the TXT UUID comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." + "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 008 exposed observer image-pull activity but lost the terminal Docker/Compose stderr to head-only bounding. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, DNS-SD, cleanup, and acceptance semantics. The only causal harness delta is PR #238 head-plus-terminal-tail lifecycle evidence so the exact terminal observer image-pull error is retained. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true. PR #220 remains HOLD unless this rep actually reaches the DNS-SD TXT UUID comparison." } From 9d0aeaad71518de3eac718c854371e6ad576f3ec Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:51:14 +0200 Subject: [PATCH 335/719] RDTE: qualify minimal static FolioRelay observer --- .../workflows/prep-foliorelay-t6-consumer.yml | 30 +- tools/foliorelay-observer/main.go | 267 ++++++++++++++++++ tools/foliorelay-observer/main_test.go | 82 ++++++ 3 files changed, 363 insertions(+), 16 deletions(-) create mode 100644 tools/foliorelay-observer/main.go create mode 100644 tools/foliorelay-observer/main_test.go diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 9aa70fba5..1ef12340f 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -50,40 +50,38 @@ jobs: with: name: foliorelay-t6-beta3-control path: ${{ runner.temp }}/foliorelay-control - - name: Smoke exact independent DNS-SD observer container + - name: Smoke minimal static independent DNS-SD observer shell: bash run: | set -euo pipefail - image='ghcr.io/truenas/apps_validation@sha256:3f38cdaa6ed9c54e5c9c43ec15790aabc0028631dd3f0c28129832a69fd1f30c' + carrier='hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8' work="$RUNNER_TEMP/foliorelay-observer-smoke" rm -rf "$work" mkdir -p "$work" - python3 - "$work/mdns_observer.py" <<'PY' - import importlib.util, pathlib, sys - scripts=pathlib.Path("scripts").resolve() - sys.path.insert(0,str(scripts)) - script=scripts/"truenas_middleware_foliorelay_t6_probe.py" - spec=importlib.util.spec_from_file_location("foliorelay_probe",script) - mod=importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - pathlib.Path(sys.argv[1]).write_text(mod.OBSERVER,encoding="utf-8") - PY + command -v go >/dev/null + GO111MODULE=off go test ./tools/foliorelay-observer + GO111MODULE=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags='-s -w -buildid=' \ + -o "$work/foliorelay-observer" ./tools/foliorelay-observer + test "$(stat -c%s "$work/foliorelay-observer")" -le 6291456 port="$(python3 - <<'PY' import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() PY )" - docker pull "$image" >/dev/null + docker pull "$carrier" >/dev/null + test "$(docker image inspect "$carrier" --format '{{.Size}}')" -le 1048576 docker rm -f foliorelay-observer-smoke >/dev/null 2>&1 || true trap 'docker rm -f foliorelay-observer-smoke >/dev/null 2>&1 || true' EXIT docker run -d --name foliorelay-observer-smoke \ --network host \ --read-only \ + --user 65534:65534 \ --cap-drop ALL \ --security-opt no-new-privileges:true \ - --mount "type=bind,src=$work,dst=/observer,readonly" \ - --entrypoint python3 \ - "$image" /observer/mdns_observer.py \ + --mount "type=bind,src=$work/foliorelay-observer,dst=/observer/foliorelay-observer,readonly" \ + --entrypoint /observer/foliorelay-observer \ + "$carrier" \ --uuid urn:uuid:01234567-89ab-4def-8123-456789abcdef \ --expected-host intentionally-absent.local \ --expected-ipp-port 8634 \ diff --git a/tools/foliorelay-observer/main.go b/tools/foliorelay-observer/main.go new file mode 100644 index 000000000..5e1440b8d --- /dev/null +++ b/tools/foliorelay-observer/main.go @@ -0,0 +1,267 @@ +package main + +import ( + "encoding/binary" + "encoding/json" + "errors" + "flag" + "fmt" + "net/http" + "os" + "strings" + "sync" + "syscall" + "time" +) + +const queryName = "_universal._sub._ipp._tcp.local" + +type resultState struct { + mu sync.RWMutex + v map[string]any +} + +func (s *resultState) set(v map[string]any) { + s.mu.Lock() + s.v = v + s.mu.Unlock() +} + +func (s *resultState) get() map[string]any { + s.mu.RLock() + defer s.mu.RUnlock() + out := make(map[string]any, len(s.v)) + for k, v := range s.v { + out[k] = v + } + return out +} + +func encodeName(name string) []byte { + out := make([]byte, 0, len(name)+2) + for _, label := range strings.Split(name, ".") { + out = append(out, byte(len(label))) + out = append(out, label...) + } + return append(out, 0) +} + +func nameAt(pkt []byte, off int, seen map[int]bool) (string, int, error) { + if seen == nil { + seen = map[int]bool{} + } + labels := []string{} + end := -1 + for { + if off >= len(pkt) { + return "", 0, errors.New("dns name overflow") + } + n := int(pkt[off]) + if n == 0 { + if end < 0 { + end = off + 1 + } + return strings.Join(labels, "."), end, nil + } + if n&0xC0 == 0xC0 { + if off+1 >= len(pkt) { + return "", 0, errors.New("dns pointer overflow") + } + ptr := ((n & 0x3f) << 8) | int(pkt[off+1]) + if seen[ptr] { + return "", 0, errors.New("dns pointer loop") + } + seen[ptr] = true + tail, _, err := nameAt(pkt, ptr, seen) + if err != nil { + return "", 0, err + } + if tail != "" { + labels = append(labels, tail) + } + if end < 0 { + end = off + 2 + } + return strings.Join(labels, "."), end, nil + } + off++ + if n > 63 || off+n > len(pkt) { + return "", 0, errors.New("dns label overflow") + } + labels = append(labels, string(pkt[off:off+n])) + off += n + } +} + +type observation struct { + universal bool + txt []string + srvTarget string + srvPort int +} + +func parsePacket(pkt []byte) (observation, error) { + var o observation + if len(pkt) < 12 { + return o, errors.New("short dns packet") + } + qd := int(binary.BigEndian.Uint16(pkt[4:6])) + an := int(binary.BigEndian.Uint16(pkt[6:8])) + ns := int(binary.BigEndian.Uint16(pkt[8:10])) + ar := int(binary.BigEndian.Uint16(pkt[10:12])) + off := 12 + for i := 0; i < qd; i++ { + _, next, err := nameAt(pkt, off, nil) + if err != nil { + return o, err + } + off = next + if off+4 > len(pkt) { + return o, errors.New("short dns question") + } + off += 4 + } + for i := 0; i < an+ns+ar; i++ { + name, next, err := nameAt(pkt, off, nil) + if err != nil { + return o, err + } + off = next + if off+10 > len(pkt) { + return o, errors.New("short dns rr") + } + typ := binary.BigEndian.Uint16(pkt[off : off+2]) + rdlen := int(binary.BigEndian.Uint16(pkt[off+8 : off+10])) + off += 10 + rstart := off + if off+rdlen > len(pkt) { + return o, errors.New("short dns rdata") + } + rdata := pkt[off : off+rdlen] + off += rdlen + + if strings.EqualFold(strings.TrimSuffix(name, "."), queryName) && typ == 12 { + o.universal = true + } + if typ == 16 { + for j := 0; j < len(rdata); { + n := int(rdata[j]) + j++ + if j+n > len(rdata) { + break + } + o.txt = append(o.txt, string(rdata[j:j+n])) + j += n + } + } + if typ == 33 && rdlen >= 7 { + o.srvPort = int(binary.BigEndian.Uint16(rdata[4:6])) + target, _, err := nameAt(pkt, rstart+6, nil) + if err == nil { + o.srvTarget = target + } + } + } + return o, nil +} + +func qualifying(o observation, expected, expectedHost string, expectedPort int) bool { + joined := strings.ToLower(strings.Join(o.txt, "\n")) + return o.universal && + strings.Contains(joined, strings.ToLower(expected)) && + strings.Contains(joined, "rp=printers/foliorelay") && + strings.Contains(joined, "pdl=application/pdf,image/urf") && + strings.EqualFold(strings.TrimSuffix(o.srvTarget, "."), strings.TrimSuffix(expectedHost, ".")) && + o.srvPort == expectedPort +} + +func observe(expected, expectedHost string, expectedPort int, seconds float64) (map[string]any, error) { + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) + if err != nil { + return nil, err + } + defer syscall.Close(fd) + _ = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + _ = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, 15, 1) + if err := syscall.Bind(fd, &syscall.SockaddrInet4{Port: 5353}); err != nil { + return nil, err + } + mreq := &syscall.IPMreq{Multiaddr: [4]byte{224, 0, 0, 251}} + if err := syscall.SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq); err != nil { + return nil, err + } + tv := syscall.NsecToTimeval(int64(time.Second)) + if err := syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &tv); err != nil { + return nil, err + } + + q := make([]byte, 12) + binary.BigEndian.PutUint16(q[4:6], 1) + q = append(q, encodeName(queryName)...) + q = append(q, 0, 12, 0, 1) + dst := &syscall.SockaddrInet4{Port: 5353, Addr: [4]byte{224, 0, 0, 251}} + if err := syscall.Sendto(fd, q, 0, dst); err != nil { + return nil, err + } + + deadline := time.Now().Add(time.Duration(seconds * float64(time.Second))) + buf := make([]byte, 65535) + for time.Now().Before(deadline) { + n, _, err := syscall.Recvfrom(fd, buf, 0) + if err != nil { + if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK || err == syscall.EINTR { + _ = syscall.Sendto(fd, q, 0, dst) + continue + } + return nil, err + } + o, err := parsePacket(buf[:n]) + if err != nil { + continue + } + if qualifying(o, expected, expectedHost, expectedPort) { + return map[string]any{ + "universal_ptr": true, + "uuid": expected, + "txt": o.txt, + "srv_target": o.srvTarget, + "srv_port": o.srvPort, + }, nil + } + } + return nil, errors.New("no qualifying _universal FolioRelay mDNS response") +} + +func main() { + uuid := flag.String("uuid", "", "expected canonical printer UUID") + host := flag.String("expected-host", "", "expected DNS-SD SRV host") + ippPort := flag.Int("expected-ipp-port", 0, "expected DNS-SD SRV port") + port := flag.Int("port", 18081, "HTTP witness port") + seconds := flag.Float64("seconds", 25, "mDNS observation window") + flag.Parse() + if *uuid == "" || *host == "" || *ippPort <= 0 { + fmt.Fprintln(os.Stderr, "uuid, expected-host, and expected-ipp-port are required") + os.Exit(2) + } + + state := &resultState{v: map[string]any{"status": "pending"}} + go func() { + r, err := observe(*uuid, *host, *ippPort, *seconds) + if err != nil { + state.set(map[string]any{"status": "error", "error": fmt.Sprintf("%T: %v", err, err)}) + return + } + r["status"] = "success" + state.set(r) + }() + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + body, _ := json.Marshal(state.get()) + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) + _, _ = w.Write(body) + }) + if err := http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", *port), nil); err != nil { + panic(err) + } +} diff --git a/tools/foliorelay-observer/main_test.go b/tools/foliorelay-observer/main_test.go new file mode 100644 index 000000000..d794b6f4a --- /dev/null +++ b/tools/foliorelay-observer/main_test.go @@ -0,0 +1,82 @@ +package main + +import ( + "encoding/binary" + "testing" +) + +func TestEncodeAndNameAt(t *testing.T) { + wire := encodeName("_universal._sub._ipp._tcp.local") + got, end, err := nameAt(wire, 0, nil) + if err != nil { t.Fatal(err) } + if got != queryName { t.Fatalf("got %q", got) } + if end != len(wire) { t.Fatalf("end=%d want=%d", end, len(wire)) } +} + +func TestCompressedNameAt(t *testing.T) { + base := encodeName("_ipp._tcp.local") + pkt := append([]byte{}, base...) + off := len(pkt) + pkt = append(pkt, 10) + pkt = append(pkt, []byte("_universal")...) + pkt = append(pkt, 0xC0, 0x00) + got, end, err := nameAt(pkt, off, nil) + if err != nil { t.Fatal(err) } + if got != "_universal._ipp._tcp.local" { t.Fatalf("got %q", got) } + if end != len(pkt) { t.Fatalf("end=%d want=%d", end, len(pkt)) } +} + +func TestQualifying(t *testing.T) { + o := observation{ + universal: true, + txt: []string{ + "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + }, + srvTarget: "foliorelay-t6.local.", + srvPort: 8634, + } + if !qualifying(o, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatal("expected qualifying observation") + } +} + +func TestParsePacket(t *testing.T) { + // One PTR owner for the universal subtype, one TXT RR, one SRV RR. + pkt := make([]byte, 12) + binary.BigEndian.PutUint16(pkt[6:8], 3) + + appendRR := func(owner string, typ uint16, rdata []byte) { + pkt = append(pkt, encodeName(owner)...) + hdr := make([]byte, 10) + binary.BigEndian.PutUint16(hdr[0:2], typ) + binary.BigEndian.PutUint16(hdr[2:4], 1) + binary.BigEndian.PutUint32(hdr[4:8], 120) + binary.BigEndian.PutUint16(hdr[8:10], uint16(len(rdata))) + pkt = append(pkt, hdr...) + pkt = append(pkt, rdata...) + } + + appendRR(queryName, 12, encodeName("FolioRelay._ipp._tcp.local")) + txt := []byte{} + for _, s := range []string{ + "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + } { + txt = append(txt, byte(len(s))) + txt = append(txt, []byte(s)...) + } + appendRR("FolioRelay._ipp._tcp.local", 16, txt) + srv := make([]byte, 6) + binary.BigEndian.PutUint16(srv[4:6], 8634) + srv = append(srv, encodeName("foliorelay-t6.local")...) + appendRR("FolioRelay._ipp._tcp.local", 33, srv) + + o, err := parsePacket(pkt) + if err != nil { t.Fatal(err) } + if !qualifying(o, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatalf("not qualifying: %+v", o) + } +} From 167dfef799ffc6049e668a5684bb2d68dac74249 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:58:33 +0200 Subject: [PATCH 336/719] RDTE: preflight-build minimal FolioRelay observer --- scripts/gha_kvm_truenas_rdte.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 90509d313..fdd638e67 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -314,8 +314,11 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 FolioRelay control client" + [[ -f "$SCRIPT_DIR/../tools/foliorelay-observer/main.go" ]] || + fail_evidence HARNESS_FAILURE preflight "missing minimal FolioRelay observer source" command -v ipptool >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: ipptool" command -v cc >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: C compiler" + command -v go >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: Go compiler" else [[ -f "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 LiteLLM control client" @@ -336,6 +339,18 @@ HOST_CPUS="$(nproc)" (( MEM_AVAIL_KIB >= MIN_HOST_MEM_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host memory headroom below 11 GiB required before allocating 8 GiB guest" (( FREE_KIB >= MIN_HOST_FREE_KIB )) || fail_evidence SKIPPED_GUARDRAIL preflight "host disk headroom below 28 GiB" +FOLIORELAY_OBSERVER_BINARY="" +if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "foliorelay" ]]; then + FOLIORELAY_OBSERVER_BINARY="$STATE_DIR/foliorelay-observer" + GO111MODULE=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags='-s -w -buildid=' \ + -o "$FOLIORELAY_OBSERVER_BINARY" "$SCRIPT_DIR/../tools/foliorelay-observer" || + fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer build failed" + observer_size="$(stat -c%s "$FOLIORELAY_OBSERVER_BINARY")" + (( observer_size <= 6291456 )) || + fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer exceeds 6 MiB budget" +fi + ISO="$STATE_DIR/$ISO_NAME" curl --fail --location --retry 3 --silent --show-error "$SHA_URL" -o "$STATE_DIR/vendor.sha256" || fail_evidence ENVIRONMENT_FAILURE acquire "vendor SHA256 sidecar download failed" @@ -887,6 +902,7 @@ elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then --control-port "$FOLIORELAY_CONTROL_HOST_PORT" \ --ipp-port "$FOLIORELAY_IPP_HOST_PORT" \ --observer-port "$FOLIORELAY_OBSERVER_HOST_PORT" \ + --observer-binary "$FOLIORELAY_OBSERVER_BINARY" \ "${MIDDLEWARE_TLS_ARG[@]}" \ --password-file "$PASSWORD_FILE" \ --control-dir "$FOUNDRY_CONTROL_DIR" \ From 460f347e1e66dfc8c7b523b3e15e676562705921 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:58:37 +0200 Subject: [PATCH 337/719] RDTE: run static observer in minimal carrier --- scripts/truenas_middleware_foliorelay_t6_probe.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index ad70ac69f..9d4bd0a95 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -27,7 +27,7 @@ OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer" EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" EXPECTED_CUPS = "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" -OBSERVER_IMAGE = "ghcr.io/truenas/apps_validation@sha256:3f38cdaa6ed9c54e5c9c43ec15790aabc0028631dd3f0c28129832a69fd1f30c" +OBSERVER_IMAGE = "hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8" DATASET = "rdtepool/foliorelay-t6" ROOT = "/mnt/rdtepool/foliorelay-t6" TOKEN_PATH = ROOT + "/secrets/control.token" @@ -462,9 +462,14 @@ def main(): p=argparse.ArgumentParser() p.add_argument("--host",default="127.0.0.1"); p.add_argument("--port",type=int,required=True) p.add_argument("--control-port",type=int,required=True); p.add_argument("--ipp-port",type=int,required=True); p.add_argument("--observer-port",type=int,required=True) + p.add_argument("--observer-binary",type=pathlib.Path,required=True) p.add_argument("--password-file",required=True); p.add_argument("--control-dir",type=pathlib.Path,required=True); p.add_argument("--foundry-commit",required=True) p.add_argument("--out",required=True); p.add_argument("--tls",action="store_true"); p.add_argument("--timeout",type=float,default=8); p.add_argument("--job-timeout",type=float,default=300); p.add_argument("--state-timeout",type=float,default=300) a=p.parse_args() + if not a.observer_binary.is_file(): + raise RuntimeError("minimal observer binary is missing") + if a.observer_binary.stat().st_size > 6291456: + raise RuntimeError("minimal observer binary exceeds 6 MiB budget") started=time.time(); ws=None; dataset_owned=False; app_created=False; observer_created=False payload={"schema":"truenas-foliorelay-foundry-t6/v1","classification":"ORACLE_FAILURE","oracleSatisfied":False,"expected_version":EXPECTED_VERSION,"foundry_commit":a.foundry_commit,"app_name":EXPECTED_APP_NAME,"secret_values_captured":False} try: @@ -560,8 +565,8 @@ def wait_state(name,state): if len(matches)!=1 or matches[0].get("artifact_sha256")!=sha or matches[0].get("substrate")!="cups": raise RuntimeError(f"{media} Inbox metadata drifted") blob=http_bytes(a.host,a.control_port,f"/api/v1/jobs/{matches[0]['job_id']}/artifact",tok,timeout=a.timeout) if sha256_bytes(blob)!=sha: raise RuntimeError(f"{media} downloaded artifact drifted") - sj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,OBSERVER_DIR+"/mdns_observer.py",OBSERVER.encode(),0o555,a.timeout); wait_job(sj,"observer upload") - obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR,"target":"/observer","read_only":True}],"entrypoint":["python3","/observer/mdns_observer.py"],"command":["--uuid",uuid,"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} + sj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,OBSERVER_DIR+"/foliorelay-observer",a.observer_binary.read_bytes(),0o555,a.timeout); wait_job(sj,"observer upload") + obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"user":"65534:65534","cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR+"/foliorelay-observer","target":"/observer/foliorelay-observer","read_only":True}],"entrypoint":["/observer/foliorelay-observer"],"command":["--uuid",uuid,"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} oj=call("app.create",[{"app_name":OBSERVER_APP_NAME,"custom_app":True,"custom_compose_config":obs_compose}]) if not isinstance(oj,int): raise RuntimeError("observer app.create did not return job") observer_created=True From 556018d73922377858d842c7a328e4fd27f4d9b8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:58:40 +0200 Subject: [PATCH 338/719] test: lock minimal static observer runtime contract --- tests/test_truenas_t6_foliorelay_control.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 0887f633f..8fd6424ab 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -45,7 +45,10 @@ def test_observer_is_separate_host_network_app(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn('OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer"', text) self.assertIn('"network_mode":"host"', text) - self.assertIn('"entrypoint":["python3","/observer/mdns_observer.py"]', text) + self.assertIn('"entrypoint":["/observer/foliorelay-observer"]', text) + self.assertIn('"user":"65534:65534"', text) + self.assertIn('hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8', text) + self.assertNotIn('ghcr.io/truenas/apps_validation@sha256:', text) self.assertIn('"distinct_observer_context":True', text) def test_observer_lifecycle_is_independent_from_mdns_oracle_result(self): @@ -128,6 +131,9 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) self.assertIn('FOLIORELAY_OBSERVER_HOST_PORT', harness) self.assertIn('--observer-port "$FOLIORELAY_OBSERVER_HOST_PORT"', harness) + self.assertIn('--observer-binary "$FOLIORELAY_OBSERVER_BINARY"', harness) + self.assertIn('CGO_ENABLED=0 GOOS=linux GOARCH=amd64', harness) + self.assertIn('observer_size <= 6291456', harness) def test_cups_uuid_oracle_preserves_canonical_urn_prefix(self): attrs = """ From 9bdee03decd7bcfdcc14fc30500796a987b8639b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:59:21 +0200 Subject: [PATCH 339/719] RDTE: remove superseded embedded Python observer --- .../truenas_middleware_foliorelay_t6_probe.py | 80 ------------------- 1 file changed, 80 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 9d4bd0a95..2b7abb3da 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -259,86 +259,6 @@ def ipptool_print(port:int, media:str, source:pathlib.Path, label:str, root:path (root/f"print-{label}.txt").write_text(p.stdout+"\n"+p.stderr,encoding="utf-8") if p.returncode: raise RuntimeError(f"{label} Print-Job failed") -OBSERVER = r'''#!/usr/bin/env python3 -import argparse, json, socket, struct, threading, time -from http.server import BaseHTTPRequestHandler, HTTPServer -Q="_universal._sub._ipp._tcp.local" -def enc(name): - return b"".join(bytes([len(x)])+x.encode() for x in name.split("."))+b"\0" -def name_at(pkt,off,seen=None): - if seen is None: seen=set() - labels=[]; end=None - while True: - if off>=len(pkt): raise ValueError("dns name overflow") - n=pkt[off] - if n==0: - if end is None: end=off+1 - return ".".join(labels),end - if n&0xC0==0xC0: - ptr=((n&0x3f)<<8)|pkt[off+1] - if ptr in seen: raise ValueError("dns pointer loop") - seen.add(ptr); tail,_=name_at(pkt,ptr,seen) - if tail: labels.append(tail) - if end is None: end=off+2 - return ".".join(labels),end - off+=1; labels.append(pkt[off:off+n].decode(errors="replace")); off+=n -def observe(expected,expected_host,expected_ipp_port,seconds=25): - qid=0; query=struct.pack("!HHHHHH",qid,0,1,0,0,0)+enc(Q)+struct.pack("!HH",12,1) - s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM,socket.IPPROTO_UDP) - s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) - try: s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEPORT,1) - except OSError: pass - s.bind(("",5353)) - mreq=socket.inet_aton("224.0.0.251")+socket.inet_aton("0.0.0.0") - s.setsockopt(socket.IPPROTO_IP,socket.IP_ADD_MEMBERSHIP,mreq); s.settimeout(1) - s.sendto(query,("224.0.0.251",5353)) - deadline=time.monotonic()+seconds; packets=[] - while time.monotonic()=7: - _,_,srv_port=struct.unpack("!HHH",r[:6]) - srv_target,_=name_at(pkt,rstart+6) - joined="\n".join(txt) - if (universal_ptr and expected.lower() in joined.lower() - and "rp=printers/FolioRelay" in joined - and "pdl=application/pdf,image/urf" in joined - and (srv_target or "").rstrip(".").lower()==expected_host.rstrip(".").lower() - and srv_port==expected_ipp_port): - return {"universal_ptr":True,"uuid":expected,"txt":txt,"srv_target":srv_target,"srv_port":srv_port} - except Exception: pass - raise RuntimeError("no qualifying _universal FolioRelay mDNS response") -class H(BaseHTTPRequestHandler): - result={"status":"pending"} - def do_GET(self): - body=json.dumps(H.result).encode(); self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(body))); self.end_headers(); self.wfile.write(body) - def log_message(self,*args): pass -def run_observer(args): - try: - result=observe(args.uuid,args.expected_host,args.expected_ipp_port,args.seconds) - H.result={"status":"success",**result} - except Exception as exc: - H.result={"status":"error","error":f"{type(exc).__name__}: {exc}"} -p=argparse.ArgumentParser(); p.add_argument("--uuid",required=True); p.add_argument("--expected-host",required=True); p.add_argument("--expected-ipp-port",type=int,required=True); p.add_argument("--port",type=int,default=18081); p.add_argument("--seconds",type=float,default=25); a=p.parse_args() -threading.Thread(target=run_observer,args=(a,),daemon=True).start() -HTTPServer(("0.0.0.0",a.port),H).serve_forever() -''' - class JobFailure(RuntimeError): def __init__(self, label: str, job: dict): From 7b472e7d007a8d9023917dd7743b985125ed0b0f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:59:26 +0200 Subject: [PATCH 340/719] test: make static observer the single authority --- tests/test_truenas_t6_foliorelay_control.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 8fd6424ab..7ec54d559 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -53,13 +53,14 @@ def test_observer_is_separate_host_network_app(self): def test_observer_lifecycle_is_independent_from_mdns_oracle_result(self): text = SCRIPT.read_text(encoding="utf-8") - self.assertIn('result={"status":"pending"}', text) - self.assertIn('threading.Thread(target=run_observer', text) - self.assertIn('{"status":"success",**result}', text) - self.assertIn('{"status":"error","error":', text) - self.assertIn('p.add_argument("--seconds",type=float,default=25)', text) + observer = (ROOT/"tools"/"foliorelay-observer"/"main.go").read_text(encoding="utf-8") + self.assertIn('"status": "pending"', observer) + self.assertIn('"status": "success"', observer) + self.assertIn('"status": "error"', observer) + self.assertIn('flag.Float64("seconds", 25', observer) self.assertIn('DNS-SD observer oracle failed:', text) self.assertIn('DNS-SD observer oracle remained pending', text) + self.assertNotIn("OBSERVER = r'''", text) def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(self): From 33b5fa5c00d6ac9ce9cd35eaf7ec3e176ef6c1b5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:01:41 +0200 Subject: [PATCH 341/719] test: point observer contract at static authority --- tests/test_truenas_t6_foliorelay_control.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 7ec54d559..d7dff2451 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -30,12 +30,14 @@ def test_probe_preserves_product_oracles_and_zero_residue(self): '"/api/v1/printer"', '"/api/v1/jobs"', 'application/pdf', 'image/urf', 'ipptool', 'UNIRAST', '"pool.dataset.create"', '"pool.dataset.delete"', '"app.create"', '"app.stop"', '"app.start"', '"app.delete"', - '"zero_residue":True', '_universal._sub._ipp._tcp.local', + '"zero_residue":True', 'dnssd_uuid_match', 'restart_preserved_identity_and_inbox', 'app.update', 'app.redeploy', 'replan_action', '"NOOP"', 'update_redeploy_preserved_identity_and_inbox', ): self.assertIn(needle, text) + observer = (ROOT/"tools"/"foliorelay-observer"/"main.go").read_text(encoding="utf-8") + self.assertIn('_universal._sub._ipp._tcp.local', observer) self.assertIn(MOD.OBSERVER_IMAGE, text) self.assertNotIn("avahi-publish-service", text) self.assertNotIn("/run/dbus", text) @@ -55,7 +57,7 @@ def test_observer_lifecycle_is_independent_from_mdns_oracle_result(self): text = SCRIPT.read_text(encoding="utf-8") observer = (ROOT/"tools"/"foliorelay-observer"/"main.go").read_text(encoding="utf-8") self.assertIn('"status": "pending"', observer) - self.assertIn('"status": "success"', observer) + self.assertIn('r["status"] = "success"', observer) self.assertIn('"status": "error"', observer) self.assertIn('flag.Float64("seconds", 25', observer) self.assertIn('DNS-SD observer oracle failed:', text) @@ -158,11 +160,13 @@ def test_forwarded_ipp_uri_preserves_product_resource_path_without_claiming_publ def test_dnssd_observer_proves_public_host_port_and_resource_path(self): text = SCRIPT.read_text(encoding="utf-8") + observer = (ROOT/"tools"/"foliorelay-observer"/"main.go").read_text(encoding="utf-8") self.assertIn('"--expected-host",PUBLIC_HOST', text) self.assertIn('"--expected-ipp-port",str(PUBLIC_IPP_PORT)', text) - self.assertIn('srv_target', text) - self.assertIn('srv_port', text) - self.assertIn('"rp=printers/FolioRelay"', text) + self.assertIn('"srv_target"', observer) + self.assertIn('"srv_port"', observer) + self.assertIn('rp=printers/foliorelay', observer.lower()) + self.assertIn('pdl=application/pdf,image/urf', observer.lower()) self.assertIn('"dnssd_public_uri_match":True', text) self.assertNotIn('if PUBLIC_URI not in attrs', text) From e0757e4ac3e9a18566c3f0af05aea867886a4111 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:06:13 +0200 Subject: [PATCH 342/719] RDTE: receipt exact FolioRelay observer fixture identity --- scripts/truenas_middleware_foliorelay_t6_probe.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 2b7abb3da..5d74dc522 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -388,10 +388,20 @@ def main(): a=p.parse_args() if not a.observer_binary.is_file(): raise RuntimeError("minimal observer binary is missing") - if a.observer_binary.stat().st_size > 6291456: + observer_bytes=a.observer_binary.read_bytes() + if not observer_bytes: + raise RuntimeError("minimal observer binary is empty") + if len(observer_bytes) > 6291456: raise RuntimeError("minimal observer binary exceeds 6 MiB budget") started=time.time(); ws=None; dataset_owned=False; app_created=False; observer_created=False payload={"schema":"truenas-foliorelay-foundry-t6/v1","classification":"ORACLE_FAILURE","oracleSatisfied":False,"expected_version":EXPECTED_VERSION,"foundry_commit":a.foundry_commit,"app_name":EXPECTED_APP_NAME,"secret_values_captured":False} + payload["observer_fixture"]={ + "implementation":"go-static", + "carrier_image":OBSERVER_IMAGE, + "binary_sha256":sha256_bytes(observer_bytes), + "binary_size_bytes":len(observer_bytes), + "run_as":"65534:65534", + } try: control,compose=load_control(a.control_dir,a.foundry_commit) payload["materialization"]={"schema":control["schema"],"foundry_ref":control["foundry_ref"],"compose_canonical_sha256":canonical_sha256(compose),"control_image":EXPECTED_CONTROL,"cups_image":EXPECTED_CUPS} @@ -485,7 +495,7 @@ def wait_state(name,state): if len(matches)!=1 or matches[0].get("artifact_sha256")!=sha or matches[0].get("substrate")!="cups": raise RuntimeError(f"{media} Inbox metadata drifted") blob=http_bytes(a.host,a.control_port,f"/api/v1/jobs/{matches[0]['job_id']}/artifact",tok,timeout=a.timeout) if sha256_bytes(blob)!=sha: raise RuntimeError(f"{media} downloaded artifact drifted") - sj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,OBSERVER_DIR+"/foliorelay-observer",a.observer_binary.read_bytes(),0o555,a.timeout); wait_job(sj,"observer upload") + sj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,OBSERVER_DIR+"/foliorelay-observer",observer_bytes,0o555,a.timeout); wait_job(sj,"observer upload") obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"user":"65534:65534","cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR+"/foliorelay-observer","target":"/observer/foliorelay-observer","read_only":True}],"entrypoint":["/observer/foliorelay-observer"],"command":["--uuid",uuid,"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} oj=call("app.create",[{"app_name":OBSERVER_APP_NAME,"custom_app":True,"custom_compose_config":obs_compose}]) if not isinstance(oj,int): raise RuntimeError("observer app.create did not return job") From 99f2e8362711df0edb239c1ef09bb6575bc4bf33 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:06:16 +0200 Subject: [PATCH 343/719] RDTE: gate and prove static FolioRelay observer source --- .github/workflows/prep-foliorelay-t6-consumer.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 1ef12340f..fde7d424d 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -10,6 +10,7 @@ on: - "scripts/gha_kvm_truenas_rdte.sh" - "scripts/truenas_middleware_foliorelay_t6_probe.py" - "tests/test_truenas_t6_foliorelay_control.py" + - "tools/foliorelay-observer/**" push: branches: - "prep/truenas-t6-foliorelay-v1" @@ -19,6 +20,7 @@ on: - "scripts/gha_kvm_truenas_rdte.sh" - "scripts/truenas_middleware_foliorelay_t6_probe.py" - "tests/test_truenas_t6_foliorelay_control.py" + - "tools/foliorelay-observer/**" permissions: contents: read @@ -63,7 +65,13 @@ jobs: GO111MODULE=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -trimpath -ldflags='-s -w -buildid=' \ -o "$work/foliorelay-observer" ./tools/foliorelay-observer + test -s "$work/foliorelay-observer" test "$(stat -c%s "$work/foliorelay-observer")" -le 6291456 + readelf -l "$work/foliorelay-observer" >"$RUNNER_TEMP/foliorelay-observer-elf.txt" + if grep -q 'INTERP' "$RUNNER_TEMP/foliorelay-observer-elf.txt"; then + echo "observer binary unexpectedly has a dynamic interpreter" >&2 + exit 1 + fi port="$(python3 - <<'PY' import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() From 3a0b5fe7115ce87bfd69bf2d2320c60b49d392c2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:06:23 +0200 Subject: [PATCH 344/719] RDTE: fail closed on FolioRelay observer static build --- scripts/gha_kvm_truenas_rdte.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index fdd638e67..afaf0c1ec 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -319,6 +319,7 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | command -v ipptool >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: ipptool" command -v cc >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: C compiler" command -v go >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: Go compiler" + command -v readelf >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: readelf" else [[ -f "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 LiteLLM control client" @@ -346,9 +347,14 @@ if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "foliorelay" ]]; then go build -trimpath -ldflags='-s -w -buildid=' \ -o "$FOLIORELAY_OBSERVER_BINARY" "$SCRIPT_DIR/../tools/foliorelay-observer" || fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer build failed" + [[ -s "$FOLIORELAY_OBSERVER_BINARY" ]] || + fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer build produced an empty binary" observer_size="$(stat -c%s "$FOLIORELAY_OBSERVER_BINARY")" (( observer_size <= 6291456 )) || fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer exceeds 6 MiB budget" + if readelf -l "$FOLIORELAY_OBSERVER_BINARY" | grep -q 'INTERP'; then + fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer unexpectedly has a dynamic interpreter" + fi fi ISO="$STATE_DIR/$ISO_NAME" From 10726ec3fa64e491bb57a9a17ae8457bd73ead1e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:06:27 +0200 Subject: [PATCH 345/719] test: exercise observer fixture CLI and receipt identity --- tests/test_truenas_t6_foliorelay_control.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index d7dff2451..a55b30653 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -177,6 +177,20 @@ def test_synthetic_urf_generator_builds_and_emits_unirast(self): self.assertTrue(out.is_file()) self.assertEqual(out.read_bytes()[:7], b"UNIRAST") + def test_probe_cli_reaches_argparse_before_observer_validation(self): + cp=subprocess.run([sys.executable,str(SCRIPT),"--help"],capture_output=True,text=True) + self.assertEqual(cp.returncode,0,cp.stderr) + self.assertIn("--observer-binary",cp.stdout) + + def test_observer_fixture_identity_is_receipted(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('payload["observer_fixture"]', text) + self.assertIn('"implementation":"go-static"', text) + self.assertIn('"carrier_image":OBSERVER_IMAGE', text) + self.assertIn('"binary_sha256":sha256_bytes(observer_bytes)', text) + self.assertIn('"binary_size_bytes":len(observer_bytes)', text) + self.assertIn('"run_as":"65534:65534"', text) + def test_probe_compiles(self): cp=subprocess.run([sys.executable,"-m","py_compile",str(SCRIPT)],capture_output=True,text=True) self.assertEqual(cp.returncode,0,cp.stderr) From 1181eb61be06269f53d3193ec8124e870f035c39 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:06:31 +0200 Subject: [PATCH 346/719] test: red-team static FolioRelay observer oracle --- tools/foliorelay-observer/main_test.go | 45 ++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tools/foliorelay-observer/main_test.go b/tools/foliorelay-observer/main_test.go index d794b6f4a..885c359da 100644 --- a/tools/foliorelay-observer/main_test.go +++ b/tools/foliorelay-observer/main_test.go @@ -80,3 +80,48 @@ func TestParsePacket(t *testing.T) { t.Fatalf("not qualifying: %+v", o) } } + + +func TestQualifyingRejectsCrossedIdentity(t *testing.T) { + base := observation{ + universal: true, + txt: []string{ + "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + }, + srvTarget: "foliorelay-t6.local.", + srvPort: 8634, + } + tests := []struct { + name string + obs observation + expected string + host string + port int + }{ + {"missing-universal", observation{txt: base.txt, srvTarget: base.srvTarget, srvPort: base.srvPort}, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634}, + {"wrong-uuid", base, "urn:uuid:ffffffff-ffff-4fff-8fff-ffffffffffff", "foliorelay-t6.local", 8634}, + {"wrong-host", base, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "other.local", 8634}, + {"wrong-port", base, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 9999}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if qualifying(tc.obs, tc.expected, tc.host, tc.port) { + t.Fatal("unexpected qualifying observation") + } + }) + } +} + +func TestParsePacketRejectsMalformedInput(t *testing.T) { + if _, err := parsePacket([]byte{0, 1, 2}); err == nil { + t.Fatal("expected short packet error") + } + loop := make([]byte, 18) + binary.BigEndian.PutUint16(loop[4:6], 1) + loop[12], loop[13] = 0xC0, 0x0C + if _, err := parsePacket(loop); err == nil { + t.Fatal("expected DNS compression pointer loop error") + } +} From 2a454bcb74193d11befe8a33ceff48de20453766 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:07:40 +0200 Subject: [PATCH 347/719] RDTE: make observer carrier registry explicit --- scripts/truenas_middleware_foliorelay_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 5d74dc522..0b8755ce1 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -27,7 +27,7 @@ OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer" EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" EXPECTED_CUPS = "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" -OBSERVER_IMAGE = "hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8" +OBSERVER_IMAGE = "docker.io/library/hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8" DATASET = "rdtepool/foliorelay-t6" ROOT = "/mnt/rdtepool/foliorelay-t6" TOKEN_PATH = ROOT + "/secrets/control.token" From 856ccd780675c31067006b539d9496d8461963d9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:07:45 +0200 Subject: [PATCH 348/719] RDTE: smoke explicit observer carrier identity --- .github/workflows/prep-foliorelay-t6-consumer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index fde7d424d..ad783022f 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -56,7 +56,7 @@ jobs: shell: bash run: | set -euo pipefail - carrier='hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8' + carrier='docker.io/library/hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8' work="$RUNNER_TEMP/foliorelay-observer-smoke" rm -rf "$work" mkdir -p "$work" From b85a40d54f3ff07c71259dd56261f959c0253290 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:07:50 +0200 Subject: [PATCH 349/719] test: lock explicit observer carrier identity --- tests/test_truenas_t6_foliorelay_control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index a55b30653..55b198a07 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -49,7 +49,10 @@ def test_observer_is_separate_host_network_app(self): self.assertIn('"network_mode":"host"', text) self.assertIn('"entrypoint":["/observer/foliorelay-observer"]', text) self.assertIn('"user":"65534:65534"', text) - self.assertIn('hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8', text) + self.assertEqual( + MOD.OBSERVER_IMAGE, + "docker.io/library/hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8", + ) self.assertNotIn('ghcr.io/truenas/apps_validation@sha256:', text) self.assertIn('"distinct_observer_context":True', text) From 135b0fbe6ab56a131c4155b11f9ebd25b1e1a9e5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:58:53 +0200 Subject: [PATCH 350/719] RDTE: request FolioRelay BETA.3 T6 rep 010 static observer --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e023d199b..b0e20f2db 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-009-terminal-pull-error", + "request_id": "tn-26-beta3-t6-foliorelay-010-static-observer", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 008 exposed observer image-pull activity but lost the terminal Docker/Compose stderr to head-only bounding. Preserve all FolioRelay product, Foundry, image, Compose, public-URI, URF, DNS-SD, cleanup, and acceptance semantics. The only causal harness delta is PR #238 head-plus-terminal-tail lifecycle evidence so the exact terminal observer image-pull error is retained. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true. PR #220 remains HOLD unless this rep actually reaches the DNS-SD TXT UUID comparison." + "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 009 demonstrated the external observer fixture image was grossly oversized relative to the bounded Apps pool and failed during pull/extraction before DNS-SD. Preserve all FolioRelay product, Foundry, control/cups images, Compose, public-URI, PDF/URF, cleanup, and acceptance semantics. The only causal harness delta is PR #245: replace the observer runtime with the cheap-qualified static CGO=0 observer and pinned scratch-like carrier under the same host-network/read-only/cap-drop/no-new-privileges contract. DNS-SD canonical-vs-TXT UUID semantics remain unchanged; PR #220 stays HOLD unless this rep reaches that comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." } From 8528f3720d1819744c543cab108c7ab3f9ad4a29 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:03:15 +0200 Subject: [PATCH 351/719] RDTE: build FolioRelay observer from repository-relative package --- scripts/gha_kvm_truenas_rdte.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index afaf0c1ec..ec22c6d69 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -343,9 +343,12 @@ HOST_CPUS="$(nproc)" FOLIORELAY_OBSERVER_BINARY="" if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "foliorelay" ]]; then FOLIORELAY_OBSERVER_BINARY="$STATE_DIR/foliorelay-observer" - GO111MODULE=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ - go build -trimpath -ldflags='-s -w -buildid=' \ - -o "$FOLIORELAY_OBSERVER_BINARY" "$SCRIPT_DIR/../tools/foliorelay-observer" || + ( + cd "$SCRIPT_DIR/.." + GO111MODULE=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + go build -trimpath -ldflags='-s -w -buildid=' \ + -o "$FOLIORELAY_OBSERVER_BINARY" ./tools/foliorelay-observer + ) || fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer build failed" [[ -s "$FOLIORELAY_OBSERVER_BINARY" ]] || fail_evidence HARNESS_FAILURE preflight "minimal FolioRelay observer build produced an empty binary" From 9f84f10e7da4435201d71ed4d978698d319974e9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:03:20 +0200 Subject: [PATCH 352/719] test: lock executable observer build invocation --- tests/test_truenas_t6_foliorelay_control.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 55b198a07..0e3a0206c 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -139,6 +139,9 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): self.assertIn('--observer-port "$FOLIORELAY_OBSERVER_HOST_PORT"', harness) self.assertIn('--observer-binary "$FOLIORELAY_OBSERVER_BINARY"', harness) self.assertIn('CGO_ENABLED=0 GOOS=linux GOARCH=amd64', harness) + self.assertIn('cd "$SCRIPT_DIR/.."', harness) + self.assertIn('./tools/foliorelay-observer', harness) + self.assertNotIn('"$SCRIPT_DIR/../tools/foliorelay-observer" ||', harness) self.assertIn('observer_size <= 6291456', harness) def test_cups_uuid_oracle_preserves_canonical_urn_prefix(self): From 0b33def35886fcdfc108aa26803750d4f493a746 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:05:45 +0200 Subject: [PATCH 353/719] RDTE: request FolioRelay BETA.3 T6 rep 011 static observer build --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index b0e20f2db..3eb0f9c9e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-010-static-observer", + "request_id": "tn-26-beta3-t6-foliorelay-011-static-observer-build", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 009 demonstrated the external observer fixture image was grossly oversized relative to the bounded Apps pool and failed during pull/extraction before DNS-SD. Preserve all FolioRelay product, Foundry, control/cups images, Compose, public-URI, PDF/URF, cleanup, and acceptance semantics. The only causal harness delta is PR #245: replace the observer runtime with the cheap-qualified static CGO=0 observer and pinned scratch-like carrier under the same host-network/read-only/cap-drop/no-new-privileges contract. DNS-SD canonical-vs-TXT UUID semantics remain unchanged; PR #220 stays HOLD unless this rep reaches that comparison. No product or matrix acceptance is inferred without SUPPORTED plus oracleSatisfied=true." + "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 010 failed closed before VM work because the harness passed an absolute Go package path. Preserve all product, Foundry, observer logic, IPP, DNS-SD, cleanup, and acceptance semantics. The only causal delta is PR #249: build the already-qualified static observer from an explicit repository-root subshell using ./tools/foliorelay-observer. PR #220 remains HOLD unless this rep reaches the DNS-SD TXT UUID comparison. No acceptance is inferred without SUPPORTED plus oracleSatisfied=true." } From 13fe17fcfc134e2ef74b9e76c8f436861078f7ad Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:09:26 +0200 Subject: [PATCH 354/719] RDTE: compare DNS-SD TXT against bare UUID projection --- tools/foliorelay-observer/main.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tools/foliorelay-observer/main.go b/tools/foliorelay-observer/main.go index 5e1440b8d..56f49714e 100644 --- a/tools/foliorelay-observer/main.go +++ b/tools/foliorelay-observer/main.go @@ -165,17 +165,24 @@ func parsePacket(pkt []byte) (observation, error) { return o, nil } -func qualifying(o observation, expected, expectedHost string, expectedPort int) bool { +func qualifying(o observation, expectedTxtUUID, expectedHost string, expectedPort int) bool { joined := strings.ToLower(strings.Join(o.txt, "\n")) + uuidMatch := false + for _, item := range o.txt { + if strings.EqualFold(item, "UUID="+expectedTxtUUID) { + uuidMatch = true + break + } + } return o.universal && - strings.Contains(joined, strings.ToLower(expected)) && + uuidMatch && strings.Contains(joined, "rp=printers/foliorelay") && strings.Contains(joined, "pdl=application/pdf,image/urf") && strings.EqualFold(strings.TrimSuffix(o.srvTarget, "."), strings.TrimSuffix(expectedHost, ".")) && o.srvPort == expectedPort } -func observe(expected, expectedHost string, expectedPort int, seconds float64) (map[string]any, error) { +func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, seconds float64) (map[string]any, error) { fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) if err != nil { return nil, err @@ -219,7 +226,7 @@ func observe(expected, expectedHost string, expectedPort int, seconds float64) ( if err != nil { continue } - if qualifying(o, expected, expectedHost, expectedPort) { + if qualifying(o, expectedTxtUUID, expectedHost, expectedPort) { return map[string]any{ "universal_ptr": true, "uuid": expected, @@ -234,19 +241,20 @@ func observe(expected, expectedHost string, expectedPort int, seconds float64) ( func main() { uuid := flag.String("uuid", "", "expected canonical printer UUID") + txtUUID := flag.String("txt-uuid", "", "expected bare DNS-SD TXT UUID") host := flag.String("expected-host", "", "expected DNS-SD SRV host") ippPort := flag.Int("expected-ipp-port", 0, "expected DNS-SD SRV port") port := flag.Int("port", 18081, "HTTP witness port") seconds := flag.Float64("seconds", 25, "mDNS observation window") flag.Parse() - if *uuid == "" || *host == "" || *ippPort <= 0 { - fmt.Fprintln(os.Stderr, "uuid, expected-host, and expected-ipp-port are required") + if *uuid == "" || *txtUUID == "" || *host == "" || *ippPort <= 0 { + fmt.Fprintln(os.Stderr, "uuid, txt-uuid, expected-host, and expected-ipp-port are required") os.Exit(2) } state := &resultState{v: map[string]any{"status": "pending"}} go func() { - r, err := observe(*uuid, *host, *ippPort, *seconds) + r, err := observe(*uuid, *txtUUID, *host, *ippPort, *seconds) if err != nil { state.set(map[string]any{"status": "error", "error": fmt.Sprintf("%T: %v", err, err)}) return From a320c840fe9c93584ed282c1357bda9e85141804 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:09:31 +0200 Subject: [PATCH 355/719] test: lock bare TXT UUID semantics --- tools/foliorelay-observer/main_test.go | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tools/foliorelay-observer/main_test.go b/tools/foliorelay-observer/main_test.go index 885c359da..523fa6891 100644 --- a/tools/foliorelay-observer/main_test.go +++ b/tools/foliorelay-observer/main_test.go @@ -30,14 +30,14 @@ func TestQualifying(t *testing.T) { o := observation{ universal: true, txt: []string{ - "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", + "UUID=01234567-89ab-4def-8123-456789abcdef", "rp=printers/FolioRelay", "pdl=application/pdf,image/urf", }, srvTarget: "foliorelay-t6.local.", srvPort: 8634, } - if !qualifying(o, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + if !qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { t.Fatal("expected qualifying observation") } } @@ -76,7 +76,7 @@ func TestParsePacket(t *testing.T) { o, err := parsePacket(pkt) if err != nil { t.Fatal(err) } - if !qualifying(o, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + if !qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { t.Fatalf("not qualifying: %+v", o) } } @@ -125,3 +125,19 @@ func TestParsePacketRejectsMalformedInput(t *testing.T) { t.Fatal("expected DNS compression pointer loop error") } } + +func TestQualifyingRejectsCanonicalURNInTXT(t *testing.T) { + o := observation{ + universal: true, + txt: []string{ + "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + }, + srvTarget: "foliorelay-t6.local.", + srvPort: 8634, + } + if qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatal("canonical URN must not satisfy bare TXT UUID projection") + } +} From 31301bdda3533d1ab35b7ea6dc38f041712a6880 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:09:37 +0200 Subject: [PATCH 356/719] RDTE: project canonical UUID to DNS-SD TXT form --- scripts/truenas_middleware_foliorelay_t6_probe.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 0b8755ce1..e535594f3 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -232,6 +232,15 @@ def extract_printer_uri(attrs: str): match = re.search(r"printer-uri-supported[^\n]*= (ipp://[^\s]+)", attrs) return match.group(1) if match else None +def dnssd_txt_uuid(printer_uuid: str) -> str: + prefix="urn:uuid:" + if not isinstance(printer_uuid,str) or not printer_uuid.lower().startswith(prefix): + raise RuntimeError("canonical printer UUID is not urn:uuid form") + value=printer_uuid[len(prefix):] + if not value: + raise RuntimeError("canonical printer UUID is empty") + return value + def forwarded_ipp_uri_has_product_path(uri: str | None) -> bool: if not uri or not uri.startswith("ipp://"): return False @@ -496,7 +505,7 @@ def wait_state(name,state): blob=http_bytes(a.host,a.control_port,f"/api/v1/jobs/{matches[0]['job_id']}/artifact",tok,timeout=a.timeout) if sha256_bytes(blob)!=sha: raise RuntimeError(f"{media} downloaded artifact drifted") sj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,OBSERVER_DIR+"/foliorelay-observer",observer_bytes,0o555,a.timeout); wait_job(sj,"observer upload") - obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"user":"65534:65534","cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR+"/foliorelay-observer","target":"/observer/foliorelay-observer","read_only":True}],"entrypoint":["/observer/foliorelay-observer"],"command":["--uuid",uuid,"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} + obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"user":"65534:65534","cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR+"/foliorelay-observer","target":"/observer/foliorelay-observer","read_only":True}],"entrypoint":["/observer/foliorelay-observer"],"command":["--uuid",uuid,"--txt-uuid",dnssd_txt_uuid(uuid),"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} oj=call("app.create",[{"app_name":OBSERVER_APP_NAME,"custom_app":True,"custom_compose_config":obs_compose}]) if not isinstance(oj,int): raise RuntimeError("observer app.create did not return job") observer_created=True From 0c027ffd91acc78b04c95b8c68eec0fcb7fe07de Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:09:42 +0200 Subject: [PATCH 357/719] RDTE: smoke bare DNS-SD TXT UUID input --- .github/workflows/prep-foliorelay-t6-consumer.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index ad783022f..8cb732f86 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -91,6 +91,7 @@ jobs: --entrypoint /observer/foliorelay-observer \ "$carrier" \ --uuid urn:uuid:01234567-89ab-4def-8123-456789abcdef \ + --txt-uuid 01234567-89ab-4def-8123-456789abcdef \ --expected-host intentionally-absent.local \ --expected-ipp-port 8634 \ --port "$port" \ From e2edd9ecdabe6460795286ee7db9d650eadc0639 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:09:46 +0200 Subject: [PATCH 358/719] test: bind canonical and TXT UUID representations --- tests/test_truenas_t6_foliorelay_control.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 0e3a0206c..117dff257 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -155,6 +155,19 @@ def test_cups_uuid_oracle_preserves_canonical_urn_prefix(self): ) self.assertIsNone(MOD.extract_printer_uuid("printer-name = FolioRelay")) + def test_dnssd_txt_uuid_projection_strips_only_canonical_urn_prefix(self): + canonical="urn:uuid:01234567-89ab-4def-8123-456789abcdef" + self.assertEqual(MOD.dnssd_txt_uuid(canonical), "01234567-89ab-4def-8123-456789abcdef") + with self.assertRaisesRegex(RuntimeError, "urn:uuid form"): + MOD.dnssd_txt_uuid("01234567-89ab-4def-8123-456789abcdef") + probe = SCRIPT.read_text(encoding="utf-8") + observer = (ROOT/"tools"/"foliorelay-observer"/"main.go").read_text(encoding="utf-8") + prep = (ROOT/".github"/"workflows"/"prep-foliorelay-t6-consumer.yml").read_text(encoding="utf-8") + self.assertIn('"--txt-uuid",dnssd_txt_uuid(uuid)', probe) + self.assertIn('flag.String("txt-uuid"', observer) + self.assertIn('"UUID="+expectedTxtUUID', observer) + self.assertIn('--txt-uuid 01234567-89ab-4def-8123-456789abcdef', prep) + def test_forwarded_ipp_uri_preserves_product_resource_path_without_claiming_public_host(self): attrs = """ printer-uri-supported (uri) = ipp://127.0.0.1:48634/printers/FolioRelay From 3a114804659d6c5a52152390be2fed15afd6d886 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:13:43 +0200 Subject: [PATCH 359/719] test: use bare TXT UUID in packet fixture --- tools/foliorelay-observer/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/foliorelay-observer/main_test.go b/tools/foliorelay-observer/main_test.go index 523fa6891..4f5b2b14e 100644 --- a/tools/foliorelay-observer/main_test.go +++ b/tools/foliorelay-observer/main_test.go @@ -61,7 +61,7 @@ func TestParsePacket(t *testing.T) { appendRR(queryName, 12, encodeName("FolioRelay._ipp._tcp.local")) txt := []byte{} for _, s := range []string{ - "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", + "UUID=01234567-89ab-4def-8123-456789abcdef", "rp=printers/FolioRelay", "pdl=application/pdf,image/urf", } { From a924064978580300c6afa8243625c8e27e4e2f90 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:14:11 +0200 Subject: [PATCH 360/719] test: make DNS-SD negative vectors causally independent --- tools/foliorelay-observer/main_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/foliorelay-observer/main_test.go b/tools/foliorelay-observer/main_test.go index 4f5b2b14e..4d321ece6 100644 --- a/tools/foliorelay-observer/main_test.go +++ b/tools/foliorelay-observer/main_test.go @@ -86,7 +86,7 @@ func TestQualifyingRejectsCrossedIdentity(t *testing.T) { base := observation{ universal: true, txt: []string{ - "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", + "UUID=01234567-89ab-4def-8123-456789abcdef", "rp=printers/FolioRelay", "pdl=application/pdf,image/urf", }, @@ -100,10 +100,10 @@ func TestQualifyingRejectsCrossedIdentity(t *testing.T) { host string port int }{ - {"missing-universal", observation{txt: base.txt, srvTarget: base.srvTarget, srvPort: base.srvPort}, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634}, - {"wrong-uuid", base, "urn:uuid:ffffffff-ffff-4fff-8fff-ffffffffffff", "foliorelay-t6.local", 8634}, - {"wrong-host", base, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "other.local", 8634}, - {"wrong-port", base, "urn:uuid:01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 9999}, + {"missing-universal", observation{txt: base.txt, srvTarget: base.srvTarget, srvPort: base.srvPort}, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634}, + {"wrong-uuid", base, "ffffffff-ffff-4fff-8fff-ffffffffffff", "foliorelay-t6.local", 8634}, + {"wrong-host", base, "01234567-89ab-4def-8123-456789abcdef", "other.local", 8634}, + {"wrong-port", base, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 9999}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { From c26553016936aef3f7218fe488f1e4d6bc74d813 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:48:18 +0200 Subject: [PATCH 361/719] RDTE: request FolioRelay BETA.3 T6 rep 012 TXT UUID projection --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3eb0f9c9e..e1456337e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-011-static-observer-build", + "request_id": "tn-26-beta3-t6-foliorelay-012-txt-uuid-projection", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 010 failed closed before VM work because the harness passed an absolute Go package path. Preserve all product, Foundry, observer logic, IPP, DNS-SD, cleanup, and acceptance semantics. The only causal delta is PR #249: build the already-qualified static observer from an explicit repository-root subshell using ./tools/foliorelay-observer. PR #220 remains HOLD unless this rep reaches the DNS-SD TXT UUID comparison. No acceptance is inferred without SUPPORTED plus oracleSatisfied=true." + "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 011 reached the DNS-SD oracle with healthy substrate/product lifecycle and zero residue but rejected the advertisement because the external observer compared the canonical urn:uuid identity against the product's intentionally bare DNS-SD TXT UUID projection. Preserve product, Foundry, image, Compose, IPP, lifecycle, cleanup, and acceptance semantics. The only causal delta is PR #253: keep canonical UUID comparison for control/CUPS identity while passing the derived bare UUID only to DNS-SD TXT matching. Service-instance correlation hardening remains separate in PR #256. No acceptance is inferred without SUPPORTED plus oracleSatisfied=true." } From 1f2c5a26190ad7a3534761f210d9b9dfe967f56c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:52:44 +0200 Subject: [PATCH 362/719] RDTE: restore Proxmox REST compute materializer on current base --- scripts/proxmox_rest_compute_probe.py | 351 ++++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 scripts/proxmox_rest_compute_probe.py diff --git a/scripts/proxmox_rest_compute_probe.py b/scripts/proxmox_rest_compute_probe.py new file mode 100644 index 000000000..a2cb62532 --- /dev/null +++ b/scripts/proxmox_rest_compute_probe.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import ssl +import time +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass +from typing import Any + + +class ProxmoxProbeError(RuntimeError): + pass + +PVE_CONTAINER_SOURCE = { + "repository": "proxmox/pve-container", + "commit": "5eb5574ee9158ac40a5230de2cf18d7d6345709f", + "lxc_api_blob": "88067ddebf5a0bd540f91ffd1db11f8eec40cefb", + "lxc_status_api_blob": "c95fc3c7732d637639ab29e496bc27eafb9d222d", + "package_version": "6.1.10", +} +PVE_QEMU_PACKAGE_SOURCE = { + "repository": "proxmox/pve-qemu", + "commit": "684796e835289dab11af8606fbf7358b93526dd6", + "package_version": "11.0.0-3", +} +PVE_MANAGER_API_SOURCE = { + "repository": "proxmox/pve-manager", + "commit": "b9984c6d90a4bd80", + "apt_api_blob": "9cb6e473436719f3024ac09fffaad8faf0d7160d", + "manager_version": "9.2.2", +} + + + +@dataclass +class Response: + status: int + data: Any + + +class PveApi: + def __init__(self, base_url: str, username: str, password: str, verify_tls: bool = False, timeout: float = 10.0): + self.base_url = base_url.rstrip("/") + self.username = username + self.password = password + self.timeout = timeout + self.ctx = ssl.create_default_context() + if not verify_tls: + self.ctx.check_hostname = False + self.ctx.verify_mode = ssl.CERT_NONE + self.ticket: str | None = None + self.csrf: str | None = None + + def _request(self, method: str, path: str, fields: dict[str, Any] | None = None) -> Response: + url = self.base_url + "/api2/json" + path + body = None + headers = {"Accept": "application/json"} + if fields is not None: + body = urllib.parse.urlencode({k: str(v) for k, v in fields.items()}).encode() + headers["Content-Type"] = "application/x-www-form-urlencoded" + if self.ticket: + headers["Cookie"] = f"PVEAuthCookie={self.ticket}" + if self.csrf and method not in {"GET", "HEAD"}: + headers["CSRFPreventionToken"] = self.csrf + req = urllib.request.Request(url, data=body, method=method, headers=headers) + try: + with urllib.request.urlopen(req, context=self.ctx, timeout=self.timeout) as r: + raw = r.read() + payload = json.loads(raw.decode()) if raw else {} + return Response(r.status, payload.get("data")) + except urllib.error.HTTPError as exc: + raw = exc.read().decode(errors="replace") + raise ProxmoxProbeError(f"{method} {path}: HTTP {exc.code}: {raw}") from exc + + def login(self) -> None: + r = self._request("POST", "/access/ticket", {"username": self.username, "password": self.password}) + if not isinstance(r.data, dict) or not r.data.get("ticket") or not r.data.get("CSRFPreventionToken"): + raise ProxmoxProbeError("ticket response missing ticket/CSRF token") + self.ticket = r.data["ticket"] + self.csrf = r.data["CSRFPreventionToken"] + + def get(self, path: str) -> Any: + return self._request("GET", path).data + + def post(self, path: str, fields: dict[str, Any] | None = None) -> Any: + return self._request("POST", path, fields or {}).data + + def put(self, path: str, fields: dict[str, Any] | None = None) -> Any: + return self._request("PUT", path, fields or {}).data + + def delete(self, path: str, fields: dict[str, Any] | None = None) -> Any: + return self._request("DELETE", path, fields or {}).data + + +def wait_task(api: PveApi, node: str, upid: str, timeout: float = 180.0) -> dict[str, Any]: + deadline = time.monotonic() + timeout + encoded = urllib.parse.quote(upid, safe="") + last: dict[str, Any] = {} + while time.monotonic() < deadline: + data = api.get(f"/nodes/{node}/tasks/{encoded}/status") + if isinstance(data, dict): + last = data + if data.get("status") == "stopped": + if data.get("exitstatus") != "OK": + raise ProxmoxProbeError(f"task failed: {data!r}") + return data + time.sleep(1) + raise ProxmoxProbeError(f"task timeout: {last!r}") + + +def node_name(api: PveApi) -> str: + rows = api.get("/nodes") + if not isinstance(rows, list) or len(rows) != 1 or not isinstance(rows[0].get("node"), str): + raise ProxmoxProbeError(f"expected one disposable PVE node: {rows!r}") + return rows[0]["node"] + + +def lxc_create_fields(vmid: int, template: str) -> dict[str, Any]: + return { + "vmid": vmid, + "hostname": f"rdte-lxc-{vmid}", + "ostemplate": template, + "rootfs": "local-lvm:2", + "memory": 256, + "cores": 1, + "unprivileged": 1, + "start": 0, + "description": "Agent Dispatch disposable REST C0 container", + } + + +def vm_create_fields(vmid: int, *, memory: int = 1024) -> dict[str, Any]: + return { + "vmid": vmid, + "name": f"rdte-vm-{vmid}", + "memory": memory, + "cores": 1, + "sockets": 1, + "cpu": "host", + "bios": "ovmf", + "machine": "q35", + "scsihw": "virtio-scsi-pci", + "net0": "virtio,bridge=vmbr0", + "ostype": "l26", + "start": 0, + "description": "Agent Dispatch disposable REST V0 VM shell", + } + + +def plan_only(kind: str, vmid: int, template: str | None) -> dict[str, Any]: + if kind == "container": + if not template: + raise ProxmoxProbeError("--template is required for container plan") + create = lxc_create_fields(vmid, template) + path = f"/nodes/{{node}}/lxc" + elif kind == "vm": + create = vm_create_fields(vmid) + path = f"/nodes/{{node}}/qemu" + else: + raise ProxmoxProbeError(f"unsupported kind {kind!r}") + return { + "schema": "proxmox-rest-compute-plan/v1", + "kind": kind, + "create_path": path, + "create_fields": create, + "lifecycle": ["observe-absent", "create", "readback", "start", "stop", "delete", "absence"], + "claim_boundary": "plan only; guest oracle and runtime qualification are separate", + } + + +def main() -> int: + p=argparse.ArgumentParser() + p.add_argument("--base-url", default="https://127.0.0.1:8006") + p.add_argument("--username", default="root@pam") + p.add_argument("--password-file", type=pathlib.Path) + p.add_argument("--verify-tls", action="store_true") + p.add_argument("--kind", choices=["container","vm"], required=True) + p.add_argument("--vmid", type=int, default=9101) + p.add_argument("--template") + p.add_argument("--observe-only", action="store_true") + p.add_argument("--expected-qemu-server-version") + p.add_argument("--apply", action="store_true") + p.add_argument("--out", type=pathlib.Path) + a=p.parse_args() + + receipt: dict[str,Any] = { + "schema":"proxmox-rest-compute-c0-v1", + "kind":a.kind, + "vmid":a.vmid, + "classification":"ORACLE_FAILURE", + "oracleSatisfied":False, + "api_materialization_oracle":False, + "guest_oracle":False, + "claim_boundary":"C0/V0 product REST lifecycle only; guest workload oracle remains separate", + "cleanup":{"attempted":False,"absent":False}, + "tasks":{}, + "source_contract":{ + "pve_container":PVE_CONTAINER_SOURCE, + "pve_qemu_package":PVE_QEMU_PACKAGE_SOURCE, + "pve_manager_api":PVE_MANAGER_API_SOURCE, + "qemu_server_api":"OPEN: exact installed qemu-server package/source identity must be observed and admitted before VM apply", + }, + } + def emit(code:int)->int: + payload=json.dumps(receipt,indent=2,sort_keys=True)+"\n" + if a.out: a.out.write_text(payload,encoding="utf-8") + print(payload,end="") + return code + + api: PveApi | None = None + node: str | None = None + base: str | None = None + ownership_claimed = False + + try: + plan=plan_only(a.kind,a.vmid,a.template) + receipt["plan"]=plan + if not a.apply and not a.observe_only: + receipt.update({"classification":"SUPPORTED","oracleSatisfied":True,"phase":"plan-only"}) + return emit(0) + if not a.password_file: + raise ProxmoxProbeError("--apply/--observe-only requires --password-file") + password=a.password_file.read_text(encoding="utf-8").strip() + if not password: + raise ProxmoxProbeError("password file empty") + api=PveApi(a.base_url,a.username,password,a.verify_tls) + api.login() + version=api.get("/version") + receipt["pve_version"]=version + node=node_name(api) + receipt["node"]=node + try: + package_rows=api.get(f"/nodes/{node}/apt/versions") + except ProxmoxProbeError as package_exc: + package_rows=[] + receipt["package_census_error"]=str(package_exc) + receipt["package_census"]=[ + x for x in (package_rows or []) + if isinstance(x,dict) and x.get("Package") in {"pve-container","pve-qemu-kvm","qemu-server","pve-manager"} + ] + if a.observe_only: + receipt.update({ + "classification":"SUPPORTED", + "oracleSatisfied":True, + "phase":"observe-only", + "detail":"authenticated PVE API/version/package census completed without compute mutation", + }) + return emit(0) + if a.kind=="vm" and not a.expected_qemu_server_version: + raise ProxmoxProbeError( + "VM apply blocked: exact qemu-server API package/source identity is not admitted; " + "run --observe-only and bind the observed package before mutation" + ) + if a.kind=="vm": + observed_qemu=[ + x for x in receipt["package_census"] + if x.get("Package")=="qemu-server" + ] + installed_versions={ + str(x.get("OldVersion")) for x in observed_qemu + if x.get("CurrentState")=="Installed" and x.get("OldVersion") + } + available_versions={str(x.get("Version")) for x in observed_qemu if x.get("Version")} + receipt["qemu_server_package_observation"]={ + "installed_versions":sorted(installed_versions), + "available_versions":sorted(available_versions), + } + if a.expected_qemu_server_version not in installed_versions: + raise ProxmoxProbeError( + f"VM apply blocked: expected installed qemu-server {a.expected_qemu_server_version!r}, " + f"observed installed {sorted(installed_versions)!r}; available={sorted(available_versions)!r}" + ) + base=f"/nodes/{node}/{'lxc' if a.kind=='container' else 'qemu'}" + rows=api.get(base) + if any(int(x.get("vmid",-1))==a.vmid for x in (rows or [])): + raise ProxmoxProbeError("preexisting vmid blocks ownership-safe apply") + fields=plan["create_fields"] + upid=api.post(base,fields) + if not isinstance(upid,str): raise ProxmoxProbeError(f"create did not return UPID: {upid!r}") + ownership_claimed=True + receipt["tasks"]["create"]=wait_task(api,node,upid) + cfg=api.get(f"{base}/{a.vmid}/config") + if not isinstance(cfg,dict): raise ProxmoxProbeError("config readback missing") + receipt["config_readback"]=cfg + upid=api.post(f"{base}/{a.vmid}/status/start") + if not isinstance(upid,str): raise ProxmoxProbeError("start did not return UPID") + receipt["tasks"]["start"]=wait_task(api,node,upid) + status=api.get(f"{base}/{a.vmid}/status/current") + if not isinstance(status,dict) or status.get("status")!="running": + raise ProxmoxProbeError(f"instance not running after start: {status!r}") + upid=api.post(f"{base}/{a.vmid}/status/stop") + if not isinstance(upid,str): raise ProxmoxProbeError("stop did not return UPID") + receipt["tasks"]["stop"]=wait_task(api,node,upid) + receipt["cleanup"]["attempted"]=True + upid=api.delete(f"{base}/{a.vmid}",{"purge":1}) + if not isinstance(upid,str): raise ProxmoxProbeError("delete did not return UPID") + receipt["tasks"]["delete"]=wait_task(api,node,upid) + rows=api.get(base) + absent=not any(int(x.get("vmid",-1))==a.vmid for x in (rows or [])) + receipt["cleanup"]["absent"]=absent + if not absent: raise ProxmoxProbeError("instance remained after REST delete") + receipt.update({ + "classification":"SUPPORTED", + "oracleSatisfied":True, + "api_materialization_oracle":True, + "phase":"rest-api-lifecycle", + "detail":"PVE REST create/readback/start/stop/delete/absence passed", + }) + return emit(0) + except (OSError,ValueError,ProxmoxProbeError) as exc: + receipt["detail"]=f"{type(exc).__name__}: {exc}" + if a.apply and ownership_claimed and api is not None and node is not None and base is not None: + receipt["cleanup"]["attempted"]=True + cleanup_errors=[] + try: + rows=api.get(base) + present=any(int(x.get("vmid",-1))==a.vmid for x in (rows or [])) + if present: + try: + status=api.get(f"{base}/{a.vmid}/status/current") + except ProxmoxProbeError: + status={} + if isinstance(status,dict) and status.get("status")=="running": + stop_upid=api.post(f"{base}/{a.vmid}/status/stop") + if isinstance(stop_upid,str): + receipt["tasks"]["failure_cleanup_stop"]=wait_task(api,node,stop_upid,timeout=60.0) + else: + cleanup_errors.append(f"failure cleanup stop did not return UPID: {stop_upid!r}") + delete_upid=api.delete(f"{base}/{a.vmid}",{"purge":1}) + if isinstance(delete_upid,str): + receipt["tasks"]["failure_cleanup_delete"]=wait_task(api,node,delete_upid,timeout=60.0) + else: + cleanup_errors.append(f"failure cleanup delete did not return UPID: {delete_upid!r}") + rows=api.get(base) + receipt["cleanup"]["absent"]=not any( + int(x.get("vmid",-1))==a.vmid for x in (rows or []) + ) + except (OSError,ValueError,ProxmoxProbeError) as cleanup_exc: + cleanup_errors.append(f"{type(cleanup_exc).__name__}: {cleanup_exc}") + if cleanup_errors: + receipt["cleanup"]["errors"]=cleanup_errors + return emit(2) + + +if __name__=="__main__": + raise SystemExit(main()) From d30ce344f090d096dc9e572cdc983874d1067c73 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:52:47 +0200 Subject: [PATCH 363/719] RDTE: restore Proxmox REST materializer contract tests --- tests/test_proxmox_rest_compute_probe.py | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_proxmox_rest_compute_probe.py diff --git a/tests/test_proxmox_rest_compute_probe.py b/tests/test_proxmox_rest_compute_probe.py new file mode 100644 index 000000000..845d1916d --- /dev/null +++ b/tests/test_proxmox_rest_compute_probe.py @@ -0,0 +1,50 @@ +import pathlib +import unittest +from scripts.proxmox_rest_compute_probe import ( + PVE_CONTAINER_SOURCE, PVE_MANAGER_API_SOURCE, PVE_QEMU_PACKAGE_SOURCE, + ProxmoxProbeError, lxc_create_fields, plan_only, vm_create_fields +) + +class ProxmoxRestComputeContractTests(unittest.TestCase): + def test_exact_container_source_is_bound(self): + self.assertEqual(PVE_CONTAINER_SOURCE["commit"],"5eb5574ee9158ac40a5230de2cf18d7d6345709f") + self.assertEqual(PVE_CONTAINER_SOURCE["package_version"],"6.1.10") + self.assertEqual(PVE_QEMU_PACKAGE_SOURCE["commit"],"684796e835289dab11af8606fbf7358b93526dd6") + self.assertEqual(PVE_MANAGER_API_SOURCE["commit"],"b9984c6d90a4bd80") + self.assertEqual(PVE_MANAGER_API_SOURCE["apt_api_blob"],"9cb6e473436719f3024ac09fffaad8faf0d7160d") + + def test_lxc_plan_uses_rest_and_unprivileged_container(self): + p=plan_only("container",9101,"local:vztmpl/debian.tar.zst") + self.assertEqual(p["create_path"],"/nodes/{node}/lxc") + self.assertEqual(p["create_fields"]["unprivileged"],1) + self.assertEqual(p["create_fields"]["start"],0) + self.assertNotIn("pct",str(p)) + + def test_vm_plan_uses_rest_and_native_vm_shape(self): + p=plan_only("vm",9201,None) + self.assertEqual(p["create_path"],"/nodes/{node}/qemu") + self.assertEqual(p["create_fields"]["bios"],"ovmf") + self.assertEqual(p["create_fields"]["machine"],"q35") + self.assertEqual(p["create_fields"]["cpu"],"host") + self.assertNotIn("qm ",str(p)) + + def test_container_requires_exact_template_identity(self): + with self.assertRaises(ProxmoxProbeError): + plan_only("container",9101,None) + + def test_vm_payload_does_not_smuggle_media_or_key(self): + f=vm_create_fields(9201) + self.assertNotIn("ide2",f) + self.assertNotIn("password",f) + self.assertNotIn("ciuser",f) + + def test_apply_failure_path_has_owned_resource_cleanup(self): + text=pathlib.Path("scripts/proxmox_rest_compute_probe.py").read_text(encoding="utf-8") + self.assertIn("ownership_claimed=True",text) + self.assertIn('failure_cleanup_stop',text) + self.assertIn('failure_cleanup_delete',text) + self.assertIn('{"purge":1}',text) + self.assertIn('receipt["cleanup"]["absent"]',text) + +if __name__=="__main__": + unittest.main() From bb450b9ee909dfcd26178a9c0d901cd510e00c6c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:52:49 +0200 Subject: [PATCH 364/719] RDTE: validate Proxmox REST materializer on current base --- .../workflows/validate-compute-materialization-contract.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-compute-materialization-contract.yml b/.github/workflows/validate-compute-materialization-contract.yml index 5b265d29b..dd521f207 100644 --- a/.github/workflows/validate-compute-materialization-contract.yml +++ b/.github/workflows/validate-compute-materialization-contract.yml @@ -10,6 +10,8 @@ on: - "tests/test_truenas_compute_container_probe.py" - "scripts/truenas_compute_vm_probe.py" - "tests/test_truenas_compute_vm_probe.py" + - "scripts/proxmox_rest_compute_probe.py" + - "tests/test_proxmox_rest_compute_probe.py" - "docs/compute-materialization-contract.md" - ".github/workflows/validate-compute-materialization-contract.yml" workflow_dispatch: @@ -23,6 +25,6 @@ jobs: steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - name: Contract tests - run: python3 -m unittest -v tests/test_compute_materialization_contract.py tests/test_truenas_compute_container_probe.py tests/test_truenas_compute_vm_probe.py + run: python3 -m unittest -v tests/test_compute_materialization_contract.py tests/test_truenas_compute_container_probe.py tests/test_truenas_compute_vm_probe.py tests/test_proxmox_rest_compute_probe.py - name: Validate exact target profiles run: python3 scripts/compute_materialization_contract.py --validate-only From 09cf9babd767786cb7f32f98a53b437505679ae6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:53:35 +0200 Subject: [PATCH 365/719] RDTE: refresh TrueNAS VM V0 compute-materialization-targets.json on current base --- config/compute-materialization-targets.json | 83 +++++++++++++++++---- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/config/compute-materialization-targets.json b/config/compute-materialization-targets.json index 5bd3b06f2..fc9699c64 100644 --- a/config/compute-materialization-targets.json +++ b/config/compute-materialization-targets.json @@ -20,7 +20,8 @@ "virt-incus" ], "source_fingerprint": { - "virt.instance": "87fc6bb365b3b09820331d90ed2c0138af41ce1b" + "virt.instance": "87fc6bb365b3b09820331d90ed2c0138af41ce1b", + "virt.instance_device": "96e68a484762388563384921738d1c7e547f4c6b" }, "container_adapters": [ { @@ -51,18 +52,28 @@ "virt.instance.delete", "virt.instance.start", "virt.instance.stop", - "virt.instance.restart" + "virt.instance.restart", + "virt.instance.device_list", + "virt.instance.device_add", + "virt.instance.device_update", + "virt.instance.device_delete", + "virt.device.nic_choices" ], "guest_oracle": "network-or-vnc/serial-capability", "restart_semantics": "native", "hypervisor_family": "incus-qemu", - "source_api_family": "v25_04_0" + "source_api_family": "v25_04_0", + "v0_device_ownership": "named-observed-parent-macvlan-nic" } ], "api_schema_fingerprint": { "virt_instance": { "path": "src/middlewared/middlewared/api/v25_04_0/virt_instance.py", "blob_sha": "f5caf3f2f98f8ce43fafc19ecccbd1aee1c7c6e0" + }, + "virt_device": { + "path": "src/middlewared/middlewared/api/v25_04_0/virt_device.py", + "blob_sha": "d6d783d3b0184544f888fcc91de68d12a5a708ba" } } }, @@ -77,7 +88,9 @@ "virt.instance": "ac22db500d9d1c6932a4cc109716cd8a3bd46223", "vm.vms": "993ed915c7058c8e8433d5d5592bb30f24cbfd1c", "vm.lifecycle": "85c5049c8c90572746ea1acf0a357a88afc88cb5", - "vm.device": "b98466b2247ed2460436c7d9329780f2c88d16ec" + "vm.device": "b98466b2247ed2460436c7d9329780f2c88d16ec", + "virt.instance_device": "96e68a484762388563384921738d1c7e547f4c6b", + "vm.info": "f6a267da921101373c620faf602f39ee83b3f527" }, "container_adapters": [ { @@ -109,7 +122,9 @@ "vm.status", "vm.start", "vm.stop", - "vm.restart" + "vm.restart", + "vm.virtualization_details", + "pool.dataset.query" ], "guest_oracle": "network-or-console-capability", "restart_semantics": "native", @@ -121,7 +136,10 @@ "vm.device.update", "vm.device.delete" ], - "v0_storage_ownership": "exact-owned-zvol-device" + "v0_storage_ownership": "exact-owned-zvol-device", + "read_only_bootstrap_methods": [ + "vm.license_active" + ] }, { "id": "truenas-virt-incus-vm", @@ -134,12 +152,18 @@ "virt.instance.delete", "virt.instance.start", "virt.instance.stop", - "virt.instance.restart" + "virt.instance.restart", + "virt.instance.device_list", + "virt.instance.device_add", + "virt.instance.device_update", + "virt.instance.device_delete", + "virt.device.nic_choices" ], "guest_oracle": "network-or-vnc/serial-capability", "restart_semantics": "native", "hypervisor_family": "incus-qemu", - "source_api_family": "v25_04_2" + "source_api_family": "v25_04_2", + "v0_device_ownership": "named-observed-parent-macvlan-nic" } ], "api_schema_fingerprint": { @@ -154,6 +178,10 @@ "vm_device": { "path": "src/middlewared/middlewared/api/v25_04_2/vm_device.py", "blob_sha": "f85240d8a804b5d8347bc9a51d76c9c93c19a813" + }, + "virt_device": { + "path": "src/middlewared/middlewared/api/v25_04_2/virt_device.py", + "blob_sha": "d6d783d3b0184544f888fcc91de68d12a5a708ba" } } }, @@ -168,7 +196,9 @@ "virt.instance": "8f3f21996b4d14385ba17405676704cb9b921bea", "vm.vms": "2532f53d54d845e983c76e8479027587dec81df7", "vm.lifecycle": "85c5049c8c90572746ea1acf0a357a88afc88cb5", - "vm.device": "7d9b0098692190015fc178d25d8483927b90d757" + "vm.device": "7d9b0098692190015fc178d25d8483927b90d757", + "virt.instance_device": "3dca6a0cfe02ca6487dfb1f0118b149d5d3d8103", + "vm.info": "0769a88d67169175bad6868697a9bffabac30f97" }, "container_adapters": [ { @@ -200,7 +230,9 @@ "vm.status", "vm.start", "vm.stop", - "vm.restart" + "vm.restart", + "vm.virtualization_details", + "pool.dataset.query" ], "guest_oracle": "network-or-console-capability", "restart_semantics": "native", @@ -212,7 +244,10 @@ "vm.device.update", "vm.device.delete" ], - "v0_storage_ownership": "exact-owned-zvol-device" + "v0_storage_ownership": "exact-owned-zvol-device", + "read_only_bootstrap_methods": [ + "vm.license_active" + ] }, { "id": "truenas-virt-incus-vm", @@ -225,12 +260,18 @@ "virt.instance.delete", "virt.instance.start", "virt.instance.stop", - "virt.instance.restart" + "virt.instance.restart", + "virt.instance.device_list", + "virt.instance.device_add", + "virt.instance.device_update", + "virt.instance.device_delete", + "virt.device.nic_choices" ], "guest_oracle": "network-or-vnc/serial-capability", "restart_semantics": "native", "hypervisor_family": "incus-qemu", - "source_api_family": "v25_10_0" + "source_api_family": "v25_10_0", + "v0_device_ownership": "named-observed-parent-macvlan-nic" } ], "api_schema_fingerprint": { @@ -245,6 +286,10 @@ "vm_device": { "path": "src/middlewared/middlewared/api/v25_10_0/vm_device.py", "blob_sha": "a7b8d406e81618db21fb98f2b57b002a9f2d87b7" + }, + "virt_device": { + "path": "src/middlewared/middlewared/api/v25_10_0/virt_device.py", + "blob_sha": "c06d0bffe77854f6e60d8a17221b323804c456f6" } } }, @@ -261,7 +306,8 @@ "container.nsenter": "90b6e245e186441a863a701c0c4714671b691184", "vm.vms": "43ed4b532b40da02ad885039d954c99a93ea22ed", "vm.lifecycle": "b313e2eeb0baa0179ab9244fc244eee9b02e02e9", - "vm.device": "b0f313b060ffec4cf08ec2e44c706bafc5121830" + "vm.device": "b0f313b060ffec4cf08ec2e44c706bafc5121830", + "vm.info": "7a3dd519a011ae0cceeaa2e32dca7f37210a8f22" }, "container_adapters": [ { @@ -292,7 +338,9 @@ "vm.status", "vm.start", "vm.stop", - "vm.restart" + "vm.restart", + "vm.virtualization_details", + "pool.dataset.query" ], "guest_oracle": "network-or-console-capability", "restart_semantics": "native", @@ -304,7 +352,10 @@ "vm.device.update", "vm.device.delete" ], - "v0_storage_ownership": "exact-owned-zvol-device" + "v0_storage_ownership": "exact-owned-zvol-device", + "read_only_bootstrap_methods": [ + "vm.license_active" + ] } ], "api_schema_fingerprint": { From 9f5240a233a7d111f8aceb182ca273f8454cdf35 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:53:38 +0200 Subject: [PATCH 366/719] RDTE: refresh TrueNAS VM V0 truenas_compute_vm_probe.py on current base --- scripts/truenas_compute_vm_probe.py | 139 ++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 9 deletions(-) diff --git a/scripts/truenas_compute_vm_probe.py b/scripts/truenas_compute_vm_probe.py index cd3a3d9ae..14c6e32ee 100644 --- a/scripts/truenas_compute_vm_probe.py +++ b/scripts/truenas_compute_vm_probe.py @@ -82,6 +82,38 @@ def owned_zvol_device(vm_id: int, zvol_name: str, size_bytes: int) -> dict[str, } +def zvol_device_path(zvol_name: str) -> str: + if not zvol_name or "/" not in zvol_name: + raise ProbeError("owned ZVOL name must include a pool/dataset prefix") + return "/dev/zvol/" + zvol_name.replace(" ", "+") + + +def legacy_owned_nic(name: str, parent: str) -> dict[str, Any]: + if not name or not parent: + raise ProbeError("legacy owned NIC requires name and observed parent") + return { + "name": name, + "description": "Agent Dispatch disposable VM V0 owned NIC", + "readonly": False, + "dev_type": "NIC", + "network": None, + "nic_type": "MACVLAN", + "parent": parent, + } + + +def validate_modern_preconditions(details: Any, licensed: Any) -> dict[str, Any]: + if not isinstance(details, dict) or not isinstance(details.get("supported"), bool): + raise ProbeError(f"vm.virtualization_details invalid: {details!r}") + if not isinstance(licensed, bool): + raise ProbeError(f"vm.license_active invalid: {licensed!r}") + if not details["supported"]: + raise ProbeError(f"VM virtualization unavailable: {details.get('error')}") + if not licensed: + raise ProbeError("system is not entitled to use native vm.*") + return {"virtualization_details": details, "license_active": licensed} + + def main() -> int: p=argparse.ArgumentParser() p.add_argument("--registry",type=pathlib.Path,default=pathlib.Path("config/compute-materialization-targets.json")) @@ -110,7 +142,8 @@ def main() -> int: "windows_w1_oracle_satisfied":False, "target_version_requested":a.target_version, "name":a.name, - "cleanup":{"attempted":False,"vm_absent":False,"device_absent":False}, + "cleanup":{"attempted":False,"vm_absent":False,"device_absent":False,"zvol_absent":False}, + "preconditions":{}, "claim_boundary":"V0 native VM/device lifecycle only; Linux guest, Firecracker and Windows 11 remain separate rungs", } ws=None @@ -163,6 +196,19 @@ def _call(method:str,params:list[Any]): return result call=_call + def wait_job(job_id: Any, label: str, timeout: float | None = None): + if not isinstance(job_id, int) or isinstance(job_id, bool): + raise ProbeError(f"{label} did not return job id: {job_id!r}") + deadline=time.monotonic()+(timeout if timeout is not None else a.job_timeout) + while time.monotonic() Date: Mon, 5 Oct 2026 12:53:40 +0200 Subject: [PATCH 367/719] RDTE: refresh TrueNAS VM V0 test_truenas_compute_vm_probe.py on current base --- tests/test_truenas_compute_vm_probe.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_truenas_compute_vm_probe.py b/tests/test_truenas_compute_vm_probe.py index f973bf2b2..9b3aa685b 100644 --- a/tests/test_truenas_compute_vm_probe.py +++ b/tests/test_truenas_compute_vm_probe.py @@ -2,10 +2,13 @@ from scripts.truenas_compute_vm_probe import ( ProbeError, + legacy_owned_nic, legacy_vm_create_payload, native_vm_create_payload, normalize_system_version, owned_zvol_device, + validate_modern_preconditions, + zvol_device_path, ) @@ -44,6 +47,29 @@ def test_zvol_requires_owned_parent_name(self): with self.assertRaises(ProbeError): owned_zvol_device(7,"bare-name",1024) + def test_zvol_readback_path_is_normalized(self): + self.assertEqual( + zvol_device_path("rdtepool/rdte vm v0"), + "/dev/zvol/rdtepool/rdte+vm+v0", + ) + + def test_legacy_owned_nic_uses_observed_parent(self): + d=legacy_owned_nic("rdte-v0-nic","enp1s0") + self.assertEqual(d["name"],"rdte-v0-nic") + self.assertEqual(d["dev_type"],"NIC") + self.assertEqual(d["nic_type"],"MACVLAN") + self.assertEqual(d["parent"],"enp1s0") + + def test_modern_preconditions_require_kvm_and_entitlement(self): + ok=validate_modern_preconditions({"supported":True,"error":None},True) + self.assertTrue(ok["license_active"]) + with self.assertRaises(ProbeError): + validate_modern_preconditions({"supported":False,"error":"no kvm"},True) + with self.assertRaises(ProbeError): + validate_modern_preconditions({"supported":True,"error":None},False) + with self.assertRaises(ProbeError): + validate_modern_preconditions({},True) + if __name__=="__main__": unittest.main() From 8f347fda6aef8790c094a5afeb0b003ac5bbc400 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:31:08 +0200 Subject: [PATCH 368/719] RDTE: bind TrueNAS container image discovery to exact API profiles --- config/compute-materialization-targets.json | 71 ++++++++++++++++++--- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/config/compute-materialization-targets.json b/config/compute-materialization-targets.json index fc9699c64..10f47e8c8 100644 --- a/config/compute-materialization-targets.json +++ b/config/compute-materialization-targets.json @@ -34,11 +34,24 @@ "virt.instance.delete", "virt.instance.start", "virt.instance.stop", - "virt.instance.restart" + "virt.instance.restart", + "virt.instance.image_choices" ], "guest_oracle": "virt.instance.get_shell-or-instance-network", "restart_semantics": "native", - "source_api_family": "v25_04_0" + "source_api_family": "v25_04_0", + "image_discovery": { + "method": "virt.instance.image_choices", + "request": { + "remote": "LINUX_CONTAINERS" + }, + "source": "https://images.linuxcontainers.org", + "selection": { + "alias": "debian/trixie", + "instance_type": "CONTAINER", + "arch": "amd64" + } + } } ], "vm_adapters": [ @@ -103,11 +116,24 @@ "virt.instance.delete", "virt.instance.start", "virt.instance.stop", - "virt.instance.restart" + "virt.instance.restart", + "virt.instance.image_choices" ], "guest_oracle": "virt.instance.get_shell-or-instance-network", "restart_semantics": "native", - "source_api_family": "v25_04_2" + "source_api_family": "v25_04_2", + "image_discovery": { + "method": "virt.instance.image_choices", + "request": { + "remote": "LINUX_CONTAINERS" + }, + "source": "https://images.linuxcontainers.org", + "selection": { + "alias": "debian/trixie", + "instance_type": "CONTAINER", + "arch": "amd64" + } + } } ], "vm_adapters": [ @@ -211,11 +237,24 @@ "virt.instance.delete", "virt.instance.start", "virt.instance.stop", - "virt.instance.restart" + "virt.instance.restart", + "virt.instance.image_choices" ], "guest_oracle": "virt.instance.get_shell-or-instance-network", "restart_semantics": "native", - "source_api_family": "v25_10_0" + "source_api_family": "v25_10_0", + "image_discovery": { + "method": "virt.instance.image_choices", + "request": { + "remote": "LINUX_CONTAINERS" + }, + "source": "https://images.linuxcontainers.org", + "selection": { + "alias": "debian/trixie", + "instance_type": "CONTAINER", + "arch": "amd64" + } + } } ], "vm_adapters": [ @@ -307,7 +346,8 @@ "vm.vms": "43ed4b532b40da02ad885039d954c99a93ea22ed", "vm.lifecycle": "b313e2eeb0baa0179ab9244fc244eee9b02e02e9", "vm.device": "b0f313b060ffec4cf08ec2e44c706bafc5121830", - "vm.info": "7a3dd519a011ae0cceeaa2e32dca7f37210a8f22" + "vm.info": "7a3dd519a011ae0cceeaa2e32dca7f37210a8f22", + "container.image": "43f01e991ceaacd46ca2292cc8cc6d460dc3dc7b" }, "container_adapters": [ { @@ -318,12 +358,21 @@ "container.update", "container.delete", "container.start", - "container.stop" + "container.stop", + "container.image.query_registry" ], "guest_oracle": "supported-shell/namespace-or-instance-network", "restart_semantics": "compose-stop-start", "implementation_family": "lxc", - "source_api_family": "v26_0_0" + "source_api_family": "v26_0_0", + "image_discovery": { + "method": "container.image.query_registry", + "source": "https://images.sys.truenas.net/streams", + "selection": { + "name": "ubuntu:noble:amd64:default", + "version_policy": "last-returned-exact" + } + } } ], "vm_adapters": [ @@ -370,6 +419,10 @@ "vm_device": { "path": "src/middlewared/middlewared/api/v26_0_0/vm_device.py", "blob_sha": "67603b6e2048da6235b802f970559b627857efc9" + }, + "container_image": { + "path": "src/middlewared/middlewared/api/v26_0_0/container_image.py", + "blob_sha": "a8eec804213c011b334ab0ddc9c9a37003a35a31" } } } From e95de71f302da68d18d8ab4c1ccb03660630a03b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:31:17 +0200 Subject: [PATCH 369/719] RDTE: fail closed on missing container image discovery contract --- scripts/compute_materialization_contract.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/compute_materialization_contract.py b/scripts/compute_materialization_contract.py index 0ed40d3b6..29befa423 100644 --- a/scripts/compute_materialization_contract.py +++ b/scripts/compute_materialization_contract.py @@ -50,6 +50,17 @@ def validate_adapter(adapter: dict[str, Any], platform: str, kind: str) -> None: methods = adapter.get("required_methods") if not isinstance(methods, list) or not methods or any(not isinstance(x, str) or "." not in x for x in methods): raise ContractError(f"{adapter['id']}: required_methods invalid") + if kind == "container": + discovery = adapter.get("image_discovery") + if not isinstance(discovery, dict): + raise ContractError(f"{adapter['id']}: image_discovery missing") + method = discovery.get("method") + if not isinstance(method, str) or method not in methods: + raise ContractError(f"{adapter['id']}: image_discovery method must be required") + if not isinstance(discovery.get("source"), str) or not discovery["source"].startswith("https://"): + raise ContractError(f"{adapter['id']}: image_discovery source invalid") + if not isinstance(discovery.get("selection"), dict) or not discovery["selection"]: + raise ContractError(f"{adapter['id']}: image_discovery selection missing") elif platform == "proxmox": templates = adapter.get("required_api_templates") if not isinstance(templates, list) or not templates or any(not isinstance(x, str) or " /" not in x for x in templates): From 136afde9a2c83ff292a1b60e1e0a0313314e9070 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:31:24 +0200 Subject: [PATCH 370/719] RDTE: test source-bound container image discovery contract --- .../test_compute_materialization_contract.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/test_compute_materialization_contract.py b/tests/test_compute_materialization_contract.py index b854cad46..894a5f7db 100644 --- a/tests/test_compute_materialization_contract.py +++ b/tests/test_compute_materialization_contract.py @@ -16,7 +16,12 @@ def registry(): "container_adapters": [{ "id": "truenas-virt-incus-container", "source_api_family": "v25_04_0", - "required_methods": ["virt.instance.query", "virt.instance.create"], + "required_methods": ["virt.instance.query", "virt.instance.create", "virt.instance.image_choices"], + "image_discovery": { + "method": "virt.instance.image_choices", + "source": "https://images.linuxcontainers.org", + "selection": {"alias": "debian/trixie"}, + }, "restart_semantics": "native", }], "vm_adapters": [{ @@ -32,7 +37,12 @@ def registry(): "container_adapters": [{ "id": "truenas-container-lxc", "source_api_family": "v26_0_0", - "required_methods": ["container.query", "container.create"], + "required_methods": ["container.query", "container.create", "container.image.query_registry"], + "image_discovery": { + "method": "container.image.query_registry", + "source": "https://images.sys.truenas.net/streams", + "selection": {"name": "ubuntu:noble:amd64:default"}, + }, "restart_semantics": "compose-stop-start", }], "vm_adapters": [{ @@ -81,17 +91,23 @@ def test_missing_truenas_api_schema_fails_closed(self): with self.assertRaises(ContractError): validate(data) + def test_missing_container_image_discovery_fails_closed(self): + data = registry() + del data["platforms"]["truenas"]["targets"][0]["container_adapters"][0]["image_discovery"] + with self.assertRaises(ContractError): + validate(data) + def test_2504_selects_legacy_incus_container(self): a = choose_adapter( registry(), "truenas", "25.04.1", "container", - {"virt.instance.query", "virt.instance.create"}, + {"virt.instance.query", "virt.instance.create", "virt.instance.image_choices"}, ) self.assertEqual(a["id"], "truenas-virt-incus-container") def test_26_selects_lxc_container(self): a = choose_adapter( registry(), "truenas", "26.0.0-BETA.3", "container", - {"container.query", "container.create"}, + {"container.query", "container.create", "container.image.query_registry"}, ) self.assertEqual(a["id"], "truenas-container-lxc") self.assertIn("stop+start", semantic_plan("container", a)) From 0113c3a8f71b54e928a3e6a04e212c8d9c6cded4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:31:29 +0200 Subject: [PATCH 371/719] RDTE: resolve exact TrueNAS container images through native APIs --- scripts/truenas_compute_container_probe.py | 82 +++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_compute_container_probe.py b/scripts/truenas_compute_container_probe.py index 274869ad3..8234e75b2 100644 --- a/scripts/truenas_compute_container_probe.py +++ b/scripts/truenas_compute_container_probe.py @@ -81,6 +81,80 @@ def build_create_payload( raise ProbeError(f"unsupported container adapter: {adapter_id}") +def resolve_image_identity( + adapter: dict[str, Any], + call, + *, + legacy_image: str | None = None, + image_name: str | None = None, + image_version: str | None = None, +) -> tuple[dict[str, str], dict[str, Any]]: + adapter_id = adapter.get("id") + discovery = adapter.get("image_discovery") + if not isinstance(discovery, dict): + raise ProbeError(f"{adapter_id}: image discovery contract missing") + method = discovery.get("method") + selection = discovery.get("selection") + if not isinstance(method, str) or not isinstance(selection, dict): + raise ProbeError(f"{adapter_id}: invalid image discovery contract") + + if adapter_id == "truenas-virt-incus-container": + request = discovery.get("request") + if not isinstance(request, dict): + raise ProbeError("legacy image discovery request missing") + choices = call(method, [request]) + if not isinstance(choices, dict): + raise ProbeError(f"legacy image choices invalid: {choices!r}") + alias = selection.get("alias") + if not isinstance(alias, str) or not alias: + raise ProbeError("legacy image alias contract missing") + if legacy_image is not None and legacy_image != alias: + raise ProbeError(f"explicit legacy image {legacy_image!r} does not match admitted alias {alias!r}") + observed = choices.get(alias) + if not isinstance(observed, dict): + raise ProbeError(f"admitted legacy image alias unavailable: {alias!r}") + if selection.get("instance_type") not in set(observed.get("instance_types") or []): + raise ProbeError(f"legacy image lacks required instance type: {observed!r}") + if selection.get("arch") not in set(observed.get("archs") or []): + raise ProbeError(f"legacy image lacks required architecture: {observed!r}") + return {"legacy_image": alias}, { + "method": method, + "source": discovery.get("source"), + "selected": {"alias": alias}, + "observed": observed, + } + + if adapter_id == "truenas-container-lxc": + rows = call(method, []) + if not isinstance(rows, list): + raise ProbeError(f"container image registry response invalid: {rows!r}") + name = selection.get("name") + if not isinstance(name, str) or not name: + raise ProbeError("LXC image name contract missing") + if image_name is not None and image_name != name: + raise ProbeError(f"explicit image name {image_name!r} does not match admitted name {name!r}") + observed = next((x for x in rows if isinstance(x, dict) and x.get("name") == name), None) + if not isinstance(observed, dict): + raise ProbeError(f"admitted LXC image name unavailable: {name!r}") + versions = [ + x.get("version") for x in (observed.get("versions") or []) + if isinstance(x, dict) and isinstance(x.get("version"), str) and x.get("version") + ] + if not versions: + raise ProbeError(f"admitted LXC image has no exact versions: {observed!r}") + chosen = image_version or versions[-1] + if chosen not in versions: + raise ProbeError(f"requested LXC image version {chosen!r} not returned by registry") + return {"image_name": name, "image_version": chosen}, { + "method": method, + "source": discovery.get("source"), + "selected": {"name": name, "version": chosen}, + "available_versions": versions, + } + + raise ProbeError(f"unsupported container adapter for image discovery: {adapter_id}") + + def update_payload(adapter_id: str, nonce: str) -> dict[str, Any]: if adapter_id == "truenas-virt-incus-container": return {"environment": {"RDTE_NONCE": nonce, "RDTE_GENERATION": "2"}} @@ -223,10 +297,14 @@ def _call(method: str, params: list[Any]): adapter_id = adapter["id"] receipt["adapter"] = adapter + resolved_image, image_resolution = resolve_image_identity( + adapter, call, + legacy_image=a.legacy_image, image_name=a.image_name, image_version=a.image_version, + ) + receipt["image_resolution"] = image_resolution create_payload = build_create_payload( adapter_id, a.name, a.nonce, - legacy_image=a.legacy_image, image_name=a.image_name, - image_version=a.image_version, pool=a.pool, + pool=a.pool, **resolved_image, ) receipt["desired_create"] = create_payload From bf5c3a31476102d59c1a4f8256422e465d6b5d84 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:31:34 +0200 Subject: [PATCH 372/719] RDTE: test native TrueNAS container image resolution --- tests/test_truenas_compute_container_probe.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_truenas_compute_container_probe.py b/tests/test_truenas_compute_container_probe.py index bfe48efe4..88b9b5d24 100644 --- a/tests/test_truenas_compute_container_probe.py +++ b/tests/test_truenas_compute_container_probe.py @@ -6,6 +6,7 @@ method_shapes, normalize_system_version, observed_state, + resolve_image_identity, update_payload, ) @@ -56,6 +57,47 @@ def test_update_generation_is_adapter_specific(self): "2", ) + def test_legacy_image_resolution_requires_exact_admitted_alias(self): + adapter = { + "id": "truenas-virt-incus-container", + "image_discovery": { + "method": "virt.instance.image_choices", + "request": {"remote": "LINUX_CONTAINERS"}, + "source": "https://images.linuxcontainers.org", + "selection": {"alias": "debian/trixie", "instance_type": "CONTAINER", "arch": "amd64"}, + }, + } + def call(method, params): + self.assertEqual(method, "virt.instance.image_choices") + self.assertEqual(params, [{"remote": "LINUX_CONTAINERS"}]) + return {"debian/trixie": {"instance_types": ["CONTAINER", "VM"], "archs": ["amd64"]}} + resolved, evidence = resolve_image_identity(adapter, call) + self.assertEqual(resolved, {"legacy_image": "debian/trixie"}) + self.assertEqual(evidence["selected"]["alias"], "debian/trixie") + + def test_lxc_image_resolution_retains_exact_returned_version(self): + adapter = { + "id": "truenas-container-lxc", + "image_discovery": { + "method": "container.image.query_registry", + "source": "https://images.sys.truenas.net/streams", + "selection": {"name": "ubuntu:noble:amd64:default", "version_policy": "last-returned-exact"}, + }, + } + def call(method, params): + self.assertEqual(method, "container.image.query_registry") + self.assertEqual(params, []) + return [{ + "name": "ubuntu:noble:amd64:default", + "versions": [{"version": "20261001"}, {"version": "20261005"}], + }] + resolved, evidence = resolve_image_identity(adapter, call) + self.assertEqual(resolved, { + "image_name": "ubuntu:noble:amd64:default", + "image_version": "20261005", + }) + self.assertEqual(evidence["selected"]["version"], "20261005") + def test_state_normalizes_legacy_and_lxc(self): self.assertEqual(observed_state({"status": "RUNNING"}), "RUNNING") self.assertEqual(observed_state({"status": {"state": "RUNNING"}}), "RUNNING") From df5d9ae05d60f6f0a0740d5fd745c9d9f5b0262f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:36:25 +0200 Subject: [PATCH 373/719] RDTE: refresh C0 harness gha-kvm-system-rdte.yml on current base --- .github/workflows/gha-kvm-system-rdte.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index cf4c5cfc2..ec166fbce 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -53,6 +53,14 @@ on: - garm-provider-g5 - official-catalog - foliorelay + truenas_compute_fixture: + description: "Optional compute fixture layered on the selected system rung" + required: true + default: none + type: choice + options: + - none + - container-c0 permissions: contents: read @@ -68,6 +76,7 @@ jobs: truenas_version: ${{ steps.scope.outputs.truenas_version }} truenas_rung: ${{ steps.scope.outputs.truenas_rung }} truenas_t6_product: ${{ steps.scope.outputs.truenas_t6_product }} + truenas_compute_fixture: ${{ steps.scope.outputs.truenas_compute_fixture }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -80,6 +89,7 @@ jobs: DISPATCH_TRUENAS_VERSION: ${{ inputs.truenas_version }} DISPATCH_TRUENAS_RUNG: ${{ inputs.truenas_rung }} DISPATCH_TRUENAS_T6_PRODUCT: ${{ inputs.truenas_t6_product }} + DISPATCH_TRUENAS_COMPUTE_FIXTURE: ${{ inputs.truenas_compute_fixture }} BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.sha }} RUN_REQUEST_PATH: config/truenas-rdte-run-request.json @@ -91,6 +101,7 @@ jobs: truenas_version="${DISPATCH_TRUENAS_VERSION:-26.0.0-BETA.3}" truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" truenas_t6_product="${DISPATCH_TRUENAS_T6_PRODUCT:-litellm}" + truenas_compute_fixture="${DISPATCH_TRUENAS_COMPUTE_FIXTURE:-none}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then case "$DISPATCH_TARGET" in proxmox) proxmox=true ;; @@ -133,18 +144,24 @@ jobs: truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" + truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null printf 'fresh TrueNAS request edge: previous=%s current=%s\n' "${previous_request_id:-}" "$current_request_id" fi + if [[ "$truenas_compute_fixture" != "none" && "$truenas_rung" != "t3" ]]; then + echo "compute fixture $truenas_compute_fixture requires truenas rung t3" >&2 + exit 2 + fi echo "proxmox=$proxmox" >>"$GITHUB_OUTPUT" echo "truenas=$truenas" >>"$GITHUB_OUTPUT" echo "qemu_topology=$qemu_topology" >>"$GITHUB_OUTPUT" echo "truenas_version=$truenas_version" >>"$GITHUB_OUTPUT" echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" echo "truenas_t6_product=$truenas_t6_product" >>"$GITHUB_OUTPUT" - printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" + echo "truenas_compute_fixture=$truenas_compute_fixture" >>"$GITHUB_OUTPUT" + printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s compute_fixture=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" contract: runs-on: ubuntu-24.04 @@ -432,6 +449,7 @@ jobs: --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-state" --target-version "${{ needs.changes.outputs.truenas_version }}" --rung "${{ needs.changes.outputs.truenas_rung }}" + --compute-fixture "${{ needs.changes.outputs.truenas_compute_fixture }}" --out "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" ) if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" ]]; then From 91c93218cd89c8c949655adc61eab1e34dfc1579 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:36:29 +0200 Subject: [PATCH 374/719] RDTE: refresh C0 harness gha_kvm_truenas_rdte.sh on current base --- scripts/gha_kvm_truenas_rdte.sh | 65 ++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index ec22c6d69..408a6f885 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,12 +17,13 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" } OUT="" STATE_DIR="" RUNG="t0" +COMPUTE_FIXTURE="none" T6_PRODUCT="litellm" FOUNDRY_CONTROL_DIR="" FOUNDRY_COMMIT="" @@ -41,6 +42,7 @@ while [[ $# -gt 0 ]]; do --state-dir) STATE_DIR="$2"; shift 2 ;; --target-version) TARGET_VERSION="$2"; shift 2 ;; --rung) RUNG="$2"; shift 2 ;; + --compute-fixture) COMPUTE_FIXTURE="$2"; shift 2 ;; --t6-product) T6_PRODUCT="$2"; shift 2 ;; --foundry-control-dir) FOUNDRY_CONTROL_DIR="$2"; shift 2 ;; --foundry-commit) FOUNDRY_COMMIT="$2"; shift 2 ;; @@ -63,6 +65,11 @@ TARGET_ENV="$(python3 "$SCRIPT_DIR/truenas_rdte_target.py" --registry "$TARGET_R # truenas_rdte_target.py emits only shell-quoted values after strict registry validation. eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } +[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } +if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then + echo "compute fixture $COMPUTE_FIXTURE requires rung t3" >&2 + exit 2 +fi if [[ "$RUNG" == "t6" ]]; then if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } @@ -128,6 +135,7 @@ QEMU_ALIVE_AT_GATE="unknown" INSTALL_RESULT_JSON="" MIDDLEWARE_RESULT_JSON="" POOL_RESULT_JSON="" +COMPUTE_RESULT_JSON="" APP_RESULT_JSON="" LIFECYCLE_RESULT_JSON="" FOUNDRY_RESULT_JSON="" @@ -155,15 +163,20 @@ write_receipt() { export R_ISO_NAME="$ISO_NAME" R_ISO_URL="$ISO_URL" R_SHA_URL="$SHA_URL" export R_MIDDLEWARE_REF="$MIDDLEWARE_REF" R_MIDDLEWARE_COMMIT="$MIDDLEWARE_COMMIT" export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_INSTALLER_RPC_PATH="$INSTALLER_RPC_PATH" R_INSTALLER_RPC_GUEST_PORT="$INSTALLER_RPC_GUEST_PORT" R_INSTALLER_SOURCE_REF="$INSTALLER_SOURCE_REF" R_INSTALLER_MAIN_BLOB_SHA="$INSTALLER_MAIN_BLOB_SHA" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" - export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" + export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_COMPUTE_FIXTURE="$COMPUTE_FIXTURE" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_VCPUS="$VCPUS" R_RAM_MIB="$RAM_MIB" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" - export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" R_SESSION_RESULT="$SESSION_RESULT_JSON" + export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_COMPUTE_RESULT="$COMPUTE_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" R_SESSION_RESULT="$SESSION_RESULT_JSON" python3 - <<'PY' import json, os, pathlib payload = { "contract": "gha-kvm-system-lab/v1", - "target": {"product": "truenas", "version": os.environ["R_TARGET_VERSION"], "rung": os.environ.get("R_RUNG", "t0").upper()}, + "target": { + "product": "truenas", + "version": os.environ["R_TARGET_VERSION"], + "rung": os.environ.get("R_RUNG", "t0").upper(), + "compute_fixture": None if os.environ.get("R_COMPUTE_FIXTURE", "none") == "none" else os.environ["R_COMPUTE_FIXTURE"], + }, "classification": os.environ["R_CLASS"], "oracleSatisfied": os.environ["R_ORACLE"].lower() == "true", "phase": os.environ["R_PHASE"], @@ -225,6 +238,7 @@ payload = { "installer_install_completed": bool(os.environ.get("R_INSTALL_RESULT")) and json.loads(os.environ["R_INSTALL_RESULT"]).get("oracleSatisfied") is True, "installed_middleware_authenticated": bool(os.environ.get("R_MIDDLEWARE_RESULT")) and json.loads(os.environ["R_MIDDLEWARE_RESULT"]).get("oracleSatisfied") is True, "data_pool_created": bool(os.environ.get("R_POOL_RESULT")) and json.loads(os.environ["R_POOL_RESULT"]).get("oracleSatisfied") is True, + "compute_materialization_exercised": bool(os.environ.get("R_COMPUTE_RESULT")) and json.loads(os.environ["R_COMPUTE_RESULT"]).get("oracleSatisfied") is True, "apps_runtime_exercised": bool(os.environ.get("R_APP_RESULT")) and json.loads(os.environ["R_APP_RESULT"]).get("oracleSatisfied") is True, "app_lifecycle_exercised": bool(os.environ.get("R_LIFECYCLE_RESULT")) and json.loads(os.environ["R_LIFECYCLE_RESULT"]).get("oracleSatisfied") is True, "foundry_materialization_exercised": bool(os.environ.get("R_FOUNDRY_RESULT")) and json.loads(os.environ["R_FOUNDRY_RESULT"]).get("oracleSatisfied") is True, @@ -233,6 +247,7 @@ payload = { "install_result": json.loads(os.environ["R_INSTALL_RESULT"]) if os.environ.get("R_INSTALL_RESULT") else None, "installed_middleware": json.loads(os.environ["R_MIDDLEWARE_RESULT"]) if os.environ.get("R_MIDDLEWARE_RESULT") else None, "data_pool": json.loads(os.environ["R_POOL_RESULT"]) if os.environ.get("R_POOL_RESULT") else None, + "compute_materialization": json.loads(os.environ["R_COMPUTE_RESULT"]) if os.environ.get("R_COMPUTE_RESULT") else None, "apps_runtime": json.loads(os.environ["R_APP_RESULT"]) if os.environ.get("R_APP_RESULT") else None, "app_lifecycle": json.loads(os.environ["R_LIFECYCLE_RESULT"]) if os.environ.get("R_LIFECYCLE_RESULT") else None, "foundry_materialization": json.loads(os.environ["R_FOUNDRY_RESULT"]) if os.environ.get("R_FOUNDRY_RESULT") else None, @@ -244,6 +259,7 @@ payload = { "T1 is version-profiled read-only installer RPC discovery.", "T2 adds vendor installation plus installed middleware authentication/health.", "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", + "Optional container-c0 on T3 proves only native product-API container lifecycle/read-back/update/restart semantics and zero-residue cleanup; C1 guest execution remains separate.", "T4 initializes Apps on that pool and runs one synthetic public-safe custom Compose app.", "T5 exercises stop/start, config mutation/read-back, redeploy, stop, and delete for that digest-pinned custom app.", "T6 consumes one exact public TrueNAS App Foundry materialization control and verifies native Custom App realization/read-back.", @@ -273,6 +289,12 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | [[ -f "$SCRIPT_DIR/truenas_middleware_pool_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T3/T4 pool client" fi + if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then + [[ -f "$SCRIPT_DIR/truenas_compute_container_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS compute container C0 client" + [[ -f "$SCRIPT_DIR/../config/compute-materialization-targets.json" ]] || + fail_evidence HARNESS_FAILURE preflight "missing compute materialization target registry" + fi if [[ "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_app_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS Apps client" @@ -705,7 +727,40 @@ PY fail_evidence ORACLE_FAILURE data-pool "installed TrueNAS did not create and independently verify the disposable ZFS mirror pool" if [[ "$RUNG" == "t3" ]]; then - write_receipt SUPPORTED true data-pool "installed TrueNAS created an ONLINE healthy two-disk mirror containing exactly the selected disposable data disks" + if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then + COMPUTE_OUT="$STATE_DIR/compute-container-c0.json" + python3 "$SCRIPT_DIR/truenas_compute_container_probe.py" \ + --registry "$SCRIPT_DIR/../config/compute-materialization-targets.json" \ + --target-version "$VERSION" \ + --pool "$DATA_POOL_NAME" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --out "$COMPUTE_OUT" --timeout 8 --job-timeout 600 --state-timeout 240 --apply \ + >/dev/null 2>&1 || true + [[ -f "$COMPUTE_OUT" ]] || + fail_evidence HARNESS_FAILURE compute-container-c0 "compute C0 client did not emit a receipt" + COMPUTE_RESULT_JSON="$(cat "$COMPUTE_OUT")" + COMPUTE_OK="$(python3 - "$COMPUTE_OUT" <<'PY' +import json, pathlib, sys +data=json.loads(pathlib.Path(sys.argv[1]).read_text()) +ok=( + data.get("classification")=="SUPPORTED" + and data.get("oracleSatisfied") is True + and data.get("c0_oracle_satisfied") is True + and data.get("c1_guest_oracle_satisfied") is False + and (data.get("cleanup") or {}).get("absent") is True + and isinstance(data.get("image_resolution"), dict) +) +print("true" if ok else "false") +PY +)" + [[ "$COMPUTE_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE compute-container-c0 "native TrueNAS container C0 lifecycle did not satisfy exact API/image/cleanup oracles" + write_receipt SUPPORTED true compute-container-c0 "native TrueNAS container API completed exact image discovery, create/read-back/start/stop/update/restart-or-stop+start/delete/absence with zero residue; guest-level C1 remains unclaimed" + else + write_receipt SUPPORTED true data-pool "installed TrueNAS created an ONLINE healthy two-disk mirror containing exactly the selected disposable data disks" + fi exit 0 fi From de00d2dbd08294eff091c0a0ddcdb8e93584fa2d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:36:33 +0200 Subject: [PATCH 375/719] RDTE: refresh C0 harness test_gha_kvm_system_rdte.py on current base --- tests/test_gha_kvm_system_rdte.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index e4bcae10c..898286a61 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -647,6 +647,27 @@ def test_truenas_t3_real_harness_contract(self): py_check = subprocess.run(["python3", "-m", "py_compile", str(TRUENAS_POOL)], text=True, capture_output=True) self.assertEqual(py_check.returncode, 0, py_check.stderr) + def test_truenas_t3_compute_c0_is_opt_in_api_native_and_c1_separate(self): + text = TRUENAS.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn('--compute-fixture', text) + self.assertIn('COMPUTE_FIXTURE="none"', text) + self.assertIn('container-c0', text) + self.assertIn('requires rung t3', text) + self.assertIn('truenas_compute_container_probe.py', text) + self.assertIn('config/compute-materialization-targets.json', text) + self.assertIn('"compute_materialization_exercised"', text) + self.assertIn('"compute_materialization"', text) + self.assertIn('"c0_oracle_satisfied"', text) + self.assertIn('"c1_guest_oracle_satisfied"', text) + self.assertIn('"image_resolution"', text) + self.assertIn('truenas_compute_fixture:', workflow) + self.assertIn('needs.changes.outputs.truenas_compute_fixture', workflow) + self.assertIn('(.compute_fixture // "none")', workflow) + self.assertNotIn('scripts/truenas_compute_container_probe.py)', workflow.split("while IFS= read -r path; do", 1)[1].split("done < <(git diff --name-only", 1)[0]) + shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) + self.assertEqual(shell_check.returncode, 0, shell_check.stderr) + def test_truenas_t4_apps_contract(self): text = TRUENAS.read_text(encoding="utf-8") app = TRUENAS_APP.read_text(encoding="utf-8") From 20ee2f9edaf60155d7b2940e42661c39fd8b2107 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:38:58 +0200 Subject: [PATCH 376/719] RDTE: request TrueNAS BETA.3 compute container C0 rep 001 --- config/truenas-rdte-run-request.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e1456337e..d3e9b8bbf 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,9 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-012-txt-uuid-projection", + "request_id": "tn-26-beta3-t3-compute-container-c0-001", "version": "26.0.0-BETA.3", - "rung": "t6", - "product": "foliorelay", - "authority_issue": 396, - "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay MVP on disposable TrueNAS 26.0.0-BETA.3 after rep 011 reached the DNS-SD oracle with healthy substrate/product lifecycle and zero residue but rejected the advertisement because the external observer compared the canonical urn:uuid identity against the product's intentionally bare DNS-SD TXT UUID projection. Preserve product, Foundry, image, Compose, IPP, lifecycle, cleanup, and acceptance semantics. The only causal delta is PR #253: keep canonical UUID comparison for control/CUPS identity while passing the derived bare UUID only to DNS-SD TXT matching. Service-instance correlation hardening remains separate in PR #256. No acceptance is inferred without SUPPORTED plus oracleSatisfied=true." + "rung": "t3", + "compute_fixture": "container-c0", + "authority_issue": 426, + "purpose": "Run the first product-API-native TrueNAS compute materialization C0 rep on exact 26.0.0-BETA.3 using the existing #396 disposable system workcell. Preserve exact ISO/middleware/T3 pool evidence, then resolve the admitted Ubuntu Noble system-container image through container.image.query_registry and exercise container.create/query/start/stop/update/stop+start/delete/absence through middleware APIs. This rep claims C0 lifecycle only; guest-level C1 execution/nonce, VM lifecycle, nested KVM, Firecracker, Windows 11, physical HIL, and cross-version inheritance remain explicitly unclaimed." } From b2b0fc11b24452571075a809412eb752819b7c72 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:41:22 +0200 Subject: [PATCH 377/719] RDTE: make compute C0 request claim boundary explicit --- config/truenas-rdte-run-request.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index d3e9b8bbf..d3d1d4371 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -5,5 +5,6 @@ "rung": "t3", "compute_fixture": "container-c0", "authority_issue": 426, - "purpose": "Run the first product-API-native TrueNAS compute materialization C0 rep on exact 26.0.0-BETA.3 using the existing #396 disposable system workcell. Preserve exact ISO/middleware/T3 pool evidence, then resolve the admitted Ubuntu Noble system-container image through container.image.query_registry and exercise container.create/query/start/stop/update/stop+start/delete/absence through middleware APIs. This rep claims C0 lifecycle only; guest-level C1 execution/nonce, VM lifecycle, nested KVM, Firecracker, Windows 11, physical HIL, and cross-version inheritance remain explicitly unclaimed." + "purpose": "Run the first product-API-native TrueNAS compute materialization C0 rep on exact 26.0.0-BETA.3 using the existing #396 disposable system workcell. Preserve exact ISO/middleware/T3 pool evidence, then resolve the admitted Ubuntu Noble system-container image through container.image.query_registry and exercise container.create/query/start/stop/update/stop+start/delete/absence through middleware APIs. This rep claims C0 lifecycle only; guest-level C1 execution/nonce, VM lifecycle, nested KVM, Firecracker, Windows 11, physical HIL, and cross-version inheritance remain explicitly unclaimed.", + "claim_boundary": "C0 native product-API container lifecycle only; C1 guest execution, VM lifecycle, Firecracker, Windows, cross-version support, and physical HIL remain OPEN" } From d10350e28948b02db76c8700430d896bd6ea613f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:43:48 +0200 Subject: [PATCH 378/719] RDTE: bind C0 request to system and compute authorities --- config/truenas-rdte-run-request.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index d3d1d4371..f89f0c1a0 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -4,7 +4,8 @@ "version": "26.0.0-BETA.3", "rung": "t3", "compute_fixture": "container-c0", - "authority_issue": 426, + "authority_issue": 396, "purpose": "Run the first product-API-native TrueNAS compute materialization C0 rep on exact 26.0.0-BETA.3 using the existing #396 disposable system workcell. Preserve exact ISO/middleware/T3 pool evidence, then resolve the admitted Ubuntu Noble system-container image through container.image.query_registry and exercise container.create/query/start/stop/update/stop+start/delete/absence through middleware APIs. This rep claims C0 lifecycle only; guest-level C1 execution/nonce, VM lifecycle, nested KVM, Firecracker, Windows 11, physical HIL, and cross-version inheritance remain explicitly unclaimed.", - "claim_boundary": "C0 native product-API container lifecycle only; C1 guest execution, VM lifecycle, Firecracker, Windows, cross-version support, and physical HIL remain OPEN" + "claim_boundary": "C0 native product-API container lifecycle only; C1 guest execution, VM lifecycle, Firecracker, Windows, cross-version support, and physical HIL remain OPEN", + "consumer_authority_issue": 426 } From ba52e347e06ea1e04feffc1c6c2ccfb7c3c94a1d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:48:59 +0200 Subject: [PATCH 379/719] RDTE: admit VM V0 compute fixture in shared TrueNAS route --- .github/workflows/gha-kvm-system-rdte.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index ec166fbce..ab5df0c5f 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -61,6 +61,7 @@ on: options: - none - container-c0 + - vm-v0 permissions: contents: read @@ -144,7 +145,7 @@ jobs: truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" - truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0")' "$RUN_REQUEST_PATH")" + truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null From e636203349f5751b0ac85c0690f8d4e51067ffe0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:49:14 +0200 Subject: [PATCH 380/719] RDTE: wire native TrueNAS VM V0 into shared T3 workcell --- scripts/gha_kvm_truenas_rdte.sh | 45 +++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 408a6f885..85d72096f 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" } OUT="" @@ -65,7 +65,7 @@ TARGET_ENV="$(python3 "$SCRIPT_DIR/truenas_rdte_target.py" --registry "$TARGET_R # truenas_rdte_target.py emits only shell-quoted values after strict registry validation. eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } -[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } +[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" || "$COMPUTE_FIXTURE" == "vm-v0" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then echo "compute fixture $COMPUTE_FIXTURE requires rung t3" >&2 exit 2 @@ -260,6 +260,7 @@ payload = { "T2 adds vendor installation plus installed middleware authentication/health.", "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", "Optional container-c0 on T3 proves only native product-API container lifecycle/read-back/update/restart semantics and zero-residue cleanup; C1 guest execution remains separate.", + "Optional vm-v0 on T3 proves only native VM/device lifecycle, source-bound preconditions and exact-owned storage cleanup; guest boot/V1, nested KVM/V2, Firecracker and Windows remain separate.", "T4 initializes Apps on that pool and runs one synthetic public-safe custom Compose app.", "T5 exercises stop/start, config mutation/read-back, redeploy, stop, and delete for that digest-pinned custom app.", "T6 consumes one exact public TrueNAS App Foundry materialization control and verifies native Custom App realization/read-back.", @@ -292,6 +293,11 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then [[ -f "$SCRIPT_DIR/truenas_compute_container_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS compute container C0 client" + elif [[ "$COMPUTE_FIXTURE" == "vm-v0" ]]; then + [[ -f "$SCRIPT_DIR/truenas_compute_vm_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS compute VM V0 client" + fi + if [[ "$COMPUTE_FIXTURE" != "none" ]]; then [[ -f "$SCRIPT_DIR/../config/compute-materialization-targets.json" ]] || fail_evidence HARNESS_FAILURE preflight "missing compute materialization target registry" fi @@ -758,6 +764,41 @@ PY [[ "$COMPUTE_OK" == "true" ]] || fail_evidence ORACLE_FAILURE compute-container-c0 "native TrueNAS container C0 lifecycle did not satisfy exact API/image/cleanup oracles" write_receipt SUPPORTED true compute-container-c0 "native TrueNAS container API completed exact image discovery, create/read-back/start/stop/update/restart-or-stop+start/delete/absence with zero residue; guest-level C1 remains unclaimed" + elif [[ "$COMPUTE_FIXTURE" == "vm-v0" ]]; then + COMPUTE_OUT="$STATE_DIR/compute-vm-v0.json" + python3 "$SCRIPT_DIR/truenas_compute_vm_probe.py" \ + --registry "$SCRIPT_DIR/../config/compute-materialization-targets.json" \ + --target-version "$VERSION" \ + --zvol-name "$DATA_POOL_NAME/rdte-compute-vm-v0" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --out "$COMPUTE_OUT" --timeout 8 --job-timeout 600 --apply \ + >/dev/null 2>&1 || true + [[ -f "$COMPUTE_OUT" ]] || + fail_evidence HARNESS_FAILURE compute-vm-v0 "compute VM V0 client did not emit a receipt" + COMPUTE_RESULT_JSON="$(cat "$COMPUTE_OUT")" + COMPUTE_OK="$(python3 - "$COMPUTE_OUT" <<'PY' +import json, pathlib, sys +data=json.loads(pathlib.Path(sys.argv[1]).read_text()) +cleanup=data.get("cleanup") or {} +ok=( + data.get("classification")=="SUPPORTED" + and data.get("oracleSatisfied") is True + and data.get("v0_oracle_satisfied") is True + and data.get("v1_guest_oracle_satisfied") is False + and data.get("v2_firecracker_oracle_satisfied") is False + and data.get("windows_w1_oracle_satisfied") is False + and cleanup.get("vm_absent") is True + and cleanup.get("device_absent") is True + and cleanup.get("zvol_absent") is True +) +print("true" if ok else "false") +PY +)" + [[ "$COMPUTE_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE compute-vm-v0 "native TrueNAS VM V0 lifecycle did not satisfy exact ownership/precondition/cleanup oracles" + write_receipt SUPPORTED true compute-vm-v0 "native TrueNAS VM/device V0 completed create/read-back/update/owned-ZVOL device CRUD and zero-residue cleanup; guest boot, nested KVM, Firecracker and Windows remain unclaimed" else write_receipt SUPPORTED true data-pool "installed TrueNAS created an ONLINE healthy two-disk mirror containing exactly the selected disposable data disks" fi From 689bc8ed1fc738e8e1769f0218383c9dfe9948f7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:49:20 +0200 Subject: [PATCH 381/719] RDTE: test shared-workcell TrueNAS VM V0 routing --- tests/test_gha_kvm_system_rdte.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 898286a61..53e35b862 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -668,6 +668,24 @@ def test_truenas_t3_compute_c0_is_opt_in_api_native_and_c1_separate(self): shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) self.assertEqual(shell_check.returncode, 0, shell_check.stderr) + def test_truenas_t3_vm_v0_is_opt_in_and_strictly_pre_guest(self): + text = TRUENAS.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertIn('vm-v0', workflow) + self.assertIn('vm-v0', text) + self.assertIn('truenas_compute_vm_probe.py', text) + self.assertIn('--zvol-name "$DATA_POOL_NAME/rdte-compute-vm-v0"', text) + self.assertIn('"v0_oracle_satisfied"', text) + self.assertIn('"v1_guest_oracle_satisfied"', text) + self.assertIn('"v2_firecracker_oracle_satisfied"', text) + self.assertIn('"windows_w1_oracle_satisfied"', text) + self.assertIn('"vm_absent"', text) + self.assertIn('"device_absent"', text) + self.assertIn('"zvol_absent"', text) + self.assertIn('guest boot, nested KVM, Firecracker and Windows remain unclaimed', text) + shell_check = subprocess.run(["bash", "-n", str(TRUENAS)], text=True, capture_output=True) + self.assertEqual(shell_check.returncode, 0, shell_check.stderr) + def test_truenas_t4_apps_contract(self): text = TRUENAS.read_text(encoding="utf-8") app = TRUENAS_APP.read_text(encoding="utf-8") From 2b5c990c0d4c2e1723e1aa5741bb5bc9ae235848 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:52:40 +0200 Subject: [PATCH 382/719] RDTE: make heavyweight Proxmox execution request-edge driven --- .github/workflows/gha-kvm-system-rdte.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index ab5df0c5f..fc738cb41 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -94,6 +94,7 @@ jobs: BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.sha }} RUN_REQUEST_PATH: config/truenas-rdte-run-request.json + PROXMOX_RUN_REQUEST_PATH: config/proxmox-rdte-run-request.json run: | set -euo pipefail proxmox=false @@ -118,7 +119,9 @@ jobs: # changes alone must never replay the previously committed request. truenas=true ;; - scripts/gha_kvm_proxmox_rdte.sh) + config/proxmox-rdte-run-request.json) + # Heavy Proxmox work is request-edge driven. Harness/probe/workflow + # changes alone must never launch or replay a heavyweight PVE rep. proxmox=true ;; scripts/gha_qemu_t3_topology_probe.py|.github/workflows/gha-kvm-system-rdte.yml) @@ -131,6 +134,21 @@ jobs: truenas=true qemu_topology=true fi + if [[ "$EVENT_NAME" != "workflow_dispatch" && "$proxmox" == "true" ]]; then + [[ -f "$PROXMOX_RUN_REQUEST_PATH" ]] || { echo "missing Proxmox run request" >&2; exit 2; } + jq -e '.schema == "gha-kvm-proxmox-run-request/v1"' "$PROXMOX_RUN_REQUEST_PATH" >/dev/null + current_proxmox_request_id="$(jq -er '.request_id | select(type == "string" and length > 0)' "$PROXMOX_RUN_REQUEST_PATH")" + previous_proxmox_request_id="" + if git cat-file -e "$BEFORE_SHA:$PROXMOX_RUN_REQUEST_PATH" 2>/dev/null; then + previous_proxmox_request_id="$(git show "$BEFORE_SHA:$PROXMOX_RUN_REQUEST_PATH" | jq -er '.request_id | select(type == "string" and length > 0)')" + fi + if [[ -n "$previous_proxmox_request_id" && "$current_proxmox_request_id" == "$previous_proxmox_request_id" ]]; then + echo "refusing stale Proxmox request replay: request_id did not change ($current_proxmox_request_id)" >&2 + exit 2 + fi + jq -e '.version == "9.2-1" and .authority_issue == 396' "$PROXMOX_RUN_REQUEST_PATH" >/dev/null + printf 'fresh Proxmox request edge: previous=%s current=%s\n' "${previous_proxmox_request_id:-}" "$current_proxmox_request_id" + fi if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then jq -e '.schema == "gha-kvm-truenas-run-request/v1"' "$RUN_REQUEST_PATH" >/dev/null current_request_id="$(jq -er '.request_id | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" From 0bd93bdb8351e4564d877e1d7ac8524711702efd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:52:44 +0200 Subject: [PATCH 383/719] RDTE: test Proxmox request-edge heavyweight gating --- tests/test_gha_kvm_system_rdte.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 53e35b862..e2346041c 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -305,6 +305,9 @@ def test_workflow_scopes_heavy_targets(self): self.assertIn("needs.changes.outputs.truenas_version", text) self.assertIn("needs.changes.outputs.truenas_rung", text) self.assertIn("config/truenas-rdte-run-request.json", text) + self.assertIn("config/proxmox-rdte-run-request.json", text) + self.assertIn("gha-kvm-proxmox-run-request/v1", text) + self.assertIn("refusing stale Proxmox request replay", text) self.assertIn( 'if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then', text, @@ -323,6 +326,8 @@ def test_workflow_scopes_heavy_targets(self): "done < <(git diff --name-only", 1 )[0] self.assertIn("config/truenas-rdte-run-request.json)", routing) + self.assertIn("config/proxmox-rdte-run-request.json)", routing) + self.assertNotIn("scripts/gha_kvm_proxmox_rdte.sh)", routing) for stale_replay_source in ( "scripts/gha_kvm_truenas_rdte.sh", "scripts/truenas_installer_rpc_probe.py", From f73849953a8d3c842c454e286551231099fd4aa7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:53:24 +0200 Subject: [PATCH 384/719] RDTE: correlate FolioRelay DNS-SD records by service instance --- tools/foliorelay-observer/main.go | 132 ++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 32 deletions(-) diff --git a/tools/foliorelay-observer/main.go b/tools/foliorelay-observer/main.go index 56f49714e..81c718238 100644 --- a/tools/foliorelay-observer/main.go +++ b/tools/foliorelay-observer/main.go @@ -93,15 +93,40 @@ func nameAt(pkt []byte, off int, seen map[int]bool) (string, int, error) { } } +func normalizeDNSName(name string) string { + return strings.ToLower(strings.TrimSuffix(name, ".")) +} + +type srvRecord struct { + target string + port int +} + type observation struct { - universal bool - txt []string - srvTarget string - srvPort int + universalTargets []string + txtByOwner map[string][]string + srvByOwner map[string]srvRecord +} + +func newObservation() observation { + return observation{ + txtByOwner: map[string][]string{}, + srvByOwner: map[string]srvRecord{}, + } +} + +func (o *observation) merge(other observation) { + o.universalTargets = append(o.universalTargets, other.universalTargets...) + for owner, items := range other.txtByOwner { + o.txtByOwner[owner] = append(o.txtByOwner[owner], items...) + } + for owner, srv := range other.srvByOwner { + o.srvByOwner[owner] = srv + } } func parsePacket(pkt []byte) (observation, error) { - var o observation + o := newObservation() if len(pkt) < 12 { return o, errors.New("short dns packet") } @@ -139,47 +164,87 @@ func parsePacket(pkt []byte) (observation, error) { } rdata := pkt[off : off+rdlen] off += rdlen + owner := normalizeDNSName(name) - if strings.EqualFold(strings.TrimSuffix(name, "."), queryName) && typ == 12 { - o.universal = true + if owner == normalizeDNSName(queryName) && typ == 12 { + target, _, err := nameAt(pkt, rstart, nil) + if err != nil { + return o, err + } + o.universalTargets = append(o.universalTargets, target) } if typ == 16 { + items := []string{} for j := 0; j < len(rdata); { n := int(rdata[j]) j++ if j+n > len(rdata) { - break + return o, errors.New("short dns txt item") } - o.txt = append(o.txt, string(rdata[j:j+n])) + items = append(items, string(rdata[j:j+n])) j += n } + o.txtByOwner[owner] = append(o.txtByOwner[owner], items...) } if typ == 33 && rdlen >= 7 { - o.srvPort = int(binary.BigEndian.Uint16(rdata[4:6])) target, _, err := nameAt(pkt, rstart+6, nil) - if err == nil { - o.srvTarget = target + if err != nil { + return o, err + } + o.srvByOwner[owner] = srvRecord{ + target: target, + port: int(binary.BigEndian.Uint16(rdata[4:6])), } } } return o, nil } -func qualifying(o observation, expectedTxtUUID, expectedHost string, expectedPort int) bool { - joined := strings.ToLower(strings.Join(o.txt, "\n")) - uuidMatch := false - for _, item := range o.txt { - if strings.EqualFold(item, "UUID="+expectedTxtUUID) { - uuidMatch = true - break +type serviceMatch struct { + instance string + txt []string + srvTarget string + srvPort int +} + +func matchObservation(o observation, expectedTxtUUID, expectedHost string, expectedPort int) (serviceMatch, bool) { + for _, instance := range o.universalTargets { + owner := normalizeDNSName(instance) + txt, ok := o.txtByOwner[owner] + if !ok { + continue + } + srv, ok := o.srvByOwner[owner] + if !ok { + continue + } + uuidMatch := false + for _, item := range txt { + if strings.EqualFold(item, "UUID="+expectedTxtUUID) { + uuidMatch = true + break + } + } + joined := strings.ToLower(strings.Join(txt, "\n")) + if uuidMatch && + strings.Contains(joined, "rp=printers/foliorelay") && + strings.Contains(joined, "pdl=application/pdf,image/urf") && + strings.EqualFold(normalizeDNSName(srv.target), normalizeDNSName(expectedHost)) && + srv.port == expectedPort { + return serviceMatch{ + instance: instance, + txt: txt, + srvTarget: srv.target, + srvPort: srv.port, + }, true } } - return o.universal && - uuidMatch && - strings.Contains(joined, "rp=printers/foliorelay") && - strings.Contains(joined, "pdl=application/pdf,image/urf") && - strings.EqualFold(strings.TrimSuffix(o.srvTarget, "."), strings.TrimSuffix(expectedHost, ".")) && - o.srvPort == expectedPort + return serviceMatch{}, false +} + +func qualifying(o observation, expectedTxtUUID, expectedHost string, expectedPort int) bool { + _, ok := matchObservation(o, expectedTxtUUID, expectedHost, expectedPort) + return ok } func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, seconds float64) (map[string]any, error) { @@ -211,6 +276,7 @@ func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, s return nil, err } + aggregate := newObservation() deadline := time.Now().Add(time.Duration(seconds * float64(time.Second))) buf := make([]byte, 65535) for time.Now().Before(deadline) { @@ -222,17 +288,19 @@ func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, s } return nil, err } - o, err := parsePacket(buf[:n]) + packetObservation, err := parsePacket(buf[:n]) if err != nil { continue } - if qualifying(o, expectedTxtUUID, expectedHost, expectedPort) { + aggregate.merge(packetObservation) + if matched, ok := matchObservation(aggregate, expectedTxtUUID, expectedHost, expectedPort); ok { return map[string]any{ - "universal_ptr": true, - "uuid": expected, - "txt": o.txt, - "srv_target": o.srvTarget, - "srv_port": o.srvPort, + "universal_ptr": true, + "service_instance": matched.instance, + "uuid": expected, + "txt": matched.txt, + "srv_target": matched.srvTarget, + "srv_port": matched.srvPort, }, nil } } From 29786725d3bd0b6015d9b235e59e0bb4ca9ba473 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:53:27 +0200 Subject: [PATCH 385/719] test: reject cross-instance DNS-SD evidence splicing --- tools/foliorelay-observer/main_test.go | 200 ++++++++++++++----------- 1 file changed, 110 insertions(+), 90 deletions(-) diff --git a/tools/foliorelay-observer/main_test.go b/tools/foliorelay-observer/main_test.go index 4d321ece6..dd6f325fc 100644 --- a/tools/foliorelay-observer/main_test.go +++ b/tools/foliorelay-observer/main_test.go @@ -1,98 +1,92 @@ package main import ( - "encoding/binary" - "testing" + "encoding/binary" + "testing" ) +const testInstance = "FolioRelay._ipp._tcp.local" + +func validObservation() observation { + o := newObservation() + o.universalTargets = []string{testInstance} + owner := normalizeDNSName(testInstance) + o.txtByOwner[owner] = []string{ + "UUID=01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + } + o.srvByOwner[owner] = srvRecord{target: "foliorelay-t6.local.", port: 8634} + return o +} + func TestEncodeAndNameAt(t *testing.T) { - wire := encodeName("_universal._sub._ipp._tcp.local") - got, end, err := nameAt(wire, 0, nil) - if err != nil { t.Fatal(err) } - if got != queryName { t.Fatalf("got %q", got) } - if end != len(wire) { t.Fatalf("end=%d want=%d", end, len(wire)) } + wire := encodeName("_universal._sub._ipp._tcp.local") + got, end, err := nameAt(wire, 0, nil) + if err != nil { t.Fatal(err) } + if got != queryName { t.Fatalf("got %q", got) } + if end != len(wire) { t.Fatalf("end=%d want=%d", end, len(wire)) } } func TestCompressedNameAt(t *testing.T) { - base := encodeName("_ipp._tcp.local") - pkt := append([]byte{}, base...) - off := len(pkt) - pkt = append(pkt, 10) - pkt = append(pkt, []byte("_universal")...) - pkt = append(pkt, 0xC0, 0x00) - got, end, err := nameAt(pkt, off, nil) - if err != nil { t.Fatal(err) } - if got != "_universal._ipp._tcp.local" { t.Fatalf("got %q", got) } - if end != len(pkt) { t.Fatalf("end=%d want=%d", end, len(pkt)) } + base := encodeName("_ipp._tcp.local") + pkt := append([]byte{}, base...) + off := len(pkt) + pkt = append(pkt, 10) + pkt = append(pkt, []byte("_universal")...) + pkt = append(pkt, 0xC0, 0x00) + got, end, err := nameAt(pkt, off, nil) + if err != nil { t.Fatal(err) } + if got != "_universal._ipp._tcp.local" { t.Fatalf("got %q", got) } + if end != len(pkt) { t.Fatalf("end=%d want=%d", end, len(pkt)) } } func TestQualifying(t *testing.T) { - o := observation{ - universal: true, - txt: []string{ - "UUID=01234567-89ab-4def-8123-456789abcdef", - "rp=printers/FolioRelay", - "pdl=application/pdf,image/urf", - }, - srvTarget: "foliorelay-t6.local.", - srvPort: 8634, - } - if !qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { - t.Fatal("expected qualifying observation") - } + o := validObservation() + if !qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatal("expected qualifying observation") + } } -func TestParsePacket(t *testing.T) { - // One PTR owner for the universal subtype, one TXT RR, one SRV RR. - pkt := make([]byte, 12) - binary.BigEndian.PutUint16(pkt[6:8], 3) - - appendRR := func(owner string, typ uint16, rdata []byte) { - pkt = append(pkt, encodeName(owner)...) - hdr := make([]byte, 10) - binary.BigEndian.PutUint16(hdr[0:2], typ) - binary.BigEndian.PutUint16(hdr[2:4], 1) - binary.BigEndian.PutUint32(hdr[4:8], 120) - binary.BigEndian.PutUint16(hdr[8:10], uint16(len(rdata))) - pkt = append(pkt, hdr...) - pkt = append(pkt, rdata...) - } +func appendRR(pkt *[]byte, owner string, typ uint16, rdata []byte) { + *pkt = append(*pkt, encodeName(owner)...) + hdr := make([]byte, 10) + binary.BigEndian.PutUint16(hdr[0:2], typ) + binary.BigEndian.PutUint16(hdr[2:4], 1) + binary.BigEndian.PutUint32(hdr[4:8], 120) + binary.BigEndian.PutUint16(hdr[8:10], uint16(len(rdata))) + *pkt = append(*pkt, hdr...) + *pkt = append(*pkt, rdata...) +} - appendRR(queryName, 12, encodeName("FolioRelay._ipp._tcp.local")) - txt := []byte{} - for _, s := range []string{ - "UUID=01234567-89ab-4def-8123-456789abcdef", - "rp=printers/FolioRelay", - "pdl=application/pdf,image/urf", - } { - txt = append(txt, byte(len(s))) - txt = append(txt, []byte(s)...) - } - appendRR("FolioRelay._ipp._tcp.local", 16, txt) - srv := make([]byte, 6) - binary.BigEndian.PutUint16(srv[4:6], 8634) - srv = append(srv, encodeName("foliorelay-t6.local")...) - appendRR("FolioRelay._ipp._tcp.local", 33, srv) +func TestParsePacket(t *testing.T) { + pkt := make([]byte, 12) + binary.BigEndian.PutUint16(pkt[6:8], 3) + appendRR(&pkt, queryName, 12, encodeName(testInstance)) + txt := []byte{} + for _, s := range []string{ + "UUID=01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + } { + txt = append(txt, byte(len(s))) + txt = append(txt, []byte(s)...) + } + appendRR(&pkt, testInstance, 16, txt) + srv := make([]byte, 6) + binary.BigEndian.PutUint16(srv[4:6], 8634) + srv = append(srv, encodeName("foliorelay-t6.local")...) + appendRR(&pkt, testInstance, 33, srv) - o, err := parsePacket(pkt) - if err != nil { t.Fatal(err) } - if !qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { - t.Fatalf("not qualifying: %+v", o) - } + o, err := parsePacket(pkt) + if err != nil { t.Fatal(err) } + if !qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatalf("not qualifying: %+v", o) + } } - func TestQualifyingRejectsCrossedIdentity(t *testing.T) { - base := observation{ - universal: true, - txt: []string{ - "UUID=01234567-89ab-4def-8123-456789abcdef", - "rp=printers/FolioRelay", - "pdl=application/pdf,image/urf", - }, - srvTarget: "foliorelay-t6.local.", - srvPort: 8634, - } + base := validObservation() tests := []struct { name string obs observation @@ -100,7 +94,9 @@ func TestQualifyingRejectsCrossedIdentity(t *testing.T) { host string port int }{ - {"missing-universal", observation{txt: base.txt, srvTarget: base.srvTarget, srvPort: base.srvPort}, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634}, + {"missing-universal", func() observation { + x := validObservation(); x.universalTargets = nil; return x + }(), "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634}, {"wrong-uuid", base, "ffffffff-ffff-4fff-8fff-ffffffffffff", "foliorelay-t6.local", 8634}, {"wrong-host", base, "01234567-89ab-4def-8123-456789abcdef", "other.local", 8634}, {"wrong-port", base, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 9999}, @@ -114,6 +110,37 @@ func TestQualifyingRejectsCrossedIdentity(t *testing.T) { } } +func TestRejectsCrossInstanceSplicing(t *testing.T) { + o := newObservation() + good := normalizeDNSName("Good._ipp._tcp.local") + other := normalizeDNSName("Other._ipp._tcp.local") + o.universalTargets = []string{"Good._ipp._tcp.local"} + o.txtByOwner[good] = []string{ + "UUID=01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + } + o.srvByOwner[other] = srvRecord{target: "foliorelay-t6.local.", port: 8634} + if qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatal("must not splice TXT and SRV from different service instances") + } +} + +func TestMergeAcrossPacketsPreservesSameInstanceCorrelation(t *testing.T) { + owner := normalizeDNSName(testInstance) + a := newObservation(); a.universalTargets = []string{testInstance} + b := newObservation(); b.txtByOwner[owner] = []string{ + "UUID=01234567-89ab-4def-8123-456789abcdef", + "rp=printers/FolioRelay", + "pdl=application/pdf,image/urf", + } + c := newObservation(); c.srvByOwner[owner] = srvRecord{target: "foliorelay-t6.local.", port: 8634} + a.merge(b); a.merge(c) + if !qualifying(a, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatal("same service instance split across packets should qualify") + } +} + func TestParsePacketRejectsMalformedInput(t *testing.T) { if _, err := parsePacket([]byte{0, 1, 2}); err == nil { t.Fatal("expected short packet error") @@ -127,17 +154,10 @@ func TestParsePacketRejectsMalformedInput(t *testing.T) { } func TestQualifyingRejectsCanonicalURNInTXT(t *testing.T) { - o := observation{ - universal: true, - txt: []string{ - "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef", - "rp=printers/FolioRelay", - "pdl=application/pdf,image/urf", - }, - srvTarget: "foliorelay-t6.local.", - srvPort: 8634, - } - if qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { - t.Fatal("canonical URN must not satisfy bare TXT UUID projection") - } + o := validObservation() + owner := normalizeDNSName(testInstance) + o.txtByOwner[owner][0] = "UUID=urn:uuid:01234567-89ab-4def-8123-456789abcdef" + if qualifying(o, "01234567-89ab-4def-8123-456789abcdef", "foliorelay-t6.local", 8634) { + t.Fatal("canonical URN must not satisfy bare TXT UUID projection") + } } From c2f68e1d7e204bc168d7904556c4ec726d05a7da Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:54:07 +0200 Subject: [PATCH 386/719] RDTE: retain matched DNS-SD service instance --- scripts/truenas_middleware_foliorelay_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index e535594f3..0bf5f47d8 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -594,7 +594,7 @@ def wait_state(name,state): "classification":"SUPPORTED","oracleSatisfied":True, "identity":{"printer_uuid":uuid,"public_uri":PUBLIC_URI,"control_cups_uuid_match":True,"dnssd_uuid_match":True,"dnssd_public_uri_match":True}, "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP"}, - "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True,"srv_target":PUBLIC_HOST,"srv_port":PUBLIC_IPP_PORT,"resource_path":PUBLIC_RESOURCE_PATH}, + "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True,"service_instance":observed.get("service_instance"),"srv_target":PUBLIC_HOST,"srv_port":PUBLIC_IPP_PORT,"resource_path":PUBLIC_RESOURCE_PATH}, "cleanup":{"apps_absent":True,"fixture_dataset_absent":True,"fixture_mountpoint_absent":True,"zero_residue":True}, "producer_gate":{"replan_noop_required":True,"runtime_does_not_reconstruct_foundry_control":True}, "detail":"exact Foundry-exported FolioRelay control realized on TrueNAS; exact three-service identity, portal/IPP, PDF+URF source preservation, independent in-guest DNS-SD observation, restart persistence, and zero-residue cleanup passed" From e28996f50b1b2266efd0930f47e0bd82ac8d4fe0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:54:17 +0200 Subject: [PATCH 387/719] test: bind FolioRelay DNS-SD evidence to one instance --- tests/test_truenas_t6_foliorelay_control.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 117dff257..b14a160c1 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -187,6 +187,11 @@ def test_dnssd_observer_proves_public_host_port_and_resource_path(self): self.assertIn('rp=printers/foliorelay', observer.lower()) self.assertIn('pdl=application/pdf,image/urf', observer.lower()) self.assertIn('"dnssd_public_uri_match":True', text) + self.assertIn('"service_instance"', observer) + self.assertIn('"service_instance":observed.get("service_instance")', text) + self.assertIn('matchObservation', observer) + self.assertIn('o.txtByOwner[owner]', observer) + self.assertIn('o.srvByOwner[owner]', observer) self.assertNotIn('if PUBLIC_URI not in attrs', text) @unittest.skipUnless(pathlib.Path("/usr/include/cups/raster.h").is_file(), "CUPS development headers not installed") From 7366ee6be3735752d880500da95848772b34f83a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:59:31 +0200 Subject: [PATCH 388/719] RDTE: exercise FolioRelay F4 F5 reconciliation and reinstall --- .../truenas_middleware_foliorelay_t6_probe.py | 69 +++++++++++++++++-- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 0bf5f47d8..11a89bc0e 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -44,6 +44,17 @@ def canonical_sha256(value): return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest() +def reconciliation_action(app_state, live_compose, desired_compose): + if app_state is None: + return "CREATE" + if app_state in {"DEPLOYING","STOPPING"}: + return "WAIT" + if app_state not in {"RUNNING","STOPPED"}: + return "FAIL_CLOSED" + if not isinstance(live_compose, dict): + return "FAIL_CLOSED" + return "NOOP" if canonical_sha256(live_compose)==canonical_sha256(desired_compose) else "UPDATE" + def sha256_bytes(value: bytes) -> str: return hashlib.sha256(value).hexdigest() @@ -563,10 +574,14 @@ def wait_state(name,state): after_printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout)) after_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout)) if (after_printer.get("identity") or {}).get("printer_uuid")!=uuid or after_jobs!=before_jobs: raise RuntimeError("identity or Inbox drifted after restart") - # Re-plan from exact live read-back. No mutation is needed when desired and observed Compose identities match. - live_compose=call("app.config",[EXPECTED_APP_NAME]) - replan_action="NOOP" if canonical_sha256(live_compose)==canonical_sha256(compose) else "UPDATE" - if replan_action!="NOOP": raise RuntimeError("exact desired/live Compose re-plan was not NOOP") + # F4: second-plan from exact live read-back must be a stable NOOP. + second_plan_app=query_optional("app.query",[["id","=",EXPECTED_APP_NAME]]) + second_plan_state=(second_plan_app or {}).get("state") + live_compose=call("app.config",[EXPECTED_APP_NAME]) if second_plan_app else None + second_plan_action=reconciliation_action(second_plan_state,live_compose,compose) + replan_action=second_plan_action + if second_plan_action!="NOOP": + raise RuntimeError(f"second-plan reconciliation was {second_plan_action}, expected NOOP") # Exercise the public update/redeploy path separately from stop/start while preserving durable identity and Inbox. uj=call("app.update",[EXPECTED_APP_NAME,{"custom_compose_config":compose}]) if not isinstance(uj,int): raise RuntimeError("app.update did not return job") @@ -579,6 +594,46 @@ def wait_state(name,state): redeploy_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout)) if (redeploy_printer.get("identity") or {}).get("printer_uuid")!=uuid or redeploy_jobs!=before_jobs: raise RuntimeError("identity or Inbox drifted after update/redeploy") + + # F5: product App deletion must retain the experiment-owned external dataset, + # and exact reinstall must recover durable identity, Inbox metadata, and bytes. + retained_before_delete=query_optional("pool.dataset.query",[["id","=",DATASET]]) + if retained_before_delete is None: + raise RuntimeError("fixture dataset missing before retain-data delete") + md=call("app.delete",[EXPECTED_APP_NAME,{"remove_images":False,"remove_ix_volumes":False,"force_remove_custom_app":False}]) + wait_job(md,"retain-data app delete"); app_created=False + if query_optional("app.query",[["id","=",EXPECTED_APP_NAME]]) is not None: + raise RuntimeError("product App remained after retain-data delete") + if query_optional("pool.dataset.query",[["id","=",DATASET]]) is None: + raise RuntimeError("external fixture dataset was not retained across App delete") + call("filesystem.stat",[TOKEN_PATH]) + + reinstall=call("app.create",[{"app_name":EXPECTED_APP_NAME,"custom_app":True,"custom_compose_config":compose}]) + if not isinstance(reinstall,int): raise RuntimeError("reinstall app.create did not return job") + app_created=True + wait_job(reinstall,"reinstall app.create"); reinstall_app=wait_state(EXPECTED_APP_NAME,"RUNNING") + if canonical_sha256(call("app.config",[EXPECTED_APP_NAME]))!=canonical_sha256(compose): + raise RuntimeError("reinstall app.config readback drifted") + if not wait_http(a.host,a.control_port,"/readyz",a.state_timeout): + raise RuntimeError("readyz failed after retain-data reinstall") + reinstall_printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout)) + reinstall_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout)) + if ((reinstall_printer.get("identity") or {}).get("printer_uuid")!=uuid + or reinstall_printer.get("public_uri")!=PUBLIC_URI + or reinstall_jobs!=before_jobs): + raise RuntimeError("identity or Inbox drifted after retain-data reinstall") + for item in (reinstall_jobs.get("items") or []): + artifact_sha=item.get("artifact_sha256") + job_id=item.get("job_id") + if not artifact_sha or not job_id: + raise RuntimeError("reinstalled Inbox item missing artifact identity") + blob=http_bytes(a.host,a.control_port,f"/api/v1/jobs/{job_id}/artifact",tok,timeout=a.timeout) + if sha256_bytes(blob)!=artifact_sha: + raise RuntimeError("retained artifact bytes drifted after reinstall") + reinstall_live=call("app.config",[EXPECTED_APP_NAME]) + reinstall_plan_action=reconciliation_action((reinstall_app or {}).get("state"),reinstall_live,compose) + if reinstall_plan_action!="NOOP": + raise RuntimeError(f"post-reinstall reconciliation was {reinstall_plan_action}, expected NOOP") od=call("app.delete",[OBSERVER_APP_NAME,{"remove_images":False,"remove_ix_volumes":False,"force_remove_custom_app":False}]); wait_job(od,"observer delete"); observer_created=False md=call("app.delete",[EXPECTED_APP_NAME,{"remove_images":False,"remove_ix_volumes":False,"force_remove_custom_app":False}]); wait_job(md,"app delete"); app_created=False if call("app.query",[[["id","in",[EXPECTED_APP_NAME,OBSERVER_APP_NAME]]]]): raise RuntimeError("app residue remains") @@ -593,11 +648,13 @@ def wait_state(name,state): payload.update({ "classification":"SUPPORTED","oracleSatisfied":True, "identity":{"printer_uuid":uuid,"public_uri":PUBLIC_URI,"control_cups_uuid_match":True,"dnssd_uuid_match":True,"dnssd_public_uri_match":True}, - "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP"}, + "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP","second_plan_noop":True,"retain_data_reinstall":True}, "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True,"service_instance":observed.get("service_instance"),"srv_target":PUBLIC_HOST,"srv_port":PUBLIC_IPP_PORT,"resource_path":PUBLIC_RESOURCE_PATH}, "cleanup":{"apps_absent":True,"fixture_dataset_absent":True,"fixture_mountpoint_absent":True,"zero_residue":True}, + "reconciliation":{"second_plan_action":second_plan_action,"post_reinstall_action":reinstall_plan_action,"inflight_policy":"WAIT","ambiguous_policy":"FAIL_CLOSED"}, + "owned_data":{"policy":"RETAIN_EXTERNAL_DATASET_ON_APP_DELETE_THEN_EXPLICIT_FIXTURE_CLEANUP","dataset_retained_across_app_delete":True,"identity_preserved_after_reinstall":True,"inbox_preserved_after_reinstall":True,"artifact_bytes_preserved_after_reinstall":True}, "producer_gate":{"replan_noop_required":True,"runtime_does_not_reconstruct_foundry_control":True}, - "detail":"exact Foundry-exported FolioRelay control realized on TrueNAS; exact three-service identity, portal/IPP, PDF+URF source preservation, independent in-guest DNS-SD observation, restart persistence, and zero-residue cleanup passed" + "detail":"exact Foundry-exported FolioRelay control realized on TrueNAS; exact three-service identity, portal/IPP, PDF+URF source preservation, independent in-guest DNS-SD observation, restart/update/redeploy persistence, second-plan NOOP reconciliation, retain-data delete/reinstall persistence, and final zero-residue cleanup passed" }) except Exception as exc: payload["detail"]=f"{type(exc).__name__}: {exc}" From d9cd7267a3a1ef980057095ece6e6214cb0f1629 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:59:45 +0200 Subject: [PATCH 389/719] test: lock FolioRelay F4 F5 acceptance semantics --- tests/test_truenas_t6_foliorelay_control.py | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index b14a160c1..36d5860c5 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -34,6 +34,7 @@ def test_probe_preserves_product_oracles_and_zero_residue(self): 'dnssd_uuid_match', 'restart_preserved_identity_and_inbox', 'app.update', 'app.redeploy', 'replan_action', '"NOOP"', 'update_redeploy_preserved_identity_and_inbox', + 'second_plan_noop', 'retain_data_reinstall', ): self.assertIn(needle, text) observer = (ROOT/"tools"/"foliorelay-observer"/"main.go").read_text(encoding="utf-8") @@ -43,6 +44,33 @@ def test_probe_preserves_product_oracles_and_zero_residue(self): self.assertNotIn("/run/dbus", text) self.assertNotIn("/var/run/docker.sock", text) + def test_f4_reconciliation_policy_and_f5_retention_contract(self): + exact={"services":{"control":{"image":"sha256:exact"}}} + drift={"services":{"control":{"image":"sha256:drift"}}} + self.assertEqual(MOD.reconciliation_action(None,None,exact),"CREATE") + self.assertEqual(MOD.reconciliation_action("DEPLOYING",None,exact),"WAIT") + self.assertEqual(MOD.reconciliation_action("STOPPING",None,exact),"WAIT") + self.assertEqual(MOD.reconciliation_action("CRASHED",exact,exact),"FAIL_CLOSED") + self.assertEqual(MOD.reconciliation_action("ERROR",exact,exact),"FAIL_CLOSED") + self.assertEqual(MOD.reconciliation_action("RUNNING",exact,exact),"NOOP") + self.assertEqual(MOD.reconciliation_action("STOPPED",exact,exact),"NOOP") + self.assertEqual(MOD.reconciliation_action("RUNNING",drift,exact),"UPDATE") + + text = SCRIPT.read_text(encoding="utf-8") + for needle in ( + 'second_plan_action=reconciliation_action', + 'second-plan reconciliation was', + '"inflight_policy":"WAIT"', + '"ambiguous_policy":"FAIL_CLOSED"', + '"retain_data_reinstall":True', + '"RETAIN_EXTERNAL_DATASET_ON_APP_DELETE_THEN_EXPLICIT_FIXTURE_CLEANUP"', + 'external fixture dataset was not retained across App delete', + 'identity or Inbox drifted after retain-data reinstall', + 'retained artifact bytes drifted after reinstall', + 'post-reinstall reconciliation was', + ): + self.assertIn(needle, text) + def test_observer_is_separate_host_network_app(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn('OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer"', text) From c5a60f4d3f747b203f6dbd2b2ecaec0b8e6b9e9a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:02:11 +0200 Subject: [PATCH 390/719] RDTE: request FolioRelay BETA.3 F0-F5 closure rep --- config/truenas-rdte-run-request.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index f89f0c1a0..799e088fd 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,11 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-container-c0-001", + "request_id": "tn-26-beta3-t6-foliorelay-013-f0-f5-closure", "version": "26.0.0-BETA.3", - "rung": "t3", - "compute_fixture": "container-c0", + "rung": "t6", + "product": "foliorelay", "authority_issue": 396, - "purpose": "Run the first product-API-native TrueNAS compute materialization C0 rep on exact 26.0.0-BETA.3 using the existing #396 disposable system workcell. Preserve exact ISO/middleware/T3 pool evidence, then resolve the admitted Ubuntu Noble system-container image through container.image.query_registry and exercise container.create/query/start/stop/update/stop+start/delete/absence through middleware APIs. This rep claims C0 lifecycle only; guest-level C1 execution/nonce, VM lifecycle, nested KVM, Firecracker, Windows 11, physical HIL, and cross-version inheritance remain explicitly unclaimed.", - "claim_boundary": "C0 native product-API container lifecycle only; C1 guest execution, VM lifecycle, Firecracker, Windows, cross-version support, and physical HIL remain OPEN", - "consumer_authority_issue": 426 + "consumer_authority_issue": 273, + "purpose": "Close the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after rep 012 earned product-specific T6 SUPPORTED/oracleSatisfied=true. Preserve exact product/Foundry/image/Compose/IPP/PDF/URF semantics. New acceptance obligations are bounded to already-cheap-qualified harness hardening: DNS-SD PTR/TXT/SRV evidence must correlate to one service instance; F4 must prove second-plan NOOP with WAIT for in-flight states and fail-closed ambiguity; F5 must delete the product App while retaining the external fixture dataset, reinstall exact Compose, prove printer identity/Inbox/artifact bytes persist, then finish with zero residue. No acceptance is inferred without SUPPORTED plus oracleSatisfied=true and all new receipt fields." } From eec73c19c858292c14b75fc0a9380797e0a185fd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:04:37 +0200 Subject: [PATCH 391/719] RDTE: route FolioRelay controls by exact TrueNAS target --- .github/workflows/gha-kvm-system-rdte.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index fc738cb41..7c1a21885 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -232,9 +232,10 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@fb41afd3d112f361b8c490aeb5915a978b956b20 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 with: - foundry_ref: fb41afd3d112f361b8c490aeb5915a978b956b20 + foundry_ref: e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 + target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -422,7 +423,7 @@ jobs: if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: - name: foliorelay-t6-beta3-control + name: foliorelay-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' @@ -506,7 +507,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="fb41afd3d112f361b8c490aeb5915a978b956b20" + foundry_commit="e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi @@ -544,7 +545,7 @@ jobs: if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: - name: gha-kvm-truenas-26-beta3-${{ github.run_attempt }} + name: gha-kvm-truenas-${{ needs.changes.outputs.truenas_version }}-${{ github.run_attempt }} path: ${{ runner.temp }}/gha-kvm-system-rdte/truenas-receipt.json if-no-files-found: error retention-days: 7 From b54e5a0f210180be49c2957ccfe133d5993ed3c6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:04:52 +0200 Subject: [PATCH 392/719] RDTE: qualify version-generic FolioRelay exporter --- .github/workflows/prep-foliorelay-t6-consumer.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 8cb732f86..95dc66b7b 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,10 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@fb41afd3d112f361b8c490aeb5915a978b956b20 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 with: - foundry_ref: fb41afd3d112f361b8c490aeb5915a978b956b20 + foundry_ref: e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 + target_version: 26.0.0-BETA.3 permissions: contents: read @@ -50,7 +51,7 @@ jobs: - name: Download exact Foundry FolioRelay control uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: - name: foliorelay-t6-beta3-control + name: foliorelay-t6-control path: ${{ runner.temp }}/foliorelay-control - name: Smoke minimal static independent DNS-SD observer shell: bash @@ -125,7 +126,7 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "fb41afd3d112f361b8c490aeb5915a978b956b20" and + .foundry_ref == "e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2" and .candidate.truenas_version == "26.0.0-BETA.3" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" and From e5b38e07aaa88c26b6b5999257dd9a5134cdd238 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:05:02 +0200 Subject: [PATCH 393/719] RDTE: make FolioRelay capsule artifact version-neutral --- config/truenas-capsule-providers.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/truenas-capsule-providers.json b/config/truenas-capsule-providers.json index 543841447..dd9f38ea1 100644 --- a/config/truenas-capsule-providers.json +++ b/config/truenas-capsule-providers.json @@ -42,7 +42,7 @@ "kind": "foundry-product", "product": "foliorelay", "probe": "scripts/truenas_middleware_foliorelay_t6_probe.py", - "control_artifact": "foliorelay-t6-beta3-control", + "control_artifact": "foliorelay-t6-control", "existing_selector": "foliorelay" } ] From a28d6984b79c84d8cebe755d0e44d4829366b78e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:05:15 +0200 Subject: [PATCH 394/719] RDTE: admit FolioRelay T6 on exact registered targets --- scripts/gha_kvm_truenas_rdte.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 85d72096f..a1df83802 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -71,7 +71,7 @@ if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then exit 2 fi if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then + if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } @@ -1012,6 +1012,8 @@ elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then --password-file "$PASSWORD_FILE" \ --control-dir "$FOUNDRY_CONTROL_DIR" \ --foundry-commit "$FOUNDRY_COMMIT" \ + --target-version "$VERSION" \ + --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 300 >/dev/null 2>&1 || true else python3 "$SCRIPT_DIR/truenas_middleware_litellm_t6_probe.py" \ From a7267f5620df2790fb4e7fa38ca45f0bdf2375cf Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:05:33 +0200 Subject: [PATCH 395/719] RDTE: bind FolioRelay probe to runtime exact target --- scripts/truenas_middleware_foliorelay_t6_probe.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 11a89bc0e..43c6622b1 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -20,8 +20,6 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for -EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" -EXPECTED_FOUNDRY_REF = "fb41afd3d112f361b8c490aeb5915a978b956b20" EXPECTED_SCHEMA = "semper-supra.foliorelay-truenas-t6-control/1" EXPECTED_APP_NAME = "rdte-t6-foliorelay" OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer" @@ -64,17 +62,17 @@ def load_json(path: pathlib.Path): raise RuntimeError(f"{path.name} must be an object") return value -def load_control(root: pathlib.Path, foundry_ref: str): +def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): control = load_json(root / "control.json") compose = load_json(root / "compose.json") if control.get("schema") != EXPECTED_SCHEMA: raise RuntimeError("unexpected FolioRelay T6 control schema") - if foundry_ref != EXPECTED_FOUNDRY_REF or control.get("foundry_ref") != EXPECTED_FOUNDRY_REF: + if control.get("foundry_ref") != foundry_ref: raise RuntimeError("Foundry source ref drifted") if control.get("secrets_captured") is not False: raise RuntimeError("control bundle does not assert secrets_captured=false") candidate = control.get("candidate") or {} - if candidate.get("truenas_version") != "26.0.0-BETA.3": + if candidate.get("truenas_version") != target_version: raise RuntimeError("TrueNAS target drifted") if candidate.get("control_image") != EXPECTED_CONTROL: raise RuntimeError("control image drifted") @@ -404,6 +402,7 @@ def main(): p.add_argument("--control-port",type=int,required=True); p.add_argument("--ipp-port",type=int,required=True); p.add_argument("--observer-port",type=int,required=True) p.add_argument("--observer-binary",type=pathlib.Path,required=True) p.add_argument("--password-file",required=True); p.add_argument("--control-dir",type=pathlib.Path,required=True); p.add_argument("--foundry-commit",required=True) + p.add_argument("--target-version",required=True); p.add_argument("--expected-system-version",required=True) p.add_argument("--out",required=True); p.add_argument("--tls",action="store_true"); p.add_argument("--timeout",type=float,default=8); p.add_argument("--job-timeout",type=float,default=300); p.add_argument("--state-timeout",type=float,default=300) a=p.parse_args() if not a.observer_binary.is_file(): @@ -414,7 +413,7 @@ def main(): if len(observer_bytes) > 6291456: raise RuntimeError("minimal observer binary exceeds 6 MiB budget") started=time.time(); ws=None; dataset_owned=False; app_created=False; observer_created=False - payload={"schema":"truenas-foliorelay-foundry-t6/v1","classification":"ORACLE_FAILURE","oracleSatisfied":False,"expected_version":EXPECTED_VERSION,"foundry_commit":a.foundry_commit,"app_name":EXPECTED_APP_NAME,"secret_values_captured":False} + payload={"schema":"truenas-foliorelay-foundry-t6/v1","classification":"ORACLE_FAILURE","oracleSatisfied":False,"expected_version":a.expected_system_version,"target_version":a.target_version,"foundry_commit":a.foundry_commit,"app_name":EXPECTED_APP_NAME,"secret_values_captured":False} payload["observer_fixture"]={ "implementation":"go-static", "carrier_image":OBSERVER_IMAGE, @@ -423,7 +422,7 @@ def main(): "run_as":"65534:65534", } try: - control,compose=load_control(a.control_dir,a.foundry_commit) + control,compose=load_control(a.control_dir,a.foundry_commit,a.target_version) payload["materialization"]={"schema":control["schema"],"foundry_ref":control["foundry_ref"],"compose_canonical_sha256":canonical_sha256(compose),"control_image":EXPECTED_CONTROL,"cups_image":EXPECTED_CUPS} password=pathlib.Path(a.password_file).read_text().strip() ws=WebSocket(a.host,a.port,timeout=a.timeout,tls=a.tls); ws.send_json({"msg":"connect","version":"1","support":["1"]}) @@ -458,7 +457,7 @@ def wait_state(name,state): raise RuntimeError(f"{name} did not reach {state}") auth=call("auth.login_ex",[{"mechanism":"PASSWORD_PLAIN","username":"truenas_admin","password":password}]) if not isinstance(auth,dict) or auth.get("response_type")!="SUCCESS": raise RuntimeError("authentication failed") - if call("system.version",[])!=EXPECTED_VERSION: raise RuntimeError("target version drifted") + if call("system.version",[])!=a.expected_system_version: raise RuntimeError("target version drifted") if call("app.query",[[["id","in",[EXPECTED_APP_NAME,OBSERVER_APP_NAME]]]]): raise RuntimeError("refusing adopted FolioRelay app state") if call("pool.dataset.query",[[["id","=",DATASET]]]): raise RuntimeError("refusing adopted FolioRelay dataset") ds=call("pool.dataset.create",[{"name":DATASET,"type":"FILESYSTEM","share_type":"GENERIC","comments":"SemperSupra disposable FolioRelay T6 fixture"}]) From ace5949478104582277ab6bb01f870d1fa0b8641 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:05:45 +0200 Subject: [PATCH 396/719] test: admit FolioRelay cross-version T6 routing --- tests/test_gha_kvm_system_rdte.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index e2346041c..0aa640a09 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -263,7 +263,7 @@ def test_g5_routing_is_source_exact_cross_version_and_public_safe(self): self.assertIn("truenas_middleware_garm_provider_g5_probe.py", text) self.assertIn('G5_MATRIX_PRODUCER', text) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" ]]; then', text, ) g5_probe = (ROOT / "scripts" / "truenas_middleware_garm_provider_g5_probe.py").read_text( From dfd9ff14310ed755bbace017dd192ed3747bba98 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:05:58 +0200 Subject: [PATCH 397/719] test: bind version-neutral FolioRelay capsule artifact --- tests/test_truenas_session_manifest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_truenas_session_manifest.py b/tests/test_truenas_session_manifest.py index f483c3b93..7f0b996a4 100644 --- a/tests/test_truenas_session_manifest.py +++ b/tests/test_truenas_session_manifest.py @@ -160,6 +160,8 @@ def test_provider_registry_wraps_existing_probes(self): } actual = {item["id"]: item["probe"] for item in registry["providers"]} self.assertEqual(actual, expected) + folio = next(item for item in registry["providers"] if item["id"] == "foliorelay-t6") + self.assertEqual(folio["control_artifact"], "foliorelay-t6-control") for probe in actual.values(): self.assertTrue((ROOT / probe).is_file(), probe) From 68a42e07e9c359f00feeda53a6b9ac9b61d611e6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:06:07 +0200 Subject: [PATCH 398/719] test: use version-neutral FolioRelay capsule artifact --- tests/test_truenas_single_capsule_adapter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_single_capsule_adapter.py b/tests/test_truenas_single_capsule_adapter.py index 58e6cb3bd..7e09e6d6d 100644 --- a/tests/test_truenas_single_capsule_adapter.py +++ b/tests/test_truenas_single_capsule_adapter.py @@ -20,7 +20,7 @@ "wow-sidecar-t6":("wow-sidecar","wow-sidecar-truenas-t6-control","scripts/truenas_middleware_wow_sidecar_t6_probe.py"), "garm-t6":("garm","garm-truenas-t6-control","scripts/truenas_middleware_garm_t6_probe.py"), "official-catalog-t6":("official-catalog","official-catalog-truenas-t6-control","scripts/truenas_middleware_official_catalog_t6_probe.py"), - "foliorelay-t6":("foliorelay","foliorelay-t6-beta3-control","scripts/truenas_middleware_foliorelay_t6_probe.py"), + "foliorelay-t6":("foliorelay","foliorelay-t6-control","scripts/truenas_middleware_foliorelay_t6_probe.py"), } def manifest(provider): From a727edecc486c87e72fd9f4df41767105f8d872e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:06:16 +0200 Subject: [PATCH 399/719] test: preserve official-catalog gate with FolioRelay exception --- tests/test_truenas_t6_official_catalog_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py index 3fa8f04be..5aa37d101 100644 --- a/tests/test_truenas_t6_official_catalog_control.py +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -182,7 +182,7 @@ def test_workflow_and_harness_bind_exact_official_catalog_producer(self): self.assertIn('"official-catalog"', harness) self.assertIn("truenas_middleware_official_catalog_t6_probe.py", harness) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" ]]; then', harness, ) From eb9e3896194001c5dd3c4901cf7445fac6efa606 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:06:39 +0200 Subject: [PATCH 400/719] test: bind FolioRelay consumer to runtime target --- tests/test_truenas_t6_foliorelay_control.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 36d5860c5..b685a46dd 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -18,7 +18,6 @@ class FolioRelayT6ContractTests(unittest.TestCase): def test_exact_product_and_foundry_identity_is_pinned(self): - self.assertEqual(MOD.EXPECTED_FOUNDRY_REF, "fb41afd3d112f361b8c490aeb5915a978b956b20") self.assertIn("@sha256:", MOD.EXPECTED_CONTROL) self.assertIn("@sha256:", MOD.EXPECTED_CUPS) self.assertEqual(MOD.DATASET, "rdtepool/foliorelay-t6") @@ -157,21 +156,35 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@" + MOD.EXPECTED_FOUNDRY_REF, workflow) - self.assertIn("name: foliorelay-t6-beta3-control", workflow) - self.assertIn('foundry_commit="' + MOD.EXPECTED_FOUNDRY_REF + '"', workflow) + self.assertIn("export-foliorelay-t6-control.yml@e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2", workflow) + self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) + self.assertIn("name: foliorelay-t6-control", workflow) + self.assertIn('foundry_commit="e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) self.assertIn('FOLIORELAY_OBSERVER_HOST_PORT', harness) self.assertIn('--observer-port "$FOLIORELAY_OBSERVER_HOST_PORT"', harness) self.assertIn('--observer-binary "$FOLIORELAY_OBSERVER_BINARY"', harness) + self.assertIn('--target-version "$VERSION"', harness) + self.assertIn('--expected-system-version "$EXPECTED_SYSTEM_VERSION"', harness) + self.assertIn('"$T6_PRODUCT" != "foliorelay"', harness) self.assertIn('CGO_ENABLED=0 GOOS=linux GOARCH=amd64', harness) self.assertIn('cd "$SCRIPT_DIR/.."', harness) self.assertIn('./tools/foliorelay-observer', harness) self.assertNotIn('"$SCRIPT_DIR/../tools/foliorelay-observer" ||', harness) self.assertIn('observer_size <= 6291456', harness) + def test_probe_binds_runtime_target_and_foundry_identity_from_arguments(self): + text = SCRIPT.read_text(encoding="utf-8") + self.assertIn('control.get("foundry_ref") != foundry_ref', text) + self.assertIn('candidate.get("truenas_version") != target_version', text) + self.assertIn('call("system.version",[])!=a.expected_system_version', text) + self.assertIn('p.add_argument("--target-version",required=True)', text) + self.assertIn('p.add_argument("--expected-system-version",required=True)', text) + self.assertNotIn('EXPECTED_FOUNDRY_REF =', text) + self.assertNotIn('EXPECTED_VERSION =', text) + def test_cups_uuid_oracle_preserves_canonical_urn_prefix(self): attrs = """ printer-uuid (uri) = urn:uuid:01234567-89ab-4def-8123-456789abcdef From 87c369780eff6e889ef43517f2dfef3dd301a078 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:27:19 +0200 Subject: [PATCH 401/719] RDTE: request FolioRelay 25.10.7 F0-F5 qualification --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 799e088fd..9f45cfb88 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-013-f0-f5-closure", - "version": "26.0.0-BETA.3", + "request_id": "tn-25-10-7-t6-foliorelay-001-f0-f5", + "version": "25.10.7", "rung": "t6", "product": "foliorelay", - "authority_issue": 396, + "authority_issue": 410, "consumer_authority_issue": 273, - "purpose": "Close the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after rep 012 earned product-specific T6 SUPPORTED/oracleSatisfied=true. Preserve exact product/Foundry/image/Compose/IPP/PDF/URF semantics. New acceptance obligations are bounded to already-cheap-qualified harness hardening: DNS-SD PTR/TXT/SRV evidence must correlate to one service instance; F4 must prove second-plan NOOP with WAIT for in-flight states and fail-closed ambiguity; F5 must delete the product App while retaining the external fixture dataset, reinstall exact Compose, prove printer identity/Inbox/artifact bytes persist, then finish with zero residue. No acceptance is inferred without SUPPORTED plus oracleSatisfied=true and all new receipt fields." + "purpose": "Run the first exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 after the BETA.3 row closed with complete F0-F5 evidence. No support is inherited from BETA.3 or generic T0-T5. Use the exact registered 25.10.7 ISO/middleware/profile, version-aware Foundry export, pinned FolioRelay control/CUPS images, service-instance-safe DNS-SD observer, exact PDF/URF source preservation, second-plan NOOP reconciliation, retain-data App delete/reinstall persistence, and final zero-residue cleanup. Acceptance requires a retained SUPPORTED/oracleSatisfied=true receipt with complete F0-F5 evidence." } From af5f9b14a499ac9e223b410c300576d514c9ca65 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:50:56 +0200 Subject: [PATCH 402/719] RDTE: preserve FolioRelay product crash evidence --- .../truenas_middleware_foliorelay_t6_probe.py | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 43c6622b1..80e6ccc6c 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -447,12 +447,60 @@ def wait_job(j,label): raise JobFailure(label, x) time.sleep(1) raise RuntimeError(f"{label} job timeout") + def capture_product_runtime_failure(name,app): + diagnostic={"app":bounded_app_snapshot(app)} + try: + download=call("core.download",["filesystem.get",["/var/log/app_lifecycle.log"],"app_lifecycle.log",True]) + if not (isinstance(download,list) and len(download)==2 and isinstance(download[0],int) and isinstance(download[1],str)): + raise RuntimeError("core.download app_lifecycle contract drifted") + wait_job(download[0],"product app lifecycle log download") + lifecycle=middleware_http_bytes(a.host,a.port,a.tls,download[1],timeout=a.timeout).decode("utf-8","replace") + diagnostic["app_lifecycle_excerpt"]=bounded_observer_lifecycle_excerpt(lifecycle,app_name=name) + except Exception as lifecycle_exc: + diagnostic["app_lifecycle_capture_error"]=f"{type(lifecycle_exc).__name__}: {lifecycle_exc}" + try: + log_tails=[] + details=((app.get("active_workloads") or {}).get("container_details") or []) if isinstance(app,dict) else [] + for detail in details[:8]: + container_id=detail.get("id") or detail.get("container_id") + if not container_id: + continue + service_name=detail.get("service_name") + container_state=str(detail.get("state") or "").lower() + if service_name!="discovery" and container_state not in {"crashed","exited","restarting"}: + continue + try: + entry=capture_container_log_tail(ws,name,str(container_id)) + entry["service_name"]=service_name + entry["container_state"]=container_state + log_tails.append(entry) + except Exception as log_exc: + log_tails.append({ + "container_id":str(container_id), + "service_name":service_name, + "container_state":container_state, + "capture_error":f"{type(log_exc).__name__}: {log_exc}", + }) + diagnostic["container_log_tails"]=log_tails + except Exception as log_group_exc: + diagnostic["container_log_capture_error"]=f"{type(log_group_exc).__name__}: {log_group_exc}" + payload["product_runtime_failure"]=diagnostic + def wait_state(name,state): deadline=time.monotonic()+a.state_timeout while time.monotonic() Date: Mon, 5 Oct 2026 19:51:13 +0200 Subject: [PATCH 403/719] test: lock product crash diagnostics before cleanup --- tests/test_truenas_t6_foliorelay_control.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index b685a46dd..3aab7acd8 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -95,6 +95,25 @@ def test_observer_lifecycle_is_independent_from_mdns_oracle_result(self): self.assertNotIn("OBSERVER = r'''", text) + def test_product_crash_preserves_discovery_diagnostics_before_cleanup(self): + text = SCRIPT.read_text(encoding="utf-8") + for needle in ( + 'def capture_product_runtime_failure(name,app):', + 'payload["product_runtime_failure"]', + 'bounded_app_snapshot(app)', + 'bounded_observer_lifecycle_excerpt(lifecycle,app_name=name)', + 'service_name!="discovery"', + 'container_state not in {"crashed","exited","restarting"}', + 'capture_container_log_tail(ws,name,str(container_id))', + '"service_name":service_name', + '"container_state":container_state', + ): + self.assertIn(needle, text) + self.assertLess( + text.index('capture_product_runtime_failure(name,x)'), + text.index('raise RuntimeError(f"{name} entered {x.get(\'state\')}")'), + ) + def test_failed_observer_create_preserves_diagnostics_and_cleans_owned_state(self): text = SCRIPT.read_text(encoding="utf-8") self.assertIn("class JobFailure", text) From eb2bab8241d5f39ef62d3fb28c80010a791340ff Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:53:36 +0200 Subject: [PATCH 404/719] RDTE: request 25.10.7 FolioRelay discovery diagnostic rep --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 9f45cfb88..24f839ced 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-foliorelay-001-f0-f5", + "request_id": "tn-25-10-7-t6-foliorelay-002-discovery-crash-diagnostics", "version": "25.10.7", "rung": "t6", "product": "foliorelay", "authority_issue": 410, "consumer_authority_issue": 273, - "purpose": "Run the first exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 after the BETA.3 row closed with complete F0-F5 evidence. No support is inherited from BETA.3 or generic T0-T5. Use the exact registered 25.10.7 ISO/middleware/profile, version-aware Foundry export, pinned FolioRelay control/CUPS images, service-instance-safe DNS-SD observer, exact PDF/URF source preservation, second-plan NOOP reconciliation, retain-data App delete/reinstall persistence, and final zero-residue cleanup. Acceptance requires a retained SUPPORTED/oracleSatisfied=true receipt with complete F0-F5 evidence." + "purpose": "Re-run exact FolioRelay F0-F5 on TrueNAS 25.10.7 after row 001 localized failure to the discovery container. Preserve product/Foundry/image/Compose/DNS-SD/lifecycle semantics unchanged. The only causal delta is bounded pre-cleanup product-crash diagnostics: App/container snapshot, discovery/crashed-container log tails, and scoped app_lifecycle excerpt. This rep exists to identify the exact discovery exit cause; it must not be promoted as support unless the unchanged product unexpectedly reaches SUPPORTED/oracleSatisfied=true." } From b9c217344cd540e21db52d8f27852d64fc87a6b7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:19:57 +0200 Subject: [PATCH 405/719] RDTE: align BETA.3 container stop options with exact API --- scripts/truenas_compute_container_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_compute_container_probe.py b/scripts/truenas_compute_container_probe.py index 8234e75b2..30a15770d 100644 --- a/scripts/truenas_compute_container_probe.py +++ b/scripts/truenas_compute_container_probe.py @@ -186,7 +186,7 @@ def method_shapes(adapter_id: str, name: str, row_id: Any | None = None) -> dict "create": ("container.create", [], True), "update": ("container.update", [row_id], False), "start": ("container.start", [row_id], False), - "stop": ("container.stop", [row_id, {"timeout": 30, "force_after_timeout": True}], True), + "stop": ("container.stop", [row_id, {"force_after_timeout": True}], True), "delete": ("container.delete", [row_id], False), } raise ProbeError(f"unsupported container adapter: {adapter_id}") From 26779ba7e8543dddf4a569bf8c1391143b401e42 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:19:59 +0200 Subject: [PATCH 406/719] RDTE: lock exact container stop option shape --- tests/test_truenas_compute_container_probe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_truenas_compute_container_probe.py b/tests/test_truenas_compute_container_probe.py index 88b9b5d24..ef132a3d7 100644 --- a/tests/test_truenas_compute_container_probe.py +++ b/tests/test_truenas_compute_container_probe.py @@ -109,6 +109,8 @@ def test_method_shapes_never_use_private_shell(self): self.assertNotIn("get_shell", {v[0] for v in legacy.values()}) lxc = method_shapes("truenas-container-lxc", "x", 7) self.assertEqual(lxc["start"][0], "container.start") + self.assertEqual(lxc["stop"], ("container.stop", [7, {"force_after_timeout": True}], True)) + self.assertNotIn("timeout", lxc["stop"][1][1]) self.assertNotIn("nsenter", {v[0] for v in lxc.values()}) From 4c4d91c9b9344a09c384751a00a36cc19f314c0a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:26:48 +0200 Subject: [PATCH 407/719] RDTE: retain read-only Proxmox REST API package census --- scripts/gha_kvm_proxmox_rdte.sh | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 79347fb2c..ce7008add 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -17,6 +17,7 @@ DISK_SIZE="40G" MIN_HOST_MEM_KIB=$((6 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((16 * 1024 * 1024)) ROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR]" @@ -46,10 +47,24 @@ API_VERSION_JSON="" HOSTFWD_API_VERSION_JSON="" GUEST_LOCAL_API_VERSION_JSON="" API_OBSERVATION_ROUTE="" +# Read-only native REST census: capture exact installed PVE/package identity for +# later product-API materialization admission without changing this system claim. +PVE_PASSWORD_FILE="$STATE_DIR/pve-rest-password" +printf '%s\n' "$ROOT_PASSWORD" >"$PVE_PASSWORD_FILE" +chmod 0400 "$PVE_PASSWORD_FILE" +REST_API_CENSUS_JSON="$( + python3 "$SCRIPT_DIR/proxmox_rest_compute_probe.py" \ + --base-url "https://127.0.0.1:$WEB_PORT" \ + --password-file "$PVE_PASSWORD_FILE" \ + --kind vm --observe-only 2>/dev/null || true +)" +rm -f "$PVE_PASSWORD_FILE" + NESTED_KVM="unknown" NESTED_KVM_INDICATORS="unknown" NESTED_KVM_VCPU_JSON="" P3_LXC_JSON="" +REST_API_CENSUS_JSON="" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" @@ -80,7 +95,7 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" - export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" R_P3_LXC="$P3_LXC_JSON" + export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" R_P3_LXC="$P3_LXC_JSON" R_REST_API_CENSUS="$REST_API_CENSUS_JSON" export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_GUEST_LOCAL_API="$GUEST_LOCAL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" @@ -101,6 +116,7 @@ def load_json_env(name): nested_vcpu = load_json_env("R_NESTED_VCPU") p3_lxc = load_json_env("R_P3_LXC") +rest_api_census = load_json_env("R_REST_API_CENSUS") payload = { "contract": "gha-kvm-system-lab/v1", @@ -138,11 +154,13 @@ payload = { "nested_kvm_observed_via_ssh": os.environ.get("R_NESTED") == "yes", "nested_kvm_vcpu_executed": bool(nested_vcpu and nested_vcpu.get("oracleSatisfied") is True), "p3_lxc_lifecycle_exercised": bool(p3_lxc and p3_lxc.get("oracleSatisfied") is True), + "rest_api_census_observed": bool(rest_api_census and rest_api_census.get("oracleSatisfied") is True and rest_api_census.get("phase") == "observe-only"), }, "api_version": json.loads(os.environ["R_API"]) if os.environ.get("R_API") else None, "nested_kvm": os.environ.get("R_NESTED"), "p5_nested_kvm": nested_vcpu, "p3_lxc": p3_lxc, + "rest_api_census": rest_api_census, "diagnostics": { "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), "ssh_hostfwd_accepted": os.environ.get("R_SSH_HOSTFWD_ACCEPTED") == "true", @@ -163,6 +181,7 @@ payload = { "Nested KVM is a separate oracle from Proxmox management-plane support.", "P5 nested KVM requires p5_nested_kvm.oracleSatisfied=true from an actual nested vCPU debug-exit; device/CPU flags alone are diagnostic.", "P3 LXC is a separate lifecycle oracle using one exact public Debian template; P2/P5 are not inferred from it.", + "REST API census is read-only discovery evidence only; it does not claim LXC or QEMU materialization.", ], } pathlib.Path(os.environ["R_OUT"]).write_text(json.dumps(payload, indent=2, sort_keys=True)+"\n", encoding="utf-8") From b83f730de29dc0b847f7277e6f17dd2d353ddcab Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:26:52 +0200 Subject: [PATCH 408/719] RDTE: lock Proxmox REST census claim boundary --- tests/test_gha_kvm_system_rdte.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 0aa640a09..0ec372d52 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -127,6 +127,14 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("losetup --find --show --read-only --partscan", text) self.assertNotIn("qemu-nbd --connect", text) + def test_proxmox_rest_api_census_is_read_only_and_retained(self): + text = PVE.read_text(encoding="utf-8") + self.assertIn("proxmox_rest_compute_probe.py", text) + self.assertIn("--kind vm --observe-only", text) + self.assertIn('"rest_api_census_observed"', text) + self.assertIn('"rest_api_census": rest_api_census', text) + self.assertIn("REST API census is read-only discovery evidence only", text) + def test_proxmox_hostfwd_is_diagnostic_not_guest_or_nested_oracle(self): text = PVE.read_text(encoding="utf-8") self.assertNotIn('NESTED_KVM="no"', text) From b682b92740d68b57d5afa8f46618c600390cb64f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:33:39 +0200 Subject: [PATCH 409/719] RDTE: request BETA.3 container C0 rep 002 on current head --- config/truenas-rdte-run-request.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 24f839ced..3046d9e17 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-foliorelay-002-discovery-crash-diagnostics", - "version": "25.10.7", - "rung": "t6", - "product": "foliorelay", - "authority_issue": 410, - "consumer_authority_issue": 273, - "purpose": "Re-run exact FolioRelay F0-F5 on TrueNAS 25.10.7 after row 001 localized failure to the discovery container. Preserve product/Foundry/image/Compose/DNS-SD/lifecycle semantics unchanged. The only causal delta is bounded pre-cleanup product-crash diagnostics: App/container snapshot, discovery/crashed-container log tails, and scoped app_lifecycle excerpt. This rep exists to identify the exact discovery exit cause; it must not be promoted as support unless the unchanged product unexpectedly reaches SUPPORTED/oracleSatisfied=true." + "request_id": "tn-26-beta3-t3-compute-container-c0-002-stop-options", + "version": "26.0.0-BETA.3", + "rung": "t3", + "compute_fixture": "container-c0", + "authority_issue": 396, + "consumer_authority_issue": 426, + "purpose": "Re-run the exact native TrueNAS 26.0.0-BETA.3 container C0 materialization fixture after rep 001 reached the product API but failed because the harness supplied unsupported container.stop options.timeout. Preserve target, image discovery, create/readback/start/update/restart-or-stop+start/delete/absence, pool, cleanup and C1 claim-boundary semantics. The only causal delta is merged PR #282: remove options.timeout while retaining force_after_timeout. No C0 acceptance is inferred without SUPPORTED plus oracleSatisfied=true and c0_oracle_satisfied=true with zero-residue cleanup." } From ea6a74c2e646feeb2d6138b2451ce8863d310111 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:37:15 +0200 Subject: [PATCH 410/719] RDTE: run Proxmox REST census after installed API gate --- scripts/gha_kvm_proxmox_rdte.sh | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index ce7008add..149c84ef1 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -47,19 +47,6 @@ API_VERSION_JSON="" HOSTFWD_API_VERSION_JSON="" GUEST_LOCAL_API_VERSION_JSON="" API_OBSERVATION_ROUTE="" -# Read-only native REST census: capture exact installed PVE/package identity for -# later product-API materialization admission without changing this system claim. -PVE_PASSWORD_FILE="$STATE_DIR/pve-rest-password" -printf '%s\n' "$ROOT_PASSWORD" >"$PVE_PASSWORD_FILE" -chmod 0400 "$PVE_PASSWORD_FILE" -REST_API_CENSUS_JSON="$( - python3 "$SCRIPT_DIR/proxmox_rest_compute_probe.py" \ - --base-url "https://127.0.0.1:$WEB_PORT" \ - --password-file "$PVE_PASSWORD_FILE" \ - --kind vm --observe-only 2>/dev/null || true -)" -rm -f "$PVE_PASSWORD_FILE" - NESTED_KVM="unknown" NESTED_KVM_INDICATORS="unknown" NESTED_KVM_VCPU_JSON="" @@ -1029,6 +1016,19 @@ if [[ "$API_VERSION_JSON" != *'"data"'* ]]; then fail_evidence ORACLE_FAILURE installed-api "installed Proxmox HTTPS API did not answer via direct hostfwd or SSH-observed guest-local HTTPS (qemu_alive=$QEMU_ALIVE_AT_API_GATE first_boot_witness=$FIRST_BOOT_WITNESS_OBSERVED)" fi +# Read-only native REST census: capture exact installed PVE/package identity only +# after the installed HTTPS API is known-good. +PVE_PASSWORD_FILE="$STATE_DIR/pve-rest-password" +printf '%s\n' "$ROOT_PASSWORD" >"$PVE_PASSWORD_FILE" +chmod 0400 "$PVE_PASSWORD_FILE" +REST_API_CENSUS_JSON="$( + python3 "$SCRIPT_DIR/proxmox_rest_compute_probe.py" \ + --base-url "https://127.0.0.1:$WEB_PORT" \ + --password-file "$PVE_PASSWORD_FILE" \ + --kind vm --observe-only 2>/dev/null || true +)" +rm -f "$PVE_PASSWORD_FILE" + NESTED_KVM="unknown" NESTED_KVM_INDICATORS="unknown" if command -v sshpass >/dev/null 2>&1; then From 5222500c7b3f96a9b69e2cfafaae6cd36213b73b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:37:18 +0200 Subject: [PATCH 411/719] RDTE: lock REST census runtime placement --- tests/test_gha_kvm_system_rdte.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 0ec372d52..aee0e4e3c 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -134,6 +134,10 @@ def test_proxmox_rest_api_census_is_read_only_and_retained(self): self.assertIn('"rest_api_census_observed"', text) self.assertIn('"rest_api_census": rest_api_census', text) self.assertIn("REST API census is read-only discovery evidence only", text) + census = text.index("# Read-only native REST census: capture exact installed PVE/package identity only") + self.assertLess(text.index('WEB_PORT="$(python3 - <<\'PY\''), census) + self.assertLess(text.index("installed Proxmox HTTPS API did not answer"), census) + self.assertEqual(text.count('--kind vm --observe-only'), 1) def test_proxmox_hostfwd_is_diagnostic_not_guest_or_nested_oracle(self): text = PVE.read_text(encoding="utf-8") From 551dddec6cbd95c2ee815799a4b95015039b7b1d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:41:30 +0200 Subject: [PATCH 412/719] RDTE: add opt-in native Proxmox REST container C0 --- scripts/gha_kvm_proxmox_rdte.sh | 172 +++++++++++++++++++++++++++----- 1 file changed, 149 insertions(+), 23 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 149c84ef1..87d70017d 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -20,20 +20,23 @@ ROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { - echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR]" + echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR] [--compute-fixture none|container-c0]" } OUT="" STATE_DIR="" +COMPUTE_FIXTURE="none" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; --state-dir) STATE_DIR="$2"; shift 2 ;; + --compute-fixture) COMPUTE_FIXTURE="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } +[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-proxmox.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -52,6 +55,7 @@ NESTED_KVM_INDICATORS="unknown" NESTED_KVM_VCPU_JSON="" P3_LXC_JSON="" REST_API_CENSUS_JSON="" +REST_C0_JSON="" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" @@ -82,7 +86,7 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" - export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" R_P3_LXC="$P3_LXC_JSON" R_REST_API_CENSUS="$REST_API_CENSUS_JSON" + export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" R_P3_LXC="$P3_LXC_JSON" R_REST_API_CENSUS="$REST_API_CENSUS_JSON" R_REST_C0="$REST_C0_JSON" R_COMPUTE_FIXTURE="$COMPUTE_FIXTURE" export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_GUEST_LOCAL_API="$GUEST_LOCAL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" @@ -104,6 +108,7 @@ def load_json_env(name): nested_vcpu = load_json_env("R_NESTED_VCPU") p3_lxc = load_json_env("R_P3_LXC") rest_api_census = load_json_env("R_REST_API_CENSUS") +rest_c0 = load_json_env("R_REST_C0") payload = { "contract": "gha-kvm-system-lab/v1", @@ -112,7 +117,7 @@ payload = { "oracleSatisfied": os.environ["R_ORACLE"].lower() == "true", "phase": os.environ["R_PHASE"], "detail": os.environ["R_DETAIL"], - "requested_shape": {"vcpus": 2, "ram_mib": 4096, "disk": "40G"}, + "requested_shape": {"vcpus": 2, "ram_mib": 4096, "disk": "40G", "compute_fixture": os.environ.get("R_COMPUTE_FIXTURE", "none")}, "source": { "iso_name": "proxmox-ve_9.2-1.iso", "iso_url": "https://enterprise.proxmox.com/iso/proxmox-ve_9.2-1.iso", @@ -142,12 +147,14 @@ payload = { "nested_kvm_vcpu_executed": bool(nested_vcpu and nested_vcpu.get("oracleSatisfied") is True), "p3_lxc_lifecycle_exercised": bool(p3_lxc and p3_lxc.get("oracleSatisfied") is True), "rest_api_census_observed": bool(rest_api_census and rest_api_census.get("oracleSatisfied") is True and rest_api_census.get("phase") == "observe-only"), + "rest_api_container_c0_exercised": bool(rest_c0 and rest_c0.get("oracleSatisfied") is True and rest_c0.get("api_materialization_oracle") is True), }, "api_version": json.loads(os.environ["R_API"]) if os.environ.get("R_API") else None, "nested_kvm": os.environ.get("R_NESTED"), "p5_nested_kvm": nested_vcpu, "p3_lxc": p3_lxc, "rest_api_census": rest_api_census, + "rest_api_container_c0": rest_c0, "diagnostics": { "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), "ssh_hostfwd_accepted": os.environ.get("R_SSH_HOSTFWD_ACCEPTED") == "true", @@ -169,6 +176,7 @@ payload = { "P5 nested KVM requires p5_nested_kvm.oracleSatisfied=true from an actual nested vCPU debug-exit; device/CPU flags alone are diagnostic.", "P3 LXC is a separate lifecycle oracle using one exact public Debian template; P2/P5 are not inferred from it.", "REST API census is read-only discovery evidence only; it does not claim LXC or QEMU materialization.", + "REST C0, when explicitly requested, proves only product-API LXC create/readback/start/stop/delete/absence; guest execution remains a separate C1 oracle.", ], } pathlib.Path(os.environ["R_OUT"]).write_text(json.dumps(payload, indent=2, sort_keys=True)+"\n", encoding="utf-8") @@ -535,42 +543,130 @@ guest_diagnostics() { -probe_lxc_lifecycle() { +P3_STAGE_CLASS="" +P3_STAGE_PHASE="" +P3_STAGE_DETAIL="" + +stage_exact_p3_template() { local template_host="$STATE_DIR/$P3_TEMPLATE_NAME" local template_guest="/var/lib/vz/template/cache/$P3_TEMPLATE_NAME" - local observed_sha="" response="" + local observed_sha="" + + P3_STAGE_CLASS="ENVIRONMENT_FAILURE" + P3_STAGE_PHASE="host-prerequisite" + P3_STAGE_DETAIL="sha512sum or scp unavailable on GHA host" + command -v sha512sum >/dev/null 2>&1 && command -v scp >/dev/null 2>&1 || return 1 + + P3_STAGE_PHASE="template-acquire" + P3_STAGE_DETAIL="exact public LXC template download failed" + curl --fail --location --retry 3 --silent --show-error "$P3_TEMPLATE_URL" -o "$template_host" || return 1 - if ! command -v sha512sum >/dev/null 2>&1 || ! command -v scp >/dev/null 2>&1; then - printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"host-prerequisite","detail":"sha512sum or scp unavailable on GHA host"}' - return 0 - fi - if ! curl --fail --location --retry 3 --silent --show-error "$P3_TEMPLATE_URL" -o "$template_host"; then - printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"template-acquire","detail":"exact public LXC template download failed"}' - return 0 - fi observed_sha="$(sha512sum "$template_host" | awk '{print $1}')" if [[ "$observed_sha" != "$P3_TEMPLATE_SHA512" ]]; then - R_OBS="$observed_sha" R_EXP="$P3_TEMPLATE_SHA512" python3 - <<'PY' -import json, os -print(json.dumps({"contract":"proxmox-lxc-lifecycle/v1","classification":"ORACLE_FAILURE","oracleSatisfied":False,"phase":"template-integrity","detail":"downloaded template SHA-512 did not match exact admitted PVE catalog","expected_sha512":os.environ["R_EXP"],"observed_sha512":os.environ["R_OBS"]},separators=(",",":"))) -PY rm -f -- "$template_host" - return 0 + P3_STAGE_CLASS="ORACLE_FAILURE" + P3_STAGE_PHASE="template-integrity" + P3_STAGE_DETAIL="downloaded template SHA-512 did not match exact admitted PVE catalog" + return 1 fi + P3_STAGE_CLASS="ENVIRONMENT_FAILURE" + P3_STAGE_PHASE="template-stage" + P3_STAGE_DETAIL="template destination already existed or guest cache could not be prepared" if ! sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ root@127.0.0.1 "test ! -e '$template_guest' && install -d -m 0755 /var/lib/vz/template/cache" >/dev/null 2>&1; then rm -f -- "$template_host" - printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"template-stage","detail":"template destination already existed or guest cache could not be prepared"}' - return 0 + return 1 fi + P3_STAGE_DETAIL="exact template could not be copied into PVE" if ! sshpass -p "$ROOT_PASSWORD" scp -q -P "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ "$template_host" "root@127.0.0.1:$template_guest"; then rm -f -- "$template_host" - printf '%s' '{"contract":"proxmox-lxc-lifecycle/v1","classification":"ENVIRONMENT_FAILURE","oracleSatisfied":false,"phase":"template-stage","detail":"exact template could not be copied into PVE"}' - return 0 + return 1 fi rm -f -- "$template_host" + return 0 +} + +stage_failure_json() { + R_CLASS="$P3_STAGE_CLASS" R_PHASE="$P3_STAGE_PHASE" R_DETAIL="$P3_STAGE_DETAIL" python3 - <<'PY' +import json, os +print(json.dumps({ + "classification": os.environ["R_CLASS"], + "oracleSatisfied": False, + "phase": os.environ["R_PHASE"], + "detail": os.environ["R_DETAIL"], +}, separators=(",", ":"))) +PY +} + +probe_rest_lxc_c0() { + local template_guest="/var/lib/vz/template/cache/$P3_TEMPLATE_NAME" + local password_file="$STATE_DIR/pve-rest-c0-password" + local probe_out="$STATE_DIR/proxmox-rest-c0.json" + local template_absent="false" + + if ! stage_exact_p3_template; then + R_STAGE="$(stage_failure_json)" python3 - <<'PY' +import json, os +p=json.loads(os.environ["R_STAGE"]) +p.update({"schema":"proxmox-rest-compute-c0-v1","kind":"container","api_materialization_oracle":False,"cleanup":{"attempted":False,"absent":False,"fixture_input_absent":False}}) +print(json.dumps(p,separators=(",",":"))) +PY + return 0 + fi + + printf '%s\n' "$ROOT_PASSWORD" >"$password_file" + chmod 0400 "$password_file" + rm -f -- "$probe_out" + python3 "$SCRIPT_DIR/proxmox_rest_compute_probe.py" \ + --base-url "https://127.0.0.1:$WEB_PORT" \ + --password-file "$password_file" \ + --kind container \ + --vmid "$P3_VMID" \ + --template "local:vztmpl/$P3_TEMPLATE_NAME" \ + --apply --out "$probe_out" >/dev/null 2>&1 || true + rm -f -- "$password_file" + + sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 "rm -f -- '$template_guest'; test ! -e '$template_guest'" >/dev/null 2>&1 && template_absent="true" + + if [[ ! -s "$probe_out" ]]; then + printf '%s' '{"schema":"proxmox-rest-compute-c0-v1","kind":"container","classification":"HARNESS_FAILURE","oracleSatisfied":false,"api_materialization_oracle":false,"phase":"rest-api-lifecycle","detail":"REST materializer exited without a receipt","cleanup":{"attempted":true,"absent":false,"fixture_input_absent":false}}' + return 0 + fi + + R_PROBE_OUT="$probe_out" R_TEMPLATE_ABSENT="$template_absent" R_TEMPLATE_SHA="$P3_TEMPLATE_SHA512" python3 - <<'PY' +import json, os, pathlib +p=json.loads(pathlib.Path(os.environ["R_PROBE_OUT"]).read_text()) +cleanup=p.setdefault("cleanup",{}) +cleanup["fixture_input_absent"]=os.environ["R_TEMPLATE_ABSENT"]=="true" +p["fixture_input"]={ + "template":"debian-13-standard_13.1-2_amd64.tar.zst", + "catalog_sha512":os.environ["R_TEMPLATE_SHA"], + "transport":"bounded SSH staging only; all container lifecycle mutation uses PVE REST", +} +if not cleanup["fixture_input_absent"]: + p["classification"]="ORACLE_FAILURE" + p["oracleSatisfied"]=False + p["api_materialization_oracle"]=False + p["detail"]=(p.get("detail","") + "; exact staged template remained after fixture cleanup").lstrip("; ") +print(json.dumps(p,separators=(",",":"))) +PY +} + +probe_lxc_lifecycle() { + local response="" + if ! stage_exact_p3_template; then + R_STAGE="$(stage_failure_json)" python3 - <<'PY' +import json, os +p=json.loads(os.environ["R_STAGE"]) +p["contract"]="proxmox-lxc-lifecycle/v1" +print(json.dumps(p,separators=(",",":"))) +PY + return 0 + fi response="$(sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ root@127.0.0.1 'bash -s' 2>/dev/null <<'REMOTE' || true @@ -1029,6 +1125,34 @@ REST_API_CENSUS_JSON="$( )" rm -f "$PVE_PASSWORD_FILE" +if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then + REST_C0_JSON="$(probe_rest_lxc_c0 || true)" + if ! R_REST_C0="$REST_C0_JSON" python3 - <<'PY' +import json, os +try: + p=json.loads(os.environ.get("R_REST_C0","")) + ok=(p.get("classification")=="SUPPORTED" and p.get("oracleSatisfied") is True and + p.get("api_materialization_oracle") is True and + p.get("cleanup",{}).get("absent") is True and + p.get("cleanup",{}).get("fixture_input_absent") is True) +except Exception: + ok=False +raise SystemExit(0 if ok else 1) +PY + then + REST_C0_CLASS="$(R_REST_C0="$REST_C0_JSON" python3 - <<'PY' +import json, os +try: + c=json.loads(os.environ.get("R_REST_C0","")).get("classification","ORACLE_FAILURE") +except Exception: + c="HARNESS_FAILURE" +print(c if c in {"HARNESS_FAILURE","ENVIRONMENT_FAILURE","ORACLE_FAILURE"} else "ORACLE_FAILURE") +PY +)" + fail_evidence "$REST_C0_CLASS" compute-container-c0 "native PVE REST container C0 did not satisfy create/readback/start/stop/delete/absence plus fixture-input cleanup" + fi +fi + NESTED_KVM="unknown" NESTED_KVM_INDICATORS="unknown" if command -v sshpass >/dev/null 2>&1; then @@ -1052,7 +1176,9 @@ then NESTED_KVM="yes" fi -if [[ "$NESTED_KVM" == "yes" ]]; then +if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then + P3_LXC_JSON='{"contract":"proxmox-lxc-lifecycle/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"superseded-by-rest-c0","detail":"CLI P3 lifecycle intentionally skipped because this rep assigns the exact container fixture to native PVE REST"}' +elif [[ "$NESTED_KVM" == "yes" ]]; then P3_LXC_JSON="$(probe_lxc_lifecycle || true)" else P3_LXC_JSON='{"contract":"proxmox-lxc-lifecycle/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"prerequisite","detail":"P5 nested-vCPU oracle was not satisfied in this rep"}' From ca88738e1d647a4fdacdc3a6d30482edb5e8b739 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:41:33 +0200 Subject: [PATCH 413/719] RDTE: route Proxmox container C0 only by fresh request --- .github/workflows/gha-kvm-system-rdte.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 7c1a21885..b37d3d0a1 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -53,6 +53,14 @@ on: - garm-provider-g5 - official-catalog - foliorelay + proxmox_compute_fixture: + description: "Optional native PVE REST compute fixture" + required: true + default: none + type: choice + options: + - none + - container-c0 truenas_compute_fixture: description: "Optional compute fixture layered on the selected system rung" required: true @@ -78,6 +86,7 @@ jobs: truenas_rung: ${{ steps.scope.outputs.truenas_rung }} truenas_t6_product: ${{ steps.scope.outputs.truenas_t6_product }} truenas_compute_fixture: ${{ steps.scope.outputs.truenas_compute_fixture }} + proxmox_compute_fixture: ${{ steps.scope.outputs.proxmox_compute_fixture }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: @@ -91,6 +100,7 @@ jobs: DISPATCH_TRUENAS_RUNG: ${{ inputs.truenas_rung }} DISPATCH_TRUENAS_T6_PRODUCT: ${{ inputs.truenas_t6_product }} DISPATCH_TRUENAS_COMPUTE_FIXTURE: ${{ inputs.truenas_compute_fixture }} + DISPATCH_PROXMOX_COMPUTE_FIXTURE: ${{ inputs.proxmox_compute_fixture }} BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.sha }} RUN_REQUEST_PATH: config/truenas-rdte-run-request.json @@ -104,6 +114,7 @@ jobs: truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" truenas_t6_product="${DISPATCH_TRUENAS_T6_PRODUCT:-litellm}" truenas_compute_fixture="${DISPATCH_TRUENAS_COMPUTE_FIXTURE:-none}" + proxmox_compute_fixture="${DISPATCH_PROXMOX_COMPUTE_FIXTURE:-none}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then case "$DISPATCH_TARGET" in proxmox) proxmox=true ;; @@ -147,6 +158,7 @@ jobs: exit 2 fi jq -e '.version == "9.2-1" and .authority_issue == 396' "$PROXMOX_RUN_REQUEST_PATH" >/dev/null + proxmox_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0")' "$PROXMOX_RUN_REQUEST_PATH")" printf 'fresh Proxmox request edge: previous=%s current=%s\n' "${previous_proxmox_request_id:-}" "$current_proxmox_request_id" fi if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then @@ -180,7 +192,8 @@ jobs: echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" echo "truenas_t6_product=$truenas_t6_product" >>"$GITHUB_OUTPUT" echo "truenas_compute_fixture=$truenas_compute_fixture" >>"$GITHUB_OUTPUT" - printf 'target scope: proxmox=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s compute_fixture=%s\n' "$proxmox" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" + echo "proxmox_compute_fixture=$proxmox_compute_fixture" >>"$GITHUB_OUTPUT" + printf 'target scope: proxmox=%s proxmox_compute_fixture=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s truenas_compute_fixture=%s\n' "$proxmox" "$proxmox_compute_fixture" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" contract: runs-on: ubuntu-24.04 @@ -338,6 +351,7 @@ jobs: mkdir -p "$RUNNER_TEMP/gha-kvm-system-rdte" bash scripts/gha_kvm_proxmox_rdte.sh \ --state-dir "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-state" \ + --compute-fixture "${{ needs.changes.outputs.proxmox_compute_fixture }}" \ --out "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/proxmox-receipt.json" - name: Preserve Proxmox harness failure evidence From 63c1881c98cd0b64f8ae72b52c74a2ed9391719f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:41:37 +0200 Subject: [PATCH 414/719] RDTE: lock Proxmox REST C0 claim boundary --- tests/test_gha_kvm_system_rdte.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index aee0e4e3c..c8aaae834 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -127,6 +127,22 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("losetup --find --show --read-only --partscan", text) self.assertNotIn("qemu-nbd --connect", text) + def test_proxmox_rest_container_c0_is_opt_in_and_api_native(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + script = PVE.read_text(encoding="utf-8") + self.assertIn("proxmox_compute_fixture", workflow) + self.assertIn('(.compute_fixture // "none") | select(. == "none" or . == "container-c0")', workflow) + self.assertIn('--compute-fixture "${{ needs.changes.outputs.proxmox_compute_fixture }}"', workflow) + self.assertIn("probe_rest_lxc_c0()", script) + rest = script.split("probe_rest_lxc_c0()", 1)[1].split("probe_lxc_lifecycle()", 1)[0] + self.assertIn("proxmox_rest_compute_probe.py", rest) + self.assertIn("--kind container", rest) + self.assertIn("--apply --out", rest) + self.assertNotIn("pct create", rest) + self.assertIn('"transport":"bounded SSH staging only; all container lifecycle mutation uses PVE REST"', rest) + self.assertIn('"rest_api_container_c0_exercised"', script) + self.assertIn("superseded-by-rest-c0", script) + def test_proxmox_rest_api_census_is_read_only_and_retained(self): text = PVE.read_text(encoding="utf-8") self.assertIn("proxmox_rest_compute_probe.py", text) From cc2b34d8ab96777e2223c60941cff62f0e028042 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:44:47 +0200 Subject: [PATCH 415/719] RDTE: keep Proxmox C0 rep outside nested-KVM work --- scripts/gha_kvm_proxmox_rdte.sh | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 87d70017d..6438cfa0a 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -1155,15 +1155,19 @@ fi NESTED_KVM="unknown" NESTED_KVM_INDICATORS="unknown" -if command -v sshpass >/dev/null 2>&1; then - if sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ - -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ - root@127.0.0.1 'test -e /dev/kvm && grep -Eq "(vmx|svm)" /proc/cpuinfo' >/dev/null 2>&1; then - NESTED_KVM_INDICATORS="yes" +if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then + NESTED_KVM_INDICATORS="skipped" + NESTED_KVM_VCPU_JSON='{"contract":"proxmox-nested-kvm-vcpu/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"not-required-for-container-c0","detail":"nested KVM is outside the bounded native REST container C0 causal rep"}' +else + if command -v sshpass >/dev/null 2>&1; then + if sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ + -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 \ + root@127.0.0.1 'test -e /dev/kvm && grep -Eq "(vmx|svm)" /proc/cpuinfo' >/dev/null 2>&1; then + NESTED_KVM_INDICATORS="yes" + fi fi -fi -NESTED_KVM_VCPU_JSON="$(probe_nested_kvm_vcpu || true)" -if R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" python3 - <<'PY' + NESTED_KVM_VCPU_JSON="$(probe_nested_kvm_vcpu || true)" + if R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" python3 - <<'PY' import json, os try: payload = json.loads(os.environ.get("R_NESTED_VCPU", "")) @@ -1172,8 +1176,9 @@ except Exception: ok = False raise SystemExit(0 if ok else 1) PY -then - NESTED_KVM="yes" + then + NESTED_KVM="yes" + fi fi if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then From d3903eb7386fe5c384e8bbf82a70fdfb8768b7bd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:44:50 +0200 Subject: [PATCH 416/719] RDTE: lock bounded C0 failure domain --- tests/test_gha_kvm_system_rdte.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index c8aaae834..25a8ec6bb 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -142,6 +142,9 @@ def test_proxmox_rest_container_c0_is_opt_in_and_api_native(self): self.assertIn('"transport":"bounded SSH staging only; all container lifecycle mutation uses PVE REST"', rest) self.assertIn('"rest_api_container_c0_exercised"', script) self.assertIn("superseded-by-rest-c0", script) + self.assertIn("not-required-for-container-c0", script) + c0_tail = script.split('if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then', 2)[2] + self.assertIn('NESTED_KVM_INDICATORS="skipped"', c0_tail) def test_proxmox_rest_api_census_is_read_only_and_retained(self): text = PVE.read_text(encoding="utf-8") From 3df6bc5e97a153a471c58e7404318bead1d45c7c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:28:12 +0200 Subject: [PATCH 417/719] RDTE: gate Proxmox request edges with static qualification --- .github/workflows/prep-rdte-static.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index b4e01c8ee..a51610df1 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -7,6 +7,7 @@ on: - '.github/workflows/gha-kvm-system-rdte.yml' - 'config/truenas-rdte-targets.json' - 'config/truenas-rdte-run-request.json' + - 'config/proxmox-rdte-run-request.json' - 'config/truenas-capsule-providers.json' - 'config/truenas-session-*.json' - 'scripts/gha_kvm_truenas_rdte.sh' From ba310e10b436e2a90afac7d169d906fd2201a91b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:28:15 +0200 Subject: [PATCH 418/719] RDTE: lock Proxmox request static-gate routing --- tests/test_gha_kvm_system_rdte.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 25a8ec6bb..c187b022e 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -323,6 +323,10 @@ def test_truenas_rpc_probe_is_dependency_free_and_read_only(self): ) self.assertEqual(cp.returncode, 0, cp.stderr) + def test_prep_static_gate_watches_proxmox_request_edge(self): + prep = (ROOT / ".github" / "workflows" / "prep-rdte-static.yml").read_text(encoding="utf-8") + self.assertIn("'config/proxmox-rdte-run-request.json'", prep) + def test_workflow_scopes_heavy_targets(self): text = WORKFLOW.read_text(encoding="utf-8") self.assertIn("fetch-depth: 0", text) From 9e171b6568c4b9c58051046a60457888581bb643 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:28:59 +0200 Subject: [PATCH 419/719] RDTE: request PVE 9.2-1 REST container C0 rep 001 --- config/proxmox-rdte-run-request.json | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config/proxmox-rdte-run-request.json diff --git a/config/proxmox-rdte-run-request.json b/config/proxmox-rdte-run-request.json new file mode 100644 index 000000000..40e087a7c --- /dev/null +++ b/config/proxmox-rdte-run-request.json @@ -0,0 +1,9 @@ +{ + "schema": "gha-kvm-proxmox-run-request/v1", + "request_id": "pve-9-2-rest-container-c0-001", + "version": "9.2-1", + "compute_fixture": "container-c0", + "authority_issue": 396, + "consumer_authority_issue": 426, + "purpose": "Run the first exact Proxmox VE 9.2-1 native REST system-container C0 parity rep after TrueNAS BETA.3 C0 acceptance. Reuse the admitted Debian 13 template identity and existing disposable PVE workcell. Template transport/staging is fixture preparation only; all container create/readback/start/stop/delete/absence mutation must use the PVE REST API. Require exact package/API census, REST materialization oracle=true, owned-resource cleanup absence, staged-template cleanup, and no guest/C1, nested-KVM, VM, Firecracker or Windows claim." +} From 4206a3ca29039c13f44bdb7c8750ac112e9bf955 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:33:49 +0200 Subject: [PATCH 420/719] RDTE: bind BETA.3 VM entitlement to public product APIs From 2ac8970d60cd3eafcca87dcfc67c8fc4a1c237c7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:33:52 +0200 Subject: [PATCH 421/719] RDTE: avoid private VM license API on BETA.3 --- scripts/truenas_compute_vm_probe.py | 42 +++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_compute_vm_probe.py b/scripts/truenas_compute_vm_probe.py index 14c6e32ee..df2e5df86 100644 --- a/scripts/truenas_compute_vm_probe.py +++ b/scripts/truenas_compute_vm_probe.py @@ -114,6 +114,31 @@ def validate_modern_preconditions(details: Any, licensed: Any) -> dict[str, Any] return {"virtualization_details": details, "license_active": licensed} +def validate_public_modern_preconditions( + details: Any, product_type: Any, enterprise_feature_enabled: Any +) -> dict[str, Any]: + if not isinstance(details, dict) or not isinstance(details.get("supported"), bool): + raise ProbeError(f"vm.virtualization_details invalid: {details!r}") + if not details["supported"]: + raise ProbeError(f"VM virtualization unavailable: {details.get('error')}") + if product_type not in {"COMMUNITY_EDITION", "ENTERPRISE"}: + raise ProbeError(f"system.product_type invalid: {product_type!r}") + if product_type == "ENTERPRISE": + if not isinstance(enterprise_feature_enabled, bool): + raise ProbeError( + f"system.feature_enabled enterprise result invalid: {enterprise_feature_enabled!r}" + ) + if not enterprise_feature_enabled: + raise ProbeError("enterprise system is not entitled to use native vm.*") + elif enterprise_feature_enabled is not None: + raise ProbeError("community edition must not depend on enterprise feature licensing") + return { + "virtualization_details": details, + "product_type": product_type, + "enterprise_vm_feature_enabled": enterprise_feature_enabled, + } + + def main() -> int: p=argparse.ArgumentParser() p.add_argument("--registry",type=pathlib.Path,default=pathlib.Path("config/compute-materialization-targets.json")) @@ -314,8 +339,21 @@ def wait_job(job_id: Any, label: str, timeout: float | None = None): raise ProbeError("legacy VM remained after delete") elif adapter_id=="truenas-vm-libvirt": details=call("vm.virtualization_details",[]) - licensed=call("vm.license_active",[]) - receipt["preconditions"]=validate_modern_preconditions(details,licensed) + public_entitlement=adapter.get("public_entitlement") + if public_entitlement: + product_type=call(public_entitlement["product_type_method"],[]) + enterprise_feature_enabled=None + if product_type=="ENTERPRISE": + enterprise_feature_enabled=call( + public_entitlement["feature_method"], + [public_entitlement["enterprise_feature"]], + ) + receipt["preconditions"]=validate_public_modern_preconditions( + details,product_type,enterprise_feature_enabled + ) + else: + licensed=call("vm.license_active",[]) + receipt["preconditions"]=validate_modern_preconditions(details,licensed) rows=call("vm.query",[[["name","=",a.name]]]) if rows: raise ProbeError(f"preexisting VM {a.name!r} blocks ownership-safe apply") From 1f9587a536f848a6b1c4b2dc06de4d3d2e3d3e60 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:33:54 +0200 Subject: [PATCH 422/719] RDTE: lock public BETA.3 VM entitlement precondition --- tests/test_truenas_compute_vm_probe.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_truenas_compute_vm_probe.py b/tests/test_truenas_compute_vm_probe.py index 9b3aa685b..eb66112ef 100644 --- a/tests/test_truenas_compute_vm_probe.py +++ b/tests/test_truenas_compute_vm_probe.py @@ -8,6 +8,7 @@ normalize_system_version, owned_zvol_device, validate_modern_preconditions, + validate_public_modern_preconditions, zvol_device_path, ) @@ -70,6 +71,24 @@ def test_modern_preconditions_require_kvm_and_entitlement(self): with self.assertRaises(ProbeError): validate_modern_preconditions({},True) + def test_beta3_public_entitlement_path_never_needs_private_license_method(self): + community=validate_public_modern_preconditions( + {"supported":True,"error":None},"COMMUNITY_EDITION",None + ) + self.assertEqual(community["product_type"],"COMMUNITY_EDITION") + enterprise=validate_public_modern_preconditions( + {"supported":True,"error":None},"ENTERPRISE",True + ) + self.assertTrue(enterprise["enterprise_vm_feature_enabled"]) + with self.assertRaises(ProbeError): + validate_public_modern_preconditions( + {"supported":True,"error":None},"ENTERPRISE",False + ) + with self.assertRaises(ProbeError): + validate_public_modern_preconditions( + {"supported":False,"error":"no kvm"},"COMMUNITY_EDITION",None + ) + if __name__=="__main__": unittest.main() From 49e8ef3b3367318ab3b9a9b34237864e57e78354 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:36:34 +0200 Subject: [PATCH 423/719] RDTE: add exact Proxmox target registry --- config/proxmox-rdte-targets.json | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 config/proxmox-rdte-targets.json diff --git a/config/proxmox-rdte-targets.json b/config/proxmox-rdte-targets.json new file mode 100644 index 000000000..ed9932e38 --- /dev/null +++ b/config/proxmox-rdte-targets.json @@ -0,0 +1,49 @@ +{ + "schema": "gha-kvm-proxmox-targets/v1", + "policy": { + "exact_version_required": true, + "floating_target_prohibited": true, + "runtime_admission_is_explicit": true, + "evidence_inheritance_across_versions": false + }, + "targets": [ + { + "version": "9.2-1", + "runtime_admitted": true, + "authority_issue": 396, + "iso_name": "proxmox-ve_9.2-1.iso", + "iso_url": "https://enterprise.proxmox.com/iso/proxmox-ve_9.2-1.iso", + "iso_sha256": "4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c", + "installer_source_version": "9.2.5", + "installer_source_commit": "32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9", + "pve_manager_source_commit": "b9984c6d90a4bd80ab72dc2088c1b9103fb167b1", + "pve_manager_package_version": "9.2.2", + "pve_manager_apt_api_blob": "9cb6e473436719f3024ac09fffaad8faf0d7160d", + "pve_access_control_source_commit": "5ccd07d9302562b73374d331b63d25b04b86766c", + "pve_container_source_commit": "5eb5574ee9158ac40a5230de2cf18d7d6345709f", + "pve_container_package_version": "6.1.10", + "pve_container_lxc_api_blob": "88067ddebf5a0bd540f91ffd1db11f8eec40cefb", + "pve_container_status_api_blob": "c95fc3c7732d637639ab29e496bc27eafb9d222d", + "pve_qemu_source_commit": "684796e835289dab11af8606fbf7358b93526dd6", + "pve_qemu_package_version": "11.0.0-3", + "pve_qemu_submodule_commit": "98b060da3a4f92b2a994ead5b16a87e783baf77c", + "container_fixture": { + "template_name": "debian-13-standard_13.1-2_amd64.tar.zst", + "template_url": "https://download.proxmox.com/images/system/debian-13-standard_13.1-2_amd64.tar.zst", + "template_sha512": "5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8" + }, + "required_rest_surfaces": [ + "/version", + "/nodes", + "/nodes/{node}/apt/versions", + "/nodes/{node}/lxc", + "/nodes/{node}/lxc/{vmid}/config", + "/nodes/{node}/lxc/{vmid}/status/current", + "/nodes/{node}/lxc/{vmid}/status/start", + "/nodes/{node}/lxc/{vmid}/status/stop", + "/nodes/{node}/tasks/{upid}/status" + ], + "vm_source_status": "OPEN_PENDING_EXACT_QEMU_SERVER_PACKAGE_CENSUS" + } + ] +} From 650fd046f8997006580d80de95a20d9e001054b1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:36:37 +0200 Subject: [PATCH 424/719] RDTE: validate exact Proxmox target admission --- scripts/proxmox_rdte_target.py | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 scripts/proxmox_rdte_target.py diff --git a/scripts/proxmox_rdte_target.py b/scripts/proxmox_rdte_target.py new file mode 100644 index 000000000..bd77c346d --- /dev/null +++ b/scripts/proxmox_rdte_target.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +from __future__ import annotations +import argparse, json, pathlib, re, sys + +SHA40=re.compile(r"^[0-9a-f]{40}$") +SHA256=re.compile(r"^[0-9a-f]{64}$") +SHA512=re.compile(r"^[0-9a-f]{128}$") + +class TargetError(RuntimeError): + pass + +def load_registry(path:pathlib.Path): + try: + doc=json.loads(path.read_text(encoding="utf-8")) + except (OSError,json.JSONDecodeError) as exc: + raise TargetError(f"cannot read target registry: {exc}") from exc + if not isinstance(doc,dict) or doc.get("schema")!="gha-kvm-proxmox-targets/v1": + raise TargetError("unsupported target registry schema") + targets=doc.get("targets") + if not isinstance(targets,list) or not targets: + raise TargetError("target registry has no targets") + result={} + for i,t in enumerate(targets): + if not isinstance(t,dict): + raise TargetError(f"targets[{i}] must be an object") + version=t.get("version") + if not isinstance(version,str) or not version or version in result: + raise TargetError(f"invalid or duplicate target version at index {i}") + if not isinstance(t.get("runtime_admitted"),bool): + raise TargetError(f"{version}: runtime_admitted must be boolean") + if not isinstance(t.get("authority_issue"),int): + raise TargetError(f"{version}: authority_issue must be integer") + for key in ( + "iso_name","iso_url","installer_source_version","vm_source_status", + "pve_manager_package_version","pve_container_package_version","pve_qemu_package_version", + ): + if not isinstance(t.get(key),str) or not t[key]: + raise TargetError(f"{version}: missing {key}") + for key in ( + "installer_source_commit","pve_manager_source_commit","pve_manager_apt_api_blob", + "pve_access_control_source_commit","pve_container_source_commit","pve_container_lxc_api_blob", + "pve_container_status_api_blob","pve_qemu_source_commit","pve_qemu_submodule_commit", + ): + if not isinstance(t.get(key),str) or not SHA40.fullmatch(t[key]): + raise TargetError(f"{version}: {key} must be exact 40-hex") + if not isinstance(t.get("iso_sha256"),str) or not SHA256.fullmatch(t["iso_sha256"]): + raise TargetError(f"{version}: iso_sha256 must be exact 64-hex") + fixture=t.get("container_fixture") + if not isinstance(fixture,dict): + raise TargetError(f"{version}: container_fixture missing") + for key in ("template_name","template_url"): + if not isinstance(fixture.get(key),str) or not fixture[key]: + raise TargetError(f"{version}: container_fixture.{key} missing") + if not isinstance(fixture.get("template_sha512"),str) or not SHA512.fullmatch(fixture["template_sha512"]): + raise TargetError(f"{version}: container_fixture.template_sha512 must be exact 128-hex") + for url in (t["iso_url"],fixture["template_url"]): + if any(token in url.lower() for token in ("latest","nightly","master+")): + raise TargetError(f"{version}: floating URL prohibited") + surfaces=t.get("required_rest_surfaces") + if not isinstance(surfaces,list) or not surfaces or not all(isinstance(x,str) and x.startswith("/") for x in surfaces): + raise TargetError(f"{version}: required_rest_surfaces invalid") + result[version]=t + return result + +def main(): + p=argparse.ArgumentParser() + p.add_argument("--registry",type=pathlib.Path,required=True) + p.add_argument("--version",required=True) + p.add_argument("--require-runtime-admitted",action="store_true") + a=p.parse_args() + try: + targets=load_registry(a.registry) + if a.version not in targets: + raise TargetError(f"exact Proxmox target is not registered: {a.version}") + target=targets[a.version] + if a.require_runtime_admitted and target["runtime_admitted"] is not True: + raise TargetError(f"Proxmox target is source-profiled but not runtime-admitted: {a.version}") + print(json.dumps(target,indent=2,sort_keys=True)) + return 0 + except TargetError as exc: + print(f"ERROR: {exc}",file=sys.stderr) + return 2 + +if __name__=="__main__": + raise SystemExit(main()) From 851fc074bdf4f7b444f1643d615d0228e0eea6e9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:36:39 +0200 Subject: [PATCH 425/719] RDTE: lock current PVE target profile --- tests/test_proxmox_rdte_target.py | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/test_proxmox_rdte_target.py diff --git a/tests/test_proxmox_rdte_target.py b/tests/test_proxmox_rdte_target.py new file mode 100644 index 000000000..341410147 --- /dev/null +++ b/tests/test_proxmox_rdte_target.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import importlib.util, json, pathlib, unittest + +ROOT=pathlib.Path(__file__).resolve().parents[1] +SPEC=importlib.util.spec_from_file_location("proxmox_rdte_target",ROOT/"scripts"/"proxmox_rdte_target.py") +MOD=importlib.util.module_from_spec(SPEC); assert SPEC.loader; SPEC.loader.exec_module(MOD) +REG=ROOT/"config"/"proxmox-rdte-targets.json" +RUN_REQUEST=ROOT/"config"/"proxmox-rdte-run-request.json" + +class ProxmoxTargetRegistryTests(unittest.TestCase): + def setUp(self): + self.targets=MOD.load_registry(REG) + + def test_current_runtime_matrix_is_exactly_one_target(self): + self.assertEqual(set(self.targets),{"9.2-1"}) + self.assertTrue(self.targets["9.2-1"]["runtime_admitted"]) + + def test_exact_source_and_media_anchors(self): + t=self.targets["9.2-1"] + self.assertEqual(t["iso_sha256"],"4e88fe416df9b527624a175f24c9aa07c714d3332afb1ee3dbf3879573ef2c6c") + self.assertEqual(t["installer_source_commit"],"32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9") + self.assertEqual(t["pve_manager_source_commit"],"b9984c6d90a4bd80ab72dc2088c1b9103fb167b1") + self.assertEqual(t["pve_container_source_commit"],"5eb5574ee9158ac40a5230de2cf18d7d6345709f") + self.assertEqual(t["pve_qemu_source_commit"],"684796e835289dab11af8606fbf7358b93526dd6") + self.assertEqual(t["container_fixture"]["template_sha512"],"5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8") + + def test_vm_source_profile_remains_explicitly_open(self): + self.assertEqual(self.targets["9.2-1"]["vm_source_status"],"OPEN_PENDING_EXACT_QEMU_SERVER_PACKAGE_CENSUS") + + def test_request_is_registered_runtime_target(self): + request=json.loads(RUN_REQUEST.read_text(encoding="utf-8")) + self.assertEqual(request["schema"],"gha-kvm-proxmox-run-request/v1") + self.assertIn(request["version"],self.targets) + target=self.targets[request["version"]] + self.assertTrue(target["runtime_admitted"]) + self.assertEqual(request["authority_issue"],target["authority_issue"]) + self.assertIn(request.get("compute_fixture","none"),{"none","container-c0"}) + + def test_unknown_target_fails_closed(self): + with self.assertRaises(MOD.TargetError): + if "9.3-1" not in self.targets: + raise MOD.TargetError("exact Proxmox target is not registered: 9.3-1") + +if __name__=="__main__": + unittest.main() From 8e6e87fe04c62a2dcc813d8cd5f998befcb18b3e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:36:42 +0200 Subject: [PATCH 426/719] RDTE: qualify Proxmox target registry changes --- .github/workflows/prep-rdte-static.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index a51610df1..1cf627474 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -8,6 +8,9 @@ on: - 'config/truenas-rdte-targets.json' - 'config/truenas-rdte-run-request.json' - 'config/proxmox-rdte-run-request.json' + - 'config/proxmox-rdte-targets.json' + - 'scripts/proxmox_rdte_target.py' + - 'tests/test_proxmox_rdte_target.py' - 'config/truenas-capsule-providers.json' - 'config/truenas-session-*.json' - 'scripts/gha_kvm_truenas_rdte.sh' @@ -89,6 +92,9 @@ jobs: if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi + if [[ -f tests/test_proxmox_rdte_target.py ]]; then + tests+=(tests/test_proxmox_rdte_target.py) + fi if [[ -f tests/test_truenas_session_manifest.py ]]; then tests+=(tests/test_truenas_session_manifest.py) fi From 2063127b6e624a79393bd5ff1e1c26fd30536311 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:36:44 +0200 Subject: [PATCH 427/719] RDTE: admit Proxmox requests through exact target registry --- .github/workflows/gha-kvm-system-rdte.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index b37d3d0a1..add311665 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -157,7 +157,14 @@ jobs: echo "refusing stale Proxmox request replay: request_id did not change ($current_proxmox_request_id)" >&2 exit 2 fi - jq -e '.version == "9.2-1" and .authority_issue == 396' "$PROXMOX_RUN_REQUEST_PATH" >/dev/null + proxmox_version="$(jq -er '.version | select(type == "string" and length > 0)' "$PROXMOX_RUN_REQUEST_PATH")" + proxmox_target_json="$(python3 scripts/proxmox_rdte_target.py --registry config/proxmox-rdte-targets.json --version "$proxmox_version" --require-runtime-admitted)" + proxmox_target_authority="$(jq -er '.authority_issue' <<<"$proxmox_target_json")" + proxmox_request_authority="$(jq -er '.authority_issue' "$PROXMOX_RUN_REQUEST_PATH")" + [[ "$proxmox_request_authority" == "$proxmox_target_authority" ]] || { + echo "Proxmox request authority does not match exact target profile" >&2 + exit 2 + } proxmox_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0")' "$PROXMOX_RUN_REQUEST_PATH")" printf 'fresh Proxmox request edge: previous=%s current=%s\n' "${previous_proxmox_request_id:-}" "$current_proxmox_request_id" fi From 66262959f596fe5db3d0c23d8849389a56571088 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:36:47 +0200 Subject: [PATCH 428/719] RDTE: lock Proxmox target registry routing --- tests/test_gha_kvm_system_rdte.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index c187b022e..b62302675 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -323,6 +323,16 @@ def test_truenas_rpc_probe_is_dependency_free_and_read_only(self): ) self.assertEqual(cp.returncode, 0, cp.stderr) + def test_proxmox_request_uses_exact_target_registry(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + prep = (ROOT / ".github" / "workflows" / "prep-rdte-static.yml").read_text(encoding="utf-8") + self.assertIn("config/proxmox-rdte-targets.json", workflow) + self.assertIn("scripts/proxmox_rdte_target.py", workflow) + self.assertIn("--require-runtime-admitted", workflow) + self.assertIn("'config/proxmox-rdte-targets.json'", prep) + self.assertIn("'scripts/proxmox_rdte_target.py'", prep) + self.assertIn("'tests/test_proxmox_rdte_target.py'", prep) + def test_prep_static_gate_watches_proxmox_request_edge(self): prep = (ROOT / ".github" / "workflows" / "prep-rdte-static.yml").read_text(encoding="utf-8") self.assertIn("'config/proxmox-rdte-run-request.json'", prep) From 0e717e101d92c5f593502360e6299e93ad26d9e7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:40:20 +0200 Subject: [PATCH 429/719] RDTE: use official PVE template transport for C0 --- scripts/gha_kvm_proxmox_rdte.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index 6438cfa0a..e76264d1d 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -8,7 +8,7 @@ PVE_INSTALLER_SOURCE_VERSION="9.2.5" PVE_INSTALLER_SOURCE_COMMIT="32afcd4cd534d8e2f99ae76aa0234a0a5c697ba9" P3_PVE_CONTAINER_SOURCE_COMMIT="5eb5574ee9158ac40a5230de2cf18d7d6345709f" P3_TEMPLATE_NAME="debian-13-standard_13.1-2_amd64.tar.zst" -P3_TEMPLATE_URL="https://download.proxmox.com/images/system/$P3_TEMPLATE_NAME" +P3_TEMPLATE_URL="http://download.proxmox.com/images/system/$P3_TEMPLATE_NAME" P3_TEMPLATE_SHA512="5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8" P3_VMID=9101 RAM_MIB=4096 From 439f5fef21e04a2ce22ebebe157a9ab448594c34 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:40:23 +0200 Subject: [PATCH 430/719] RDTE: bind PVE template transport to exact catalog source --- config/proxmox-rdte-targets.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/config/proxmox-rdte-targets.json b/config/proxmox-rdte-targets.json index ed9932e38..ebe39eaf2 100644 --- a/config/proxmox-rdte-targets.json +++ b/config/proxmox-rdte-targets.json @@ -29,7 +29,14 @@ "pve_qemu_submodule_commit": "98b060da3a4f92b2a994ead5b16a87e783baf77c", "container_fixture": { "template_name": "debian-13-standard_13.1-2_amd64.tar.zst", - "template_url": "https://download.proxmox.com/images/system/debian-13-standard_13.1-2_amd64.tar.zst", + "template_url": "http://download.proxmox.com/images/system/debian-13-standard_13.1-2_amd64.tar.zst", + "catalog_source": { + "repository": "proxmox/pve-manager", + "commit": "b9984c6d90a4bd80ab72dc2088c1b9103fb167b1", + "path": "PVE/APLInfo.pm", + "blob_sha": "433a8ef4bf78d606508c6bae162c49e9ef0ecfd3", + "base_url": "http://download.proxmox.com/images" + }, "template_sha512": "5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8" }, "required_rest_surfaces": [ From 5ada5a1e84a68c23460274a5ac4f63969052fac2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:40:26 +0200 Subject: [PATCH 431/719] RDTE: lock official PVE template transport --- tests/test_gha_kvm_system_rdte.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index b62302675..1e1c5ebd6 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -198,6 +198,8 @@ def test_proxmox_p3_uses_exact_public_template_and_supported_pct_lifecycle(self) text = PVE.read_text(encoding="utf-8") self.assertIn('P3_TEMPLATE_NAME="debian-13-standard_13.1-2_amd64.tar.zst"', text) self.assertIn('P3_TEMPLATE_SHA512="5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8"', text) + self.assertIn('P3_TEMPLATE_URL="http://download.proxmox.com/images/system/$P3_TEMPLATE_NAME"', text) + self.assertNotIn('P3_TEMPLATE_URL="https://download.proxmox.com/images/system/', text) self.assertIn('P3_PVE_CONTAINER_SOURCE_COMMIT="5eb5574ee9158ac40a5230de2cf18d7d6345709f"', text) self.assertIn("probe_lxc_lifecycle", text) self.assertIn('pct create "$VMID"', text) From a389ac4741e4bea752c9362769d21f08b36ea902 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:40:28 +0200 Subject: [PATCH 432/719] RDTE: lock template catalog provenance --- tests/test_proxmox_rdte_target.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_proxmox_rdte_target.py b/tests/test_proxmox_rdte_target.py index 341410147..1df89da16 100644 --- a/tests/test_proxmox_rdte_target.py +++ b/tests/test_proxmox_rdte_target.py @@ -23,6 +23,9 @@ def test_exact_source_and_media_anchors(self): self.assertEqual(t["pve_container_source_commit"],"5eb5574ee9158ac40a5230de2cf18d7d6345709f") self.assertEqual(t["pve_qemu_source_commit"],"684796e835289dab11af8606fbf7358b93526dd6") self.assertEqual(t["container_fixture"]["template_sha512"],"5aec4ab2ac5c16c7c8ecb87bfeeb10213abe96db6b85e2463585cea492fc861d7c390b3f9c95629bf690b95e9dfe1037207fc69c0912429605f208d5cb2621f8") + self.assertEqual(t["container_fixture"]["template_url"],"http://download.proxmox.com/images/system/debian-13-standard_13.1-2_amd64.tar.zst") + self.assertEqual(t["container_fixture"]["catalog_source"]["blob_sha"],"433a8ef4bf78d606508c6bae162c49e9ef0ecfd3") + self.assertEqual(t["container_fixture"]["catalog_source"]["base_url"],"http://download.proxmox.com/images") def test_vm_source_profile_remains_explicitly_open(self): self.assertEqual(self.targets["9.2-1"]["vm_source_status"],"OPEN_PENDING_EXACT_QEMU_SERVER_PACKAGE_CENSUS") From cdf9e60ce2baff91f56660480f1bcbcd9ca3b852 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:41:21 +0200 Subject: [PATCH 433/719] RDTE: request PVE REST C0 rep 002 official transport --- config/proxmox-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/proxmox-rdte-run-request.json b/config/proxmox-rdte-run-request.json index 40e087a7c..bf8110c48 100644 --- a/config/proxmox-rdte-run-request.json +++ b/config/proxmox-rdte-run-request.json @@ -1,9 +1,9 @@ { "schema": "gha-kvm-proxmox-run-request/v1", - "request_id": "pve-9-2-rest-container-c0-001", + "request_id": "pve-9-2-rest-container-c0-002-official-template-transport", "version": "9.2-1", "compute_fixture": "container-c0", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Run the first exact Proxmox VE 9.2-1 native REST system-container C0 parity rep after TrueNAS BETA.3 C0 acceptance. Reuse the admitted Debian 13 template identity and existing disposable PVE workcell. Template transport/staging is fixture preparation only; all container create/readback/start/stop/delete/absence mutation must use the PVE REST API. Require exact package/API census, REST materialization oracle=true, owned-resource cleanup absence, staged-template cleanup, and no guest/C1, nested-KVM, VM, Firecracker or Windows claim." + "purpose": "Re-run the exact Proxmox VE 9.2-1 native REST system-container C0 parity fixture after rep 001 retained healthy install/API/package census but failed before mutation because the harness used a non-canonical HTTPS transport for download.proxmox.com. Preserve exact target, REST lifecycle, template identity, SHA-512 integrity, cleanup and claim boundaries. The only causal delta is merged #294: use the exact pve-manager-declared http://download.proxmox.com/images catalog transport while retaining SHA-512 verification. Require REST materialization oracle=true, owned-resource absence and fixture-input cleanup; no guest/C1, nested-KVM, VM, Firecracker or Windows claim." } From 9f2b5ed094f67aab175d4fcb64b661547689a807 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:43:09 +0200 Subject: [PATCH 434/719] RDTE: bind installed qemu-server 9.1.15 source --- config/proxmox-rdte-targets.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/proxmox-rdte-targets.json b/config/proxmox-rdte-targets.json index ebe39eaf2..f6d64c4e4 100644 --- a/config/proxmox-rdte-targets.json +++ b/config/proxmox-rdte-targets.json @@ -50,7 +50,15 @@ "/nodes/{node}/lxc/{vmid}/status/stop", "/nodes/{node}/tasks/{upid}/status" ], - "vm_source_status": "OPEN_PENDING_EXACT_QEMU_SERVER_PACKAGE_CENSUS" + "qemu_server_source": { + "repository": "proxmox/qemu-server", + "commit": "6785065b3f766f15f6f151af8ec27ec8bb5b07ab", + "package_version": "9.1.15", + "api2_qemu_blob": "e029a204d121f3c8b104457ef14eb6d5ce029464", + "qemu_server_blob": "118f26bc94d9ee8e8c4c39a3d710e67c14f61bc0", + "changelog_blob": "63ded16d9d06ae0dd114a67d19bb36e14c71c980" + }, + "vm_source_status": "SOURCE_BOUND_RUNTIME_NOT_YET_QUALIFIED" } ] } From c5d3f68f651b2629ac3589eb0b0bdb64e71cce7b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:43:12 +0200 Subject: [PATCH 435/719] RDTE: admit only exact source-bound qemu-server VM API --- scripts/proxmox_rest_compute_probe.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scripts/proxmox_rest_compute_probe.py b/scripts/proxmox_rest_compute_probe.py index a2cb62532..97161de69 100644 --- a/scripts/proxmox_rest_compute_probe.py +++ b/scripts/proxmox_rest_compute_probe.py @@ -28,6 +28,14 @@ class ProxmoxProbeError(RuntimeError): "commit": "684796e835289dab11af8606fbf7358b93526dd6", "package_version": "11.0.0-3", } +PVE_QEMU_SERVER_SOURCE = { + "repository": "proxmox/qemu-server", + "commit": "6785065b3f766f15f6f151af8ec27ec8bb5b07ab", + "package_version": "9.1.15", + "api2_qemu_blob": "e029a204d121f3c8b104457ef14eb6d5ce029464", + "qemu_server_blob": "118f26bc94d9ee8e8c4c39a3d710e67c14f61bc0", + "changelog_blob": "63ded16d9d06ae0dd114a67d19bb36e14c71c980", +} PVE_MANAGER_API_SOURCE = { "repository": "proxmox/pve-manager", "commit": "b9984c6d90a4bd80", @@ -202,8 +210,9 @@ def main() -> int: "source_contract":{ "pve_container":PVE_CONTAINER_SOURCE, "pve_qemu_package":PVE_QEMU_PACKAGE_SOURCE, + "qemu_server":PVE_QEMU_SERVER_SOURCE, "pve_manager_api":PVE_MANAGER_API_SOURCE, - "qemu_server_api":"OPEN: exact installed qemu-server package/source identity must be observed and admitted before VM apply", + "qemu_server_api":"SOURCE_BOUND: exact installed qemu-server 9.1.15 is admitted for V0 product-REST shell lifecycle; runtime VM qualification remains separate", }, } def emit(code:int)->int: @@ -253,8 +262,7 @@ def emit(code:int)->int: return emit(0) if a.kind=="vm" and not a.expected_qemu_server_version: raise ProxmoxProbeError( - "VM apply blocked: exact qemu-server API package/source identity is not admitted; " - "run --observe-only and bind the observed package before mutation" + "VM apply blocked: --expected-qemu-server-version must explicitly select the source-bound installed package" ) if a.kind=="vm": observed_qemu=[ @@ -270,6 +278,11 @@ def emit(code:int)->int: "installed_versions":sorted(installed_versions), "available_versions":sorted(available_versions), } + if a.expected_qemu_server_version != PVE_QEMU_SERVER_SOURCE["package_version"]: + raise ProxmoxProbeError( + f"VM apply blocked: requested qemu-server source version {a.expected_qemu_server_version!r} " + f"is not the admitted {PVE_QEMU_SERVER_SOURCE['package_version']!r}" + ) if a.expected_qemu_server_version not in installed_versions: raise ProxmoxProbeError( f"VM apply blocked: expected installed qemu-server {a.expected_qemu_server_version!r}, " From d332398c74710d4c299569cf7df7bf5233b5fceb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:43:15 +0200 Subject: [PATCH 436/719] RDTE: lock qemu-server source admission --- tests/test_proxmox_rest_compute_probe.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_proxmox_rest_compute_probe.py b/tests/test_proxmox_rest_compute_probe.py index 845d1916d..1b4e1de6c 100644 --- a/tests/test_proxmox_rest_compute_probe.py +++ b/tests/test_proxmox_rest_compute_probe.py @@ -1,7 +1,7 @@ import pathlib import unittest from scripts.proxmox_rest_compute_probe import ( - PVE_CONTAINER_SOURCE, PVE_MANAGER_API_SOURCE, PVE_QEMU_PACKAGE_SOURCE, + PVE_CONTAINER_SOURCE, PVE_MANAGER_API_SOURCE, PVE_QEMU_PACKAGE_SOURCE, PVE_QEMU_SERVER_SOURCE, ProxmoxProbeError, lxc_create_fields, plan_only, vm_create_fields ) @@ -11,6 +11,9 @@ def test_exact_container_source_is_bound(self): self.assertEqual(PVE_CONTAINER_SOURCE["package_version"],"6.1.10") self.assertEqual(PVE_QEMU_PACKAGE_SOURCE["commit"],"684796e835289dab11af8606fbf7358b93526dd6") self.assertEqual(PVE_MANAGER_API_SOURCE["commit"],"b9984c6d90a4bd80") + self.assertEqual(PVE_QEMU_SERVER_SOURCE["commit"],"6785065b3f766f15f6f151af8ec27ec8bb5b07ab") + self.assertEqual(PVE_QEMU_SERVER_SOURCE["package_version"],"9.1.15") + self.assertEqual(PVE_QEMU_SERVER_SOURCE["api2_qemu_blob"],"e029a204d121f3c8b104457ef14eb6d5ce029464") self.assertEqual(PVE_MANAGER_API_SOURCE["apt_api_blob"],"9cb6e473436719f3024ac09fffaad8faf0d7160d") def test_lxc_plan_uses_rest_and_unprivileged_container(self): @@ -32,6 +35,11 @@ def test_container_requires_exact_template_identity(self): with self.assertRaises(ProxmoxProbeError): plan_only("container",9101,None) + def test_vm_apply_source_gate_is_exact_package_bound(self): + text=pathlib.Path("scripts/proxmox_rest_compute_probe.py").read_text(encoding="utf-8") + self.assertIn('a.expected_qemu_server_version != PVE_QEMU_SERVER_SOURCE["package_version"]',text) + self.assertIn('"qemu_server":PVE_QEMU_SERVER_SOURCE',text) + def test_vm_payload_does_not_smuggle_media_or_key(self): f=vm_create_fields(9201) self.assertNotIn("ide2",f) From 3a4487b911e2d29ea86fb61a3a7a4c005fb0607f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:43:17 +0200 Subject: [PATCH 437/719] RDTE: record qemu-server source-bound runtime hold --- tests/test_proxmox_rdte_target.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_proxmox_rdte_target.py b/tests/test_proxmox_rdte_target.py index 1df89da16..37dad25cf 100644 --- a/tests/test_proxmox_rdte_target.py +++ b/tests/test_proxmox_rdte_target.py @@ -27,8 +27,12 @@ def test_exact_source_and_media_anchors(self): self.assertEqual(t["container_fixture"]["catalog_source"]["blob_sha"],"433a8ef4bf78d606508c6bae162c49e9ef0ecfd3") self.assertEqual(t["container_fixture"]["catalog_source"]["base_url"],"http://download.proxmox.com/images") - def test_vm_source_profile_remains_explicitly_open(self): - self.assertEqual(self.targets["9.2-1"]["vm_source_status"],"OPEN_PENDING_EXACT_QEMU_SERVER_PACKAGE_CENSUS") + def test_vm_source_profile_is_exact_but_runtime_unqualified(self): + t=self.targets["9.2-1"] + self.assertEqual(t["vm_source_status"],"SOURCE_BOUND_RUNTIME_NOT_YET_QUALIFIED") + self.assertEqual(t["qemu_server_source"]["package_version"],"9.1.15") + self.assertEqual(t["qemu_server_source"]["commit"],"6785065b3f766f15f6f151af8ec27ec8bb5b07ab") + self.assertEqual(t["qemu_server_source"]["api2_qemu_blob"],"e029a204d121f3c8b104457ef14eb6d5ce029464") def test_request_is_registered_runtime_target(self): request=json.loads(RUN_REQUEST.read_text(encoding="utf-8")) From 5f62664732b76865f1ca21ed2335971ff3f8b5b4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:45:25 +0200 Subject: [PATCH 438/719] RDTE: retain full exact pve-manager source SHA --- scripts/proxmox_rest_compute_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/proxmox_rest_compute_probe.py b/scripts/proxmox_rest_compute_probe.py index 97161de69..fec850d4b 100644 --- a/scripts/proxmox_rest_compute_probe.py +++ b/scripts/proxmox_rest_compute_probe.py @@ -38,7 +38,7 @@ class ProxmoxProbeError(RuntimeError): } PVE_MANAGER_API_SOURCE = { "repository": "proxmox/pve-manager", - "commit": "b9984c6d90a4bd80", + "commit": "b9984c6d90a4bd80ab72dc2088c1b9103fb167b1", "apt_api_blob": "9cb6e473436719f3024ac09fffaad8faf0d7160d", "manager_version": "9.2.2", } From c18c97d9d6ec60a7c2c8e353066b1894e4860a20 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:45:28 +0200 Subject: [PATCH 439/719] RDTE: lock full pve-manager source SHA --- tests/test_proxmox_rest_compute_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_proxmox_rest_compute_probe.py b/tests/test_proxmox_rest_compute_probe.py index 1b4e1de6c..ca4e3e35b 100644 --- a/tests/test_proxmox_rest_compute_probe.py +++ b/tests/test_proxmox_rest_compute_probe.py @@ -10,7 +10,7 @@ def test_exact_container_source_is_bound(self): self.assertEqual(PVE_CONTAINER_SOURCE["commit"],"5eb5574ee9158ac40a5230de2cf18d7d6345709f") self.assertEqual(PVE_CONTAINER_SOURCE["package_version"],"6.1.10") self.assertEqual(PVE_QEMU_PACKAGE_SOURCE["commit"],"684796e835289dab11af8606fbf7358b93526dd6") - self.assertEqual(PVE_MANAGER_API_SOURCE["commit"],"b9984c6d90a4bd80") + self.assertEqual(PVE_MANAGER_API_SOURCE["commit"],"b9984c6d90a4bd80ab72dc2088c1b9103fb167b1") self.assertEqual(PVE_QEMU_SERVER_SOURCE["commit"],"6785065b3f766f15f6f151af8ec27ec8bb5b07ab") self.assertEqual(PVE_QEMU_SERVER_SOURCE["package_version"],"9.1.15") self.assertEqual(PVE_QEMU_SERVER_SOURCE["api2_qemu_blob"],"e029a204d121f3c8b104457ef14eb6d5ce029464") From 8105712d4be31de1bee0150531991d457a85418e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:49:06 +0200 Subject: [PATCH 440/719] RDTE: align PVE REST task and DELETE semantics --- scripts/proxmox_rest_compute_probe.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/scripts/proxmox_rest_compute_probe.py b/scripts/proxmox_rest_compute_probe.py index fec850d4b..45b90be46 100644 --- a/scripts/proxmox_rest_compute_probe.py +++ b/scripts/proxmox_rest_compute_probe.py @@ -4,6 +4,7 @@ import argparse import json import pathlib +import re import ssl import time import urllib.error @@ -102,7 +103,11 @@ def put(self, path: str, fields: dict[str, Any] | None = None) -> Any: return self._request("PUT", path, fields or {}).data def delete(self, path: str, fields: dict[str, Any] | None = None) -> Any: - return self._request("DELETE", path, fields or {}).data + # PVE rejects request content for DELETE. DELETE options are query parameters. + if fields: + query = urllib.parse.urlencode({k: str(v) for k, v in fields.items()}) + path = f"{path}?{query}" + return self._request("DELETE", path).data def wait_task(api: PveApi, node: str, upid: str, timeout: float = 180.0) -> dict[str, Any]: @@ -114,9 +119,14 @@ def wait_task(api: PveApi, node: str, upid: str, timeout: float = 180.0) -> dict if isinstance(data, dict): last = data if data.get("status") == "stopped": - if data.get("exitstatus") != "OK": - raise ProxmoxProbeError(f"task failed: {data!r}") - return data + exitstatus = data.get("exitstatus") + # PVE::UPID::status_is_error() treats OK and WARNINGS: as non-errors. + if exitstatus == "OK" or ( + isinstance(exitstatus, str) + and re.fullmatch(r"WARNINGS: \\d+", exitstatus) + ): + return data + raise ProxmoxProbeError(f"task failed: {data!r}") time.sleep(1) raise ProxmoxProbeError(f"task timeout: {last!r}") From ae62e0d527ebeeb6f19eb5c9528ef53ac0c100bd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:49:09 +0200 Subject: [PATCH 441/719] RDTE: lock PVE warning and DELETE query behavior --- tests/test_proxmox_rest_compute_probe.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_proxmox_rest_compute_probe.py b/tests/test_proxmox_rest_compute_probe.py index ca4e3e35b..ec801e40b 100644 --- a/tests/test_proxmox_rest_compute_probe.py +++ b/tests/test_proxmox_rest_compute_probe.py @@ -2,7 +2,7 @@ import unittest from scripts.proxmox_rest_compute_probe import ( PVE_CONTAINER_SOURCE, PVE_MANAGER_API_SOURCE, PVE_QEMU_PACKAGE_SOURCE, PVE_QEMU_SERVER_SOURCE, - ProxmoxProbeError, lxc_create_fields, plan_only, vm_create_fields + ProxmoxProbeError, PveApi, Response, lxc_create_fields, plan_only, vm_create_fields, wait_task ) class ProxmoxRestComputeContractTests(unittest.TestCase): @@ -46,6 +46,26 @@ def test_vm_payload_does_not_smuggle_media_or_key(self): self.assertNotIn("password",f) self.assertNotIn("ciuser",f) + def test_wait_task_accepts_proxmox_warning_terminal_state(self): + class FakeApi: + def get(self, path): + return {"status":"stopped","exitstatus":"WARNINGS: 1","upid":"UPID:test"} + result=wait_task(FakeApi(),"pve-rdte","UPID:test",timeout=0.1) + self.assertEqual(result["exitstatus"],"WARNINGS: 1") + + def test_delete_options_are_query_parameters_not_request_body(self): + api=PveApi("https://example.invalid","root@pam","unused") + captured={} + def fake_request(method,path,fields=None): + captured.update(method=method,path=path,fields=fields) + return Response(200,"UPID:test") + api._request=fake_request + result=api.delete("/nodes/pve/lxc/9101",{"purge":1}) + self.assertEqual(result,"UPID:test") + self.assertEqual(captured["method"],"DELETE") + self.assertEqual(captured["path"],"/nodes/pve/lxc/9101?purge=1") + self.assertIsNone(captured["fields"]) + def test_apply_failure_path_has_owned_resource_cleanup(self): text=pathlib.Path("scripts/proxmox_rest_compute_probe.py").read_text(encoding="utf-8") self.assertIn("ownership_claimed=True",text) From e4c32e2d3c252bd3b5e9da8e6f104a3554026e6f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:49:54 +0200 Subject: [PATCH 442/719] RDTE: fix warning-state regex literal --- scripts/proxmox_rest_compute_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/proxmox_rest_compute_probe.py b/scripts/proxmox_rest_compute_probe.py index 45b90be46..77fe29237 100644 --- a/scripts/proxmox_rest_compute_probe.py +++ b/scripts/proxmox_rest_compute_probe.py @@ -123,7 +123,7 @@ def wait_task(api: PveApi, node: str, upid: str, timeout: float = 180.0) -> dict # PVE::UPID::status_is_error() treats OK and WARNINGS: as non-errors. if exitstatus == "OK" or ( isinstance(exitstatus, str) - and re.fullmatch(r"WARNINGS: \\d+", exitstatus) + and re.fullmatch(r"WARNINGS: \d+", exitstatus) ): return data raise ProxmoxProbeError(f"task failed: {data!r}") From 7543885012c607362a584ec98a30177eb0c67c58 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:50:43 +0200 Subject: [PATCH 443/719] RDTE: request PVE REST C0 rep 003 --- config/proxmox-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/proxmox-rdte-run-request.json b/config/proxmox-rdte-run-request.json index bf8110c48..7c26be33a 100644 --- a/config/proxmox-rdte-run-request.json +++ b/config/proxmox-rdte-run-request.json @@ -1,9 +1,9 @@ { "schema": "gha-kvm-proxmox-run-request/v1", - "request_id": "pve-9-2-rest-container-c0-002-official-template-transport", + "request_id": "pve-9-2-rest-container-c0-003-task-delete-semantics", "version": "9.2-1", "compute_fixture": "container-c0", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Re-run the exact Proxmox VE 9.2-1 native REST system-container C0 parity fixture after rep 001 retained healthy install/API/package census but failed before mutation because the harness used a non-canonical HTTPS transport for download.proxmox.com. Preserve exact target, REST lifecycle, template identity, SHA-512 integrity, cleanup and claim boundaries. The only causal delta is merged #294: use the exact pve-manager-declared http://download.proxmox.com/images catalog transport while retaining SHA-512 verification. Require REST materialization oracle=true, owned-resource absence and fixture-input cleanup; no guest/C1, nested-KVM, VM, Firecracker or Windows claim." + "purpose": "Re-run the exact Proxmox VE 9.2-1 native REST system-container C0 parity fixture after rep 002 reached REST create but the adapter incorrectly rejected Proxmox's non-error WARNINGS: N terminal task state and incorrectly sent purge=1 as DELETE request content. Preserve exact target, source-declared template transport, SHA-512 integrity, REST lifecycle, cleanup and claim boundaries. The only causal delta is merged #298: accept OK or WARNINGS: N per Proxmox task semantics and encode DELETE options as query parameters. Require REST materialization oracle=true, owned-resource absence and fixture-input cleanup; no guest/C1, nested-KVM, VM, Firecracker or Windows claim." } From 4cfc1496d77ca859add5a2b0c8f8c55eea7fc9c1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:52:49 +0200 Subject: [PATCH 444/719] RDTE: bind BETA.3 VM adapter to public entitlement APIs --- config/compute-materialization-targets.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/config/compute-materialization-targets.json b/config/compute-materialization-targets.json index 10f47e8c8..8edaafc0b 100644 --- a/config/compute-materialization-targets.json +++ b/config/compute-materialization-targets.json @@ -389,7 +389,9 @@ "vm.stop", "vm.restart", "vm.virtualization_details", - "pool.dataset.query" + "pool.dataset.query", + "system.product_type", + "system.feature_enabled" ], "guest_oracle": "network-or-console-capability", "restart_semantics": "native", @@ -402,9 +404,11 @@ "vm.device.delete" ], "v0_storage_ownership": "exact-owned-zvol-device", - "read_only_bootstrap_methods": [ - "vm.license_active" - ] + "public_entitlement": { + "product_type_method": "system.product_type", + "feature_method": "system.feature_enabled", + "enterprise_feature": "VMS" + } } ], "api_schema_fingerprint": { From b4a27bacc4b3e5d222b7390f9cc6e6bc7c3a6110 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:52:51 +0200 Subject: [PATCH 445/719] RDTE: lock BETA.3 public entitlement adapter binding --- .../test_compute_materialization_contract.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_compute_materialization_contract.py b/tests/test_compute_materialization_contract.py index 894a5f7db..e8501d10e 100644 --- a/tests/test_compute_materialization_contract.py +++ b/tests/test_compute_materialization_contract.py @@ -104,6 +104,26 @@ def test_2504_selects_legacy_incus_container(self): ) self.assertEqual(a["id"], "truenas-virt-incus-container") + def test_beta3_vm_adapter_binds_public_entitlement_only(self): + adapter=choose_adapter( + registry(), "truenas", "26.0.0-BETA.3", "vm", + { + "vm.query","vm.create","vm.update","vm.delete","vm.status", + "vm.start","vm.stop","vm.restart","vm.virtualization_details", + "pool.dataset.query","system.product_type","system.feature_enabled", + "vm.device.query","vm.device.create","vm.device.update","vm.device.delete", + }, + ) + self.assertEqual(adapter["id"],"truenas-vm-libvirt") + self.assertEqual(adapter["public_entitlement"],{ + "product_type_method":"system.product_type", + "feature_method":"system.feature_enabled", + "enterprise_feature":"VMS", + }) + self.assertNotIn("read_only_bootstrap_methods",adapter) + self.assertIn("system.product_type",adapter["required_methods"]) + self.assertIn("system.feature_enabled",adapter["required_methods"]) + def test_26_selects_lxc_container(self): a = choose_adapter( registry(), "truenas", "26.0.0-BETA.3", "container", From f052ed1baaab8043e3aa0876d5752c2937d7d76f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 03:35:02 +0200 Subject: [PATCH 446/719] test: align BETA.3 VM fixture with public entitlement contract --- tests/test_compute_materialization_contract.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_compute_materialization_contract.py b/tests/test_compute_materialization_contract.py index e8501d10e..22843d52a 100644 --- a/tests/test_compute_materialization_contract.py +++ b/tests/test_compute_materialization_contract.py @@ -49,7 +49,19 @@ def registry(): "id": "truenas-vm-libvirt", "source_api_family": "v26_0_0", "preferred": True, - "required_methods": ["vm.query", "vm.create"], + "required_methods": [ + "vm.query", "vm.create", "vm.update", "vm.delete", "vm.status", + "vm.start", "vm.stop", "vm.restart", "vm.virtualization_details", + "pool.dataset.query", "system.product_type", "system.feature_enabled", + ], + "required_device_methods": [ + "vm.device.query", "vm.device.create", "vm.device.update", "vm.device.delete", + ], + "public_entitlement": { + "product_type_method": "system.product_type", + "feature_method": "system.feature_enabled", + "enterprise_feature": "VMS", + }, }], }, ] From 578797817869b51877ff953aa09869adc2f45056 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 03:35:53 +0200 Subject: [PATCH 447/719] RDTE: request TrueNAS BETA.3 VM V0 rep 001 --- config/truenas-rdte-run-request.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3046d9e17..43bfc7cbc 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-container-c0-002-stop-options", + "request_id": "tn-26-beta3-t3-compute-vm-v0-001-public-entitlement", "version": "26.0.0-BETA.3", "rung": "t3", - "compute_fixture": "container-c0", + "compute_fixture": "vm-v0", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Re-run the exact native TrueNAS 26.0.0-BETA.3 container C0 materialization fixture after rep 001 reached the product API but failed because the harness supplied unsupported container.stop options.timeout. Preserve target, image discovery, create/readback/start/update/restart-or-stop+start/delete/absence, pool, cleanup and C1 claim-boundary semantics. The only causal delta is merged PR #282: remove options.timeout while retaining force_after_timeout. No C0 acceptance is inferred without SUPPORTED plus oracleSatisfied=true and c0_oracle_satisfied=true with zero-residue cleanup." + "purpose": "Exercise the exact native TrueNAS 26.0.0-BETA.3 VM V0 materialization fixture after cross-platform container C0 parity is proven. Require the BETA.3 adapter's public entitlement gate via system.product_type plus system.feature_enabled(VMS), then prove source/capability-gated vm.* create/update/readback and exact-owned ZVOL device create/update/delete with independent dataset observation and zero-residue cleanup. This is strictly pre-guest: no guest boot, nonce, nested KVM, Firecracker, or Windows 11 claim is permitted. No V0 acceptance is inferred without SUPPORTED plus oracleSatisfied=true and v0_oracle_satisfied=true with zero owned residue." } From 0026de80222f3fa90b7dfdf18b7a8c76d7f51267 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:10:56 +0200 Subject: [PATCH 448/719] RDTE: enforce exact TrueNAS VM name contract --- scripts/truenas_compute_vm_probe.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_compute_vm_probe.py b/scripts/truenas_compute_vm_probe.py index df2e5df86..42970527d 100644 --- a/scripts/truenas_compute_vm_probe.py +++ b/scripts/truenas_compute_vm_probe.py @@ -4,6 +4,7 @@ import argparse import json import pathlib +import re import time from typing import Any @@ -26,6 +27,9 @@ def normalize_system_version(value: Any) -> str: def native_vm_create_payload(name: str, generation: int = 1) -> dict[str, Any]: + # TrueNAS vm.* validates names against ^[a-zA-Z_0-9]+$ before create. + if not re.fullmatch(r"[A-Za-z0-9_]+", name): + raise ProbeError("native TrueNAS VM name must contain only letters, digits, or underscore") return { "name": name, "description": f"Agent Dispatch disposable VM V0 fixture generation {generation}", @@ -143,7 +147,7 @@ def main() -> int: p=argparse.ArgumentParser() p.add_argument("--registry",type=pathlib.Path,default=pathlib.Path("config/compute-materialization-targets.json")) p.add_argument("--target-version",required=True) - p.add_argument("--name",default="rdte-compute-vm-v0") + p.add_argument("--name",default="rdtecomputevmv0") p.add_argument("--legacy-image") p.add_argument("--zvol-name") p.add_argument("--zvol-size-bytes",type=int,default=2*1024*1024*1024) From fa3a37a64a1f2bcdd67f4eeb7d4d8ea4a6821940 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:10:59 +0200 Subject: [PATCH 449/719] test: reject invalid native VM names --- tests/test_truenas_compute_vm_probe.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_compute_vm_probe.py b/tests/test_truenas_compute_vm_probe.py index eb66112ef..9e6a33cb3 100644 --- a/tests/test_truenas_compute_vm_probe.py +++ b/tests/test_truenas_compute_vm_probe.py @@ -18,14 +18,20 @@ def test_version_normalization(self): self.assertEqual(normalize_system_version("TrueNAS-25.10.7"),"25.10.7") def test_native_vm_defaults_are_stopped_safe_contract(self): - p=native_vm_create_payload("rdte-v0") - self.assertEqual(p["name"],"rdte-v0") + p=native_vm_create_payload("rdtecomputevmv0") + self.assertEqual(p["name"],"rdtecomputevmv0") self.assertFalse(p["autostart"]) self.assertFalse(p["ensure_display_device"]) self.assertEqual(p["bootloader"],"UEFI") self.assertFalse(p["trusted_platform_module"]) self.assertFalse(p["enable_secure_boot"]) + def test_native_vm_name_matches_exact_trueNAS_contract(self): + self.assertEqual(native_vm_create_payload("rdte_vm_v0")["name"], "rdte_vm_v0") + for invalid in ("rdte-v0", "rdte vm v0", "rdte/v0"): + with self.assertRaises(ProbeError): + native_vm_create_payload(invalid) + def test_legacy_vm_uses_vm_instance_type(self): p=legacy_vm_create_payload("rdte-v0","ubuntu/24.04") self.assertEqual(p["instance_type"],"VM") From cf8d2ee34366e870c5b6ca3c32feb9c154b1b3f6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:11:01 +0200 Subject: [PATCH 450/719] RDTE: use legal BETA.3 VM V0 fixture name --- scripts/gha_kvm_truenas_rdte.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index a1df83802..87a8451d3 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -769,6 +769,7 @@ PY python3 "$SCRIPT_DIR/truenas_compute_vm_probe.py" \ --registry "$SCRIPT_DIR/../config/compute-materialization-targets.json" \ --target-version "$VERSION" \ + --name "rdtecomputevmv0" \ --zvol-name "$DATA_POOL_NAME/rdte-compute-vm-v0" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ "${MIDDLEWARE_TLS_ARG[@]}" \ From eea79a21d2ef88869e1848a39c2f90c89d72a646 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:11:42 +0200 Subject: [PATCH 451/719] RDTE: align BETA.3 disk device payload to exact schema --- scripts/truenas_compute_vm_probe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/truenas_compute_vm_probe.py b/scripts/truenas_compute_vm_probe.py index 42970527d..4ad6f6a3d 100644 --- a/scripts/truenas_compute_vm_probe.py +++ b/scripts/truenas_compute_vm_probe.py @@ -80,7 +80,6 @@ def owned_zvol_device(vm_id: int, zvol_name: str, size_bytes: int) -> dict[str, "create_zvol": True, "zvol_name": zvol_name, "zvol_volsize": size_bytes, - "boot": False, }, "order": 1000, } From a631d9277a7ac1ac840d2944daabb3a9bd7b1215 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:11:45 +0200 Subject: [PATCH 452/719] test: reject unsupported VM disk boot field --- tests/test_truenas_compute_vm_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_compute_vm_probe.py b/tests/test_truenas_compute_vm_probe.py index 9e6a33cb3..486635061 100644 --- a/tests/test_truenas_compute_vm_probe.py +++ b/tests/test_truenas_compute_vm_probe.py @@ -47,7 +47,7 @@ def test_owned_zvol_device_is_explicit_and_nonbooting(self): self.assertEqual(d["vm"],7) self.assertTrue(d["attributes"]["create_zvol"]) self.assertEqual(d["attributes"]["zvol_name"],"rdtepool/rdte-compute-vm-v0") - self.assertFalse(d["attributes"]["boot"]) + self.assertNotIn("boot", d["attributes"]) self.assertEqual(d["order"],1000) def test_zvol_requires_owned_parent_name(self): From 3fa22c1ce6c393ae1b351b871948d74cc30f427a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:13:30 +0200 Subject: [PATCH 453/719] RDTE: request TrueNAS BETA.3 VM V0 rep 002 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 43bfc7cbc..d8afcfb93 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-vm-v0-001-public-entitlement", + "request_id": "tn-26-beta3-t3-compute-vm-v0-002-name-device-schema", "version": "26.0.0-BETA.3", "rung": "t3", "compute_fixture": "vm-v0", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Exercise the exact native TrueNAS 26.0.0-BETA.3 VM V0 materialization fixture after cross-platform container C0 parity is proven. Require the BETA.3 adapter's public entitlement gate via system.product_type plus system.feature_enabled(VMS), then prove source/capability-gated vm.* create/update/readback and exact-owned ZVOL device create/update/delete with independent dataset observation and zero-residue cleanup. This is strictly pre-guest: no guest boot, nonce, nested KVM, Firecracker, or Windows 11 claim is permitted. No V0 acceptance is inferred without SUPPORTED plus oracleSatisfied=true and v0_oracle_satisfied=true with zero owned residue." + "purpose": "Re-run the exact native TrueNAS 26.0.0-BETA.3 VM V0 materialization fixture after rep 001 reached vm.create and falsified the fixture name. The causal repair uses exact-source-valid VM name rdtecomputevmv0, enforces the BETA.3 VM name regex locally, and removes the unsupported DISK attributes.boot field found by source-level red-team before mutation. Preserve the public entitlement gate, exact-owned ZVOL/device lifecycle, independent dataset observation, and zero-residue cleanup. No guest boot, nonce, nested KVM, Firecracker, or Windows claim is permitted." } From 2f6b43dbb0d845554bdb30763aed6897166f9407 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:17:06 +0200 Subject: [PATCH 454/719] RDTE: pin portable Linux V1 guest fixture --- config/compute-guest-profiles.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/config/compute-guest-profiles.json b/config/compute-guest-profiles.json index 8f6900053..108119feb 100644 --- a/config/compute-guest-profiles.json +++ b/config/compute-guest-profiles.json @@ -146,5 +146,25 @@ "V2": "observe nested KVM and, iff present, execute Firecracker portable contract from #277", "W1": "Windows 11 evaluation/user media install + firmware/TPM/Secure Boot readback + bounded guest oracle + lifecycle/cleanup", "WB": "WinBot backend candidacy only after W1; admission decision remains windows-utilities#26" + }, + "fixtures": { + "linux_v1": { + "id": "cirros-0.6.3-x86_64-nocloud-serial-nonce", + "release": "0.6.3", + "architecture": "x86_64", + "source": { + "url": "https://download.cirros-cloud.net/0.6.3/cirros-0.6.3-x86_64-disk.img", + "sha256": "7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b", + "checksum_url": "https://download.cirros-cloud.net/0.6.3/SHA256SUMS" + }, + "datasource": "nocloud", + "user_data_contract": "executable-shell", + "oracle": { + "transport": "serial-console", + "nonce_prefix": "AGENT_DISPATCH_V1_NONCE=", + "must_match_injected_nonce": true + }, + "claim_boundary": "V1 proves pinned Linux guest boot plus exact externally observed per-run nonce only; nested KVM, Firecracker and Windows remain separate rungs" + } } } From cc000f4bbe6630d19d690d9c6b0f9eeefcf9fd86 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:17:10 +0200 Subject: [PATCH 455/719] RDTE: validate exact V1 guest artifact and nonce oracle --- scripts/compute_guest_profile_contract.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scripts/compute_guest_profile_contract.py b/scripts/compute_guest_profile_contract.py index 03bd6b9d0..02a8d7940 100644 --- a/scripts/compute_guest_profile_contract.py +++ b/scripts/compute_guest_profile_contract.py @@ -29,6 +29,26 @@ def validate(p:dict[str,Any])->dict[str,Any]: "guest_oracle_required_before_backend_admission", )): raise ProfileError("safety/claim policy incomplete") + fixtures=p.get("fixtures") + if not isinstance(fixtures,dict) or set(fixtures)!={"linux_v1"}: + raise ProfileError("exact linux_v1 fixture missing") + v1=fixtures["linux_v1"] + if v1.get("id")!="cirros-0.6.3-x86_64-nocloud-serial-nonce": + raise ProfileError("linux_v1 fixture id drift") + if v1.get("release")!="0.6.3" or v1.get("architecture")!="x86_64": + raise ProfileError("linux_v1 release/architecture drift") + source=v1.get("source") + if not isinstance(source,dict) or source.get("url")!="https://download.cirros-cloud.net/0.6.3/cirros-0.6.3-x86_64-disk.img": + raise ProfileError("linux_v1 source URL drift") + if source.get("sha256")!="7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b": + raise ProfileError("linux_v1 source digest drift") + if source.get("checksum_url")!="https://download.cirros-cloud.net/0.6.3/SHA256SUMS": + raise ProfileError("linux_v1 checksum authority drift") + if v1.get("datasource")!="nocloud" or v1.get("user_data_contract")!="executable-shell": + raise ProfileError("linux_v1 datasource contract drift") + oracle=v1.get("oracle") + if not isinstance(oracle,dict) or oracle.get("transport")!="serial-console" or oracle.get("nonce_prefix")!="AGENT_DISPATCH_V1_NONCE=" or oracle.get("must_match_injected_nonce") is not True: + raise ProfileError("linux_v1 guest nonce oracle incomplete") tn=p.get("truenas") expected={"25.04.1","25.04.2.6","25.10.7","26.0.0-BETA.3"} if not isinstance(tn,dict) or set(tn)!=expected: From 51d692edd986c97b0aea2c043deadb667e2c861c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:17:12 +0200 Subject: [PATCH 456/719] test: lock V1 fixture provenance and claim boundary --- tests/test_compute_guest_profile_contract.py | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_compute_guest_profile_contract.py b/tests/test_compute_guest_profile_contract.py index 0b07e1e77..7370f3e44 100644 --- a/tests/test_compute_guest_profile_contract.py +++ b/tests/test_compute_guest_profile_contract.py @@ -16,6 +16,26 @@ def fixture(): "windows_product_key_must_not_be_embedded":True, "guest_oracle_required_before_backend_admission":True, }, + "fixtures":{ + "linux_v1":{ + "id":"cirros-0.6.3-x86_64-nocloud-serial-nonce", + "release":"0.6.3", + "architecture":"x86_64", + "source":{ + "url":"https://download.cirros-cloud.net/0.6.3/cirros-0.6.3-x86_64-disk.img", + "sha256":"7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b", + "checksum_url":"https://download.cirros-cloud.net/0.6.3/SHA256SUMS", + }, + "datasource":"nocloud", + "user_data_contract":"executable-shell", + "oracle":{ + "transport":"serial-console", + "nonce_prefix":"AGENT_DISPATCH_V1_NONCE=", + "must_match_injected_nonce":True, + }, + "claim_boundary":"x", + } + }, "truenas":{}, "proxmox":{ "9.2-1":{ @@ -63,6 +83,16 @@ def test_runtime_claim_fails_closed(self): p["truenas"]["25.10.7"]["windows11"]["runtime_status"]="PASS" with self.assertRaises(ProfileError): validate(p) + def test_linux_v1_fixture_digest_fails_closed(self): + p=self.profile() + p["fixtures"]["linux_v1"]["source"]["sha256"]="0"*64 + with self.assertRaises(ProfileError): + validate(p) + def test_linux_v1_nonce_oracle_fails_closed(self): + p=self.profile() + p["fixtures"]["linux_v1"]["oracle"]["must_match_injected_nonce"]=False + with self.assertRaises(ProfileError): + validate(p) def test_target_growth_fails_closed(self): p=self.profile() p["truenas"]["27.0.0"]=tnrow("27.0.0") From 2bd9db2c13be476553fabcc49623ce0c36113b1f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:18:56 +0200 Subject: [PATCH 457/719] RDTE: add deterministic V1 NoCloud seed generator --- scripts/compute_guest_seed.py | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 scripts/compute_guest_seed.py diff --git a/scripts/compute_guest_seed.py b/scripts/compute_guest_seed.py new file mode 100644 index 000000000..1d1e7ecff --- /dev/null +++ b/scripts/compute_guest_seed.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import pathlib +import re + +NONCE_RE = re.compile(r"^[A-Za-z0-9_-]{16,64}$") +INSTANCE_RE = re.compile(r"^[A-Za-z0-9_.-]{1,64}$") +PREFIX = "AGENT_DISPATCH_V1_NONCE=" + + +class SeedError(RuntimeError): + pass + + +def validate_token(value: str, pattern: re.Pattern[str], label: str) -> str: + if not pattern.fullmatch(value): + raise SeedError(f"{label} violates deterministic seed contract") + return value + + +def render_seed(nonce: str, instance_id: str) -> tuple[str, str]: + nonce = validate_token(nonce, NONCE_RE, "nonce") + instance_id = validate_token(instance_id, INSTANCE_RE, "instance-id") + meta = f"instance-id: {instance_id}\nlocal-hostname: rdtev1\n" + user = ( + "#!/bin/sh\n" + "set -eu\n" + f"nonce='{nonce}'\n" + f"printf '%s%s\\n' '{PREFIX}' \"$nonce\" >/dev/ttyS0\n" + f"printf '%s%s\\n' '{PREFIX}' \"$nonce\" >/dev/console 2>/dev/null || true\n" + ) + return meta, user + + +def write_seed(out_dir: pathlib.Path, nonce: str, instance_id: str) -> None: + meta, user = render_seed(nonce, instance_id) + out_dir.mkdir(parents=True, exist_ok=True) + (out_dir / "meta-data").write_text(meta, encoding="utf-8") + user_path = out_dir / "user-data" + user_path.write_text(user, encoding="utf-8") + user_path.chmod(0o755) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--nonce", required=True) + ap.add_argument("--instance-id", required=True) + ap.add_argument("--out-dir", type=pathlib.Path, required=True) + args = ap.parse_args() + try: + write_seed(args.out_dir, args.nonce, args.instance_id) + except (SeedError, OSError) as exc: + print(f"ERROR: {exc}") + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From c48df872133bc08b6ae932cfbf8c39e6d90ca18c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:18:58 +0200 Subject: [PATCH 458/719] test: lock V1 serial nonce seed contract --- tests/test_compute_guest_seed.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/test_compute_guest_seed.py diff --git a/tests/test_compute_guest_seed.py b/tests/test_compute_guest_seed.py new file mode 100644 index 000000000..89ac80d8f --- /dev/null +++ b/tests/test_compute_guest_seed.py @@ -0,0 +1,35 @@ +import pathlib +import tempfile +import unittest + +from scripts.compute_guest_seed import PREFIX, SeedError, render_seed, write_seed + + +class ComputeGuestSeedTests(unittest.TestCase): + def test_seed_is_deterministic_and_serial_nonce_is_external(self): + nonce = "rep001nonceABCDEF12" + meta, user = render_seed(nonce, "rdte-v1-rep001") + self.assertEqual(meta, "instance-id: rdte-v1-rep001\nlocal-hostname: rdtev1\n") + self.assertTrue(user.startswith("#!/bin/sh\nset -eu\n")) + self.assertIn(PREFIX, user) + self.assertIn(">/dev/ttyS0", user) + self.assertIn(">/dev/console", user) + self.assertNotIn("curl ", user) + self.assertNotIn("wget ", user) + + def test_seed_rejects_shell_metacharacters(self): + for bad in ("short", "nonce;touch-pwned-1234", "$(id)-abcdefghijkl"): + with self.assertRaises(SeedError): + render_seed(bad, "rdte-v1-rep001") + + def test_write_seed_uses_expected_names_and_executable_userdata(self): + with tempfile.TemporaryDirectory() as td: + root = pathlib.Path(td) + write_seed(root, "rep002nonceABCDEF12", "rdte-v1-rep002") + self.assertTrue((root / "meta-data").is_file()) + self.assertTrue((root / "user-data").is_file()) + self.assertEqual((root / "user-data").stat().st_mode & 0o777, 0o755) + + +if __name__ == "__main__": + unittest.main() From d1c72dce0f9e516eb130388fe93c5545b062c978 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:19:01 +0200 Subject: [PATCH 459/719] CI: validate V1 NoCloud seed generator --- .github/workflows/validate-compute-guest-profile-contract.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-compute-guest-profile-contract.yml b/.github/workflows/validate-compute-guest-profile-contract.yml index 619c9429f..4ba177bcd 100644 --- a/.github/workflows/validate-compute-guest-profile-contract.yml +++ b/.github/workflows/validate-compute-guest-profile-contract.yml @@ -4,7 +4,9 @@ on: paths: - "config/compute-guest-profiles.json" - "scripts/compute_guest_profile_contract.py" + - "scripts/compute_guest_seed.py" - "tests/test_compute_guest_profile_contract.py" + - "tests/test_compute_guest_seed.py" - ".github/workflows/validate-compute-guest-profile-contract.yml" workflow_dispatch: permissions: @@ -14,5 +16,5 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py + - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py - run: python3 scripts/compute_guest_profile_contract.py From 87c89f3de67c1bf8e8c851cec1f8847ef81144b8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:23:24 +0200 Subject: [PATCH 460/719] RDTE: bind BETA.3 V1 to public VM console surfaces --- config/compute-guest-profiles.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config/compute-guest-profiles.json b/config/compute-guest-profiles.json index 108119feb..cdff69110 100644 --- a/config/compute-guest-profiles.json +++ b/config/compute-guest-profiles.json @@ -124,6 +124,17 @@ "cpu_passthrough_control": "vm.create.cpu_mode=HOST-PASSTHROUGH", "nested_kvm_requirement": "observe /dev/kvm inside guest before invoking Firecracker", "runtime_status": "OPEN" + }, + "linux_v1": { + "source_status": "CANDIDATE", + "runtime_status": "OPEN", + "fixture": "cirros-0.6.3-x86_64-nocloud-serial-nonce", + "public_surfaces": { + "boot_disk": "vm.device.create RAW exists=true boot=true", + "seed_media": "vm.device.create CDROM", + "console_discovery": "vm.get_console" + }, + "observation_requirement": "consume guest console through supported TrueNAS console surface; do not inject raw.qemu or invoke virsh directly" } } }, From 64e81552f09e88ffef62e5f6c5a2658d7c8b6154 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:23:27 +0200 Subject: [PATCH 461/719] RDTE: validate BETA.3 V1 public surface binding --- scripts/compute_guest_profile_contract.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/compute_guest_profile_contract.py b/scripts/compute_guest_profile_contract.py index 02a8d7940..5890075ea 100644 --- a/scripts/compute_guest_profile_contract.py +++ b/scripts/compute_guest_profile_contract.py @@ -65,6 +65,22 @@ def validate(p:dict[str,Any])->dict[str,Any]: else: if row["firecracker"].get("cpu_passthrough_control")!="vm.create.cpu_mode=HOST-PASSTHROUGH": raise ProfileError(f"{version}: native Firecracker candidate must bind HOST-PASSTHROUGH") + if version=="26.0.0-BETA.3": + linux=row.get("linux_v1") + if not isinstance(linux,dict) or linux.get("source_status")!="CANDIDATE" or linux.get("runtime_status")!="OPEN": + raise ProfileError("BETA.3 Linux V1 must remain source candidate/runtime OPEN") + if linux.get("fixture")!="cirros-0.6.3-x86_64-nocloud-serial-nonce": + raise ProfileError("BETA.3 Linux V1 fixture binding drift") + surfaces=linux.get("public_surfaces") + if surfaces != { + "boot_disk":"vm.device.create RAW exists=true boot=true", + "seed_media":"vm.device.create CDROM", + "console_discovery":"vm.get_console", + }: + raise ProfileError("BETA.3 Linux V1 public surface binding drift") + observation=linux.get("observation_requirement","") + if "raw.qemu" not in observation or "virsh directly" not in observation: + raise ProfileError("BETA.3 Linux V1 must prohibit private hypervisor console escape") win=row["windows11"] if win.get("source_status")!="CANDIDATE": raise ProfileError(f"{version}: Windows source status must remain CANDIDATE") From 2d83905f4e4e4159eb605c9e1aecb24b31a6cb58 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:23:30 +0200 Subject: [PATCH 462/719] test: keep V1 console off private hypervisor escape --- tests/test_compute_guest_profile_contract.py | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_compute_guest_profile_contract.py b/tests/test_compute_guest_profile_contract.py index 7370f3e44..008b7a725 100644 --- a/tests/test_compute_guest_profile_contract.py +++ b/tests/test_compute_guest_profile_contract.py @@ -48,7 +48,7 @@ def fixture(): def tnrow(version): legacy=version=="25.04.1" - return { + row = { "windows11":{ "source_status":"CANDIDATE", "runtime_status":"OPEN", @@ -64,6 +64,19 @@ def tnrow(version): "runtime_status":"OPEN", }, } + if version=="26.0.0-BETA.3": + row["linux_v1"]={ + "source_status":"CANDIDATE", + "runtime_status":"OPEN", + "fixture":"cirros-0.6.3-x86_64-nocloud-serial-nonce", + "public_surfaces":{ + "boot_disk":"vm.device.create RAW exists=true boot=true", + "seed_media":"vm.device.create CDROM", + "console_discovery":"vm.get_console", + }, + "observation_requirement":"consume guest console through supported TrueNAS console surface; do not inject raw.qemu or invoke virsh directly", + } + return row class Tests(unittest.TestCase): def profile(self): @@ -93,6 +106,16 @@ def test_linux_v1_nonce_oracle_fails_closed(self): p["fixtures"]["linux_v1"]["oracle"]["must_match_injected_nonce"]=False with self.assertRaises(ProfileError): validate(p) + def test_beta3_v1_console_must_remain_public_surface(self): + p=self.profile() + p["truenas"]["26.0.0-BETA.3"]["linux_v1"]["public_surfaces"]["console_discovery"]="virsh console" + with self.assertRaises(ProfileError): + validate(p) + def test_beta3_v1_runtime_claim_fails_closed(self): + p=self.profile() + p["truenas"]["26.0.0-BETA.3"]["linux_v1"]["runtime_status"]="PASS" + with self.assertRaises(ProfileError): + validate(p) def test_target_growth_fails_closed(self): p=self.profile() p["truenas"]["27.0.0"]=tnrow("27.0.0") From 6e29e5e7dc3ff7e09146b9827816bf8feec25c52 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:26:46 +0200 Subject: [PATCH 463/719] RDTE: add public TrueNAS V1 console nonce observer --- scripts/truenas_vm_console_probe.py | 148 ++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 scripts/truenas_vm_console_probe.py diff --git a/scripts/truenas_vm_console_probe.py b/scripts/truenas_vm_console_probe.py new file mode 100644 index 000000000..aeb9244b7 --- /dev/null +++ b/scripts/truenas_vm_console_probe.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import struct +import time + +from scripts.compute_guest_seed import NONCE_RE, PREFIX, SeedError, validate_token +from scripts.truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +class ShellWebSocket(WebSocket): + def send_binary(self, payload: bytes) -> None: + self._send_frame(0x2, payload) + + def recv_payload(self) -> tuple[int, bytes]: + while True: + b1, b2 = self._read_exact(2) + opcode = b1 & 0x0F + n = b2 & 0x7F + if n == 126: + n = struct.unpack("!H", self._read_exact(2))[0] + elif n == 127: + n = struct.unpack("!Q", self._read_exact(8))[0] + masked = bool(b2 & 0x80) + mask = self._read_exact(4) if masked else b"" + payload = self._read_exact(n) + if masked: + payload = bytes(b ^ mask[i % 4] for i, b in enumerate(payload)) + if opcode == 0x8: + raise EOFError("websocket close frame") + if opcode == 0x9: + self._send_frame(0xA, payload) + continue + if opcode in {0x1, 0x2}: + return opcode, payload + + +def nonce_marker(nonce: str) -> bytes: + validate_token(nonce, NONCE_RE, "nonce") + return f"{PREFIX}{nonce}".encode("ascii") + + +def observe_console_nonce( + host: str, + port: int, + password: str, + vm_id: int, + nonce: str, + timeout: float, + tls: bool, +) -> dict: + marker = nonce_marker(nonce) + receipt = { + "schema": "truenas-vm-console-nonce/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "vm_id": vm_id, + "nonce_marker": marker.decode(), + "transport": "wss:/websocket/shell" if tls else "ws:/websocket/shell", + "claim_boundary": "external guest-console nonce only; no nested-KVM or guest capability claim", + } + ddp = shell = None + started = time.monotonic() + try: + ddp = WebSocket(host, port, timeout=min(timeout, 8), tls=tls) + ddp.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ddp, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError(f"DDP connect failed: {connected!r}") + auth = ddp_call(ddp, "1", "auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError(f"authentication failed: {auth!r}") + token = ddp_call(ddp, "2", "auth.generate_token", [300, {}, False]) + if not isinstance(token, str) or not token: + raise RuntimeError(f"auth.generate_token returned invalid token: {token!r}") + + shell = ShellWebSocket(host, port, path="/websocket/shell", timeout=min(timeout, 8), tls=tls) + shell.send_json({"token": token, "options": {"vm_id": vm_id}}) + buffer = bytearray() + deadline = time.monotonic() + timeout + connected_shell = False + while time.monotonic() < deadline: + opcode, payload = shell.recv_payload() + if opcode == 0x1: + try: + message = json.loads(payload.decode()) + except Exception: + message = None + if isinstance(message, dict) and message.get("msg") == "connected": + connected_shell = True + else: + buffer.extend(payload) + if len(buffer) > 65536: + del buffer[:-65536] + if marker in buffer: + receipt.update({ + "classification": "SUPPORTED", + "oracleSatisfied": True, + "shell_connected": connected_shell, + "elapsed_seconds": round(time.monotonic() - started, 3), + "detail": "exact injected V1 nonce observed through supported TrueNAS VM console websocket", + }) + return receipt + raise TimeoutError("exact V1 nonce not observed before console deadline") + except Exception as exc: + receipt["detail"] = f"{type(exc).__name__}: {exc}" + return receipt + finally: + if shell is not None: + shell.close() + if ddp is not None: + ddp.close() + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="127.0.0.1") + ap.add_argument("--port", type=int, required=True) + ap.add_argument("--password-file", type=pathlib.Path, required=True) + ap.add_argument("--vm-id", type=int, required=True) + ap.add_argument("--nonce", required=True) + ap.add_argument("--timeout", type=float, default=60) + ap.add_argument("--tls", action="store_true") + ap.add_argument("--out", type=pathlib.Path, required=True) + a = ap.parse_args() + try: + password = a.password_file.read_text(encoding="utf-8").strip() + result = observe_console_nonce(a.host, a.port, password, a.vm_id, a.nonce, a.timeout, a.tls) + except (OSError, SeedError) as exc: + result = { + "schema": "truenas-vm-console-nonce/v1", + "classification": "HARNESS_FAILURE", + "oracleSatisfied": False, + "detail": f"{type(exc).__name__}: {exc}", + } + a.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 0269ccf984079dbfbed8590f9228e55a39b3e842 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:26:48 +0200 Subject: [PATCH 464/719] test: lock exact V1 console nonce marker --- tests/test_truenas_vm_console_probe.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/test_truenas_vm_console_probe.py diff --git a/tests/test_truenas_vm_console_probe.py b/tests/test_truenas_vm_console_probe.py new file mode 100644 index 000000000..393973a0f --- /dev/null +++ b/tests/test_truenas_vm_console_probe.py @@ -0,0 +1,20 @@ +import unittest + +from scripts.compute_guest_seed import SeedError +from scripts.truenas_vm_console_probe import nonce_marker + + +class TrueNASVmConsoleProbeTests(unittest.TestCase): + def test_nonce_marker_is_exact_and_ascii(self): + self.assertEqual( + nonce_marker("rep001nonceABCDEF12"), + b"AGENT_DISPATCH_V1_NONCE=rep001nonceABCDEF12", + ) + + def test_nonce_marker_rejects_shell_metacharacters(self): + with self.assertRaises(SeedError): + nonce_marker("nonce;touch-pwned-1234") + + +if __name__ == "__main__": + unittest.main() From ac336fa931308c360980175ed9996420598d5ee0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:26:50 +0200 Subject: [PATCH 465/719] CI: validate public VM console observer --- .github/workflows/validate-compute-guest-profile-contract.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-compute-guest-profile-contract.yml b/.github/workflows/validate-compute-guest-profile-contract.yml index 4ba177bcd..06f41908a 100644 --- a/.github/workflows/validate-compute-guest-profile-contract.yml +++ b/.github/workflows/validate-compute-guest-profile-contract.yml @@ -5,8 +5,10 @@ on: - "config/compute-guest-profiles.json" - "scripts/compute_guest_profile_contract.py" - "scripts/compute_guest_seed.py" + - "scripts/truenas_vm_console_probe.py" - "tests/test_compute_guest_profile_contract.py" - "tests/test_compute_guest_seed.py" + - "tests/test_truenas_vm_console_probe.py" - ".github/workflows/validate-compute-guest-profile-contract.yml" workflow_dispatch: permissions: @@ -16,5 +18,5 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py + - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py tests/test_truenas_vm_console_probe.py - run: python3 scripts/compute_guest_profile_contract.py From 1e4f8242cb9051640b484be190a0caef20d7f6f0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:33:17 +0200 Subject: [PATCH 466/719] RDTE: add native TrueNAS Linux V1 probe --- scripts/truenas_compute_vm_v1_probe.py | 324 +++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 scripts/truenas_compute_vm_v1_probe.py diff --git a/scripts/truenas_compute_vm_v1_probe.py b/scripts/truenas_compute_vm_v1_probe.py new file mode 100644 index 000000000..ffdfbf42d --- /dev/null +++ b/scripts/truenas_compute_vm_v1_probe.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import base64 +import hashlib +import http.client +import json +import pathlib +import ssl +import time +from typing import Any + +from scripts.compute_guest_seed import NONCE_RE, SeedError, validate_token +from scripts.truenas_compute_vm_probe import native_vm_create_payload, normalize_system_version +from scripts.truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for +from scripts.truenas_vm_console_probe import observe_console_nonce + +PINNED_CIRROS_SHA256 = "7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b" + + +class V1ProbeError(RuntimeError): + pass + + +def sha256_file(path: pathlib.Path) -> str: + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def stage_paths(pool: str, stem: str = "rdtev1") -> dict[str, str]: + if not pool or "/" in pool or not pool.replace("_", "").isalnum(): + raise V1ProbeError("pool name violates V1 staging contract") + dataset = f"{pool}/{stem}stage" + mount = f"/mnt/{dataset}" + return { + "dataset": dataset, + "raw_image": f"{mount}/cirros-0.6.3-x86_64.raw", + "seed_iso": f"{mount}/seed.iso", + } + + +def raw_boot_device(vm_id: int, path: str) -> dict[str, Any]: + return { + "vm": vm_id, + "attributes": { + "dtype": "RAW", + "path": path, + "type": "VIRTIO", + "exists": True, + "boot": True, + }, + "order": 100, + } + + +def seed_cdrom_device(vm_id: int, path: str) -> dict[str, Any]: + return { + "vm": vm_id, + "attributes": {"dtype": "CDROM", "path": path}, + "order": 1000, + } + + +def multipart_upload_body(remote_path: str, filename: str, content: bytes, boundary: str) -> bytes: + request = json.dumps( + {"method": "filesystem.put", "params": [remote_path]}, + separators=(",", ":"), + ).encode() + parts = [ + b"--" + boundary.encode() + b"\r\n" + b'Content-Disposition: form-data; name="data"\r\n' + b"Content-Type: application/json\r\n\r\n" + request + b"\r\n", + b"--" + boundary.encode() + b"\r\n" + + f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode() + + b"Content-Type: application/octet-stream\r\n\r\n" + + content + b"\r\n", + b"--" + boundary.encode() + b"--\r\n", + ] + return b"".join(parts) + + +def upload_file(host: str, port: int, username: str, password: str, local: pathlib.Path, + remote: str, tls: bool, timeout: float) -> int: + boundary = "agentdispatchv1upload" + body = multipart_upload_body(remote, local.name, local.read_bytes(), boundary) + auth = base64.b64encode(f"{username}:{password}".encode()).decode() + if tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + conn = http.client.HTTPSConnection(host, port, timeout=timeout, context=context) + else: + conn = http.client.HTTPConnection(host, port, timeout=timeout) + try: + conn.request( + "POST", + "/_upload/", + body=body, + headers={ + "Authorization": f"Basic {auth}", + "Content-Type": f"multipart/form-data; boundary={boundary}", + "Content-Length": str(len(body)), + }, + ) + response = conn.getresponse() + payload = response.read() + if response.status // 100 != 2: + raise V1ProbeError(f"filesystem.put upload HTTP {response.status}: {payload[:1000]!r}") + decoded = json.loads(payload.decode()) + job_id = decoded.get("job_id") + if not isinstance(job_id, int) or isinstance(job_id, bool): + raise V1ProbeError(f"filesystem.put upload returned invalid job id: {decoded!r}") + return job_id + finally: + conn.close() + + +def wait_job(ws: WebSocket, job_id: int, timeout: float, label: str) -> Any: + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + row = ddp_call(ws, f"job-{label}-{job_id}", "core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if row and row.get("state") == "SUCCESS": + return row.get("result") + if row and row.get("state") in {"FAILED", "ABORTED"}: + raise V1ProbeError(f"{label} job {row.get('state')}: {row.get('error') or row.get('exception')}") + time.sleep(1) + raise V1ProbeError(f"{label} job timed out") + + +def plan(pool: str, name: str, nonce: str) -> dict[str, Any]: + validate_token(nonce, NONCE_RE, "nonce") + paths = stage_paths(pool) + return { + "schema": "truenas-compute-vm-v1-plan/v1", + "name": name, + "staging": paths, + "vm_create": native_vm_create_payload(name), + "devices": [ + raw_boot_device(0, paths["raw_image"]), + seed_cdrom_device(0, paths["seed_iso"]), + ], + "oracle": { + "surface": "/websocket/shell", + "nonce": f"AGENT_DISPATCH_V1_NONCE={nonce}", + }, + "claim_boundary": "pinned Linux guest boot + exact external nonce only", + } + + +def run_apply(a: argparse.Namespace) -> dict[str, Any]: + validate_token(a.nonce, NONCE_RE, "nonce") + source_sha = sha256_file(a.source_image) + if source_sha != PINNED_CIRROS_SHA256: + raise V1ProbeError(f"source CirrOS digest mismatch: {source_sha}") + raw_sha = sha256_file(a.raw_image) + seed_sha = sha256_file(a.seed_iso) + password = a.password_file.read_text(encoding="utf-8").strip() + paths = stage_paths(a.pool) + receipt: dict[str, Any] = { + "schema": "truenas-compute-vm-v1/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "v1_oracle_satisfied": False, + "source": { + "cirros_release": "0.6.3", + "cirros_sha256": source_sha, + "converted_raw_sha256": raw_sha, + "seed_iso_sha256": seed_sha, + }, + "staging": paths, + "cleanup": {"attempted": False, "vm_absent": False, "staging_dataset_absent": False}, + "claim_boundary": "V1 Linux guest boot + exact external nonce only; nested KVM, Firecracker and Windows remain separate", + } + ws: WebSocket | None = None + vm_id: int | None = None + stage_owned = False + try: + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise V1ProbeError(f"DDP connection failed: {connected!r}") + auth = ddp_call(ws, "auth", "auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": a.username, + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise V1ProbeError(f"authentication failed: {auth!r}") + version = normalize_system_version(ddp_call(ws, "version", "system.version", [])) + if version != a.target_version: + raise V1ProbeError(f"target version mismatch: observed={version} expected={a.target_version}") + + if ddp_call(ws, "pre-vm", "vm.query", [[["name", "=", a.name]]]): + raise V1ProbeError(f"preexisting VM {a.name!r} blocks ownership-safe V1") + if ddp_call(ws, "pre-stage", "pool.dataset.query", [[["id", "=", paths["dataset"]]]]): + raise V1ProbeError(f"preexisting staging dataset {paths['dataset']!r} blocks ownership-safe V1") + + ddp_call(ws, "mk-stage", "pool.dataset.create", [{ + "name": paths["dataset"], "type": "FILESYSTEM", "exec": "OFF", "share_type": "GENERIC", + }]) + stage_owned = True + + upload_job = upload_file(a.host, a.port, a.username, password, a.raw_image, paths["raw_image"], a.tls, a.timeout) + wait_job(ws, upload_job, a.job_timeout, "upload-raw") + upload_job = upload_file(a.host, a.port, a.username, password, a.seed_iso, paths["seed_iso"], a.tls, a.timeout) + wait_job(ws, upload_job, a.job_timeout, "upload-seed") + for label, path in (("raw", paths["raw_image"]), ("seed", paths["seed_iso"])): + stat = ddp_call(ws, f"stat-{label}", "filesystem.stat", [path]) + if not isinstance(stat, dict) or stat.get("type") != "FILE": + raise V1ProbeError(f"uploaded {label} media not observable through filesystem.stat: {stat!r}") + + vm = ddp_call(ws, "vm-create", "vm.create", [native_vm_create_payload(a.name)]) + if not isinstance(vm, dict) or not isinstance(vm.get("id"), int): + raise V1ProbeError(f"vm.create returned invalid VM: {vm!r}") + vm_id = vm["id"] + raw = raw_boot_device(vm_id, paths["raw_image"]) + seed = seed_cdrom_device(vm_id, paths["seed_iso"]) + ddp_call(ws, "raw-device", "vm.device.create", [raw]) + ddp_call(ws, "seed-device", "vm.device.create", [seed]) + receipt["desired_devices"] = [raw, seed] + + ddp_call(ws, "vm-start", "vm.start", [vm_id, {"overcommit": False}]) + console = observe_console_nonce( + a.host, a.port, password, vm_id, a.nonce, a.guest_timeout, a.tls, + ) + receipt["guest_oracle"] = console + if console.get("classification") != "SUPPORTED" or console.get("oracleSatisfied") is not True: + raise V1ProbeError(f"guest nonce oracle failed: {console.get('detail')}") + + receipt.update({ + "classification": "SUPPORTED", + "oracleSatisfied": True, + "v1_oracle_satisfied": True, + "detail": "pinned CirrOS guest booted and exact injected nonce was observed through supported TrueNAS VM console", + }) + return receipt + except Exception as exc: + receipt["detail"] = f"{type(exc).__name__}: {exc}" + return receipt + finally: + receipt["cleanup"]["attempted"] = True + if ws is not None: + if vm_id is not None: + try: + ddp_call(ws, "cleanup-stop", "vm.stop", [vm_id, {"force": True, "force_after_timeout": True}]) + except Exception: + pass + try: + ddp_call(ws, "cleanup-delete-vm", "vm.delete", [vm_id, {"zvols": False, "force": True}]) + except Exception: + pass + try: + receipt["cleanup"]["vm_absent"] = not bool( + ddp_call(ws, "cleanup-query-vm", "vm.query", [[["id", "=", vm_id]]]) + ) + except Exception: + pass + else: + receipt["cleanup"]["vm_absent"] = True + if stage_owned: + try: + ddp_call(ws, "cleanup-stage", "pool.dataset.delete", [ + paths["dataset"], {"recursive": True, "force": True} + ]) + except Exception: + pass + try: + receipt["cleanup"]["staging_dataset_absent"] = not bool( + ddp_call(ws, "cleanup-query-stage", "pool.dataset.query", [[["id", "=", paths["dataset"]]]]) + ) + except Exception: + pass + else: + receipt["cleanup"]["staging_dataset_absent"] = True + ws.close() + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8) + p.add_argument("--job-timeout", type=float, default=300) + p.add_argument("--guest-timeout", type=float, default=120) + p.add_argument("--username", default="truenas_admin") + p.add_argument("--password-file", type=pathlib.Path) + p.add_argument("--target-version", default="26.0.0-BETA.3") + p.add_argument("--pool", default="rdtepool") + p.add_argument("--name", default="rdtecomputevmv1") + p.add_argument("--nonce", required=True) + p.add_argument("--source-image", type=pathlib.Path) + p.add_argument("--raw-image", type=pathlib.Path) + p.add_argument("--seed-iso", type=pathlib.Path) + p.add_argument("--out", type=pathlib.Path, required=True) + p.add_argument("--apply", action="store_true") + a = p.parse_args() + + try: + if a.apply: + if not all((a.port, a.password_file, a.source_image, a.raw_image, a.seed_iso)): + raise V1ProbeError("apply requires port/password-file/source-image/raw-image/seed-iso") + result = run_apply(a) + else: + result = plan(a.pool, a.name, a.nonce) + except (V1ProbeError, SeedError, OSError) as exc: + result = { + "schema": "truenas-compute-vm-v1/v1", + "classification": "HARNESS_FAILURE", + "oracleSatisfied": False, + "v1_oracle_satisfied": False, + "detail": f"{type(exc).__name__}: {exc}", + } + a.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 288a5b8357b055a0e2c4cfedba18dfc33c6c9a80 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:33:20 +0200 Subject: [PATCH 467/719] test: lock TrueNAS V1 media/device/oracle contract --- tests/test_truenas_compute_vm_v1_probe.py | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/test_truenas_compute_vm_v1_probe.py diff --git a/tests/test_truenas_compute_vm_v1_probe.py b/tests/test_truenas_compute_vm_v1_probe.py new file mode 100644 index 000000000..eed892d06 --- /dev/null +++ b/tests/test_truenas_compute_vm_v1_probe.py @@ -0,0 +1,56 @@ +import unittest + +from scripts.truenas_compute_vm_v1_probe import ( + PINNED_CIRROS_SHA256, + multipart_upload_body, + plan, + raw_boot_device, + seed_cdrom_device, + stage_paths, +) + + +class TrueNASVmV1ProbeTests(unittest.TestCase): + def test_pinned_cirros_digest(self): + self.assertEqual( + PINNED_CIRROS_SHA256, + "7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b", + ) + + def test_stage_paths_are_owned_under_pool(self): + paths = stage_paths("rdtepool") + self.assertEqual(paths["dataset"], "rdtepool/rdtev1stage") + self.assertTrue(paths["raw_image"].startswith("/mnt/rdtepool/rdtev1stage/")) + self.assertTrue(paths["seed_iso"].endswith("/seed.iso")) + + def test_devices_use_public_raw_and_cdrom_shapes(self): + raw = raw_boot_device(7, "/mnt/rdtepool/rdtev1stage/cirros.raw") + self.assertEqual(raw["attributes"]["dtype"], "RAW") + self.assertTrue(raw["attributes"]["exists"]) + self.assertTrue(raw["attributes"]["boot"]) + self.assertEqual(raw["attributes"]["type"], "VIRTIO") + cd = seed_cdrom_device(7, "/mnt/rdtepool/rdtev1stage/seed.iso") + self.assertEqual(cd["attributes"], {"dtype": "CDROM", "path": "/mnt/rdtepool/rdtev1stage/seed.iso"}) + + def test_plan_keeps_v1_claim_narrow(self): + p = plan("rdtepool", "rdtecomputevmv1", "rep001nonceABCDEF12") + self.assertEqual(p["schema"], "truenas-compute-vm-v1-plan/v1") + self.assertEqual(p["oracle"]["surface"], "/websocket/shell") + self.assertNotIn("firecracker", str(p).lower()) + self.assertNotIn("nested-kvm", str(p).lower()) + + def test_upload_matches_trueNAS_filesystem_put_form(self): + body = multipart_upload_body( + "/mnt/rdtepool/rdtev1stage/seed.iso", + "seed.iso", + b"abc", + "boundary", + ) + self.assertIn(b'"method":"filesystem.put"', body) + self.assertIn(b'"params":["/mnt/rdtepool/rdtev1stage/seed.iso"]', body) + self.assertIn(b'name="file"; filename="seed.iso"', body) + self.assertTrue(body.endswith(b"--boundary--\r\n")) + + +if __name__ == "__main__": + unittest.main() From aa53a0a2b9ca9e06ce75250a4fb26b7821a158fd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:33:23 +0200 Subject: [PATCH 468/719] CI: validate native TrueNAS V1 probe --- .github/workflows/validate-compute-guest-profile-contract.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-compute-guest-profile-contract.yml b/.github/workflows/validate-compute-guest-profile-contract.yml index 06f41908a..ab40ea831 100644 --- a/.github/workflows/validate-compute-guest-profile-contract.yml +++ b/.github/workflows/validate-compute-guest-profile-contract.yml @@ -18,5 +18,5 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py tests/test_truenas_vm_console_probe.py + - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py tests/test_truenas_vm_console_probe.py tests/test_truenas_compute_vm_v1_probe.py - run: python3 scripts/compute_guest_profile_contract.py From ee3c3584db02ba47bd80ef7a2f568f5d2013c760 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:35:26 +0200 Subject: [PATCH 469/719] RDTE: wire bounded TrueNAS Linux V1 fixture --- scripts/gha_kvm_truenas_rdte.sh | 64 +++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 87a8451d3..01769dc1d 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -17,7 +17,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" } OUT="" @@ -65,7 +65,7 @@ TARGET_ENV="$(python3 "$SCRIPT_DIR/truenas_rdte_target.py" --registry "$TARGET_R # truenas_rdte_target.py emits only shell-quoted values after strict registry validation. eval "$TARGET_ENV" [[ "$RUNG" == "t0" || "$RUNG" == "t1" || "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" || "$RUNG" == "t6" ]] || { echo "rung must be t0, t1, t2, t3, t4, t5, or t6" >&2; exit 2; } -[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" || "$COMPUTE_FIXTURE" == "vm-v0" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } +[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" || "$COMPUTE_FIXTURE" == "vm-v0" || "$COMPUTE_FIXTURE" == "vm-v1" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then echo "compute fixture $COMPUTE_FIXTURE requires rung t3" >&2 exit 2 @@ -261,6 +261,7 @@ payload = { "T3 adds two experiment-owned sparse data disks and a real middleware-created ZFS mirror pool.", "Optional container-c0 on T3 proves only native product-API container lifecycle/read-back/update/restart semantics and zero-residue cleanup; C1 guest execution remains separate.", "Optional vm-v0 on T3 proves only native VM/device lifecycle, source-bound preconditions and exact-owned storage cleanup; guest boot/V1, nested KVM/V2, Firecracker and Windows remain separate.", + "Optional vm-v1 on T3 proves only pinned Linux guest boot plus an exact externally observed nonce through the supported VM console; nested KVM/V2, Firecracker and Windows remain separate.", "T4 initializes Apps on that pool and runs one synthetic public-safe custom Compose app.", "T5 exercises stop/start, config mutation/read-back, redeploy, stop, and delete for that digest-pinned custom app.", "T6 consumes one exact public TrueNAS App Foundry materialization control and verifies native Custom App realization/read-back.", @@ -296,6 +297,15 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | elif [[ "$COMPUTE_FIXTURE" == "vm-v0" ]]; then [[ -f "$SCRIPT_DIR/truenas_compute_vm_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS compute VM V0 client" + elif [[ "$COMPUTE_FIXTURE" == "vm-v1" ]]; then + for required in truenas_compute_vm_v1_probe.py truenas_vm_console_probe.py compute_guest_seed.py; do + [[ -f "$SCRIPT_DIR/$required" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS compute VM V1 component: $required" + done + command -v qemu-img >/dev/null 2>&1 || + fail_evidence HARNESS_FAILURE preflight "qemu-img required for pinned V1 image conversion" + command -v xorriso >/dev/null 2>&1 || + fail_evidence HARNESS_FAILURE preflight "xorriso required for deterministic NoCloud seed media" fi if [[ "$COMPUTE_FIXTURE" != "none" ]]; then [[ -f "$SCRIPT_DIR/../config/compute-materialization-targets.json" ]] || @@ -800,6 +810,56 @@ PY [[ "$COMPUTE_OK" == "true" ]] || fail_evidence ORACLE_FAILURE compute-vm-v0 "native TrueNAS VM V0 lifecycle did not satisfy exact ownership/precondition/cleanup oracles" write_receipt SUPPORTED true compute-vm-v0 "native TrueNAS VM/device V0 completed create/read-back/update/owned-ZVOL device CRUD and zero-residue cleanup; guest boot, nested KVM, Firecracker and Windows remain unclaimed" + elif [[ "$COMPUTE_FIXTURE" == "vm-v1" ]]; then + V1_DIR="$STATE_DIR/compute-vm-v1" + V1_SOURCE="$V1_DIR/cirros-0.6.3-x86_64-disk.img" + V1_RAW="$V1_DIR/cirros-0.6.3-x86_64.raw" + V1_SEED_DIR="$V1_DIR/seed" + V1_SEED_ISO="$V1_DIR/seed.iso" + V1_SOURCE_URL="https://download.cirros-cloud.net/0.6.3/cirros-0.6.3-x86_64-disk.img" + V1_SOURCE_SHA256="7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b" + V1_NONCE="v1${GITHUB_RUN_ID:-00000000000}nonce" + V1_INSTANCE_ID="rdte-v1-${GITHUB_RUN_ID:-local}" + mkdir -p "$V1_SEED_DIR" + curl --fail --location --retry 3 --silent --show-error "$V1_SOURCE_URL" -o "$V1_SOURCE" || + fail_evidence ENVIRONMENT_FAILURE compute-vm-v1-source "failed to acquire pinned CirrOS V1 guest image" + V1_OBSERVED_SHA256="$(sha256sum "$V1_SOURCE" | awk '{print $1}')" + [[ "$V1_OBSERVED_SHA256" == "$V1_SOURCE_SHA256" ]] || + fail_evidence ORACLE_FAILURE compute-vm-v1-source "CirrOS V1 guest image digest did not match pinned release" + qemu-img convert -f qcow2 -O raw "$V1_SOURCE" "$V1_RAW" || + fail_evidence HARNESS_FAILURE compute-vm-v1-media "failed to convert pinned CirrOS qcow2 image to RAW" + python3 "$SCRIPT_DIR/compute_guest_seed.py" --nonce "$V1_NONCE" --instance-id "$V1_INSTANCE_ID" --out-dir "$V1_SEED_DIR" || + fail_evidence HARNESS_FAILURE compute-vm-v1-media "failed to create deterministic NoCloud nonce seed" + ( + cd "$V1_SEED_DIR" + xorriso -as mkisofs -quiet -output "$V1_SEED_ISO" -volid cidata -joliet -rock user-data meta-data + ) || fail_evidence HARNESS_FAILURE compute-vm-v1-media "failed to package deterministic NoCloud seed ISO" + + COMPUTE_OUT="$STATE_DIR/compute-vm-v1.json" + python3 "$SCRIPT_DIR/truenas_compute_vm_v1_probe.py" --target-version "$VERSION" --pool "$DATA_POOL_NAME" --name "rdtecomputevmv1" --nonce "$V1_NONCE" --source-image "$V1_SOURCE" --raw-image "$V1_RAW" --seed-iso "$V1_SEED_ISO" --host 127.0.0.1 --port "$MIDDLEWARE_PORT" "${MIDDLEWARE_TLS_ARG[@]}" --password-file "$PASSWORD_FILE" --out "$COMPUTE_OUT" --timeout 8 --job-timeout 600 --guest-timeout 180 --apply >/dev/null 2>&1 || true + [[ -f "$COMPUTE_OUT" ]] || + fail_evidence HARNESS_FAILURE compute-vm-v1 "compute VM V1 client did not emit a receipt" + COMPUTE_RESULT_JSON="$(cat "$COMPUTE_OUT")" + COMPUTE_OK="$(python3 - "$COMPUTE_OUT" <<'PY' +import json, pathlib, sys +data=json.loads(pathlib.Path(sys.argv[1]).read_text()) +cleanup=data.get("cleanup") or {} +guest=data.get("guest_oracle") or {} +ok=( + data.get("classification")=="SUPPORTED" + and data.get("oracleSatisfied") is True + and data.get("v1_oracle_satisfied") is True + and guest.get("classification")=="SUPPORTED" + and guest.get("oracleSatisfied") is True + and cleanup.get("vm_absent") is True + and cleanup.get("staging_dataset_absent") is True +) +print("true" if ok else "false") +PY +)" + [[ "$COMPUTE_OK" == "true" ]] || + fail_evidence ORACLE_FAILURE compute-vm-v1 "native TrueNAS VM V1 did not satisfy pinned guest boot/nonce/cleanup oracles" + write_receipt SUPPORTED true compute-vm-v1 "pinned CirrOS guest booted and emitted the exact injected nonce through the supported TrueNAS VM console with zero owned VM/media residue; nested KVM, Firecracker and Windows remain unclaimed" else write_receipt SUPPORTED true data-pool "installed TrueNAS created an ONLINE healthy two-disk mirror containing exactly the selected disposable data disks" fi From dbccb2e31f9a95b1fa93fe755787a76b5d4f440e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:35:30 +0200 Subject: [PATCH 470/719] RDTE: route TrueNAS VM V1 only by fresh request --- .github/workflows/gha-kvm-system-rdte.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index add311665..d5a04c4d6 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -70,6 +70,7 @@ on: - none - container-c0 - vm-v0 + - vm-v1 permissions: contents: read @@ -182,7 +183,7 @@ jobs: truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" - truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0")' "$RUN_REQUEST_PATH")" + truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null From abac58d3ff145447fa11713c484865890af9f3c8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:35:33 +0200 Subject: [PATCH 471/719] test: lock V1 heavyweight claim boundary --- tests/test_gha_kvm_system_rdte.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 1e1c5ebd6..2bf981b77 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -127,6 +127,24 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): self.assertIn("losetup --find --show --read-only --partscan", text) self.assertNotIn("qemu-nbd --connect", text) + def test_truenas_vm_v1_is_request_driven_and_claim_bounded(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + script = TN.read_text(encoding="utf-8") + self.assertIn('"container-c0" or . == "vm-v0" or . == "vm-v1"', workflow) + self.assertIn('elif [[ "$COMPUTE_FIXTURE" == "vm-v1" ]]', script) + self.assertIn("cirros-0.6.3-x86_64-disk.img", script) + self.assertIn("7d6355852aeb6dbcd191bcda7cd74f1536cfe5cbf8a10495a7283a8396e4b75b", script) + self.assertIn("qemu-img convert -f qcow2 -O raw", script) + self.assertIn("-volid cidata", script) + self.assertIn("truenas_compute_vm_v1_probe.py", script) + self.assertIn('"v1_oracle_satisfied"', script) + self.assertIn('"staging_dataset_absent"', script) + self.assertIn("nested KVM, Firecracker and Windows remain unclaimed", script) + v1 = script.split('elif [[ "$COMPUTE_FIXTURE" == "vm-v1" ]]', 1)[1].split("\n else\n", 1)[0] + self.assertNotIn("ssh ", v1) + self.assertNotIn("virsh", v1) + self.assertNotIn("raw.qemu", v1) + def test_proxmox_rest_container_c0_is_opt_in_and_api_native(self): workflow = WORKFLOW.read_text(encoding="utf-8") script = PVE.read_text(encoding="utf-8") From 6c8b89c92782849e487d56271fe0423e774e351c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:36:17 +0200 Subject: [PATCH 472/719] test: use canonical TrueNAS harness fixture --- tests/test_gha_kvm_system_rdte.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 2bf981b77..0ae74dd26 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -129,7 +129,7 @@ def test_proxmox_first_boot_witness_uses_vendor_hook(self): def test_truenas_vm_v1_is_request_driven_and_claim_bounded(self): workflow = WORKFLOW.read_text(encoding="utf-8") - script = TN.read_text(encoding="utf-8") + script = TRUENAS.read_text(encoding="utf-8") self.assertIn('"container-c0" or . == "vm-v0" or . == "vm-v1"', workflow) self.assertIn('elif [[ "$COMPUTE_FIXTURE" == "vm-v1" ]]', script) self.assertIn("cirros-0.6.3-x86_64-disk.img", script) From 13d7ad7de10cb9ca06cf3c8998a948f5eb5ba5aa Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:37:11 +0200 Subject: [PATCH 473/719] RDTE: request TrueNAS BETA.3 Linux VM V1 rep 001 --- config/truenas-rdte-run-request.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index d8afcfb93..8e9150949 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-vm-v0-002-name-device-schema", + "request_id": "tn-26-beta3-t3-compute-vm-v1-001-cirros-nocloud-console", "version": "26.0.0-BETA.3", "rung": "t3", - "compute_fixture": "vm-v0", + "compute_fixture": "vm-v1", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Re-run the exact native TrueNAS 26.0.0-BETA.3 VM V0 materialization fixture after rep 001 reached vm.create and falsified the fixture name. The causal repair uses exact-source-valid VM name rdtecomputevmv0, enforces the BETA.3 VM name regex locally, and removes the unsupported DISK attributes.boot field found by source-level red-team before mutation. Preserve the public entitlement gate, exact-owned ZVOL/device lifecycle, independent dataset observation, and zero-residue cleanup. No guest boot, nonce, nested KVM, Firecracker, or Windows claim is permitted." + "purpose": "Advance the accepted BETA.3 VM V0 shell to the bounded Linux V1 guest rung. Download and verify pinned CirrOS 0.6.3 x86_64 source, convert it to RAW on the GHA host, generate deterministic NoCloud seed media with the exact per-run nonce, stage media only through supported TrueNAS filesystem.put, create native RAW/CDROM VM devices, start the guest through vm.start, and require the exact nonce through the supported /websocket/shell VM console. Acceptance additionally requires owned VM and staging dataset absence. Nested KVM, Firecracker and Windows are explicitly outside this rep." } From 0296347081452903a0150f0ebab66ca61c816b86 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:39:12 +0200 Subject: [PATCH 474/719] RDTE: add bounded V2 nested-KVM observation seed --- scripts/compute_guest_seed.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/compute_guest_seed.py b/scripts/compute_guest_seed.py index 1d1e7ecff..406db28ae 100644 --- a/scripts/compute_guest_seed.py +++ b/scripts/compute_guest_seed.py @@ -34,6 +34,25 @@ def render_seed(nonce: str, instance_id: str) -> tuple[str, str]: return meta, user +def render_v2_observation_seed(nonce: str, instance_id: str) -> tuple[str, str]: + nonce = validate_token(nonce, NONCE_RE, "nonce") + instance_id = validate_token(instance_id, INSTANCE_RE, "instance-id") + meta = f"instance-id: {instance_id}\nlocal-hostname: rdtev2\n" + user = ( + "#!/bin/sh\n" + "set -eu\n" + f"nonce='{nonce}'\n" + "kvm=0\n" + "cpu=0\n" + "[ -c /dev/kvm ] && kvm=1 || true\n" + "grep -Eq '(^|[[:space:]])(vmx|svm)([[:space:]]|$)' /proc/cpuinfo && cpu=1 || true\n" + f"printf '%s%s\\n' '{PREFIX}' \"$nonce\" >/dev/ttyS0\n" + "printf 'AGENT_DISPATCH_V2_KVM_PRESENT=%s\\n' \"$kvm\" >/dev/ttyS0\n" + "printf 'AGENT_DISPATCH_V2_CPU_VMX_SVM=%s\\n' \"$cpu\" >/dev/ttyS0\n" + ) + return meta, user + + def write_seed(out_dir: pathlib.Path, nonce: str, instance_id: str) -> None: meta, user = render_seed(nonce, instance_id) out_dir.mkdir(parents=True, exist_ok=True) From f73c0e607c0e62088bbb2b4433019f8a2a9ed317 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:39:15 +0200 Subject: [PATCH 475/719] test: keep V2 observation non-mutating --- tests/test_compute_guest_seed.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_compute_guest_seed.py b/tests/test_compute_guest_seed.py index 89ac80d8f..b0e042108 100644 --- a/tests/test_compute_guest_seed.py +++ b/tests/test_compute_guest_seed.py @@ -2,7 +2,7 @@ import tempfile import unittest -from scripts.compute_guest_seed import PREFIX, SeedError, render_seed, write_seed +from scripts.compute_guest_seed import PREFIX, SeedError, render_seed, render_v2_observation_seed, write_seed class ComputeGuestSeedTests(unittest.TestCase): @@ -22,6 +22,16 @@ def test_seed_rejects_shell_metacharacters(self): with self.assertRaises(SeedError): render_seed(bad, "rdte-v1-rep001") + def test_v2_observation_seed_only_observes_nested_kvm_prerequisites(self): + _, user = render_v2_observation_seed("rep002nonceABCDEF12", "rdte-v2-rep002") + self.assertIn("[ -c /dev/kvm ]", user) + self.assertIn("(vmx|svm)", user) + self.assertIn("AGENT_DISPATCH_V2_KVM_PRESENT=", user) + self.assertIn("AGENT_DISPATCH_V2_CPU_VMX_SVM=", user) + self.assertNotIn("firecracker", user.lower()) + self.assertNotIn("modprobe", user.lower()) + self.assertNotIn("chmod", user.lower()) + def test_write_seed_uses_expected_names_and_executable_userdata(self): with tempfile.TemporaryDirectory() as td: root = pathlib.Path(td) From f9c2fb78150e9f993e46935795496764c91cc0e2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:39:19 +0200 Subject: [PATCH 476/719] RDTE: bind BETA.3 V2 nested-KVM observation contract --- config/compute-guest-profiles.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/config/compute-guest-profiles.json b/config/compute-guest-profiles.json index cdff69110..5708f69c9 100644 --- a/config/compute-guest-profiles.json +++ b/config/compute-guest-profiles.json @@ -135,6 +135,20 @@ "console_discovery": "vm.get_console" }, "observation_requirement": "consume guest console through supported TrueNAS console surface; do not inject raw.qemu or invoke virsh directly" + }, + "linux_v2": { + "source_status": "OBSERVATION_CANDIDATE", + "runtime_status": "OPEN", + "prerequisite": "V1 accepted", + "fixture": "cirros-0.6.3-x86_64-nocloud-serial-nonce", + "vm_cpu_mode": "HOST-PASSTHROUGH", + "observation": { + "kvm_device_marker": "AGENT_DISPATCH_V2_KVM_PRESENT=", + "cpu_extension_marker": "AGENT_DISPATCH_V2_CPU_VMX_SVM=", + "transport": "supported TrueNAS VM console" + }, + "firecracker_gate": "execute #277 portable Firecracker contract only when /dev/kvm and vmx|svm are both observed inside the guest", + "negative_boundary": "nested-KVM absence is a nested-virtualization capability result and does not invalidate V0/V1 VM support" } } }, From defed4e4e4df0e1a282cfde32eef9a7020e9aa2e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:39:22 +0200 Subject: [PATCH 477/719] RDTE: validate V2 authority and negative claim boundary --- scripts/compute_guest_profile_contract.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/compute_guest_profile_contract.py b/scripts/compute_guest_profile_contract.py index 5890075ea..8c8f3e6fc 100644 --- a/scripts/compute_guest_profile_contract.py +++ b/scripts/compute_guest_profile_contract.py @@ -65,6 +65,21 @@ def validate(p:dict[str,Any])->dict[str,Any]: else: if row["firecracker"].get("cpu_passthrough_control")!="vm.create.cpu_mode=HOST-PASSTHROUGH": raise ProfileError(f"{version}: native Firecracker candidate must bind HOST-PASSTHROUGH") + if version=="26.0.0-BETA.3": + linux2=row.get("linux_v2") + if not isinstance(linux2,dict) or linux2.get("source_status")!="OBSERVATION_CANDIDATE" or linux2.get("runtime_status")!="OPEN": + raise ProfileError("BETA.3 Linux V2 must remain observation candidate/runtime OPEN") + if linux2.get("prerequisite")!="V1 accepted" or linux2.get("vm_cpu_mode")!="HOST-PASSTHROUGH": + raise ProfileError("BETA.3 Linux V2 prerequisite/cpu mode drift") + obs=linux2.get("observation") + if obs != { + "kvm_device_marker":"AGENT_DISPATCH_V2_KVM_PRESENT=", + "cpu_extension_marker":"AGENT_DISPATCH_V2_CPU_VMX_SVM=", + "transport":"supported TrueNAS VM console", + }: + raise ProfileError("BETA.3 Linux V2 observation contract drift") + if "#277" not in linux2.get("firecracker_gate","") or "does not invalidate V0/V1" not in linux2.get("negative_boundary",""): + raise ProfileError("BETA.3 Linux V2 claim boundary incomplete") if version=="26.0.0-BETA.3": linux=row.get("linux_v1") if not isinstance(linux,dict) or linux.get("source_status")!="CANDIDATE" or linux.get("runtime_status")!="OPEN": From 23a1d7eaebb2b457f2daf31bb96937e23eccfefd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:39:25 +0200 Subject: [PATCH 478/719] test: lock V2 nested-KVM claim semantics --- tests/test_compute_guest_profile_contract.py | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_compute_guest_profile_contract.py b/tests/test_compute_guest_profile_contract.py index 008b7a725..8598bd77b 100644 --- a/tests/test_compute_guest_profile_contract.py +++ b/tests/test_compute_guest_profile_contract.py @@ -64,6 +64,21 @@ def tnrow(version): "runtime_status":"OPEN", }, } + if version=="26.0.0-BETA.3": + row["linux_v2"]={ + "source_status":"OBSERVATION_CANDIDATE", + "runtime_status":"OPEN", + "prerequisite":"V1 accepted", + "fixture":"cirros-0.6.3-x86_64-nocloud-serial-nonce", + "vm_cpu_mode":"HOST-PASSTHROUGH", + "observation":{ + "kvm_device_marker":"AGENT_DISPATCH_V2_KVM_PRESENT=", + "cpu_extension_marker":"AGENT_DISPATCH_V2_CPU_VMX_SVM=", + "transport":"supported TrueNAS VM console", + }, + "firecracker_gate":"execute #277 portable Firecracker contract only when /dev/kvm and vmx|svm are both observed inside the guest", + "negative_boundary":"nested-KVM absence is a nested-virtualization capability result and does not invalidate V0/V1 VM support", + } if version=="26.0.0-BETA.3": row["linux_v1"]={ "source_status":"CANDIDATE", @@ -116,6 +131,16 @@ def test_beta3_v1_runtime_claim_fails_closed(self): p["truenas"]["26.0.0-BETA.3"]["linux_v1"]["runtime_status"]="PASS" with self.assertRaises(ProfileError): validate(p) + def test_beta3_v2_nested_kvm_negative_does_not_revoke_vm_support(self): + p=self.profile() + row=p["truenas"]["26.0.0-BETA.3"]["linux_v2"] + self.assertIn("does not invalidate V0/V1", row["negative_boundary"]) + self.assertEqual(row["runtime_status"], "OPEN") + def test_beta3_v2_firecracker_gate_remains_authority_bound(self): + p=self.profile() + p["truenas"]["26.0.0-BETA.3"]["linux_v2"]["firecracker_gate"]="run whatever" + with self.assertRaises(ProfileError): + validate(p) def test_target_growth_fails_closed(self): p=self.profile() p["truenas"]["27.0.0"]=tnrow("27.0.0") From 353a0d1c1df54111fbc90ea3c214fd59e3deeb5d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:40:42 +0200 Subject: [PATCH 479/719] RDTE: add bounded TrueNAS V2 console observer --- scripts/truenas_v2_console_probe.py | 139 ++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 scripts/truenas_v2_console_probe.py diff --git a/scripts/truenas_v2_console_probe.py b/scripts/truenas_v2_console_probe.py new file mode 100644 index 000000000..b5aee0e71 --- /dev/null +++ b/scripts/truenas_v2_console_probe.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import re +import time + +from scripts.compute_guest_seed import NONCE_RE, PREFIX, SeedError, validate_token +from scripts.truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for +from scripts.truenas_vm_console_probe import ShellWebSocket + +KVM_RE = re.compile(rb"AGENT_DISPATCH_V2_KVM_PRESENT=([01])") +CPU_RE = re.compile(rb"AGENT_DISPATCH_V2_CPU_VMX_SVM=([01])") + + +def parse_v2_observation(buffer: bytes, nonce: str) -> dict | None: + validate_token(nonce, NONCE_RE, "nonce") + nonce_marker = f"{PREFIX}{nonce}".encode("ascii") + if nonce_marker not in buffer: + return None + km = KVM_RE.search(buffer) + cm = CPU_RE.search(buffer) + if not km or not cm: + return None + kvm = km.group(1) == b"1" + cpu = cm.group(1) == b"1" + return { + "nonce_observed": True, + "kvm_device_present": kvm, + "cpu_vmx_or_svm_present": cpu, + "nested_kvm_observed": kvm and cpu, + "firecracker_eligible": kvm and cpu, + } + + +def observe_v2( + host: str, + port: int, + username: str, + password: str, + vm_id: int, + nonce: str, + timeout: float, + tls: bool, +) -> dict: + validate_token(nonce, NONCE_RE, "nonce") + receipt = { + "schema": "truenas-vm-v2-nested-kvm-observation/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "vm_id": vm_id, + "nested_kvm_observed": False, + "firecracker_eligible": False, + "claim_boundary": ( + "observes nested-KVM prerequisites only; Firecracker execution remains authority #277 " + "and is forbidden unless both /dev/kvm and vmx|svm are observed" + ), + } + ddp = shell = None + try: + ddp = WebSocket(host, port, timeout=min(timeout, 8), tls=tls) + ddp.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ddp, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError(f"DDP connect failed: {connected!r}") + auth = ddp_call(ddp, "1", "auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": username, + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError(f"authentication failed: {auth!r}") + token = ddp_call(ddp, "2", "auth.generate_token", [300, {}, False]) + if not isinstance(token, str) or not token: + raise RuntimeError(f"auth.generate_token returned invalid token: {token!r}") + + shell = ShellWebSocket(host, port, path="/websocket/shell", timeout=min(timeout, 8), tls=tls) + shell.send_json({"token": token, "options": {"vm_id": vm_id}}) + deadline = time.monotonic() + timeout + buffer = bytearray() + while time.monotonic() < deadline: + _, payload = shell.recv_payload() + buffer.extend(payload) + if len(buffer) > 65536: + del buffer[:-65536] + observed = parse_v2_observation(bytes(buffer), nonce) + if observed is not None: + receipt.update(observed) + receipt["classification"] = "SUPPORTED" + receipt["oracleSatisfied"] = True + receipt["detail"] = ( + "nested KVM prerequisites observed inside guest" + if observed["nested_kvm_observed"] + else "guest observation completed; nested KVM prerequisites are not both exposed" + ) + return receipt + raise TimeoutError("V2 nested-KVM markers not observed before deadline") + except Exception as exc: + receipt["detail"] = f"{type(exc).__name__}: {exc}" + return receipt + finally: + if shell is not None: + shell.close() + if ddp is not None: + ddp.close() + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="127.0.0.1") + ap.add_argument("--port", type=int, required=True) + ap.add_argument("--username", default="truenas_admin") + ap.add_argument("--password-file", type=pathlib.Path, required=True) + ap.add_argument("--vm-id", type=int, required=True) + ap.add_argument("--nonce", required=True) + ap.add_argument("--timeout", type=float, default=90) + ap.add_argument("--tls", action="store_true") + ap.add_argument("--out", type=pathlib.Path, required=True) + a = ap.parse_args() + try: + password = a.password_file.read_text(encoding="utf-8").strip() + result = observe_v2(a.host, a.port, a.username, password, a.vm_id, a.nonce, a.timeout, a.tls) + except (OSError, SeedError) as exc: + result = { + "schema": "truenas-vm-v2-nested-kvm-observation/v1", + "classification": "HARNESS_FAILURE", + "oracleSatisfied": False, + "nested_kvm_observed": False, + "firecracker_eligible": False, + "detail": f"{type(exc).__name__}: {exc}", + } + a.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From fb8442069785a6377d3b6d04e8a8a9398f907b26 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:40:44 +0200 Subject: [PATCH 480/719] test: distinguish nested-KVM capability from VM failure --- tests/test_truenas_v2_console_probe.py | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_truenas_v2_console_probe.py diff --git a/tests/test_truenas_v2_console_probe.py b/tests/test_truenas_v2_console_probe.py new file mode 100644 index 000000000..3c595ed28 --- /dev/null +++ b/tests/test_truenas_v2_console_probe.py @@ -0,0 +1,34 @@ +import unittest + +from scripts.truenas_v2_console_probe import parse_v2_observation + + +class TrueNASV2ConsoleProbeTests(unittest.TestCase): + NONCE = "rep003nonceABCDEF12" + + def test_positive_nested_kvm_observation(self): + data = ( + b"boot\nAGENT_DISPATCH_V1_NONCE=" + self.NONCE.encode() + + b"\nAGENT_DISPATCH_V2_KVM_PRESENT=1\nAGENT_DISPATCH_V2_CPU_VMX_SVM=1\n" + ) + result = parse_v2_observation(data, self.NONCE) + self.assertTrue(result["nested_kvm_observed"]) + self.assertTrue(result["firecracker_eligible"]) + + def test_negative_is_valid_observation_not_firecracker_eligible(self): + data = ( + b"AGENT_DISPATCH_V1_NONCE=" + self.NONCE.encode() + + b"\nAGENT_DISPATCH_V2_KVM_PRESENT=0\nAGENT_DISPATCH_V2_CPU_VMX_SVM=1\n" + ) + result = parse_v2_observation(data, self.NONCE) + self.assertFalse(result["nested_kvm_observed"]) + self.assertFalse(result["firecracker_eligible"]) + self.assertTrue(result["nonce_observed"]) + + def test_incomplete_markers_do_not_pass(self): + data = b"AGENT_DISPATCH_V1_NONCE=" + self.NONCE.encode() + self.assertIsNone(parse_v2_observation(data, self.NONCE)) + + +if __name__ == "__main__": + unittest.main() From a6efd1dcbd0b7649d16b0b40ecaeb009a3a116ae Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:40:46 +0200 Subject: [PATCH 481/719] CI: validate bounded V2 console observation --- .github/workflows/validate-compute-guest-profile-contract.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-compute-guest-profile-contract.yml b/.github/workflows/validate-compute-guest-profile-contract.yml index ab40ea831..dec68fbd4 100644 --- a/.github/workflows/validate-compute-guest-profile-contract.yml +++ b/.github/workflows/validate-compute-guest-profile-contract.yml @@ -18,5 +18,5 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py tests/test_truenas_vm_console_probe.py tests/test_truenas_compute_vm_v1_probe.py + - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py tests/test_truenas_vm_console_probe.py tests/test_truenas_v2_console_probe.py tests/test_truenas_compute_vm_v1_probe.py - run: python3 scripts/compute_guest_profile_contract.py From b40a81575a470d17702bb4e660b2c1becd21af36 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:06:15 +0200 Subject: [PATCH 482/719] RDTE: invoke TrueNAS V1 client as repo module --- scripts/gha_kvm_truenas_rdte.sh | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 01769dc1d..2648110df 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -2,6 +2,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" TARGET_REGISTRY="$SCRIPT_DIR/../config/truenas-rdte-targets.json" TARGET_VERSION="26.0.0-BETA.3" @@ -836,9 +837,26 @@ PY ) || fail_evidence HARNESS_FAILURE compute-vm-v1-media "failed to package deterministic NoCloud seed ISO" COMPUTE_OUT="$STATE_DIR/compute-vm-v1.json" - python3 "$SCRIPT_DIR/truenas_compute_vm_v1_probe.py" --target-version "$VERSION" --pool "$DATA_POOL_NAME" --name "rdtecomputevmv1" --nonce "$V1_NONCE" --source-image "$V1_SOURCE" --raw-image "$V1_RAW" --seed-iso "$V1_SEED_ISO" --host 127.0.0.1 --port "$MIDDLEWARE_PORT" "${MIDDLEWARE_TLS_ARG[@]}" --password-file "$PASSWORD_FILE" --out "$COMPUTE_OUT" --timeout 8 --job-timeout 600 --guest-timeout 180 --apply >/dev/null 2>&1 || true - [[ -f "$COMPUTE_OUT" ]] || - fail_evidence HARNESS_FAILURE compute-vm-v1 "compute VM V1 client did not emit a receipt" + V1_CLIENT_STDERR="$V1_DIR/client.stderr.log" + ( + cd "$REPO_ROOT" + python3 -m scripts.truenas_compute_vm_v1_probe \ + --target-version "$VERSION" \ + --pool "$DATA_POOL_NAME" \ + --name "rdtecomputevmv1" \ + --nonce "$V1_NONCE" \ + --source-image "$V1_SOURCE" \ + --raw-image "$V1_RAW" \ + --seed-iso "$V1_SEED_ISO" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --out "$COMPUTE_OUT" --timeout 8 --job-timeout 600 --guest-timeout 180 --apply + ) >/dev/null 2>"$V1_CLIENT_STDERR" || true + if [[ ! -f "$COMPUTE_OUT" ]]; then + V1_CLIENT_ERROR="$(tail -c 2000 "$V1_CLIENT_STDERR" 2>/dev/null | tr '\n\r' ' ' || true)" + fail_evidence HARNESS_FAILURE compute-vm-v1 "compute VM V1 client did not emit a receipt; client stderr: ${V1_CLIENT_ERROR:-}" + fi COMPUTE_RESULT_JSON="$(cat "$COMPUTE_OUT")" COMPUTE_OK="$(python3 - "$COMPUTE_OUT" <<'PY' import json, pathlib, sys From aaa44bb9f9f148447a06cd2e5501642457755b45 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:06:18 +0200 Subject: [PATCH 483/719] RDTE: retain V1 client launch diagnostics --- tests/test_gha_kvm_system_rdte.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 0ae74dd26..87de49943 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -137,6 +137,10 @@ def test_truenas_vm_v1_is_request_driven_and_claim_bounded(self): self.assertIn("qemu-img convert -f qcow2 -O raw", script) self.assertIn("-volid cidata", script) self.assertIn("truenas_compute_vm_v1_probe.py", script) + self.assertIn('python3 -m scripts.truenas_compute_vm_v1_probe', script) + self.assertIn('cd "$REPO_ROOT"', script) + self.assertIn('client.stderr.log', script) + self.assertIn('client stderr:', script) self.assertIn('"v1_oracle_satisfied"', script) self.assertIn('"staging_dataset_absent"', script) self.assertIn("nested KVM, Firecracker and Windows remain unclaimed", script) From efabc3b2bbdb892947d3c86d6a8a4217cc259823 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:08:30 +0200 Subject: [PATCH 484/719] RDTE: request TrueNAS BETA.3 Linux VM V1 rep 002 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 8e9150949..f12b0eb0b 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-vm-v1-001-cirros-nocloud-console", + "request_id": "tn-26-beta3-t3-compute-vm-v1-002-module-launch", "version": "26.0.0-BETA.3", "rung": "t3", "compute_fixture": "vm-v1", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Advance the accepted BETA.3 VM V0 shell to the bounded Linux V1 guest rung. Download and verify pinned CirrOS 0.6.3 x86_64 source, convert it to RAW on the GHA host, generate deterministic NoCloud seed media with the exact per-run nonce, stage media only through supported TrueNAS filesystem.put, create native RAW/CDROM VM devices, start the guest through vm.start, and require the exact nonce through the supported /websocket/shell VM console. Acceptance additionally requires owned VM and staging dataset absence. Nested KVM, Firecracker and Windows are explicitly outside this rep." + "purpose": "Re-run the exact TrueNAS 26.0.0-BETA.3 Linux VM V1 fixture after rep 001 reached the real workcell but the V1 client emitted no receipt because the package-importing client was launched by absolute script path with stderr discarded. Preserve the exact pinned CirrOS 0.6.3 digest, deterministic NoCloud nonce seed, supported filesystem.put staging, RAW/CDROM native devices, vm.start, supported /websocket/shell exact-nonce oracle, and zero-residue cleanup. The only causal harness delta is merged #314: invoke the client from repository root with python -m and retain bounded stderr. Nested KVM, Firecracker and Windows remain outside this rep." } From 82767d0e42dd681388ef70e6edd482b1ba32306a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:37:25 +0200 Subject: [PATCH 485/719] fix: tolerate idle TrueNAS VM console reads --- scripts/truenas_vm_console_probe.py | 87 ++++++++++++++++++++--------- 1 file changed, 62 insertions(+), 25 deletions(-) diff --git a/scripts/truenas_vm_console_probe.py b/scripts/truenas_vm_console_probe.py index aeb9244b7..4f1d9f010 100644 --- a/scripts/truenas_vm_console_probe.py +++ b/scripts/truenas_vm_console_probe.py @@ -43,6 +43,54 @@ def nonce_marker(nonce: str) -> bytes: return f"{PREFIX}{nonce}".encode("ascii") +def _observe_nonce_frames(shell: ShellWebSocket, marker: bytes, timeout: float) -> dict: + """Observe VM-console frames until the exact marker or the overall deadline. + + The websocket itself intentionally keeps a short socket timeout so a dead + transport remains responsive. A quiet guest console can legitimately be + idle longer than that while firmware/kernel/userspace boots, so per-read + TimeoutError is transient here; only the caller's overall deadline is an + oracle failure. + """ + buffer = bytearray() + deadline = time.monotonic() + timeout + connected_shell = False + idle_read_timeouts = 0 + while time.monotonic() < deadline: + try: + opcode, payload = shell.recv_payload() + except TimeoutError: + idle_read_timeouts += 1 + continue + + if opcode == 0x1: + try: + message = json.loads(payload.decode()) + except Exception: + message = None + if isinstance(message, dict) and message.get("msg") == "connected": + connected_shell = True + continue + + buffer.extend(payload) + if len(buffer) > 65536: + del buffer[:-65536] + if marker in buffer: + return { + "found": True, + "shell_connected": connected_shell, + "idle_read_timeouts": idle_read_timeouts, + "console_tail": bytes(buffer[-4096:]).decode("utf-8", "replace"), + } + + return { + "found": False, + "shell_connected": connected_shell, + "idle_read_timeouts": idle_read_timeouts, + "console_tail": bytes(buffer[-4096:]).decode("utf-8", "replace"), + } + + def observe_console_nonce( host: str, port: int, @@ -83,31 +131,20 @@ def observe_console_nonce( shell = ShellWebSocket(host, port, path="/websocket/shell", timeout=min(timeout, 8), tls=tls) shell.send_json({"token": token, "options": {"vm_id": vm_id}}) - buffer = bytearray() - deadline = time.monotonic() + timeout - connected_shell = False - while time.monotonic() < deadline: - opcode, payload = shell.recv_payload() - if opcode == 0x1: - try: - message = json.loads(payload.decode()) - except Exception: - message = None - if isinstance(message, dict) and message.get("msg") == "connected": - connected_shell = True - else: - buffer.extend(payload) - if len(buffer) > 65536: - del buffer[:-65536] - if marker in buffer: - receipt.update({ - "classification": "SUPPORTED", - "oracleSatisfied": True, - "shell_connected": connected_shell, - "elapsed_seconds": round(time.monotonic() - started, 3), - "detail": "exact injected V1 nonce observed through supported TrueNAS VM console websocket", - }) - return receipt + observed = _observe_nonce_frames(shell, marker, timeout) + receipt.update({ + "shell_connected": observed["shell_connected"], + "idle_read_timeouts": observed["idle_read_timeouts"], + "console_tail": observed["console_tail"], + }) + if observed["found"]: + receipt.update({ + "classification": "SUPPORTED", + "oracleSatisfied": True, + "elapsed_seconds": round(time.monotonic() - started, 3), + "detail": "exact injected V1 nonce observed through supported TrueNAS VM console websocket", + }) + return receipt raise TimeoutError("exact V1 nonce not observed before console deadline") except Exception as exc: receipt["detail"] = f"{type(exc).__name__}: {exc}" From 3e7121cc94cdd6f3e77bd9431cfe17707a5a7977 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:37:28 +0200 Subject: [PATCH 486/719] test: cover transient VM console socket timeout --- tests/test_truenas_vm_console_probe.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_vm_console_probe.py b/tests/test_truenas_vm_console_probe.py index 393973a0f..131ba0a44 100644 --- a/tests/test_truenas_vm_console_probe.py +++ b/tests/test_truenas_vm_console_probe.py @@ -1,7 +1,7 @@ import unittest from scripts.compute_guest_seed import SeedError -from scripts.truenas_vm_console_probe import nonce_marker +from scripts.truenas_vm_console_probe import _observe_nonce_frames, nonce_marker class TrueNASVmConsoleProbeTests(unittest.TestCase): @@ -15,6 +15,29 @@ def test_nonce_marker_rejects_shell_metacharacters(self): with self.assertRaises(SeedError): nonce_marker("nonce;touch-pwned-1234") + def test_idle_socket_timeout_is_transient_until_overall_deadline(self): + marker = nonce_marker("rep003nonceABCDEF12") + + class FakeShell: + def __init__(self): + self.events = [ + TimeoutError("timed out"), + (0x1, b'{"msg":"connected"}'), + (0x2, b"booting...\nAGENT_DISPATCH_V1_NONCE=rep003nonceABCDEF12\n"), + ] + + def recv_payload(self): + event = self.events.pop(0) + if isinstance(event, BaseException): + raise event + return event + + observed = _observe_nonce_frames(FakeShell(), marker, 1.0) + self.assertTrue(observed["found"]) + self.assertTrue(observed["shell_connected"]) + self.assertEqual(observed["idle_read_timeouts"], 1) + self.assertIn(marker.decode(), observed["console_tail"]) + if __name__ == "__main__": unittest.main() From e8e745cc51f82a6f2a7e843ce62a2e8437368243 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:39:36 +0200 Subject: [PATCH 487/719] RDTE: request TrueNAS BETA.3 Linux VM V1 rep 003 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index f12b0eb0b..73e53b86a 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-vm-v1-002-module-launch", + "request_id": "tn-26-beta3-t3-compute-vm-v1-003-console-idle-deadline", "version": "26.0.0-BETA.3", "rung": "t3", "compute_fixture": "vm-v1", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Re-run the exact TrueNAS 26.0.0-BETA.3 Linux VM V1 fixture after rep 001 reached the real workcell but the V1 client emitted no receipt because the package-importing client was launched by absolute script path with stderr discarded. Preserve the exact pinned CirrOS 0.6.3 digest, deterministic NoCloud nonce seed, supported filesystem.put staging, RAW/CDROM native devices, vm.start, supported /websocket/shell exact-nonce oracle, and zero-residue cleanup. The only causal harness delta is merged #314: invoke the client from repository root with python -m and retain bounded stderr. Nested KVM, Firecracker and Windows remain outside this rep." + "purpose": "Re-run the exact TrueNAS 26.0.0-BETA.3 Linux VM V1 fixture after rep 002 reached the supported VM console oracle but the observer let its 8-second per-read websocket timeout escape the intended 180-second guest deadline. Preserve the pinned CirrOS 0.6.3 digest, deterministic NoCloud nonce seed, supported filesystem.put staging, RAW/CDROM native devices, vm.start, supported /websocket/shell exact-nonce oracle, and zero-residue cleanup. The only causal observer delta is merged #316: idle per-read TimeoutError is transient until the overall deadline, with bounded console-tail diagnostics retained. Nested KVM, Firecracker and Windows remain outside this rep." } From c86596247c8f2fa4fcca236c1eb2ba610b343b6c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:44:22 +0200 Subject: [PATCH 488/719] feat: admit request-driven PVE REST VM V0 --- .github/workflows/gha-kvm-system-rdte.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index d5a04c4d6..b5077d3f1 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -61,6 +61,7 @@ on: options: - none - container-c0 + - vm-v0 truenas_compute_fixture: description: "Optional compute fixture layered on the selected system rung" required: true @@ -166,7 +167,7 @@ jobs: echo "Proxmox request authority does not match exact target profile" >&2 exit 2 } - proxmox_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0")' "$PROXMOX_RUN_REQUEST_PATH")" + proxmox_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0")' "$PROXMOX_RUN_REQUEST_PATH")" printf 'fresh Proxmox request edge: previous=%s current=%s\n' "${previous_proxmox_request_id:-}" "$current_proxmox_request_id" fi if [[ "$EVENT_NAME" != "workflow_dispatch" && "$truenas" == "true" ]]; then From 1bbf1b24e45c3441b613c03482b58fcc613e38d2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:44:25 +0200 Subject: [PATCH 489/719] feat: retain source-bound PVE REST VM V0 receipt --- scripts/gha_kvm_proxmox_rdte.sh | 51 +++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/scripts/gha_kvm_proxmox_rdte.sh b/scripts/gha_kvm_proxmox_rdte.sh index e76264d1d..3d6ca614e 100755 --- a/scripts/gha_kvm_proxmox_rdte.sh +++ b/scripts/gha_kvm_proxmox_rdte.sh @@ -20,7 +20,7 @@ ROOT_PASSWORD="rdte-proxmox-${RANDOM}-${RANDOM}-${RANDOM}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { - echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR] [--compute-fixture none|container-c0]" + echo "Usage: gha_kvm_proxmox_rdte.sh --out RECEIPT [--state-dir DIR] [--compute-fixture none|container-c0|vm-v0]" } OUT="" @@ -36,7 +36,7 @@ while [[ $# -gt 0 ]]; do esac done [[ -n "$OUT" ]] || { usage >&2; exit 2; } -[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } +[[ "$COMPUTE_FIXTURE" == "none" || "$COMPUTE_FIXTURE" == "container-c0" || "$COMPUTE_FIXTURE" == "vm-v0" ]] || { echo "unsupported compute fixture: $COMPUTE_FIXTURE" >&2; exit 2; } if [[ -z "$STATE_DIR" ]]; then STATE_DIR="$(mktemp -d -t gha-kvm-proxmox.XXXXXX)"; fi mkdir -p "$STATE_DIR" "$(dirname "$OUT")" @@ -56,6 +56,7 @@ NESTED_KVM_VCPU_JSON="" P3_LXC_JSON="" REST_API_CENSUS_JSON="" REST_C0_JSON="" +REST_VM_V0_JSON="" SSH_HOSTFWD_ACCEPTED="false" API_HOSTFWD_ACCEPTED="false" QEMU_ALIVE_AT_API_GATE="unknown" @@ -86,7 +87,7 @@ write_receipt() { fi export R_OUT="$OUT" R_CLASS="$classification" R_ORACLE="$oracle" R_PHASE="$phase" R_DETAIL="$detail" export R_SERIAL="$serial_tail" R_ISO_SHA="$OBSERVED_ISO_SHA" R_API="$API_VERSION_JSON" R_NESTED="$NESTED_KVM" - export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" R_P3_LXC="$P3_LXC_JSON" R_REST_API_CENSUS="$REST_API_CENSUS_JSON" R_REST_C0="$REST_C0_JSON" R_COMPUTE_FIXTURE="$COMPUTE_FIXTURE" + export R_NESTED_INDICATORS="$NESTED_KVM_INDICATORS" R_NESTED_VCPU="$NESTED_KVM_VCPU_JSON" R_P3_LXC="$P3_LXC_JSON" R_REST_API_CENSUS="$REST_API_CENSUS_JSON" R_REST_C0="$REST_C0_JSON" R_REST_VM_V0="$REST_VM_V0_JSON" R_COMPUTE_FIXTURE="$COMPUTE_FIXTURE" export R_HOSTFWD_API="$HOSTFWD_API_VERSION_JSON" R_GUEST_LOCAL_API="$GUEST_LOCAL_API_VERSION_JSON" R_API_ROUTE="$API_OBSERVATION_ROUTE" export R_SSH_HOSTFWD_ACCEPTED="$SSH_HOSTFWD_ACCEPTED" R_API_HOSTFWD_ACCEPTED="$API_HOSTFWD_ACCEPTED" export R_QEMU_ALIVE="$QEMU_ALIVE_AT_API_GATE" R_GUEST_DIAGNOSTICS="$GUEST_DIAGNOSTICS" @@ -109,6 +110,7 @@ nested_vcpu = load_json_env("R_NESTED_VCPU") p3_lxc = load_json_env("R_P3_LXC") rest_api_census = load_json_env("R_REST_API_CENSUS") rest_c0 = load_json_env("R_REST_C0") +rest_vm_v0 = load_json_env("R_REST_VM_V0") payload = { "contract": "gha-kvm-system-lab/v1", @@ -148,6 +150,7 @@ payload = { "p3_lxc_lifecycle_exercised": bool(p3_lxc and p3_lxc.get("oracleSatisfied") is True), "rest_api_census_observed": bool(rest_api_census and rest_api_census.get("oracleSatisfied") is True and rest_api_census.get("phase") == "observe-only"), "rest_api_container_c0_exercised": bool(rest_c0 and rest_c0.get("oracleSatisfied") is True and rest_c0.get("api_materialization_oracle") is True), + "rest_api_vm_v0_exercised": bool(rest_vm_v0 and rest_vm_v0.get("oracleSatisfied") is True and rest_vm_v0.get("api_materialization_oracle") is True and rest_vm_v0.get("kind") == "vm"), }, "api_version": json.loads(os.environ["R_API"]) if os.environ.get("R_API") else None, "nested_kvm": os.environ.get("R_NESTED"), @@ -155,6 +158,7 @@ payload = { "p3_lxc": p3_lxc, "rest_api_census": rest_api_census, "rest_api_container_c0": rest_c0, + "rest_api_vm_v0": rest_vm_v0, "diagnostics": { "qemu_alive_at_api_gate": os.environ.get("R_QEMU_ALIVE"), "ssh_hostfwd_accepted": os.environ.get("R_SSH_HOSTFWD_ACCEPTED") == "true", @@ -177,6 +181,7 @@ payload = { "P3 LXC is a separate lifecycle oracle using one exact public Debian template; P2/P5 are not inferred from it.", "REST API census is read-only discovery evidence only; it does not claim LXC or QEMU materialization.", "REST C0, when explicitly requested, proves only product-API LXC create/readback/start/stop/delete/absence; guest execution remains a separate C1 oracle.", + "REST VM V0, when explicitly requested, proves only source-bound product-API QEMU shell create/readback/start/stop/delete/absence; guest boot/workload and nested-KVM remain separate.", ], } pathlib.Path(os.environ["R_OUT"]).write_text(json.dumps(payload, indent=2, sort_keys=True)+"\n", encoding="utf-8") @@ -1125,6 +1130,40 @@ REST_API_CENSUS_JSON="$( )" rm -f "$PVE_PASSWORD_FILE" +if [[ "$COMPUTE_FIXTURE" == "vm-v0" ]]; then + REST_VM_V0_OUT="$STATE_DIR/proxmox-rest-vm-v0.json" + rm -f -- "$REST_VM_V0_OUT" + PVE_PASSWORD_FILE="$STATE_DIR/pve-rest-vm-v0-password" + printf '%s\n' "$ROOT_PASSWORD" >"$PVE_PASSWORD_FILE" + chmod 0400 "$PVE_PASSWORD_FILE" + python3 "$SCRIPT_DIR/proxmox_rest_compute_probe.py" \ + --base-url "https://127.0.0.1:$WEB_PORT" \ + --password-file "$PVE_PASSWORD_FILE" \ + --kind vm --vmid 9201 \ + --expected-qemu-server-version "9.1.15" \ + --apply --out "$REST_VM_V0_OUT" >/dev/null 2>&1 || true + rm -f "$PVE_PASSWORD_FILE" + if [[ -s "$REST_VM_V0_OUT" ]]; then + REST_VM_V0_JSON="$(cat "$REST_VM_V0_OUT")" + else + REST_VM_V0_JSON='{"schema":"proxmox-rest-compute-c0-v1","kind":"vm","classification":"HARNESS_FAILURE","oracleSatisfied":false,"api_materialization_oracle":false,"phase":"rest-api-lifecycle","detail":"REST VM V0 materializer exited without a receipt","cleanup":{"attempted":true,"absent":false}}' + fi + if ! R_REST_VM_V0="$REST_VM_V0_JSON" python3 - <<'PY' +import json, os +try: + p=json.loads(os.environ.get("R_REST_VM_V0","")) + ok=(p.get("kind")=="vm" and p.get("classification")=="SUPPORTED" and + p.get("oracleSatisfied") is True and p.get("api_materialization_oracle") is True and + p.get("cleanup",{}).get("absent") is True) +except Exception: + ok=False +raise SystemExit(0 if ok else 1) +PY + then + fail_evidence ORACLE_FAILURE compute-vm-v0 "native PVE REST VM V0 did not satisfy exact source/create/readback/start/stop/delete/absence cleanup oracle" + fi +fi + if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then REST_C0_JSON="$(probe_rest_lxc_c0 || true)" if ! R_REST_C0="$REST_C0_JSON" python3 - <<'PY' @@ -1155,9 +1194,9 @@ fi NESTED_KVM="unknown" NESTED_KVM_INDICATORS="unknown" -if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then +if [[ "$COMPUTE_FIXTURE" == "container-c0" || "$COMPUTE_FIXTURE" == "vm-v0" ]]; then NESTED_KVM_INDICATORS="skipped" - NESTED_KVM_VCPU_JSON='{"contract":"proxmox-nested-kvm-vcpu/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"not-required-for-container-c0","detail":"nested KVM is outside the bounded native REST container C0 causal rep"}' + NESTED_KVM_VCPU_JSON='{"contract":"proxmox-nested-kvm-vcpu/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"not-required-for-bounded-compute-fixture","detail":"nested KVM is outside the bounded native REST container C0 / VM V0 causal rep"}' else if command -v sshpass >/dev/null 2>&1; then if sshpass -p "$ROOT_PASSWORD" ssh -p "$SSH_PORT" \ @@ -1183,6 +1222,8 @@ fi if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then P3_LXC_JSON='{"contract":"proxmox-lxc-lifecycle/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"superseded-by-rest-c0","detail":"CLI P3 lifecycle intentionally skipped because this rep assigns the exact container fixture to native PVE REST"}' +elif [[ "$COMPUTE_FIXTURE" == "vm-v0" ]]; then + P3_LXC_JSON='{"contract":"proxmox-lxc-lifecycle/v1","classification":"SKIPPED_GUARDRAIL","oracleSatisfied":false,"phase":"not-required-for-vm-v0","detail":"CLI P3 LXC lifecycle intentionally skipped because this rep is bounded to native PVE REST VM V0"}' elif [[ "$NESTED_KVM" == "yes" ]]; then P3_LXC_JSON="$(probe_lxc_lifecycle || true)" else From 6c43956841477a6480711ce7b993c0fb88a797e6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 07:44:29 +0200 Subject: [PATCH 490/719] test: cover bounded PVE REST VM V0 --- tests/test_gha_kvm_system_rdte.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 87de49943..cf3fa2eac 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -153,7 +153,7 @@ def test_proxmox_rest_container_c0_is_opt_in_and_api_native(self): workflow = WORKFLOW.read_text(encoding="utf-8") script = PVE.read_text(encoding="utf-8") self.assertIn("proxmox_compute_fixture", workflow) - self.assertIn('(.compute_fixture // "none") | select(. == "none" or . == "container-c0")', workflow) + self.assertIn('(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0")', workflow) self.assertIn('--compute-fixture "${{ needs.changes.outputs.proxmox_compute_fixture }}"', workflow) self.assertIn("probe_rest_lxc_c0()", script) rest = script.split("probe_rest_lxc_c0()", 1)[1].split("probe_lxc_lifecycle()", 1)[0] @@ -164,9 +164,29 @@ def test_proxmox_rest_container_c0_is_opt_in_and_api_native(self): self.assertIn('"transport":"bounded SSH staging only; all container lifecycle mutation uses PVE REST"', rest) self.assertIn('"rest_api_container_c0_exercised"', script) self.assertIn("superseded-by-rest-c0", script) - self.assertIn("not-required-for-container-c0", script) - c0_tail = script.split('if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]; then', 2)[2] - self.assertIn('NESTED_KVM_INDICATORS="skipped"', c0_tail) + self.assertIn("not-required-for-bounded-compute-fixture", script) + self.assertIn( + 'if [[ "$COMPUTE_FIXTURE" == "container-c0" || "$COMPUTE_FIXTURE" == "vm-v0" ]]; then', + script, + ) + self.assertIn('NESTED_KVM_INDICATORS="skipped"', script) + + def test_proxmox_rest_vm_v0_is_request_driven_and_source_bound(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + script = PVE.read_text(encoding="utf-8") + self.assertIn('(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0")', workflow) + self.assertIn('if [[ "$COMPUTE_FIXTURE" == "vm-v0" ]]', script) + self.assertIn("--kind vm --vmid 9201", script) + self.assertIn('--expected-qemu-server-version "9.1.15"', script) + self.assertIn('"rest_api_vm_v0_exercised"', script) + self.assertIn('"rest_api_vm_v0": rest_vm_v0', script) + self.assertIn("not-required-for-vm-v0", script) + self.assertIn("not-required-for-bounded-compute-fixture", script) + vmv0 = script.split('if [[ "$COMPUTE_FIXTURE" == "vm-v0" ]]', 1)[1].split('if [[ "$COMPUTE_FIXTURE" == "container-c0" ]]', 1)[0] + self.assertIn("proxmox_rest_compute_probe.py", vmv0) + self.assertIn("--apply --out", vmv0) + self.assertNotIn("qm create", vmv0) + self.assertNotIn("pct ", vmv0) def test_proxmox_rest_api_census_is_read_only_and_retained(self): text = PVE.read_text(encoding="utf-8") From 5d89c35708ea3f0a87ea246ee87a0f69403167c2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:23:10 +0200 Subject: [PATCH 491/719] RDTE: repair PVE VM V0 request contract test --- tests/test_proxmox_rdte_target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_proxmox_rdte_target.py b/tests/test_proxmox_rdte_target.py index 37dad25cf..f111dfd32 100644 --- a/tests/test_proxmox_rdte_target.py +++ b/tests/test_proxmox_rdte_target.py @@ -41,7 +41,7 @@ def test_request_is_registered_runtime_target(self): target=self.targets[request["version"]] self.assertTrue(target["runtime_admitted"]) self.assertEqual(request["authority_issue"],target["authority_issue"]) - self.assertIn(request.get("compute_fixture","none"),{"none","container-c0"}) + self.assertIn(request.get("compute_fixture","none"),{"none","container-c0","vm-v0"}) def test_unknown_target_fails_closed(self): with self.assertRaises(MOD.TargetError): From b87508d140a8b9f5d73f3605ebbfec317b7d6aae Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:25:21 +0200 Subject: [PATCH 492/719] RDTE: request PVE 9.2-1 native REST VM V0 rep 001 --- config/proxmox-rdte-run-request.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/proxmox-rdte-run-request.json b/config/proxmox-rdte-run-request.json index 7c26be33a..849fa7412 100644 --- a/config/proxmox-rdte-run-request.json +++ b/config/proxmox-rdte-run-request.json @@ -1,9 +1,9 @@ { "schema": "gha-kvm-proxmox-run-request/v1", - "request_id": "pve-9-2-rest-container-c0-003-task-delete-semantics", + "request_id": "pve-9-2-compute-vm-v0-001-source-bound-rest-restaged", "version": "9.2-1", - "compute_fixture": "container-c0", + "compute_fixture": "vm-v0", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Re-run the exact Proxmox VE 9.2-1 native REST system-container C0 parity fixture after rep 002 reached REST create but the adapter incorrectly rejected Proxmox's non-error WARNINGS: N terminal task state and incorrectly sent purge=1 as DELETE request content. Preserve exact target, source-declared template transport, SHA-512 integrity, REST lifecycle, cleanup and claim boundaries. The only causal delta is merged #298: accept OK or WARNINGS: N per Proxmox task semantics and encode DELETE options as query parameters. Require REST materialization oracle=true, owned-resource absence and fixture-input cleanup; no guest/C1, nested-KVM, VM, Firecracker or Windows claim." + "purpose": "Exercise the first bounded Proxmox VE 9.2-1 native REST VM V0 materialization rep after source/profile binding, merged #318 runtime support, and #320 request-contract repair. Require exact installed qemu-server 9.1.15 admission, REST VM create/readback/start/stop/delete/absence, provider-owned cleanup and zero residue. This rep proves only the native VM/config lifecycle shell (V0); no Linux guest boot/nonce, nested KVM, Firecracker, Windows, Hyper-V or WinBot provider claim." } From 59677931f942f98f3701ecafb73515a30483a9c6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 09:39:19 +0200 Subject: [PATCH 493/719] RDTE: repair CirrOS NoCloud metadata contract * fix(compute): emit CirrOS NoCloud metadata as JSON * test(compute): bind CirrOS NoCloud JSON metadata --- scripts/compute_guest_seed.py | 13 +++++++++++-- tests/test_compute_guest_seed.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/scripts/compute_guest_seed.py b/scripts/compute_guest_seed.py index 406db28ae..fe152b720 100644 --- a/scripts/compute_guest_seed.py +++ b/scripts/compute_guest_seed.py @@ -2,6 +2,7 @@ from __future__ import annotations import argparse +import json import pathlib import re @@ -23,7 +24,11 @@ def validate_token(value: str, pattern: re.Pattern[str], label: str) -> str: def render_seed(nonce: str, instance_id: str) -> tuple[str, str]: nonce = validate_token(nonce, NONCE_RE, "nonce") instance_id = validate_token(instance_id, INSTANCE_RE, "instance-id") - meta = f"instance-id: {instance_id}\nlocal-hostname: rdtev1\n" + meta = json.dumps( + {"instance-id": instance_id, "local-hostname": "rdtev1"}, + sort_keys=True, + separators=(",", ":"), + ) + "\n" user = ( "#!/bin/sh\n" "set -eu\n" @@ -37,7 +42,11 @@ def render_seed(nonce: str, instance_id: str) -> tuple[str, str]: def render_v2_observation_seed(nonce: str, instance_id: str) -> tuple[str, str]: nonce = validate_token(nonce, NONCE_RE, "nonce") instance_id = validate_token(instance_id, INSTANCE_RE, "instance-id") - meta = f"instance-id: {instance_id}\nlocal-hostname: rdtev2\n" + meta = json.dumps( + {"instance-id": instance_id, "local-hostname": "rdtev2"}, + sort_keys=True, + separators=(",", ":"), + ) + "\n" user = ( "#!/bin/sh\n" "set -eu\n" diff --git a/tests/test_compute_guest_seed.py b/tests/test_compute_guest_seed.py index b0e042108..f2fa0fb97 100644 --- a/tests/test_compute_guest_seed.py +++ b/tests/test_compute_guest_seed.py @@ -1,3 +1,4 @@ +import json import pathlib import tempfile import unittest @@ -9,7 +10,11 @@ class ComputeGuestSeedTests(unittest.TestCase): def test_seed_is_deterministic_and_serial_nonce_is_external(self): nonce = "rep001nonceABCDEF12" meta, user = render_seed(nonce, "rdte-v1-rep001") - self.assertEqual(meta, "instance-id: rdte-v1-rep001\nlocal-hostname: rdtev1\n") + self.assertEqual( + json.loads(meta), + {"instance-id": "rdte-v1-rep001", "local-hostname": "rdtev1"}, + ) + self.assertEqual(meta, '{"instance-id":"rdte-v1-rep001","local-hostname":"rdtev1"}\n') self.assertTrue(user.startswith("#!/bin/sh\nset -eu\n")) self.assertIn(PREFIX, user) self.assertIn(">/dev/ttyS0", user) @@ -23,7 +28,11 @@ def test_seed_rejects_shell_metacharacters(self): render_seed(bad, "rdte-v1-rep001") def test_v2_observation_seed_only_observes_nested_kvm_prerequisites(self): - _, user = render_v2_observation_seed("rep002nonceABCDEF12", "rdte-v2-rep002") + meta, user = render_v2_observation_seed("rep002nonceABCDEF12", "rdte-v2-rep002") + self.assertEqual( + json.loads(meta), + {"instance-id": "rdte-v2-rep002", "local-hostname": "rdtev2"}, + ) self.assertIn("[ -c /dev/kvm ]", user) self.assertIn("(vmx|svm)", user) self.assertIn("AGENT_DISPATCH_V2_KVM_PRESENT=", user) From 9c0fb5f3b00953911b03ea02b7cb2b792af286db Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:38:00 +0200 Subject: [PATCH 494/719] RDTE: enforce target-specific FolioRelay discovery coupling --- .../truenas_middleware_foliorelay_t6_probe.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 80e6ccc6c..d3a144be7 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -22,6 +22,15 @@ EXPECTED_SCHEMA = "semper-supra.foliorelay-truenas-t6-control/1" EXPECTED_APP_NAME = "rdte-t6-foliorelay" +DBUS_SOCKET = "/run/dbus/system_bus_socket" +AVAHI_TARGETS = {"25.04.1","25.04.2.6","25.10.7"} +DIRECT_TARGETS = {"26.0.0-BETA.3"} +AVAHI_DISCOVERY_USER = "65534:10001" +AVAHI_DISCOVERY_COMMAND = [ + "-identity-file","/var/lib/foliorelay-control/config/printer.json", + "-backend","avahi", + "-dbus-address","unix:path=/run/dbus/system_bus_socket", +] OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer" EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" EXPECTED_CUPS = "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" @@ -62,6 +71,42 @@ def load_json(path: pathlib.Path): raise RuntimeError(f"{path.name} must be an object") return value +def validate_discovery_materialization(services: dict, target_version: str): + discovery=services.get("discovery") or {} + dbus_mounts=[] + for service_name,service in services.items(): + for mount in (service or {}).get("volumes") or []: + if not isinstance(mount,dict): + continue + paths=[mount.get("source"),mount.get("target")] + if any(isinstance(x,str) and (x=="/run/dbus" or x.startswith("/run/dbus/") or x=="/var/run/dbus" or x.startswith("/var/run/dbus/")) for x in paths): + dbus_mounts.append((service_name,mount)) + if target_version in AVAHI_TARGETS: + if discovery.get("command")!=AVAHI_DISCOVERY_COMMAND: + raise RuntimeError("25.x discovery command drifted from exact Avahi contract") + if discovery.get("user")!=AVAHI_DISCOVERY_USER: + raise RuntimeError("25.x discovery user drifted from exact Avahi contract") + if len(dbus_mounts)!=1: + raise RuntimeError("25.x requires exactly one host D-Bus mount") + service_name,mount=dbus_mounts[0] + if not ( + service_name=="discovery" + and mount.get("type")=="bind" + and mount.get("source")==DBUS_SOCKET + and mount.get("target")==DBUS_SOCKET + and mount.get("read_only") is True + ): + raise RuntimeError("25.x host D-Bus coupling exceeds exact read-only Avahi socket") + return "avahi" + if target_version in DIRECT_TARGETS: + if dbus_mounts: + raise RuntimeError("direct discovery target must not couple to host D-Bus") + command=discovery.get("command") or [] + if "-backend" in command or "-dbus-address" in command or "user" in discovery: + raise RuntimeError("direct discovery target drifted into Avahi materialization") + return "direct" + raise RuntimeError(f"unsupported FolioRelay target version {target_version!r}") + def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): control = load_json(root / "control.json") compose = load_json(root / "compose.json") @@ -87,6 +132,9 @@ def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): raise RuntimeError("control/discovery exact image identity drifted") if services["cups"].get("image") != EXPECTED_CUPS: raise RuntimeError("CUPS exact image identity drifted") + discovery_backend=validate_discovery_materialization(services,target_version) + if candidate.get("discovery_backend") not in (None,discovery_backend): + raise RuntimeError("control discovery backend disagrees with rendered Compose") required = set(control.get("required_oracles") or []) expected = { "app-create-running", "config-readback-exact-compose", "portal-ready", From e187353aa647c73f8de0830a88408b31787d47c9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:38:09 +0200 Subject: [PATCH 495/719] test: lock FolioRelay Avahi consumer boundary --- tests/test_truenas_t6_foliorelay_control.py | 45 ++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 3aab7acd8..cebc09593 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -40,9 +40,52 @@ def test_probe_preserves_product_oracles_and_zero_residue(self): self.assertIn('_universal._sub._ipp._tcp.local', observer) self.assertIn(MOD.OBSERVER_IMAGE, text) self.assertNotIn("avahi-publish-service", text) - self.assertNotIn("/run/dbus", text) + self.assertIn('DBUS_SOCKET = "/run/dbus/system_bus_socket"', text) + self.assertIn('25.x requires exactly one host D-Bus mount', text) + self.assertIn('direct discovery target must not couple to host D-Bus', text) self.assertNotIn("/var/run/docker.sock", text) + def test_discovery_materialization_is_exact_per_target(self): + base={ + "control":{"volumes":[]}, + "cups":{"volumes":[]}, + "discovery":{ + "volumes":[], + "command":["-identity-file","/var/lib/foliorelay-control/config/printer.json"], + }, + } + self.assertEqual(MOD.validate_discovery_materialization(base,"26.0.0-BETA.3"),"direct") + + avahi=json.loads(json.dumps(base)) + avahi["discovery"]["user"]="65534:10001" + avahi["discovery"]["command"]=list(MOD.AVAHI_DISCOVERY_COMMAND) + avahi["discovery"]["volumes"].append({ + "type":"bind", + "source":MOD.DBUS_SOCKET, + "target":MOD.DBUS_SOCKET, + "read_only":True, + }) + self.assertEqual(MOD.validate_discovery_materialization(avahi,"25.10.7"),"avahi") + + widened=json.loads(json.dumps(avahi)) + widened["control"]["volumes"].append({ + "type":"bind","source":"/run/dbus/other","target":"/run/dbus/other","read_only":True, + }) + with self.assertRaisesRegex(RuntimeError,"exactly one host D-Bus mount"): + MOD.validate_discovery_materialization(widened,"25.10.7") + + wrong_user=json.loads(json.dumps(avahi)) + wrong_user["discovery"]["user"]="10001:10001" + with self.assertRaisesRegex(RuntimeError,"discovery user"): + MOD.validate_discovery_materialization(wrong_user,"25.10.7") + + direct_dbus=json.loads(json.dumps(base)) + direct_dbus["discovery"]["volumes"].append({ + "type":"bind","source":MOD.DBUS_SOCKET,"target":MOD.DBUS_SOCKET,"read_only":True, + }) + with self.assertRaisesRegex(RuntimeError,"must not couple to host D-Bus"): + MOD.validate_discovery_materialization(direct_dbus,"26.0.0-BETA.3") + def test_f4_reconciliation_policy_and_f5_retention_contract(self): exact={"services":{"control":{"image":"sha256:exact"}}} drift={"services":{"control":{"image":"sha256:drift"}}} From d916b00d5b8cdf59c8011ce02ca21eb825486521 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:41:38 +0200 Subject: [PATCH 496/719] test: import json for discovery materialization fixtures --- tests/test_truenas_t6_foliorelay_control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index cebc09593..c0fa7e1b7 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import importlib.util +import json import pathlib import subprocess import tempfile From 52bdadaed8c1600d1a6b5c99309c284d6d6e5305 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:51:49 +0200 Subject: [PATCH 497/719] RDTE: adopt Avahi-capable FolioRelay authority in gha-kvm-system-rdte.yml --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index b5077d3f1..0aabc0d7e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -254,9 +254,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 with: - foundry_ref: e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 + foundry_ref: 1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -530,7 +530,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2" + foundry_commit="1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From b10d00a47bdbda600c805ec2cf76719e6b61aab4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:52:01 +0200 Subject: [PATCH 498/719] RDTE: adopt Avahi-capable FolioRelay authority in prep-foliorelay-t6-consumer.yml --- .github/workflows/prep-foliorelay-t6-consumer.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 95dc66b7b..9c72d3462 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 with: - foundry_ref: e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2 + foundry_ref: 1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 target_version: 26.0.0-BETA.3 permissions: contents: read @@ -126,10 +126,10 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2" and + .foundry_ref == "1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9" and .candidate.truenas_version == "26.0.0-BETA.3" and - .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" and - .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" and + .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" and + .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and .secrets_captured == false ' "$root/control.json" >/dev/null jq -e ' From a5489f65069390fdc24400f64c47a151bb152903 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:52:15 +0200 Subject: [PATCH 499/719] RDTE: adopt Avahi-capable FolioRelay authority in truenas_middleware_foliorelay_t6_probe.py --- scripts/truenas_middleware_foliorelay_t6_probe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index d3a144be7..c3f46a7f6 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -32,8 +32,8 @@ "-dbus-address","unix:path=/run/dbus/system_bus_socket", ] OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer" -EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce" -EXPECTED_CUPS = "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f" +EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" +EXPECTED_CUPS = "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" OBSERVER_IMAGE = "docker.io/library/hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8" DATASET = "rdtepool/foliorelay-t6" ROOT = "/mnt/rdtepool/foliorelay-t6" From d95ee97396bc13e0653d3ad06266a72a173e2168 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:52:28 +0200 Subject: [PATCH 500/719] RDTE: adopt Avahi-capable FolioRelay authority in gha_kvm_truenas_rdte.sh --- scripts/gha_kvm_truenas_rdte.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 2648110df..f0561ce43 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -204,7 +204,7 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" else {"name": "rdte-t6-catalog-ntfy", "catalog_app": "ntfy", "catalog_version": "1.1.21"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" - else {"name": "rdte-t6-foliorelay", "images": ["ghcr.io/sempersupra/foliorelay-control@sha256:0ffabcc1ced0325c41c54d860c6fe248e4fc8afeea3994dcebb999d6a14ee1ce", "ghcr.io/sempersupra/foliorelay-cups@sha256:b644b4b1e064a1d10c18fbbb9f9aa09a2835e7e9a48ccda5a67d44cbda006b4f"]} + else {"name": "rdte-t6-foliorelay", "images": ["ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d", "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814"]} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" From 29739892b1dc4891fceaffd1ccc4333fa2564ca1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:52:42 +0200 Subject: [PATCH 501/719] RDTE: adopt Avahi-capable FolioRelay authority in test_truenas_t6_foliorelay_control.py --- tests/test_truenas_t6_foliorelay_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index c0fa7e1b7..929168d24 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -219,10 +219,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2", workflow) + self.assertIn("export-foliorelay-t6-control.yml@1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="e4fbab1da4d08dbdf505ecd40c12acd2adba3ff2"', workflow) + self.assertIn('foundry_commit="1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From fc9792c8bf7c0edc697471d381064efc161f06b1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:53:24 +0200 Subject: [PATCH 502/719] RDTE: preflight FolioRelay against exact 25.10.7 Avahi render --- .github/workflows/prep-foliorelay-t6-consumer.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 9c72d3462..429e60d2a 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -30,7 +30,7 @@ jobs: uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 with: foundry_ref: 1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 - target_version: 26.0.0-BETA.3 + target_version: 25.10.7 permissions: contents: read @@ -127,7 +127,7 @@ jobs: jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and .foundry_ref == "1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9" and - .candidate.truenas_version == "26.0.0-BETA.3" and + .candidate.truenas_version == "25.10.7" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and .secrets_captured == false From dfe21047725a5f5f2dd66feb17f33f7602168046 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:13:10 +0200 Subject: [PATCH 503/719] RDTE: request TrueNAS VM V1 rep004 after JSON seed proof --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 73e53b86a..f6722f510 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-vm-v1-003-console-idle-deadline", + "request_id": "tn-26-beta3-t3-compute-vm-v1-004-cirros-json-seed", "version": "26.0.0-BETA.3", "rung": "t3", "compute_fixture": "vm-v1", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Re-run the exact TrueNAS 26.0.0-BETA.3 Linux VM V1 fixture after rep 002 reached the supported VM console oracle but the observer let its 8-second per-read websocket timeout escape the intended 180-second guest deadline. Preserve the pinned CirrOS 0.6.3 digest, deterministic NoCloud nonce seed, supported filesystem.put staging, RAW/CDROM native devices, vm.start, supported /websocket/shell exact-nonce oracle, and zero-residue cleanup. The only causal observer delta is merged #316: idle per-read TimeoutError is transient until the overall deadline, with bounded console-tail diagnostics retained. Nested KVM, Firecracker and Windows remain outside this rep." + "purpose": "Re-run the exact TrueNAS 26.0.0-BETA.3 Linux VM V1 fixture after rep003 and the plain-QEMU falsifier isolated the remaining failure to CirrOS 0.6.3 NoCloud meta-data encoding. Preserve the pinned CirrOS 0.6.3 image digest, supported filesystem.put staging, RAW boot disk, CDROM cidata seed, vm.start, supported /websocket/shell exact-nonce oracle, repaired 180-second overall guest deadline, and zero-residue cleanup. The only guest-fixture semantic delta is merged commit 59677931f942f98f3701ecafb73515a30483a9c6: emit CirrOS NoCloud meta-data as JSON for json2fstree. Cheap QEMU TCG smoke run 37465128038 independently proved the repaired exact seed under q35/UEFI and BIOS with uefi_nonce=true and bios_nonce=true; artifact 11414348724 digest sha256:202f5bbf787fcdd19b174d49d5d8b0ee4428a575cc9d3d6231fe4c6cd811beb8. Nested KVM, Firecracker and Windows remain outside this rep." } From d77bd43dba8b67398a522dd5cdef9c8cf2d3f631 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:17:16 +0200 Subject: [PATCH 504/719] compute: bind qualified Windows W1 media and PVE source profile --- config/compute-guest-profiles.json | 49 +++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/config/compute-guest-profiles.json b/config/compute-guest-profiles.json index 5708f69c9..5790104f7 100644 --- a/config/compute-guest-profiles.json +++ b/config/compute-guest-profiles.json @@ -3,7 +3,8 @@ "authority": "SemperSupra/agent-dispatch-private#426", "dependencies": { "firecracker": "SemperSupra/agent-dispatch-private#277", - "windows_embodiment": "mark-e-deyoung/windows-utilities#26" + "windows_embodiment": "mark-e-deyoung/windows-utilities#26", + "windows_media": "mark-e-deyoung/WinBot#70" }, "policy": { "source_capability_is_not_runtime_qualification": true, @@ -117,7 +118,8 @@ "source_proven": true } }, - "runtime_status": "OPEN" + "runtime_status": "OPEN", + "fixture": "windows-11-enterprise-evaluation-26h2-x64-en-us" }, "firecracker": { "source_status": "CANDIDATE", @@ -156,8 +158,27 @@ "9.2-1": { "vm_adapter": "proxmox-vm-rest", "windows11": { - "source_status": "PROFILE_PENDING_REST_PAYLOAD_BINDING", - "runtime_status": "OPEN" + "source_status": "CANDIDATE", + "runtime_status": "OPEN", + "capabilities": { + "secure_boot": { + "surface": "qemu config efidisk0 efitype=4m,pre-enrolled-keys=1", + "source_proven": true + }, + "tpm": { + "surface": "qemu config tpmstate0 version=v2.0", + "source_proven": true + }, + "uefi_q35": { + "surface": "qemu config bios=ovmf + machine=q35", + "source_proven": true + } + }, + "source_binding": { + "qemu_server_commit": "6785065b3f766f15f6f151af8ec27ec8bb5b07ab", + "package_version": "9.1.15" + }, + "fixture": "windows-11-enterprise-evaluation-26h2-x64-en-us" }, "firecracker": { "source_status": "PROFILE_PENDING_REST_PAYLOAD_BINDING", @@ -190,6 +211,26 @@ "must_match_injected_nonce": true }, "claim_boundary": "V1 proves pinned Linux guest boot plus exact externally observed per-run nonce only; nested KVM, Firecracker and Windows remain separate rungs" + }, + "windows_w1": { + "id": "windows-11-enterprise-evaluation-26h2-x64-en-us", + "product": "Windows 11 Enterprise Evaluation", + "version": "26H2", + "build": "26300.9457", + "edition": "Enterprise", + "architecture": "x64", + "language": "English", + "source": { + "host": "software-static.download.prss.microsoft.com", + "sha256": "bc3f24086ebadc94489066b5ad78089e2cf5c3491e90e790bb81a2b199c10e38", + "size_bytes": 8225329152, + "qualification_authority": "mark-e-deyoung/WinBot#70", + "qualification_run": 36695017130, + "accepted_artifact": 11087331509, + "accepted_artifact_digest": "sha256:9f94fea45f82fe97056a89ac656d7009885a9d8b846747cf6f5f57d0943451dd" + }, + "product_key_policy": "none-embedded", + "oracle_boundary": "bounded guest readiness/nonce plus lifecycle; no WinBot backend admission claim" } } } From 366d7b297b2b50910625d9ca3dff4230008efb78 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:18:28 +0200 Subject: [PATCH 505/719] compute: validate qualified Windows W1 source binding --- scripts/compute_guest_profile_contract.py | 38 +++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/scripts/compute_guest_profile_contract.py b/scripts/compute_guest_profile_contract.py index 8c8f3e6fc..1cd586c2f 100644 --- a/scripts/compute_guest_profile_contract.py +++ b/scripts/compute_guest_profile_contract.py @@ -19,6 +19,8 @@ def validate(p:dict[str,Any])->dict[str,Any]: raise ProfileError("Firecracker authority must remain #277") if p.get("dependencies",{}).get("windows_embodiment")!="mark-e-deyoung/windows-utilities#26": raise ProfileError("Windows embodiment authority must remain windows-utilities#26") + if p.get("dependencies",{}).get("windows_media")!="mark-e-deyoung/WinBot#70": + raise ProfileError("Windows media authority must remain WinBot#70") policy=p.get("policy") if not isinstance(policy,dict) or any(policy.get(k) is not True for k in ( "source_capability_is_not_runtime_qualification", @@ -30,8 +32,8 @@ def validate(p:dict[str,Any])->dict[str,Any]: )): raise ProfileError("safety/claim policy incomplete") fixtures=p.get("fixtures") - if not isinstance(fixtures,dict) or set(fixtures)!={"linux_v1"}: - raise ProfileError("exact linux_v1 fixture missing") + if not isinstance(fixtures,dict) or set(fixtures)!={"linux_v1","windows_w1"}: + raise ProfileError("exact guest fixture set missing") v1=fixtures["linux_v1"] if v1.get("id")!="cirros-0.6.3-x86_64-nocloud-serial-nonce": raise ProfileError("linux_v1 fixture id drift") @@ -49,6 +51,28 @@ def validate(p:dict[str,Any])->dict[str,Any]: oracle=v1.get("oracle") if not isinstance(oracle,dict) or oracle.get("transport")!="serial-console" or oracle.get("nonce_prefix")!="AGENT_DISPATCH_V1_NONCE=" or oracle.get("must_match_injected_nonce") is not True: raise ProfileError("linux_v1 guest nonce oracle incomplete") + w1=fixtures["windows_w1"] + if w1.get("id")!="windows-11-enterprise-evaluation-26h2-x64-en-us": + raise ProfileError("Windows W1 fixture id drift") + if any(w1.get(k)!=v for k,v in { + "product":"Windows 11 Enterprise Evaluation", + "version":"26H2", + "build":"26300.9457", + "edition":"Enterprise", + "architecture":"x64", + "language":"English", + "product_key_policy":"none-embedded", + }.items()): + raise ProfileError("Windows W1 product identity drift") + wsrc=w1.get("source") + if not isinstance(wsrc,dict) or wsrc.get("host")!="software-static.download.prss.microsoft.com": + raise ProfileError("Windows W1 source host drift") + if wsrc.get("sha256")!="bc3f24086ebadc94489066b5ad78089e2cf5c3491e90e790bb81a2b199c10e38" or wsrc.get("size_bytes")!=8225329152: + raise ProfileError("Windows W1 media identity drift") + if wsrc.get("qualification_authority")!="mark-e-deyoung/WinBot#70" or wsrc.get("qualification_run")!=36695017130: + raise ProfileError("Windows W1 qualification authority drift") + if wsrc.get("accepted_artifact")!=11087331509 or wsrc.get("accepted_artifact_digest")!="sha256:9f94fea45f82fe97056a89ac656d7009885a9d8b846747cf6f5f57d0943451dd": + raise ProfileError("Windows W1 accepted artifact identity drift") tn=p.get("truenas") expected={"25.04.1","25.04.2.6","25.10.7","26.0.0-BETA.3"} if not isinstance(tn,dict) or set(tn)!=expected: @@ -108,6 +132,16 @@ def validate(p:dict[str,Any])->dict[str,Any]: for kind in ("windows11","firecracker"): if prox["9.2-1"][kind].get("runtime_status")!="OPEN": raise ProfileError(f"Proxmox {kind} runtime must remain OPEN") + pwin=prox["9.2-1"]["windows11"] + if pwin.get("source_status")!="CANDIDATE": + raise ProfileError("Proxmox Windows source profile must be CANDIDATE") + if pwin.get("fixture")!="windows-11-enterprise-evaluation-26h2-x64-en-us": + raise ProfileError("Proxmox Windows fixture binding drift") + if pwin.get("source_binding")!={"qemu_server_commit":"6785065b3f766f15f6f151af8ec27ec8bb5b07ab","package_version":"9.1.15"}: + raise ProfileError("Proxmox Windows qemu-server source binding drift") + pcaps=pwin.get("capabilities") + if not isinstance(pcaps,dict) or set(pcaps)!={"secure_boot","tpm","uefi_q35"} or any(pcaps[k].get("source_proven") is not True for k in pcaps): + raise ProfileError("Proxmox Windows source capability binding incomplete") rungs=p.get("rungs") if not isinstance(rungs,dict) or list(rungs)!=["V0","V1","V2","W1","WB"]: raise ProfileError("guest rung order/coverage invalid") From 11207503410179686e81577d6222671df921fa66 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:19:00 +0200 Subject: [PATCH 506/719] test: guard Windows W1 media and PVE source binding --- tests/test_compute_guest_profile_contract.py | 46 +++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/test_compute_guest_profile_contract.py b/tests/test_compute_guest_profile_contract.py index 8598bd77b..f4843de49 100644 --- a/tests/test_compute_guest_profile_contract.py +++ b/tests/test_compute_guest_profile_contract.py @@ -7,6 +7,7 @@ def fixture(): "dependencies":{ "firecracker":"SemperSupra/agent-dispatch-private#277", "windows_embodiment":"mark-e-deyoung/windows-utilities#26", + "windows_media":"mark-e-deyoung/WinBot#70", }, "policy":{ "source_capability_is_not_runtime_qualification":True, @@ -34,12 +35,45 @@ def fixture(): "must_match_injected_nonce":True, }, "claim_boundary":"x", + }, + "windows_w1":{ + "id":"windows-11-enterprise-evaluation-26h2-x64-en-us", + "product":"Windows 11 Enterprise Evaluation", + "version":"26H2", + "build":"26300.9457", + "edition":"Enterprise", + "architecture":"x64", + "language":"English", + "source":{ + "host":"software-static.download.prss.microsoft.com", + "sha256":"bc3f24086ebadc94489066b5ad78089e2cf5c3491e90e790bb81a2b199c10e38", + "size_bytes":8225329152, + "qualification_authority":"mark-e-deyoung/WinBot#70", + "qualification_run":36695017130, + "accepted_artifact":11087331509, + "accepted_artifact_digest":"sha256:9f94fea45f82fe97056a89ac656d7009885a9d8b846747cf6f5f57d0943451dd", + }, + "product_key_policy":"none-embedded", + "oracle_boundary":"x", } }, "truenas":{}, "proxmox":{ "9.2-1":{ - "windows11":{"runtime_status":"OPEN"}, + "windows11":{ + "source_status":"CANDIDATE", + "runtime_status":"OPEN", + "fixture":"windows-11-enterprise-evaluation-26h2-x64-en-us", + "source_binding":{ + "qemu_server_commit":"6785065b3f766f15f6f151af8ec27ec8bb5b07ab", + "package_version":"9.1.15", + }, + "capabilities":{ + "secure_boot":{"source_proven":True}, + "tpm":{"source_proven":True}, + "uefi_q35":{"source_proven":True}, + }, + }, "firecracker":{"runtime_status":"OPEN"}, } }, @@ -141,6 +175,16 @@ def test_beta3_v2_firecracker_gate_remains_authority_bound(self): p["truenas"]["26.0.0-BETA.3"]["linux_v2"]["firecracker_gate"]="run whatever" with self.assertRaises(ProfileError): validate(p) + def test_windows_media_digest_fails_closed(self): + p=self.profile() + p["fixtures"]["windows_w1"]["source"]["sha256"]="0"*64 + with self.assertRaises(ProfileError): + validate(p) + def test_proxmox_windows_source_binding_fails_closed(self): + p=self.profile() + p["proxmox"]["9.2-1"]["windows11"]["source_binding"]["package_version"]="9.1.14" + with self.assertRaises(ProfileError): + validate(p) def test_target_growth_fails_closed(self): p=self.profile() p["truenas"]["27.0.0"]=tnrow("27.0.0") From 271cba1c6ca83cb28f171233becccf83d1d49e10 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:20:48 +0200 Subject: [PATCH 507/719] compute: fail closed on BETA.3 Windows fixture drift --- scripts/compute_guest_profile_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/compute_guest_profile_contract.py b/scripts/compute_guest_profile_contract.py index 1cd586c2f..2f04a79b8 100644 --- a/scripts/compute_guest_profile_contract.py +++ b/scripts/compute_guest_profile_contract.py @@ -120,6 +120,8 @@ def validate(p:dict[str,Any])->dict[str,Any]: observation=linux.get("observation_requirement","") if "raw.qemu" not in observation or "virsh directly" not in observation: raise ProfileError("BETA.3 Linux V1 must prohibit private hypervisor console escape") + if row.get("windows11",{}).get("fixture")!="windows-11-enterprise-evaluation-26h2-x64-en-us": + raise ProfileError("BETA.3 Windows W1 fixture binding drift") win=row["windows11"] if win.get("source_status")!="CANDIDATE": raise ProfileError(f"{version}: Windows source status must remain CANDIDATE") From 09716ec2b4fb1a7379dd029d5216f624be738616 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:20:51 +0200 Subject: [PATCH 508/719] test: guard BETA.3 Windows fixture binding --- tests/test_compute_guest_profile_contract.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_compute_guest_profile_contract.py b/tests/test_compute_guest_profile_contract.py index f4843de49..1f6d5a59d 100644 --- a/tests/test_compute_guest_profile_contract.py +++ b/tests/test_compute_guest_profile_contract.py @@ -99,6 +99,7 @@ def tnrow(version): }, } if version=="26.0.0-BETA.3": + row["windows11"]["fixture"]="windows-11-enterprise-evaluation-26h2-x64-en-us" row["linux_v2"]={ "source_status":"OBSERVATION_CANDIDATE", "runtime_status":"OPEN", @@ -175,6 +176,11 @@ def test_beta3_v2_firecracker_gate_remains_authority_bound(self): p["truenas"]["26.0.0-BETA.3"]["linux_v2"]["firecracker_gate"]="run whatever" with self.assertRaises(ProfileError): validate(p) + def test_beta3_windows_fixture_binding_fails_closed(self): + p=self.profile() + p["truenas"]["26.0.0-BETA.3"]["windows11"]["fixture"]="different" + with self.assertRaises(ProfileError): + validate(p) def test_windows_media_digest_fails_closed(self): p=self.profile() p["fixtures"]["windows_w1"]["source"]["sha256"]="0"*64 From c3ac0e6957af9d6a2b5aa7b5af3192f2848907c9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:32:23 +0200 Subject: [PATCH 509/719] compute: add pure Windows W1 native planner --- scripts/windows_w1_native_plan.py | 324 ++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 scripts/windows_w1_native_plan.py diff --git a/scripts/windows_w1_native_plan.py b/scripts/windows_w1_native_plan.py new file mode 100644 index 000000000..6a30e6d34 --- /dev/null +++ b/scripts/windows_w1_native_plan.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import re +from typing import Any + +from scripts.compute_guest_profile_contract import load, validate + + +class WindowsW1PlanError(RuntimeError): + pass + + +FIXTURE_ID = "windows-11-enterprise-evaluation-26h2-x64-en-us" +MEDIA_SHA256 = "bc3f24086ebadc94489066b5ad78089e2cf5c3491e90e790bb81a2b199c10e38" +MEDIA_SIZE = 8225329152 +MEMORY_MIB = 4096 +VCPUS = 2 +SYSTEM_DISK_GIB = 64 +SYSTEM_DISK_BYTES = SYSTEM_DISK_GIB * 1024**3 + +TRUENAS_SOURCE = { + "middleware_commit": "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3", + "vm_api_blob": "0944382fedc0a8cbcfc55fb8a653a9e5758e88fd", + "vm_device_api_blob": "67603b6e2048da6235b802f970559b627857efc9", + "vm_service_blob": "43ed4b532b40da02ad885039d954c99a93ea22ed", +} +PROXMOX_SOURCE = { + "qemu_server_commit": "6785065b3f766f15f6f151af8ec27ec8bb5b07ab", + "qemu_server_blob": "118f26bc94d9ee8e8c4c39a3d710e67c14f61bc0", + "secure_boot_tpm_fixture_blob": "51e525b34ab3e670af887260e436973bdb2f1755", + "package_version": "9.1.15", +} + + +def _nonempty(value: str, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise WindowsW1PlanError(f"{label} must be non-empty") + return value.strip() + + +def _safe_binding(value: str, label: str) -> str: + value = _nonempty(value, label) + if any(c in value for c in "\r\n\x00"): + raise WindowsW1PlanError(f"{label} contains invalid control characters") + return value + + +def _profile(path: pathlib.Path) -> dict[str, Any]: + profile = load(path) + validate(profile) + fixture = profile.get("fixtures", {}).get("windows_w1") + if not isinstance(fixture, dict) or fixture.get("id") != FIXTURE_ID: + raise WindowsW1PlanError("qualified Windows W1 fixture is absent") + source = fixture.get("source") + if not isinstance(source, dict): + raise WindowsW1PlanError("qualified Windows W1 source is absent") + if source.get("sha256") != MEDIA_SHA256 or source.get("size_bytes") != MEDIA_SIZE: + raise WindowsW1PlanError("Windows W1 media identity drift") + return profile + + +def portable_intent(profile: dict[str, Any]) -> dict[str, Any]: + fixture = profile["fixtures"]["windows_w1"] + return { + "schema": "windows-w1-portable-intent/v1", + "fixture": FIXTURE_ID, + "media": { + "product": fixture["product"], + "version": fixture["version"], + "build": fixture["build"], + "edition": fixture["edition"], + "architecture": fixture["architecture"], + "language": fixture["language"], + "sha256": MEDIA_SHA256, + "size_bytes": MEDIA_SIZE, + "qualification_authority": fixture["source"]["qualification_authority"], + "product_key_policy": fixture["product_key_policy"], + }, + "requirements": { + "virtual_processors": VCPUS, + "memory_mib": MEMORY_MIB, + "system_disk_gib": SYSTEM_DISK_GIB, + "firmware": "UEFI", + "secure_boot": True, + "tpm_version": "2.0", + "cpu_semantics": "host-passthrough", + "install_storage_semantics": "inbox-driver-compatible-ahci-sata", + "install_network_semantics": "inbox-driver-compatible-e1000", + }, + "lifecycle": [ + "observe-preconditions", + "create", + "readback", + "start-install", + "unattended-install", + "guest-readiness-oracle", + "restart", + "detach-install-media", + "system-disk-boot-oracle", + "stop", + "delete", + "absence", + ], + "claim_boundary": ( + "Windows W1 native VM install/readiness/restart/cleanup only; " + "nested virtualization, Hyper-V and WinBot-provider admission remain separate" + ), + } + + +def truenas_plan( + profile: dict[str, Any], + *, + pool: str, + iso_path: str, + bridge: str, + name: str = "rdtewindowsw1", +) -> dict[str, Any]: + row = profile["truenas"]["26.0.0-BETA.3"]["windows11"] + if row.get("fixture") != FIXTURE_ID or row.get("runtime_status") != "OPEN": + raise WindowsW1PlanError("TrueNAS BETA.3 Windows W1 profile is not source-bound OPEN") + if not re.fullmatch(r"[A-Za-z0-9_]+", name): + raise WindowsW1PlanError("TrueNAS VM name violates exact product name grammar") + pool = _safe_binding(pool, "pool") + if "/" in pool: + raise WindowsW1PlanError("TrueNAS pool binding must be a pool name, not a dataset path") + iso_path = _safe_binding(iso_path, "iso-path") + if not iso_path.startswith("/mnt/"): + raise WindowsW1PlanError("TrueNAS ISO binding must be a staged /mnt path") + bridge = _safe_binding(bridge, "bridge") + + zvol_name = f"{pool}/{name}system" + return { + "schema": "windows-w1-native-plan/v1", + "platform": "truenas", + "target": "26.0.0-BETA.3", + "portable_intent": portable_intent(profile), + "source_binding": TRUENAS_SOURCE, + "external_bindings": { + "install_media_path": iso_path, + "install_media_required_sha256": MEDIA_SHA256, + "network_attach": bridge, + "system_zvol": zvol_name, + }, + "vm_create": { + "name": name, + "description": "Agent Dispatch disposable Windows W1 fixture", + "vcpus": 1, + "cores": 2, + "threads": 1, + "memory": MEMORY_MIB, + "cpu_mode": "HOST-PASSTHROUGH", + "autostart": False, + "ensure_display_device": True, + "time": "LOCAL", + "bootloader": "UEFI", + "bootloader_ovmf": "OVMF_CODE_4M.secboot.fd", + "arch_type": "x86_64", + "machine_type": "pc-q35-6.2", + "trusted_platform_module": True, + "hyperv_enlightenments": True, + "enable_secure_boot": True, + "shutdown_timeout": 90, + }, + "device_templates": [ + { + "role": "install-media", + "vm_binding": "created_vm_id", + "order": 100, + "attributes": {"dtype": "CDROM", "path": iso_path}, + }, + { + "role": "system-disk", + "vm_binding": "created_vm_id", + "order": 200, + "attributes": { + "dtype": "DISK", + "path": None, + "type": "AHCI", + "create_zvol": True, + "zvol_name": zvol_name, + "zvol_volsize": SYSTEM_DISK_BYTES, + }, + }, + { + "role": "network", + "vm_binding": "created_vm_id", + "attributes": {"dtype": "NIC", "type": "E1000", "nic_attach": bridge}, + }, + ], + "post_install_transition": { + "action": "delete-owned-install-media-device", + "reason": "force restart/boot proof from the installed system disk without firmware boot ambiguity", + }, + "mutation_authorized": False, + } + + +def proxmox_plan( + profile: dict[str, Any], + *, + vmid: int, + storage: str, + iso_volume: str, + bridge: str, + name: str = "rdte-windows-w1", +) -> dict[str, Any]: + row = profile["proxmox"]["9.2-1"]["windows11"] + if ( + row.get("fixture") != FIXTURE_ID + or row.get("runtime_status") != "OPEN" + or row.get("source_status") != "CANDIDATE" + ): + raise WindowsW1PlanError("PVE 9.2-1 Windows W1 profile is not source-bound CANDIDATE/OPEN") + if not isinstance(vmid, int) or isinstance(vmid, bool) or vmid < 100: + raise WindowsW1PlanError("PVE vmid must be an integer >= 100") + storage = _safe_binding(storage, "storage") + if not re.fullmatch(r"[A-Za-z0-9_.-]+", storage): + raise WindowsW1PlanError("PVE storage binding contains unsupported characters") + iso_volume = _safe_binding(iso_volume, "iso-volume") + if ":" not in iso_volume: + raise WindowsW1PlanError("PVE ISO binding must be a storage volume id") + bridge = _safe_binding(bridge, "bridge") + if "," in bridge: + raise WindowsW1PlanError("PVE bridge binding must not contain commas") + name = _safe_binding(name, "name") + + return { + "schema": "windows-w1-native-plan/v1", + "platform": "proxmox", + "target": "9.2-1", + "portable_intent": portable_intent(profile), + "source_binding": PROXMOX_SOURCE, + "external_bindings": { + "install_media_volume": iso_volume, + "install_media_required_sha256": MEDIA_SHA256, + "vm_storage": storage, + "network_bridge": bridge, + }, + "create_path": "/nodes/{node}/qemu", + "create_fields": { + "vmid": vmid, + "name": name, + "memory": MEMORY_MIB, + "cores": 2, + "sockets": 1, + "cpu": "host", + "bios": "ovmf", + "machine": "q35", + "ostype": "win11", + "start": 0, + "net0": f"e1000,bridge={bridge}", + "sata0": f"{storage}:64", + "ide2": f"{iso_volume},media=cdrom", + "efidisk0": f"{storage}:1,efitype=4m,pre-enrolled-keys=1", + "tpmstate0": f"{storage}:1,version=v2.0", + "boot": "order=ide2;sata0", + "description": "Agent Dispatch disposable Windows W1 fixture", + }, + "post_install_transition": { + "method": "PUT", + "path": "/nodes/{node}/qemu/{vmid}/config", + "fields": {"delete": "ide2", "boot": "order=sata0"}, + "reason": "force restart/boot proof from the installed system disk without firmware boot ambiguity", + }, + "mutation_authorized": False, + } + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--profile", type=pathlib.Path, default=pathlib.Path("config/compute-guest-profiles.json")) + p.add_argument("--platform", choices=["truenas", "proxmox"], required=True) + p.add_argument("--storage", required=True) + p.add_argument("--media-ref", required=True) + p.add_argument("--network", required=True) + p.add_argument("--name") + p.add_argument("--vmid", type=int, default=9301) + p.add_argument("--out", type=pathlib.Path) + a = p.parse_args() + try: + profile = _profile(a.profile) + if a.platform == "truenas": + result = truenas_plan( + profile, + pool=a.storage, + iso_path=a.media_ref, + bridge=a.network, + name=a.name or "rdtewindowsw1", + ) + else: + result = proxmox_plan( + profile, + vmid=a.vmid, + storage=a.storage, + iso_volume=a.media_ref, + bridge=a.network, + name=a.name or "rdte-windows-w1", + ) + except (OSError, ValueError, KeyError, WindowsW1PlanError) as exc: + result = { + "schema": "windows-w1-native-plan/v1", + "classification": "PLAN_FAILURE", + "mutation_authorized": False, + "detail": f"{type(exc).__name__}: {exc}", + } + code = 2 + else: + result["classification"] = "SUPPORTED" + result["oracleSatisfied"] = True + code = 0 + payload = json.dumps(result, indent=2, sort_keys=True) + "\n" + if a.out: + a.out.write_text(payload, encoding="utf-8") + print(payload, end="") + return code + + +if __name__ == "__main__": + raise SystemExit(main()) From ef24d12ab060a648dc765f1c543de33e42564756 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:32:28 +0200 Subject: [PATCH 510/719] test: qualify portable Windows W1 native lowering --- tests/test_windows_w1_native_plan.py | 108 +++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tests/test_windows_w1_native_plan.py diff --git a/tests/test_windows_w1_native_plan.py b/tests/test_windows_w1_native_plan.py new file mode 100644 index 000000000..84944ffd7 --- /dev/null +++ b/tests/test_windows_w1_native_plan.py @@ -0,0 +1,108 @@ +import pathlib +import unittest + +from scripts.compute_guest_profile_contract import load, validate +from scripts.windows_w1_native_plan import ( + FIXTURE_ID, + MEDIA_SHA256, + MEMORY_MIB, + SYSTEM_DISK_BYTES, + VCPUS, + proxmox_plan, + truenas_plan, + WindowsW1PlanError, +) + + +class WindowsW1NativePlanTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.profile = load(pathlib.Path("config/compute-guest-profiles.json")) + validate(cls.profile) + + def test_portable_semantics_match_across_platforms(self): + tn = truenas_plan( + self.profile, + pool="rdtepool", + iso_path="/mnt/rdtepool/windows/windows11.iso", + bridge="br0", + ) + pve = proxmox_plan( + self.profile, + vmid=9301, + storage="local-lvm", + iso_volume="local:iso/windows11.iso", + bridge="vmbr0", + ) + self.assertEqual(tn["portable_intent"], pve["portable_intent"]) + req = tn["portable_intent"]["requirements"] + self.assertEqual(req["virtual_processors"], VCPUS) + self.assertEqual(req["memory_mib"], MEMORY_MIB) + self.assertEqual(req["system_disk_gib"], 64) + self.assertTrue(req["secure_boot"]) + self.assertEqual(req["tpm_version"], "2.0") + self.assertEqual(tn["portable_intent"]["fixture"], FIXTURE_ID) + self.assertEqual(tn["portable_intent"]["media"]["sha256"], MEDIA_SHA256) + + def test_truenas_lowering_preserves_windows_requirements(self): + plan = truenas_plan( + self.profile, + pool="rdtepool", + iso_path="/mnt/rdtepool/windows/windows11.iso", + bridge="br0", + ) + create = plan["vm_create"] + self.assertEqual(create["vcpus"] * create["cores"] * create["threads"], 2) + self.assertEqual(create["memory"], 4096) + self.assertEqual(create["cpu_mode"], "HOST-PASSTHROUGH") + self.assertEqual(create["bootloader"], "UEFI") + self.assertEqual(create["bootloader_ovmf"], "OVMF_CODE_4M.secboot.fd") + self.assertEqual(create["machine_type"], "pc-q35-6.2") + self.assertTrue(create["trusted_platform_module"]) + self.assertTrue(create["enable_secure_boot"]) + self.assertTrue(create["hyperv_enlightenments"]) + disk = next(x for x in plan["device_templates"] if x["role"] == "system-disk") + self.assertEqual(disk["attributes"]["type"], "AHCI") + self.assertEqual(disk["attributes"]["zvol_volsize"], SYSTEM_DISK_BYTES) + nic = next(x for x in plan["device_templates"] if x["role"] == "network") + self.assertEqual(nic["attributes"]["type"], "E1000") + self.assertFalse(plan["mutation_authorized"]) + + def test_proxmox_lowering_preserves_windows_requirements(self): + plan = proxmox_plan( + self.profile, + vmid=9301, + storage="local-lvm", + iso_volume="local:iso/windows11.iso", + bridge="vmbr0", + ) + create = plan["create_fields"] + self.assertEqual(create["memory"], 4096) + self.assertEqual(create["cores"] * create["sockets"], 2) + self.assertEqual(create["cpu"], "host") + self.assertEqual(create["bios"], "ovmf") + self.assertEqual(create["machine"], "q35") + self.assertEqual(create["ostype"], "win11") + self.assertEqual(create["sata0"], "local-lvm:64") + self.assertEqual(create["ide2"], "local:iso/windows11.iso,media=cdrom") + self.assertIn("pre-enrolled-keys=1", create["efidisk0"]) + self.assertIn("version=v2.0", create["tpmstate0"]) + self.assertEqual(create["net0"], "e1000,bridge=vmbr0") + self.assertEqual(create["boot"], "order=ide2;sata0") + self.assertFalse(plan["mutation_authorized"]) + + def test_external_binding_validation_fails_closed(self): + with self.assertRaises(WindowsW1PlanError): + truenas_plan(self.profile, pool="pool/dataset", iso_path="/mnt/pool/w.iso", bridge="br0") + with self.assertRaises(WindowsW1PlanError): + proxmox_plan( + self.profile, + vmid=9301, + storage="local-lvm", + iso_volume="not-a-volume-id", + bridge="vmbr0", + ) + + +if __name__ == "__main__": + unittest.main() From 2d3981f9e170c0658ac5fe8a63caa1cbe13df229 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:33:08 +0200 Subject: [PATCH 511/719] ci: qualify pure Windows W1 native planner --- .github/workflows/validate-compute-guest-profile-contract.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/validate-compute-guest-profile-contract.yml b/.github/workflows/validate-compute-guest-profile-contract.yml index dec68fbd4..3541aecbc 100644 --- a/.github/workflows/validate-compute-guest-profile-contract.yml +++ b/.github/workflows/validate-compute-guest-profile-contract.yml @@ -6,9 +6,11 @@ on: - "scripts/compute_guest_profile_contract.py" - "scripts/compute_guest_seed.py" - "scripts/truenas_vm_console_probe.py" + - "scripts/windows_w1_native_plan.py" - "tests/test_compute_guest_profile_contract.py" - "tests/test_compute_guest_seed.py" - "tests/test_truenas_vm_console_probe.py" + - "tests/test_windows_w1_native_plan.py" - ".github/workflows/validate-compute-guest-profile-contract.yml" workflow_dispatch: permissions: @@ -18,5 +20,5 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 - - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py tests/test_truenas_vm_console_probe.py tests/test_truenas_v2_console_probe.py tests/test_truenas_compute_vm_v1_probe.py + - run: python3 -m unittest -v tests/test_compute_guest_profile_contract.py tests/test_compute_guest_seed.py tests/test_truenas_vm_console_probe.py tests/test_truenas_v2_console_probe.py tests/test_truenas_compute_vm_v1_probe.py tests/test_windows_w1_native_plan.py - run: python3 scripts/compute_guest_profile_contract.py From 9eb1ff0a62e12d9c044612ca441e54ddb1e66329 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:48:16 +0200 Subject: [PATCH 512/719] RDTE: lower TrueNAS V1 boot media through owned ZVOL DISK --- scripts/truenas_compute_vm_v1_probe.py | 96 +++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/scripts/truenas_compute_vm_v1_probe.py b/scripts/truenas_compute_vm_v1_probe.py index ffdfbf42d..3b46766e9 100644 --- a/scripts/truenas_compute_vm_v1_probe.py +++ b/scripts/truenas_compute_vm_v1_probe.py @@ -36,22 +36,28 @@ def stage_paths(pool: str, stem: str = "rdtev1") -> dict[str, str]: raise V1ProbeError("pool name violates V1 staging contract") dataset = f"{pool}/{stem}stage" mount = f"/mnt/{dataset}" + boot_zvol = f"{pool}/{stem}boot" return { "dataset": dataset, "raw_image": f"{mount}/cirros-0.6.3-x86_64.raw", "seed_iso": f"{mount}/seed.iso", + "boot_zvol": boot_zvol, + "boot_zvol_path": f"/dev/zvol/{boot_zvol}", } -def raw_boot_device(vm_id: int, path: str) -> dict[str, Any]: +def zvol_boot_device(vm_id: int, zvol_name: str, size_bytes: int = 1024 * 1024 * 1024) -> dict[str, Any]: + if not zvol_name or "/" not in zvol_name: + raise V1ProbeError("V1 boot ZVOL name must include pool prefix") return { "vm": vm_id, "attributes": { - "dtype": "RAW", - "path": path, + "dtype": "DISK", + "path": None, "type": "VIRTIO", - "exists": True, - "boot": True, + "create_zvol": True, + "zvol_name": zvol_name, + "zvol_volsize": size_bytes, }, "order": 100, } @@ -140,9 +146,14 @@ def plan(pool: str, name: str, nonce: str) -> dict[str, Any]: "staging": paths, "vm_create": native_vm_create_payload(name), "devices": [ - raw_boot_device(0, paths["raw_image"]), + zvol_boot_device(0, paths["boot_zvol"]), seed_cdrom_device(0, paths["seed_iso"]), ], + "media_lowering": { + "source": paths["raw_image"], + "destination": paths["boot_zvol_path"], + "method": "vm.device.convert", + }, "oracle": { "surface": "/websocket/shell", "nonce": f"AGENT_DISPATCH_V1_NONCE={nonce}", @@ -172,11 +183,17 @@ def run_apply(a: argparse.Namespace) -> dict[str, Any]: "seed_iso_sha256": seed_sha, }, "staging": paths, - "cleanup": {"attempted": False, "vm_absent": False, "staging_dataset_absent": False}, + "cleanup": { + "attempted": False, + "vm_absent": False, + "boot_zvol_absent": False, + "staging_dataset_absent": False, + }, "claim_boundary": "V1 Linux guest boot + exact external nonce only; nested KVM, Firecracker and Windows remain separate", } ws: WebSocket | None = None vm_id: int | None = None + boot_device_id: int | None = None stage_owned = False try: ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) @@ -199,6 +216,8 @@ def run_apply(a: argparse.Namespace) -> dict[str, Any]: raise V1ProbeError(f"preexisting VM {a.name!r} blocks ownership-safe V1") if ddp_call(ws, "pre-stage", "pool.dataset.query", [[["id", "=", paths["dataset"]]]]): raise V1ProbeError(f"preexisting staging dataset {paths['dataset']!r} blocks ownership-safe V1") + if ddp_call(ws, "pre-boot-zvol", "pool.dataset.query", [[["id", "=", paths["boot_zvol"]]]]): + raise V1ProbeError(f"preexisting boot ZVOL {paths['boot_zvol']!r} blocks ownership-safe V1") ddp_call(ws, "mk-stage", "pool.dataset.create", [{ "name": paths["dataset"], "type": "FILESYSTEM", "exec": "OFF", "share_type": "GENERIC", @@ -218,11 +237,31 @@ def run_apply(a: argparse.Namespace) -> dict[str, Any]: if not isinstance(vm, dict) or not isinstance(vm.get("id"), int): raise V1ProbeError(f"vm.create returned invalid VM: {vm!r}") vm_id = vm["id"] - raw = raw_boot_device(vm_id, paths["raw_image"]) + boot = zvol_boot_device(vm_id, paths["boot_zvol"]) seed = seed_cdrom_device(vm_id, paths["seed_iso"]) - ddp_call(ws, "raw-device", "vm.device.create", [raw]) + created_boot = ddp_call(ws, "boot-device", "vm.device.create", [boot]) + if not isinstance(created_boot, dict) or not isinstance(created_boot.get("id"), int): + raise V1ProbeError(f"boot DISK/ZVOL create returned invalid device: {created_boot!r}") + boot_device_id = created_boot["id"] + if created_boot.get("attributes", {}).get("path") != paths["boot_zvol_path"]: + raise V1ProbeError( + f"boot DISK/ZVOL path mismatch: {created_boot.get('attributes', {}).get('path')!r}" + ) + if not ddp_call(ws, "observe-boot-zvol", "pool.dataset.query", [[["id", "=", paths["boot_zvol"]]]]): + raise V1ProbeError("owned boot ZVOL was not independently observable after device create") + convert_job = ddp_call(ws, "convert-boot", "vm.device.convert", [{ + "source": paths["raw_image"], + "destination": paths["boot_zvol_path"], + }]) + wait_job(ws, convert_job, a.job_timeout, "convert-boot") ddp_call(ws, "seed-device", "vm.device.create", [seed]) - receipt["desired_devices"] = [raw, seed] + receipt["desired_devices"] = [boot, seed] + receipt["media_lowering"] = { + "source": paths["raw_image"], + "destination": paths["boot_zvol_path"], + "method": "vm.device.convert", + "classification": "SUPPORTED", + } ddp_call(ws, "vm-start", "vm.start", [vm_id, {"overcommit": False}]) console = observe_console_nonce( @@ -250,6 +289,20 @@ def run_apply(a: argparse.Namespace) -> dict[str, Any]: ddp_call(ws, "cleanup-stop", "vm.stop", [vm_id, {"force": True, "force_after_timeout": True}]) except Exception: pass + if boot_device_id is not None: + try: + boot_rows = ddp_call( + ws, "cleanup-query-boot-device", "vm.device.query", [[["id", "=", boot_device_id]]] + ) + if boot_rows: + ddp_call( + ws, + "cleanup-delete-boot-device", + "vm.device.delete", + [boot_device_id, {"force": True, "zvol": True, "raw_file": False}], + ) + except Exception: + pass try: ddp_call(ws, "cleanup-delete-vm", "vm.delete", [vm_id, {"zvols": False, "force": True}]) except Exception: @@ -262,6 +315,29 @@ def run_apply(a: argparse.Namespace) -> dict[str, Any]: pass else: receipt["cleanup"]["vm_absent"] = True + + try: + zvol_rows = ddp_call( + ws, "cleanup-query-boot-zvol", "pool.dataset.query", [[["id", "=", paths["boot_zvol"]]]] + ) + if zvol_rows: + ddp_call( + ws, + "cleanup-delete-boot-zvol", + "pool.dataset.delete", + [paths["boot_zvol"], {"recursive": True, "force": True}], + ) + receipt["cleanup"]["boot_zvol_absent"] = not bool( + ddp_call( + ws, + "cleanup-confirm-boot-zvol", + "pool.dataset.query", + [[["id", "=", paths["boot_zvol"]]]], + ) + ) + except Exception: + pass + if stage_owned: try: ddp_call(ws, "cleanup-stage", "pool.dataset.delete", [ From cbf38576bfa1cad0af98cffd221512565d80eaf5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:48:46 +0200 Subject: [PATCH 513/719] test: bind TrueNAS V1 ZVOL boot lowering --- tests/test_truenas_compute_vm_v1_probe.py | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/tests/test_truenas_compute_vm_v1_probe.py b/tests/test_truenas_compute_vm_v1_probe.py index eed892d06..c993228f8 100644 --- a/tests/test_truenas_compute_vm_v1_probe.py +++ b/tests/test_truenas_compute_vm_v1_probe.py @@ -4,7 +4,7 @@ PINNED_CIRROS_SHA256, multipart_upload_body, plan, - raw_boot_device, + zvol_boot_device, seed_cdrom_device, stage_paths, ) @@ -22,20 +22,31 @@ def test_stage_paths_are_owned_under_pool(self): self.assertEqual(paths["dataset"], "rdtepool/rdtev1stage") self.assertTrue(paths["raw_image"].startswith("/mnt/rdtepool/rdtev1stage/")) self.assertTrue(paths["seed_iso"].endswith("/seed.iso")) + self.assertEqual(paths["boot_zvol"], "rdtepool/rdtev1boot") + self.assertEqual(paths["boot_zvol_path"], "/dev/zvol/rdtepool/rdtev1boot") - def test_devices_use_public_raw_and_cdrom_shapes(self): - raw = raw_boot_device(7, "/mnt/rdtepool/rdtev1stage/cirros.raw") - self.assertEqual(raw["attributes"]["dtype"], "RAW") - self.assertTrue(raw["attributes"]["exists"]) - self.assertTrue(raw["attributes"]["boot"]) - self.assertEqual(raw["attributes"]["type"], "VIRTIO") + def test_devices_use_owned_zvol_disk_and_public_cdrom_shapes(self): + boot = zvol_boot_device(7, "rdtepool/rdtev1boot") + self.assertEqual(boot["attributes"]["dtype"], "DISK") + self.assertIsNone(boot["attributes"]["path"]) + self.assertTrue(boot["attributes"]["create_zvol"]) + self.assertEqual(boot["attributes"]["zvol_name"], "rdtepool/rdtev1boot") + self.assertEqual(boot["attributes"]["zvol_volsize"], 1024 * 1024 * 1024) + self.assertEqual(boot["attributes"]["type"], "VIRTIO") + self.assertEqual(boot["order"], 100) cd = seed_cdrom_device(7, "/mnt/rdtepool/rdtev1stage/seed.iso") self.assertEqual(cd["attributes"], {"dtype": "CDROM", "path": "/mnt/rdtepool/rdtev1stage/seed.iso"}) + self.assertEqual(cd["order"], 1000) def test_plan_keeps_v1_claim_narrow(self): p = plan("rdtepool", "rdtecomputevmv1", "rep001nonceABCDEF12") self.assertEqual(p["schema"], "truenas-compute-vm-v1-plan/v1") self.assertEqual(p["oracle"]["surface"], "/websocket/shell") + self.assertEqual(p["devices"][0]["attributes"]["dtype"], "DISK") + self.assertEqual(p["media_lowering"]["method"], "vm.device.convert") + self.assertEqual(p["media_lowering"]["source"], p["staging"]["raw_image"]) + self.assertEqual(p["media_lowering"]["destination"], p["staging"]["boot_zvol_path"]) + self.assertNotIn('"dtype": "RAW"', str(p)) self.assertNotIn("firecracker", str(p).lower()) self.assertNotIn("nested-kvm", str(p).lower()) From 308820f019c8c243010abde8b8495f655e678497 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:50:23 +0200 Subject: [PATCH 514/719] CI: trigger guest contract on TrueNAS V1 probe changes --- .github/workflows/validate-compute-guest-profile-contract.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/validate-compute-guest-profile-contract.yml b/.github/workflows/validate-compute-guest-profile-contract.yml index 3541aecbc..f26804885 100644 --- a/.github/workflows/validate-compute-guest-profile-contract.yml +++ b/.github/workflows/validate-compute-guest-profile-contract.yml @@ -6,10 +6,12 @@ on: - "scripts/compute_guest_profile_contract.py" - "scripts/compute_guest_seed.py" - "scripts/truenas_vm_console_probe.py" + - "scripts/truenas_compute_vm_v1_probe.py" - "scripts/windows_w1_native_plan.py" - "tests/test_compute_guest_profile_contract.py" - "tests/test_compute_guest_seed.py" - "tests/test_truenas_vm_console_probe.py" + - "tests/test_truenas_compute_vm_v1_probe.py" - "tests/test_windows_w1_native_plan.py" - ".github/workflows/validate-compute-guest-profile-contract.yml" workflow_dispatch: From 3e1851984f73b31688485feb664a60793ffa1cee Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:58:27 +0200 Subject: [PATCH 515/719] request: TrueNAS VM V1 rep005 ZVOL DISK causal rep --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index f6722f510..f84af92aa 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-vm-v1-004-cirros-json-seed", + "request_id": "tn-26-beta3-t3-compute-vm-v1-005-zvol-disk", "version": "26.0.0-BETA.3", "rung": "t3", "compute_fixture": "vm-v1", "authority_issue": 396, "consumer_authority_issue": 426, - "purpose": "Re-run the exact TrueNAS 26.0.0-BETA.3 Linux VM V1 fixture after rep003 and the plain-QEMU falsifier isolated the remaining failure to CirrOS 0.6.3 NoCloud meta-data encoding. Preserve the pinned CirrOS 0.6.3 image digest, supported filesystem.put staging, RAW boot disk, CDROM cidata seed, vm.start, supported /websocket/shell exact-nonce oracle, repaired 180-second overall guest deadline, and zero-residue cleanup. The only guest-fixture semantic delta is merged commit 59677931f942f98f3701ecafb73515a30483a9c6: emit CirrOS NoCloud meta-data as JSON for json2fstree. Cheap QEMU TCG smoke run 37465128038 independently proved the repaired exact seed under q35/UEFI and BIOS with uefi_nonce=true and bios_nonce=true; artifact 11414348724 digest sha256:202f5bbf787fcdd19b174d49d5d8b0ee4428a575cc9d3d6231fe4c6cd811beb8. Nested KVM, Firecracker and Windows remain outside this rep." + "purpose": "Exercise one bounded TrueNAS 26.0.0-BETA.3 Linux VM V1 rep after cheap falsification isolated generic libvirt plus file-backed RAW as reproducing the GRUB-to-silence failure while plain QEMU UEFI/q35 with the same repaired CirrOS 0.6.3 image and JSON cidata seed reaches userspace and emits the exact nonce. Preserve the pinned CirrOS source, repaired JSON NoCloud seed, native VM create payload, supported /websocket/shell nonce oracle, overall guest deadline, and zero-residue cleanup. The sole material native-lowering delta is merged commit 1256c6dcdde450f2a5d932567b43fe3ff435f3af: stage the same RAW image, convert it through public job-backed vm.device.convert into an experiment-owned ZVOL, attach that ZVOL as a DISK VIRTIO boot device, independently observe it, and prove boot-ZVOL absence in cleanup. Machine type was independently ruled out by cheap run 37481329210. Generic libvirt RAW+PTY reproduced the failure in run 37483398291. Nested KVM, Firecracker, Windows, Hyper-V and WinBot-provider acceptance remain outside this rep." } From 82b6923449b8a7799944d8a9d0c2a70cd9cfa11c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:17:54 +0200 Subject: [PATCH 516/719] ci: join compute RDTE to repo-wide heavyweight queue --- .github/workflows/gha-kvm-system-rdte.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index b5077d3f1..a04e10917 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -338,7 +338,8 @@ jobs: proxmox-9-2: concurrency: - group: gha-kvm-system-rdte-heavy + group: agent-dispatch-heavyweight-rdte + queue: max cancel-in-progress: false needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.proxmox == 'true' @@ -392,7 +393,8 @@ jobs: truenas-26-beta3: concurrency: - group: gha-kvm-system-rdte-heavy + group: agent-dispatch-heavyweight-rdte + queue: max cancel-in-progress: false needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] if: >- From 9a1e9e6f51a0e1c267784dbc0801137145f74c99 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:22:38 +0200 Subject: [PATCH 517/719] test: enforce repo-wide heavyweight queue contract --- tests/test_gha_kvm_system_rdte.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index cf3fa2eac..006e20533 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -448,11 +448,13 @@ def test_only_heavy_jobs_share_non_cancelling_concurrency_group(self): expected = ( f" {job}:\n" " concurrency:\n" - " group: gha-kvm-system-rdte-heavy\n" + " group: agent-dispatch-heavyweight-rdte\n" + " queue: max\n" " cancel-in-progress: false\n" ) self.assertIn(expected, text) - self.assertEqual(text.count("group: gha-kvm-system-rdte-heavy"), 2) + self.assertEqual(text.count("group: agent-dispatch-heavyweight-rdte"), 2) + self.assertEqual(text.count("queue: max"), 2) self.assertEqual(text.count("cancel-in-progress: false"), 2) self.assertNotIn("cancel-in-progress: true", text) From 8a4a542b960af796ebb25d5d5c2aa26ee2b8df00 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:09:13 +0200 Subject: [PATCH 518/719] RDTE: qualify Foundry publication-bound exporter head --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 0aabc0d7e..4b57775a4 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -254,9 +254,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@428dc047219ed30215c9369ee290d7390e258243 with: - foundry_ref: 1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 + foundry_ref: 428dc047219ed30215c9369ee290d7390e258243 target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -530,7 +530,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9" + foundry_commit="428dc047219ed30215c9369ee290d7390e258243" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 5b932d6850b28377b06e2805c54011189bfb7951 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:09:21 +0200 Subject: [PATCH 519/719] RDTE: qualify Foundry publication-bound exporter head --- .github/workflows/prep-foliorelay-t6-consumer.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 429e60d2a..0baa65d20 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@428dc047219ed30215c9369ee290d7390e258243 with: - foundry_ref: 1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9 + foundry_ref: 428dc047219ed30215c9369ee290d7390e258243 target_version: 25.10.7 permissions: contents: read @@ -126,7 +126,7 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9" and + .foundry_ref == "428dc047219ed30215c9369ee290d7390e258243" and .candidate.truenas_version == "25.10.7" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and From 478ca253e41f7ee26f08849e3a3ef07458cfe3f6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:09:28 +0200 Subject: [PATCH 520/719] RDTE: qualify Foundry publication-bound exporter head --- tests/test_truenas_t6_foliorelay_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 929168d24..50b060f3f 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -219,10 +219,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9", workflow) + self.assertIn("export-foliorelay-t6-control.yml@428dc047219ed30215c9369ee290d7390e258243", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="1ae18cde62d8ff4bb68f22146b27fd72dcc7e1b9"', workflow) + self.assertIn('foundry_commit="428dc047219ed30215c9369ee290d7390e258243"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From b458fae5d4c30b7c0e855f010a029b55ead68fb6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:12:33 +0200 Subject: [PATCH 521/719] RDTE: pin merged publication-bound Foundry authority --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 4b57775a4..06d0c96ea 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -254,9 +254,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@428dc047219ed30215c9369ee290d7390e258243 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e44bc2271de4c2065c68a8f7db53034fff36b4c5 with: - foundry_ref: 428dc047219ed30215c9369ee290d7390e258243 + foundry_ref: e44bc2271de4c2065c68a8f7db53034fff36b4c5 target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -530,7 +530,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="428dc047219ed30215c9369ee290d7390e258243" + foundry_commit="e44bc2271de4c2065c68a8f7db53034fff36b4c5" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From c2b3f274dd5eea5ea7f7cf3ca6532a6b6df5c5db Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:12:39 +0200 Subject: [PATCH 522/719] RDTE: pin merged publication-bound Foundry authority --- .github/workflows/prep-foliorelay-t6-consumer.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 0baa65d20..ecb293ba9 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@428dc047219ed30215c9369ee290d7390e258243 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e44bc2271de4c2065c68a8f7db53034fff36b4c5 with: - foundry_ref: 428dc047219ed30215c9369ee290d7390e258243 + foundry_ref: e44bc2271de4c2065c68a8f7db53034fff36b4c5 target_version: 25.10.7 permissions: contents: read @@ -126,7 +126,7 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "428dc047219ed30215c9369ee290d7390e258243" and + .foundry_ref == "e44bc2271de4c2065c68a8f7db53034fff36b4c5" and .candidate.truenas_version == "25.10.7" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and From 1a54c95288806550797b0a41b05720a65a0e5a3f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:12:45 +0200 Subject: [PATCH 523/719] RDTE: pin merged publication-bound Foundry authority --- tests/test_truenas_t6_foliorelay_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 50b060f3f..004518d73 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -219,10 +219,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@428dc047219ed30215c9369ee290d7390e258243", workflow) + self.assertIn("export-foliorelay-t6-control.yml@e44bc2271de4c2065c68a8f7db53034fff36b4c5", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="428dc047219ed30215c9369ee290d7390e258243"', workflow) + self.assertIn('foundry_commit="e44bc2271de4c2065c68a8f7db53034fff36b4c5"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From e0f293770418b9415cacc78d8ed58198963c6ffb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:15:06 +0200 Subject: [PATCH 524/719] RDTE: request FolioRelay 25.10.7 Avahi F0-F5 rep --- config/truenas-rdte-run-request.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index f84af92aa..37b49f6d3 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t3-compute-vm-v1-005-zvol-disk", - "version": "26.0.0-BETA.3", - "rung": "t3", - "compute_fixture": "vm-v1", - "authority_issue": 396, - "consumer_authority_issue": 426, - "purpose": "Exercise one bounded TrueNAS 26.0.0-BETA.3 Linux VM V1 rep after cheap falsification isolated generic libvirt plus file-backed RAW as reproducing the GRUB-to-silence failure while plain QEMU UEFI/q35 with the same repaired CirrOS 0.6.3 image and JSON cidata seed reaches userspace and emits the exact nonce. Preserve the pinned CirrOS source, repaired JSON NoCloud seed, native VM create payload, supported /websocket/shell nonce oracle, overall guest deadline, and zero-residue cleanup. The sole material native-lowering delta is merged commit 1256c6dcdde450f2a5d932567b43fe3ff435f3af: stage the same RAW image, convert it through public job-backed vm.device.convert into an experiment-owned ZVOL, attach that ZVOL as a DISK VIRTIO boot device, independently observe it, and prove boot-ZVOL absence in cleanup. Machine type was independently ruled out by cheap run 37481329210. Generic libvirt RAW+PTY reproduced the failure in run 37483398291. Nested KVM, Firecracker, Windows, Hyper-V and WinBot-provider acceptance remain outside this rep." + "request_id": "tn-25-10-7-t6-foliorelay-004-avahi-f0-f5", + "version": "25.10.7", + "rung": "t6", + "product": "foliorelay", + "authority_issue": 410, + "consumer_authority_issue": 273, + "purpose": "Run the exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 using the earned cooperative-Avahi design after row 001/diagnostic row 002 localized the prior failure to competing UDP/5353 ownership. Require merged Foundry authority e44bc2271de4c2065c68a8f7db53034fff36b4c5, immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814 from signed publication run 37457117549/artifact 11410550658, exact 25.x Avahi command/user/read-only system-bus coupling, independent same-instance DNS-SD observation, PDF/URF exact-source preservation, F4 second-plan NOOP with WAIT/fail-closed policy, F5 retain-data delete/reinstall persistence, and final zero residue. No support is inherited from BETA.3 or generic T0-T5. Acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." } From 8b996e2aa277b9a58d6b4d3ad6378018ee786a91 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:39:16 +0200 Subject: [PATCH 525/719] RDTE: allow discovery group to traverse control fixture root --- scripts/truenas_middleware_foliorelay_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index c3f46a7f6..1f096eaaf 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -560,7 +560,7 @@ def wait_state(name,state): if not isinstance(ds,dict) or ds.get("id")!=DATASET: raise RuntimeError("dataset identity mismatch") dataset_owned=True dirs=[ - (ROOT+"/control","700",10001),(ROOT+"/artifacts","700",10001),(ROOT+"/cups-state","755",10001),(ROOT+"/cups-spool","755",10001), + (ROOT+"/control","710",10001),(ROOT+"/artifacts","700",10001),(ROOT+"/cups-state","755",10001),(ROOT+"/cups-spool","755",10001), (ROOT+"/secrets","700",10001),(OBSERVER_DIR,"755",0), ] for path,mode,uid in dirs: From 2724d536ea1e63f4545659cd6ca7c40c4b584fed Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:39:23 +0200 Subject: [PATCH 526/719] test: lock least-privilege control-root traversal for discovery --- tests/test_truenas_t6_foliorelay_control.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 004518d73..5e72f2511 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -87,6 +87,11 @@ def test_discovery_materialization_is_exact_per_target(self): with self.assertRaisesRegex(RuntimeError,"must not couple to host D-Bus"): MOD.validate_discovery_materialization(direct_dbus,"26.0.0-BETA.3") + def test_control_fixture_parent_is_group_traversable_without_group_listing(self): + probe=(ROOT/"scripts"/"truenas_middleware_foliorelay_t6_probe.py").read_text(encoding="utf-8") + self.assertIn('(ROOT+"/control","710",10001)', probe) + self.assertNotIn('(ROOT+"/control","700",10001)', probe) + def test_f4_reconciliation_policy_and_f5_retention_contract(self): exact={"services":{"control":{"image":"sha256:exact"}}} drift={"services":{"control":{"image":"sha256:drift"}}} From 3b0da922da427671438282478f0362676b29d1b1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:40:49 +0200 Subject: [PATCH 527/719] RDTE: request 25.10.7 FolioRelay control-traverse rep 005 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 37b49f6d3..e9669f243 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-foliorelay-004-avahi-f0-f5", + "request_id": "tn-25-10-7-t6-foliorelay-005-control-traverse", "version": "25.10.7", "rung": "t6", "product": "foliorelay", "authority_issue": 410, "consumer_authority_issue": 273, - "purpose": "Run the exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 using the earned cooperative-Avahi design after row 001/diagnostic row 002 localized the prior failure to competing UDP/5353 ownership. Require merged Foundry authority e44bc2271de4c2065c68a8f7db53034fff36b4c5, immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814 from signed publication run 37457117549/artifact 11410550658, exact 25.x Avahi command/user/read-only system-bus coupling, independent same-instance DNS-SD observation, PDF/URF exact-source preservation, F4 second-plan NOOP with WAIT/fail-closed policy, F5 retain-data delete/reinstall persistence, and final zero residue. No support is inherited from BETA.3 or generic T0-T5. Acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." + "purpose": "Re-run the exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 after rep 004 retained ORACLE_FAILURE evidence showing discovery uid 65534/gid 10001 could not traverse the experiment-owned control bind root because Agent Dispatch provisioned it mode 0700. Preserve merged Foundry authority e44bc2271de4c2065c68a8f7db53034fff36b4c5, immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814, cooperative Avahi command/user/read-only system-bus coupling, same-instance DNS-SD oracle, PDF/URF exact-source preservation, F4 reconciliation semantics, F5 retain-data delete/reinstall persistence, and zero residue. The sole causal runtime delta is the experiment-owned control parent mode 0700->0710, granting gid 10001 traverse only while retaining all per-file permissions. Acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." } From 1902ed529e9085f88bbd653ec5691053d4286b72 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:52:40 +0200 Subject: [PATCH 528/719] RDTE: materialize FolioRelay host paths from Foundry control --- .../truenas_middleware_foliorelay_t6_probe.py | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 1f096eaaf..d90de8a73 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -107,6 +107,42 @@ def validate_discovery_materialization(services: dict, target_version: str): return "direct" raise RuntimeError(f"unsupported FolioRelay target version {target_version!r}") +def validate_host_path_requirements(control: dict, target_version: str): + runtime=control.get("runtime") or {} + requirements=runtime.get("host_path_requirements") + if not isinstance(requirements,list): + raise RuntimeError("Foundry control missing host_path_requirements") + expected={ + ROOT+"/control":("directory","0710" if target_version in AVAHI_TARGETS else "0700"), + ROOT+"/artifacts":("directory","0700"), + ROOT+"/cups-state":("directory","0755"), + ROOT+"/cups-spool":("directory","0755"), + ROOT+"/secrets":("directory","0700"), + TOKEN_PATH:("file","0400"), + } + by_path={} + for item in requirements: + if not isinstance(item,dict): + raise RuntimeError("host_path_requirements entries must be objects") + path=item.get("path") + if not isinstance(path,str) or path in by_path: + raise RuntimeError("host_path_requirements path missing or duplicated") + by_path[path]=item + if set(by_path)!=set(expected): + raise RuntimeError("Foundry host path set drifted") + for path,(kind,mode) in expected.items(): + item=by_path[path] + if item.get("kind")!=kind or item.get("mode")!=mode: + raise RuntimeError(f"Foundry host path kind/mode drifted: {path}") + if item.get("uid")!=10001 or item.get("gid")!=10001: + raise RuntimeError(f"Foundry host path ownership drifted: {path}") + return [by_path[item["path"]] for item in requirements] + +def middleware_mode(mode: str) -> str: + if not isinstance(mode,str) or not re.fullmatch(r"0?[0-7]{3}",mode): + raise RuntimeError(f"invalid host path mode {mode!r}") + return mode[-3:] + def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): control = load_json(root / "control.json") compose = load_json(root / "compose.json") @@ -145,6 +181,7 @@ def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): } if not expected.issubset(required): raise RuntimeError("required FolioRelay oracle contract drifted") + validate_host_path_requirements(control,target_version) return control, compose def multipart_upload(host, port, tls, username, password, remote_path, content, mode, timeout): @@ -559,17 +596,23 @@ def wait_state(name,state): ds=call("pool.dataset.create",[{"name":DATASET,"type":"FILESYSTEM","share_type":"GENERIC","comments":"SemperSupra disposable FolioRelay T6 fixture"}]) if not isinstance(ds,dict) or ds.get("id")!=DATASET: raise RuntimeError("dataset identity mismatch") dataset_owned=True - dirs=[ - (ROOT+"/control","710",10001),(ROOT+"/artifacts","700",10001),(ROOT+"/cups-state","755",10001),(ROOT+"/cups-spool","755",10001), - (ROOT+"/secrets","700",10001),(OBSERVER_DIR,"755",0), - ] - for path,mode,uid in dirs: + host_requirements=validate_host_path_requirements(control,a.target_version) + for item in host_requirements: + if item["kind"]!="directory": + continue + path=item["path"]; mode=middleware_mode(item["mode"]); uid=item["uid"]; gid=item["gid"] d=call("filesystem.mkdir",[{"path":path,"options":{"mode":mode,"raise_chmod_error":True}}]) if not isinstance(d,dict) or d.get("path")!=path: raise RuntimeError(f"mkdir failed: {path}") - j=call("filesystem.setperm",[{"path":path,"uid":uid,"gid":uid,"mode":mode,"options":{"stripacl":True,"recursive":False,"traverse":False}}]) + j=call("filesystem.setperm",[{"path":path,"uid":uid,"gid":gid,"mode":mode,"options":{"stripacl":True,"recursive":False,"traverse":False}}]) if isinstance(j,int): wait_job(j,f"setperm {path}") - tj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,TOKEN_PATH,TOKEN,0o400,a.timeout); wait_job(tj,"token upload") - j=call("filesystem.setperm",[{"path":TOKEN_PATH,"uid":10001,"gid":10001,"mode":"400","options":{"stripacl":True,"recursive":False,"traverse":False}}]) + d=call("filesystem.mkdir",[{"path":OBSERVER_DIR,"options":{"mode":"755","raise_chmod_error":True}}]) + if not isinstance(d,dict) or d.get("path")!=OBSERVER_DIR: raise RuntimeError(f"mkdir failed: {OBSERVER_DIR}") + j=call("filesystem.setperm",[{"path":OBSERVER_DIR,"uid":0,"gid":0,"mode":"755","options":{"stripacl":True,"recursive":False,"traverse":False}}]) + if isinstance(j,int): wait_job(j,f"setperm {OBSERVER_DIR}") + token_req=next(item for item in host_requirements if item["path"]==TOKEN_PATH) + token_mode=middleware_mode(token_req["mode"]) + tj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,TOKEN_PATH,TOKEN,int(token_req["mode"],8),a.timeout); wait_job(tj,"token upload") + j=call("filesystem.setperm",[{"path":TOKEN_PATH,"uid":token_req["uid"],"gid":token_req["gid"],"mode":token_mode,"options":{"stripacl":True,"recursive":False,"traverse":False}}]) if isinstance(j,int): wait_job(j,"token setperm") create=call("app.create",[{"app_name":EXPECTED_APP_NAME,"custom_app":True,"custom_compose_config":compose}]) if not isinstance(create,int): raise RuntimeError("app.create did not return job") From 371ddb15853b17cb011097039afc3aa932539a5e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:53:16 +0200 Subject: [PATCH 529/719] RDTE: qualify Foundry host-path contract authority --- .github/workflows/prep-foliorelay-t6-consumer.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index ecb293ba9..97f26f184 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e44bc2271de4c2065c68a8f7db53034fff36b4c5 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@c969b815faeecdf044ad1edf327916c90c9a9c06 with: - foundry_ref: e44bc2271de4c2065c68a8f7db53034fff36b4c5 + foundry_ref: c969b815faeecdf044ad1edf327916c90c9a9c06 target_version: 25.10.7 permissions: contents: read @@ -126,7 +126,7 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "e44bc2271de4c2065c68a8f7db53034fff36b4c5" and + .foundry_ref == "c969b815faeecdf044ad1edf327916c90c9a9c06" and .candidate.truenas_version == "25.10.7" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and From 9914153c2728b00e30a82af1cbfe745a9d60f86d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:53:26 +0200 Subject: [PATCH 530/719] RDTE: qualify Foundry host-path contract authority --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 42738ff2d..3eba91e48 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -254,9 +254,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@e44bc2271de4c2065c68a8f7db53034fff36b4c5 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@c969b815faeecdf044ad1edf327916c90c9a9c06 with: - foundry_ref: e44bc2271de4c2065c68a8f7db53034fff36b4c5 + foundry_ref: c969b815faeecdf044ad1edf327916c90c9a9c06 target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -532,7 +532,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="e44bc2271de4c2065c68a8f7db53034fff36b4c5" + foundry_commit="c969b815faeecdf044ad1edf327916c90c9a9c06" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 3eee9978c3934af7f6a680a73b350c2ff9668093 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 22:53:49 +0200 Subject: [PATCH 531/719] test: consume Foundry host-path permission contract --- tests/test_truenas_t6_foliorelay_control.py | 34 +++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 5e72f2511..757d73112 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -87,10 +87,32 @@ def test_discovery_materialization_is_exact_per_target(self): with self.assertRaisesRegex(RuntimeError,"must not couple to host D-Bus"): MOD.validate_discovery_materialization(direct_dbus,"26.0.0-BETA.3") - def test_control_fixture_parent_is_group_traversable_without_group_listing(self): - probe=(ROOT/"scripts"/"truenas_middleware_foliorelay_t6_probe.py").read_text(encoding="utf-8") - self.assertIn('(ROOT+"/control","710",10001)', probe) - self.assertNotIn('(ROOT+"/control","700",10001)', probe) + def test_host_path_contract_drives_least_privilege_materialization(self): + common=[ + {"path":MOD.ROOT+"/artifacts","kind":"directory","uid":10001,"gid":10001,"mode":"0700"}, + {"path":MOD.ROOT+"/cups-state","kind":"directory","uid":10001,"gid":10001,"mode":"0755"}, + {"path":MOD.ROOT+"/cups-spool","kind":"directory","uid":10001,"gid":10001,"mode":"0755"}, + {"path":MOD.ROOT+"/secrets","kind":"directory","uid":10001,"gid":10001,"mode":"0700"}, + {"path":MOD.TOKEN_PATH,"kind":"file","uid":10001,"gid":10001,"mode":"0400"}, + ] + avahi={"runtime":{"host_path_requirements":[ + {"path":MOD.ROOT+"/control","kind":"directory","uid":10001,"gid":10001,"mode":"0710"}, + *common, + ]}} + direct={"runtime":{"host_path_requirements":[ + {"path":MOD.ROOT+"/control","kind":"directory","uid":10001,"gid":10001,"mode":"0700"}, + *common, + ]}} + got=MOD.validate_host_path_requirements(avahi,"25.10.7") + self.assertEqual(next(x for x in got if x["path"]==MOD.ROOT+"/control")["mode"],"0710") + self.assertEqual(MOD.middleware_mode("0710"),"710") + self.assertEqual(MOD.middleware_mode("0400"),"400") + self.assertEqual(next(x for x in MOD.validate_host_path_requirements(direct,"26.0.0-BETA.3") if x["path"]==MOD.ROOT+"/control")["mode"],"0700") + + widened=json.loads(json.dumps(avahi)) + widened["runtime"]["host_path_requirements"][0]["mode"]="0750" + with self.assertRaisesRegex(RuntimeError,"kind/mode drifted"): + MOD.validate_host_path_requirements(widened,"25.10.7") def test_f4_reconciliation_policy_and_f5_retention_contract(self): exact={"services":{"control":{"image":"sha256:exact"}}} @@ -224,10 +246,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@e44bc2271de4c2065c68a8f7db53034fff36b4c5", workflow) + self.assertIn("export-foliorelay-t6-control.yml@c969b815faeecdf044ad1edf327916c90c9a9c06", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="e44bc2271de4c2065c68a8f7db53034fff36b4c5"', workflow) + self.assertIn('foundry_commit="c969b815faeecdf044ad1edf327916c90c9a9c06"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From 6fba49db132aefeb77f5c1a16ab8cd7c88ad69b0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:03:00 +0200 Subject: [PATCH 532/719] RDTE: observe Avahi mDNS via legacy-unicast query --- tools/foliorelay-observer/main.go | 47 +++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/tools/foliorelay-observer/main.go b/tools/foliorelay-observer/main.go index 81c718238..f60b92e6b 100644 --- a/tools/foliorelay-observer/main.go +++ b/tools/foliorelay-observer/main.go @@ -15,6 +15,7 @@ import ( ) const queryName = "_universal._sub._ipp._tcp.local" +const legacyQueryID uint16 = 0x4652 type resultState struct { mu sync.RWMutex @@ -247,7 +248,23 @@ func qualifying(o observation, expectedTxtUUID, expectedHost string, expectedPor return ok } -func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, seconds float64) (map[string]any, error) { +func observationSocketPlan(legacyUnicast bool) (bindPort int, joinMulticast bool, queryID uint16) { + if legacyUnicast { + return 0, false, legacyQueryID + } + return 5353, true, 0 +} + +func buildQuery(queryID uint16) []byte { + q := make([]byte, 12) + binary.BigEndian.PutUint16(q[0:2], queryID) + binary.BigEndian.PutUint16(q[4:6], 1) + q = append(q, encodeName(queryName)...) + q = append(q, 0, 12, 0, 1) + return q +} + +func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, seconds float64, legacyUnicast bool) (map[string]any, error) { fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) if err != nil { return nil, err @@ -255,11 +272,17 @@ func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, s defer syscall.Close(fd) _ = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) _ = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, 15, 1) - if err := syscall.Bind(fd, &syscall.SockaddrInet4{Port: 5353}); err != nil { + bindPort, joinMulticast, queryID := observationSocketPlan(legacyUnicast) + if err := syscall.Bind(fd, &syscall.SockaddrInet4{Port: bindPort}); err != nil { return nil, err } - mreq := &syscall.IPMreq{Multiaddr: [4]byte{224, 0, 0, 251}} - if err := syscall.SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq); err != nil { + if joinMulticast { + mreq := &syscall.IPMreq{Multiaddr: [4]byte{224, 0, 0, 251}} + if err := syscall.SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq); err != nil { + return nil, err + } + } + if err := syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_MULTICAST_TTL, 255); err != nil { return nil, err } tv := syscall.NsecToTimeval(int64(time.Second)) @@ -267,10 +290,7 @@ func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, s return nil, err } - q := make([]byte, 12) - binary.BigEndian.PutUint16(q[4:6], 1) - q = append(q, encodeName(queryName)...) - q = append(q, 0, 12, 0, 1) + q := buildQuery(queryID) dst := &syscall.SockaddrInet4{Port: 5353, Addr: [4]byte{224, 0, 0, 251}} if err := syscall.Sendto(fd, q, 0, dst); err != nil { return nil, err @@ -288,13 +308,21 @@ func observe(expected, expectedTxtUUID, expectedHost string, expectedPort int, s } return nil, err } + if legacyUnicast && n >= 2 && binary.BigEndian.Uint16(buf[:2]) != queryID { + continue + } packetObservation, err := parsePacket(buf[:n]) if err != nil { continue } aggregate.merge(packetObservation) if matched, ok := matchObservation(aggregate, expectedTxtUUID, expectedHost, expectedPort); ok { + transport := "multicast-5353" + if legacyUnicast { + transport = "legacy-unicast" + } return map[string]any{ + "query_transport": transport, "universal_ptr": true, "service_instance": matched.instance, "uuid": expected, @@ -314,6 +342,7 @@ func main() { ippPort := flag.Int("expected-ipp-port", 0, "expected DNS-SD SRV port") port := flag.Int("port", 18081, "HTTP witness port") seconds := flag.Float64("seconds", 25, "mDNS observation window") + legacyUnicast := flag.Bool("legacy-unicast", false, "query mDNS from an ephemeral source port and receive the RFC 6762 legacy-unicast reply") flag.Parse() if *uuid == "" || *txtUUID == "" || *host == "" || *ippPort <= 0 { fmt.Fprintln(os.Stderr, "uuid, txt-uuid, expected-host, and expected-ipp-port are required") @@ -322,7 +351,7 @@ func main() { state := &resultState{v: map[string]any{"status": "pending"}} go func() { - r, err := observe(*uuid, *txtUUID, *host, *ippPort, *seconds) + r, err := observe(*uuid, *txtUUID, *host, *ippPort, *seconds, *legacyUnicast) if err != nil { state.set(map[string]any{"status": "error", "error": fmt.Sprintf("%T: %v", err, err)}) return From ace3b5e03d64bdd1c719badaa42f8f88d61ffc79 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:03:10 +0200 Subject: [PATCH 533/719] test: lock legacy-unicast observer socket/query plan --- tools/foliorelay-observer/main_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tools/foliorelay-observer/main_test.go b/tools/foliorelay-observer/main_test.go index dd6f325fc..f92c8a33d 100644 --- a/tools/foliorelay-observer/main_test.go +++ b/tools/foliorelay-observer/main_test.go @@ -20,6 +20,30 @@ func validObservation() observation { return o } +func TestObservationSocketPlan(t *testing.T) { + port, join, id := observationSocketPlan(false) + if port != 5353 || !join || id != 0 { + t.Fatalf("multicast plan = port %d join %v id %d", port, join, id) + } + port, join, id = observationSocketPlan(true) + if port != 0 || join || id != legacyQueryID { + t.Fatalf("legacy plan = port %d join %v id %d", port, join, id) + } +} + +func TestBuildLegacyQueryCarriesCorrelationID(t *testing.T) { + q := buildQuery(legacyQueryID) + if len(q) < 12 { + t.Fatal("short query") + } + if got := binary.BigEndian.Uint16(q[0:2]); got != legacyQueryID { + t.Fatalf("query id=%#x want=%#x", got, legacyQueryID) + } + if got := binary.BigEndian.Uint16(q[4:6]); got != 1 { + t.Fatalf("question count=%d want=1", got) + } +} + func TestEncodeAndNameAt(t *testing.T) { wire := encodeName("_universal._sub._ipp._tcp.local") got, end, err := nameAt(wire, 0, nil) From b04e01b578faa63efde3cc951395f9f038d1c3fa Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:03:17 +0200 Subject: [PATCH 534/719] RDTE: select legacy-unicast observer on Avahi targets --- scripts/truenas_middleware_foliorelay_t6_probe.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 1f096eaaf..d031d747e 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -611,7 +611,12 @@ def wait_state(name,state): blob=http_bytes(a.host,a.control_port,f"/api/v1/jobs/{matches[0]['job_id']}/artifact",tok,timeout=a.timeout) if sha256_bytes(blob)!=sha: raise RuntimeError(f"{media} downloaded artifact drifted") sj=multipart_upload(a.host,a.port,a.tls,"truenas_admin",password,OBSERVER_DIR+"/foliorelay-observer",observer_bytes,0o555,a.timeout); wait_job(sj,"observer upload") - obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"user":"65534:65534","cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR+"/foliorelay-observer","target":"/observer/foliorelay-observer","read_only":True}],"entrypoint":["/observer/foliorelay-observer"],"command":["--uuid",uuid,"--txt-uuid",dnssd_txt_uuid(uuid),"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"]}}} + observer_command=["--uuid",uuid,"--txt-uuid",dnssd_txt_uuid(uuid),"--expected-host",PUBLIC_HOST,"--expected-ipp-port",str(PUBLIC_IPP_PORT),"--port","18081"] + expected_observer_transport="multicast-5353" + if a.target_version in AVAHI_TARGETS: + observer_command.append("--legacy-unicast") + expected_observer_transport="legacy-unicast" + obs_compose={"services":{"observer":{"image":OBSERVER_IMAGE,"network_mode":"host","read_only":True,"user":"65534:65534","cap_drop":["ALL"],"security_opt":["no-new-privileges:true"],"volumes":[{"type":"bind","source":OBSERVER_DIR+"/foliorelay-observer","target":"/observer/foliorelay-observer","read_only":True}],"entrypoint":["/observer/foliorelay-observer"],"command":observer_command}}} oj=call("app.create",[{"app_name":OBSERVER_APP_NAME,"custom_app":True,"custom_compose_config":obs_compose}]) if not isinstance(oj,int): raise RuntimeError("observer app.create did not return job") observer_created=True @@ -658,7 +663,8 @@ def wait_state(name,state): time.sleep(1) if not observed or observed.get("status")!="success": raise RuntimeError("DNS-SD observer oracle remained pending") - if (observed.get("universal_ptr") is not True or observed.get("uuid")!=uuid + if (observed.get("query_transport")!=expected_observer_transport + or observed.get("universal_ptr") is not True or observed.get("uuid")!=uuid or (observed.get("srv_target") or "").rstrip(".").lower()!=PUBLIC_HOST.lower() or observed.get("srv_port")!=PUBLIC_IPP_PORT): raise RuntimeError("DNS-SD observer public URI identity mismatch") From 47f362459f5692cd093acea1aa30089e65badf69 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:03:30 +0200 Subject: [PATCH 535/719] RDTE: request 25.10.7 legacy-unicast observer rep 006 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e9669f243..a17a7418e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-foliorelay-005-control-traverse", + "request_id": "tn-25-10-7-t6-foliorelay-006-legacy-unicast-observer", "version": "25.10.7", "rung": "t6", "product": "foliorelay", "authority_issue": 410, "consumer_authority_issue": 273, - "purpose": "Re-run the exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 after rep 004 retained ORACLE_FAILURE evidence showing discovery uid 65534/gid 10001 could not traverse the experiment-owned control bind root because Agent Dispatch provisioned it mode 0700. Preserve merged Foundry authority e44bc2271de4c2065c68a8f7db53034fff36b4c5, immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814, cooperative Avahi command/user/read-only system-bus coupling, same-instance DNS-SD oracle, PDF/URF exact-source preservation, F4 reconciliation semantics, F5 retain-data delete/reinstall persistence, and zero residue. The sole causal runtime delta is the experiment-owned control parent mode 0700->0710, granting gid 10001 traverse only while retaining all per-file permissions. Acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." + "purpose": "Re-run the exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 after rep 005 proved all three product containers RUNNING and localized the remaining failure to the independent observer competing with host Avahi for UDP/5353. Preserve the cooperative-Avahi product, Foundry e44bc2271de4c2065c68a8f7db53034fff36b4c5, immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d, CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814, 0710 control-root traversal, product functional oracles, F4/F5 semantics and zero-residue cleanup. The sole causal oracle delta is that on 25.x the separate observer issues an RFC 6762 legacy one-shot mDNS query from an ephemeral UDP source port and requires the correlated unicast response, avoiding a competing UDP/5353 listener while still parsing same-instance PTR/TXT/SRV records independently. BETA.3 retains the existing multicast-5353 observer path. Acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." } From a3f2d5179aa7204951b232ab8f2122cacfe95099 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:28:36 +0200 Subject: [PATCH 536/719] RDTE: pin merged FolioRelay host-path Foundry authority --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 3eba91e48..73f2eb013 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -254,9 +254,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@c969b815faeecdf044ad1edf327916c90c9a9c06 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@6494dbd336260b549772ffa5c398d7a104fabf15 with: - foundry_ref: c969b815faeecdf044ad1edf327916c90c9a9c06 + foundry_ref: 6494dbd336260b549772ffa5c398d7a104fabf15 target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -532,7 +532,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="c969b815faeecdf044ad1edf327916c90c9a9c06" + foundry_commit="6494dbd336260b549772ffa5c398d7a104fabf15" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 7b790f7ab7efc08f21661edaafeaa986a0a053df Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:28:42 +0200 Subject: [PATCH 537/719] RDTE: pin merged FolioRelay host-path Foundry authority --- .github/workflows/prep-foliorelay-t6-consumer.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 97f26f184..9b682ae84 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@c969b815faeecdf044ad1edf327916c90c9a9c06 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@6494dbd336260b549772ffa5c398d7a104fabf15 with: - foundry_ref: c969b815faeecdf044ad1edf327916c90c9a9c06 + foundry_ref: 6494dbd336260b549772ffa5c398d7a104fabf15 target_version: 25.10.7 permissions: contents: read @@ -126,7 +126,7 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "c969b815faeecdf044ad1edf327916c90c9a9c06" and + .foundry_ref == "6494dbd336260b549772ffa5c398d7a104fabf15" and .candidate.truenas_version == "25.10.7" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and From aee61d413b368c870f55f1a16054737fe21aac79 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:28:49 +0200 Subject: [PATCH 538/719] RDTE: pin merged FolioRelay host-path Foundry authority --- tests/test_truenas_t6_foliorelay_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 757d73112..0195b2690 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -246,10 +246,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@c969b815faeecdf044ad1edf327916c90c9a9c06", workflow) + self.assertIn("export-foliorelay-t6-control.yml@6494dbd336260b549772ffa5c398d7a104fabf15", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="c969b815faeecdf044ad1edf327916c90c9a9c06"', workflow) + self.assertIn('foundry_commit="6494dbd336260b549772ffa5c398d7a104fabf15"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From 2b631460859d37b6793eef1e1df9b3258681be5e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:31:43 +0200 Subject: [PATCH 539/719] RDTE: request FolioRelay 25.04.2.6 F0-F5 rep --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index a17a7418e..2bb6b3530 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-foliorelay-006-legacy-unicast-observer", - "version": "25.10.7", + "request_id": "tn-25-04-2-6-t6-foliorelay-001-avahi-hostpath-f0-f5", + "version": "25.04.2.6", "rung": "t6", "product": "foliorelay", - "authority_issue": 410, + "authority_issue": 423, "consumer_authority_issue": 273, - "purpose": "Re-run the exact FolioRelay F0-F5 qualification row on TrueNAS 25.10.7 after rep 005 proved all three product containers RUNNING and localized the remaining failure to the independent observer competing with host Avahi for UDP/5353. Preserve the cooperative-Avahi product, Foundry e44bc2271de4c2065c68a8f7db53034fff36b4c5, immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d, CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814, 0710 control-root traversal, product functional oracles, F4/F5 semantics and zero-residue cleanup. The sole causal oracle delta is that on 25.x the separate observer issues an RFC 6762 legacy one-shot mDNS query from an ephemeral UDP source port and requires the correlated unicast response, avoiding a competing UDP/5353 listener while still parsing same-instance PTR/TXT/SRV records independently. BETA.3 retains the existing multicast-5353 observer path. Acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." + "purpose": "Run the first exact FolioRelay F0-F5 qualification row on TrueNAS 25.04.2.6 after 25.10.7 rep 006 earned PASS. Require merged Foundry host-path authority 6494dbd336260b549772ffa5c398d7a104fabf15, publication-bound immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814, exact 25.x Avahi command/user/read-only system-bus coupling, Foundry-driven control-root 0710 traversal contract, independent legacy-unicast same-instance DNS-SD observation, PDF/URF exact-source preservation, F4 second-plan NOOP with WAIT/fail-closed policy, F5 retain-data delete/reinstall persistence, and final zero residue. No support is inherited from 25.10.7, 25.04.1, BETA.3, or generic T0-T5. Acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." } From 51131fd2ef9fa15215723f828601c48c4a53346a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 06:41:59 +0200 Subject: [PATCH 540/719] RDTE: request FolioRelay 25.04.1 final F0-F5 rep --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 2bb6b3530..6ec51c570 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-foliorelay-001-avahi-hostpath-f0-f5", - "version": "25.04.2.6", + "request_id": "tn-25-04-1-t6-foliorelay-001-avahi-hostpath-f0-f5", + "version": "25.04.1", "rung": "t6", "product": "foliorelay", - "authority_issue": 423, + "authority_issue": 411, "consumer_authority_issue": 273, - "purpose": "Run the first exact FolioRelay F0-F5 qualification row on TrueNAS 25.04.2.6 after 25.10.7 rep 006 earned PASS. Require merged Foundry host-path authority 6494dbd336260b549772ffa5c398d7a104fabf15, publication-bound immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814, exact 25.x Avahi command/user/read-only system-bus coupling, Foundry-driven control-root 0710 traversal contract, independent legacy-unicast same-instance DNS-SD observation, PDF/URF exact-source preservation, F4 second-plan NOOP with WAIT/fail-closed policy, F5 retain-data delete/reinstall persistence, and final zero residue. No support is inherited from 25.10.7, 25.04.1, BETA.3, or generic T0-T5. Acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." + "purpose": "Run the exact final FolioRelay F0-F5 qualification row on TrueNAS 25.04.1 using the earned cooperative-Avahi architecture, RFC 6762 legacy-unicast independent observer for 25.x, and merged Foundry host-path contract 6494dbd336260b549772ffa5c398d7a104fabf15 consumed by Agent Dispatch. Require immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814 from signed publication run 37457117549/artifact 11410550658, exact 25.04.1 profile/source identity, exact Avahi command/user/read-only system-bus coupling, provider-owned path permissions from Foundry metadata, same-instance DNS-SD, PDF/URF exact-source Inbox preservation, restart/update/redeploy persistence, F4 second-plan NOOP with WAIT/fail-closed ambiguity, F5 retain-data delete/reinstall persistence, and final zero residue. No support is inherited from 25.04.2.6, 25.10.7, BETA.3, or generic T0-T5. Acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." } From bfb4c33a8c60d0589ce01068768898e191f5fdf8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:15:30 +0200 Subject: [PATCH 541/719] rdte: add exact-target GARM controller F0-F5 oracle --- .../truenas_middleware_garm_f0_f5_probe.py | 670 ++++++++++++++++++ 1 file changed, 670 insertions(+) create mode 100644 scripts/truenas_middleware_garm_f0_f5_probe.py diff --git a/scripts/truenas_middleware_garm_f0_f5_probe.py b/scripts/truenas_middleware_garm_f0_f5_probe.py new file mode 100644 index 000000000..059ebcdec --- /dev/null +++ b/scripts/truenas_middleware_garm_f0_f5_probe.py @@ -0,0 +1,670 @@ +#!/usr/bin/env python3 +"""Exact-target GARM controller Foundry F0-F5 oracle on disposable TrueNAS.""" +from __future__ import annotations + +import argparse +import copy +import hashlib +import http.client +import json +import pathlib +import secrets +import ssl +import string +import subprocess +import tempfile +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + +SCHEMA = "semper-supra.garm-truenas-f0-f5-control/1" +EXPECTED_APP_NAME = "rdte-t6-garm" +EXPECTED_IMAGE = "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9" +EXPECTED_CONTROLLER_SOURCE = "8a72cb23deacc712786945ac5e7ee63f8b4976d6" +EXPECTED_PROVIDER_SOURCE = "14535745dc3aa3c0b5466da7c704bca4d23dcec5" +EXPECTED_FIXTURE_DATASET = "rdtepool/garm-t6" +EXPECTED_FIXTURE_ROOT = "/mnt/rdtepool/garm-t6" +EXPECTED_CONFIG_DIR = EXPECTED_FIXTURE_ROOT + "/config" +EXPECTED_CONFIG_PATH = EXPECTED_CONFIG_DIR + "/config.toml" +EXPECTED_DB_PATH = EXPECTED_CONFIG_DIR + "/garm.db" +BOOTSTRAP_CONFIG = "garm-initial-config" +TLS_CERT_CONFIG = "garm-tls-certificate" +TLS_KEY_CONFIG = "garm-tls-private-key" +BOOTSTRAP_PLACEHOLDER = "__EPHEMERAL_NESTED_GARM_CONFIG__" +TLS_PREFIX = "__EPHEMERAL_NESTED_TLS__:" +REQUIRED_METHODS = { + "system.version", "core.get_methods", + "app.query", "app.config", "app.create", "app.update", "app.redeploy", + "app.stop", "app.start", "app.delete", + "pool.dataset.query", "pool.dataset.create", "pool.dataset.delete", + "filesystem.mkdir", "filesystem.stat", +} + + +def canonical_sha256(value: object) -> str: + raw = json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8") + return hashlib.sha256(raw).hexdigest() + + +def reconciliation_action(app_state, live_compose, desired_compose): + if app_state is None: + return "CREATE" + if app_state in {"DEPLOYING", "STOPPING"}: + return "WAIT" + if app_state not in {"RUNNING", "STOPPED"}: + return "FAIL_CLOSED" + if not isinstance(live_compose, dict): + return "FAIL_CLOSED" + return ( + "NOOP" + if canonical_sha256(live_compose) == canonical_sha256(desired_compose) + else "UPDATE" + ) + + +def load_json(path: pathlib.Path) -> dict: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise RuntimeError(f"{path} must contain an object") + return value + + +def normalize_secret_configs(compose: dict) -> dict: + normalized = copy.deepcopy(compose) + configs = normalized.get("configs") or {} + if ( + BOOTSTRAP_CONFIG not in configs + or TLS_CERT_CONFIG not in configs + or TLS_KEY_CONFIG not in configs + ): + raise RuntimeError("GARM secret-bearing config inventory drifted") + configs[BOOTSTRAP_CONFIG]["content"] = BOOTSTRAP_PLACEHOLDER + configs[TLS_CERT_CONFIG]["content"] = TLS_PREFIX + TLS_CERT_CONFIG + configs[TLS_KEY_CONFIG]["content"] = TLS_PREFIX + TLS_KEY_CONFIG + return normalized + + +def load_control( + directory: pathlib.Path, + foundry_commit: str, + target_version: str, + expected_system_version: str, + expected_middleware_commit: str, + expected_profile_path: str, + expected_profile_blob: str, +) -> tuple[dict, dict]: + control = load_json(directory / "control.json") + compose = load_json(directory / "compose.json") + deployment = load_json(directory / "deployment.json") + + if control.get("schema") != SCHEMA: + raise RuntimeError("unexpected GARM F0-F5 control schema") + if control.get("foundry_ref") != foundry_commit: + raise RuntimeError("Foundry source ref drifted") + target = control.get("target") or {} + expected_target = { + "truenas_version": target_version, + "expected_system_version": expected_system_version, + "profile_path": expected_profile_path, + "profile_git_blob_sha": expected_profile_blob, + "middleware_commit": expected_middleware_commit, + } + for key, expected in expected_target.items(): + if target.get(key) != expected: + raise RuntimeError( + f"GARM exact target profile drifted for {key}: " + f"{target.get(key)!r} != {expected!r}" + ) + if control.get("appliance_reference") != EXPECTED_IMAGE: + raise RuntimeError("GARM appliance identity drifted") + if control.get("controller_source") != EXPECTED_CONTROLLER_SOURCE: + raise RuntimeError("GARM controller source identity drifted") + if control.get("provider_source") != EXPECTED_PROVIDER_SOURCE: + raise RuntimeError("TrueNAS provider source identity drifted") + if control.get("private_secrets_captured") is not False: + raise RuntimeError("control does not assert private_secrets_captured=false") + if control.get("fixture_secret_values_retained") is not False: + raise RuntimeError("control retained fixture secret values") + if control.get("github_credentials_present") is not False: + raise RuntimeError("control claims GitHub credentials are present") + + runtime = control.get("runtime") or {} + if runtime.get("app_name") != EXPECTED_APP_NAME: + raise RuntimeError("GARM app name drifted") + if runtime.get("config_root") != EXPECTED_CONFIG_DIR: + raise RuntimeError("GARM config root drifted") + if int(runtime.get("host_port") or 0) != 30880: + raise RuntimeError("GARM guest service port drifted") + + if control.get("runtime_compose_sha256") != canonical_sha256(compose): + raise RuntimeError("GARM runtime Compose identity does not match control") + if deployment.get("artifact_sha256") != control.get("deployment_artifact_sha256"): + raise RuntimeError("GARM deployment artifact identity drifted") + if deployment.get("materialization_identity") != control.get("materialization_identity"): + raise RuntimeError("GARM materialization identity drifted") + + services = compose.get("services") or {} + if set(services) != {"garm", "garm-config-seed"}: + raise RuntimeError("unexpected GARM service inventory") + for name in ("garm", "garm-config-seed"): + service = services[name] + if service.get("image") != EXPECTED_IMAGE: + raise RuntimeError(f"{name} image identity drifted") + if service.get("privileged") is True: + raise RuntimeError(f"{name} became privileged") + if "ALL" not in {str(x).upper() for x in service.get("cap_drop") or []}: + raise RuntimeError(f"{name} lost cap_drop ALL") + opts = {str(x).lower().replace(":", "=") for x in service.get("security_opt") or []} + if not any(x.startswith("no-new-privileges=true") for x in opts): + raise RuntimeError(f"{name} lost no-new-privileges") + + serialized = json.dumps(compose, sort_keys=True) + for forbidden in ( + "BEGIN PRIVATE KEY", + "PRIVATE-QUALIFICATION-FIXTURE", + "PUBLIC-QUALIFICATION-FIXTURE", + "ghp_", + "github_pat_", + ): + if forbidden in serialized: + raise RuntimeError(f"secret-like retained control content: {forbidden}") + if BOOTSTRAP_PLACEHOLDER not in serialized or TLS_PREFIX not in serialized: + raise RuntimeError("run-local secret placeholders are absent") + return control, compose + + +def build_ephemeral_runtime_compose(compose: dict) -> tuple[dict, dict[str, str]]: + runtime = copy.deepcopy(compose) + jwt_secret = secrets.token_urlsafe(36) + alphabet = string.ascii_letters + string.digits + db_passphrase = "".join(secrets.choice(alphabet) for _ in range(32)) + with tempfile.TemporaryDirectory(prefix="garm-f0-f5-tls-") as td: + root = pathlib.Path(td) + key_path = root / "tls.key" + cert_path = root / "tls.crt" + subprocess.run( + [ + "openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", + "-subj", "/CN=garm-t6.invalid", + "-addext", "subjectAltName=DNS:garm-t6.invalid,IP:127.0.0.1", + "-keyout", str(key_path), "-out", str(cert_path), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + tls_key = key_path.read_text(encoding="utf-8") + tls_cert = cert_path.read_text(encoding="utf-8") + + bootstrap = f"""[default] +enable_webhook_management = false + +[logging] +log_level = "info" + +[metrics] +disable_auth = false + +[jwt_auth] +secret = "{jwt_secret}" +time_to_live = "1h" + +[apiserver] +bind = "0.0.0.0" +port = 8080 +use_tls = true + +[apiserver.tls] +certificate = "/garm-tls.crt" +key = "/garm-tls.key" + +[apiserver.webui] +enable = true + +[database] +backend = "sqlite3" +passphrase = "{db_passphrase}" + +[database.sqlite3] +db_file = "/etc/garm/garm.db" +""" + configs = runtime["configs"] + configs[BOOTSTRAP_CONFIG]["content"] = bootstrap + configs[TLS_CERT_CONFIG]["content"] = tls_cert + configs[TLS_KEY_CONFIG]["content"] = tls_key + return runtime, { + "jwt_secret_sha256": hashlib.sha256(jwt_secret.encode()).hexdigest(), + "database_passphrase_sha256": hashlib.sha256(db_passphrase.encode()).hexdigest(), + "certificate_sha256": hashlib.sha256(tls_cert.encode()).hexdigest(), + } + + +def https_ui_probe(port: int, timeout: float = 8.0) -> int: + context = ssl._create_unverified_context() + conn = http.client.HTTPSConnection("127.0.0.1", port, timeout=timeout, context=context) + try: + conn.request("GET", "/ui/") + response = conn.getresponse() + response.read(4096) + return int(response.status) + finally: + conn.close() + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", type=int, required=True) + p.add_argument("--service-port", type=int, required=True) + p.add_argument("--password-file", required=True) + p.add_argument("--control-dir", type=pathlib.Path, required=True) + p.add_argument("--foundry-commit", required=True) + p.add_argument("--target-version", required=True) + p.add_argument("--expected-system-version", required=True) + p.add_argument("--expected-middleware-commit", required=True) + p.add_argument("--expected-profile-path", required=True) + p.add_argument("--expected-profile-blob", required=True) + p.add_argument("--out", required=True) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=300.0) + p.add_argument("--state-timeout", type=float, default=240.0) + a = p.parse_args() + + started = time.time() + payload = { + "schema": "truenas-garm-controller-f0-f5/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "f0_f5_complete": False, + "target_version": a.target_version, + "expected_version": a.expected_system_version, + "foundry_commit": a.foundry_commit, + "app_name": EXPECTED_APP_NAME, + "appliance": EXPECTED_IMAGE, + "controller_source": EXPECTED_CONTROLLER_SOURCE, + "provider_source": EXPECTED_PROVIDER_SOURCE, + "github_credentials_present": False, + "github_jit_registration_exercised": False, + "physical_truenas_mutation": False, + "capacity_promotion": False, + "secret_values_recorded": False, + } + ws = None + request_id = 1 + app_owned = False + dataset_owned = False + + try: + control, retained_compose = load_control( + a.control_dir, + a.foundry_commit, + a.target_version, + a.expected_system_version, + a.expected_middleware_commit, + a.expected_profile_path, + a.expected_profile_blob, + ) + runtime_compose, fingerprints = build_ephemeral_runtime_compose(retained_compose) + retained_sha = canonical_sha256(retained_compose) + payload["f0"] = { + "profile_id": control["target"]["profile_id"], + "profile_path": control["target"]["profile_path"], + "profile_git_blob_sha": control["target"]["profile_git_blob_sha"], + "middleware_commit": control["target"]["middleware_commit"], + "foundry_ref": control["foundry_ref"], + "appliance": control["appliance_reference"], + } + payload["f1"] = { + "retained_compose_sha256": retained_sha, + "deployment_artifact_sha256": control["deployment_artifact_sha256"], + "materialization_identity": control["materialization_identity"], + "run_local_secret_fingerprints": fingerprints, + } + + password = pathlib.Path(a.password_file).read_text(encoding="utf-8").strip() + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise RuntimeError("DDP connection failed") + + def call(method, params): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + def wait_job(job_id, label): + if not isinstance(job_id, int): + raise RuntimeError(f"{label} did not return a job id") + deadline = time.monotonic() + a.job_timeout + last = None + while time.monotonic() < deadline: + last = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if last and last.get("state") == "SUCCESS": + return last + if last and last.get("state") in {"FAILED", "ABORTED"}: + raise RuntimeError( + f"{label} job {last.get('state')}: " + f"{last.get('error') or last.get('exception')}" + ) + time.sleep(1) + raise RuntimeError(f"{label} job did not reach SUCCESS: {last!r}") + + def query_optional(method, filters): + values = call(method, [filters]) + if not values: + return None + if not isinstance(values, list) or len(values) != 1: + raise RuntimeError(f"{method} cardinality was not exactly one") + return values[0] + + def app_query(): + return query_optional("app.query", [["id", "=", EXPECTED_APP_NAME]]) + + def wait_state(expected): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + last = app_query() + if last and last.get("state") == expected: + if expected != "RUNNING": + return last + details = (last.get("active_workloads") or {}).get("container_details") or [] + exact = [ + item for item in details + if item.get("service_name") == "garm" + and item.get("image") == EXPECTED_IMAGE + and item.get("state") == "running" + ] + if len(exact) == 1: + return last + if last and last.get("state") in {"CRASHED", "ERROR"}: + raise RuntimeError(f"GARM app entered {last.get('state')}") + time.sleep(1) + raise RuntimeError(f"GARM app did not reach {expected}: {last!r}") + + def assert_persistent_state(label): + config_meta = call("filesystem.stat", [EXPECTED_CONFIG_PATH]) + db_meta = call("filesystem.stat", [EXPECTED_DB_PATH]) + state = { + "config_toml_size_positive": int(config_meta.get("size") or 0) > 0, + "garm_db_size_positive": int(db_meta.get("size") or 0) > 0, + } + if not all(state.values()): + raise RuntimeError(f"{label}: persistent config/db state was empty") + return state + + def normalized_live_compose(): + return normalize_secret_configs(call("app.config", [EXPECTED_APP_NAME])) + + auth = call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise RuntimeError("authentication did not return SUCCESS") + + observed_version = call("system.version", []) + if observed_version != a.expected_system_version: + raise RuntimeError( + f"target version drifted: {observed_version!r} != {a.expected_system_version!r}" + ) + methods = call("core.get_methods", []) + method_names = set(methods if isinstance(methods, list) else methods.keys()) + missing = sorted(REQUIRED_METHODS - method_names) + if missing: + raise RuntimeError(f"required middleware methods missing: {missing}") + payload["f0"]["system_version"] = observed_version + payload["f0"]["required_methods_present"] = True + + if app_query() is not None: + raise RuntimeError("refusing adopted GARM app state") + if query_optional("pool.dataset.query", [["id", "=", EXPECTED_FIXTURE_DATASET]]) is not None: + raise RuntimeError("refusing adopted GARM fixture dataset") + + dataset = call("pool.dataset.create", [{ + "name": EXPECTED_FIXTURE_DATASET, + "type": "FILESYSTEM", + "share_type": "GENERIC", + "comments": "SemperSupra disposable GARM F0-F5 fixture", + }]) + if not isinstance(dataset, dict) or dataset.get("id") != EXPECTED_FIXTURE_DATASET: + raise RuntimeError("pool.dataset.create did not return exact fixture dataset") + dataset_owned = True + + mkdir = call("filesystem.mkdir", [{ + "path": EXPECTED_CONFIG_DIR, + "options": {"mode": "750", "raise_chmod_error": True}, + }]) + if not isinstance(mkdir, dict) or mkdir.get("path") != EXPECTED_CONFIG_DIR: + raise RuntimeError("filesystem.mkdir did not create exact config directory") + + create_id = call("app.create", [{ + "app_name": EXPECTED_APP_NAME, + "custom_app": True, + "custom_compose_config": runtime_compose, + }]) + wait_job(create_id, "app.create") + app_owned = True + app = wait_state("RUNNING") + if app.get("custom_app") is not True: + raise RuntimeError("GARM runtime did not materialize as a custom app") + if canonical_sha256(normalized_live_compose()) != retained_sha: + raise RuntimeError("F2 secret-normalized app.config does not match retained Compose") + f2_state = assert_persistent_state("F2") + ui_status = https_ui_probe(a.service_port, timeout=a.timeout) + if not 200 <= ui_status < 400: + raise RuntimeError(f"F2 GARM UI returned HTTP {ui_status}") + payload["f2"] = { + "app_running": True, + "compose_readback_exact": True, + "persistent_state": f2_state, + "https_ui_status": ui_status, + } + + wait_job(call("app.stop", [EXPECTED_APP_NAME]), "app.stop") + wait_state("STOPPED") + wait_job(call("app.start", [EXPECTED_APP_NAME]), "app.start") + wait_state("RUNNING") + restart_sha = canonical_sha256(normalized_live_compose()) + if restart_sha != retained_sha: + raise RuntimeError("F3 Compose identity drifted after restart") + restart_state = assert_persistent_state("F3 restart") + + update_id = call( + "app.update", + [EXPECTED_APP_NAME, {"custom_compose_config": runtime_compose}], + ) + wait_job(update_id, "app.update") + wait_state("RUNNING") + wait_job(call("app.redeploy", [EXPECTED_APP_NAME]), "app.redeploy") + wait_state("RUNNING") + if canonical_sha256(normalized_live_compose()) != retained_sha: + raise RuntimeError("F3 Compose identity drifted after update/redeploy") + redeploy_state = assert_persistent_state("F3 update/redeploy") + redeploy_status = https_ui_probe(a.service_port, timeout=a.timeout) + if not 200 <= redeploy_status < 400: + raise RuntimeError(f"F3 GARM UI returned HTTP {redeploy_status}") + payload["f3"] = { + "stop_start_pass": True, + "update_redeploy_pass": True, + "compose_identity_preserved": True, + "restart_persistent_state": restart_state, + "redeploy_persistent_state": redeploy_state, + "https_ui_status": redeploy_status, + } + + second_app = app_query() + second_action = reconciliation_action( + (second_app or {}).get("state"), + normalized_live_compose() if second_app else None, + retained_compose, + ) + if second_action != "NOOP": + raise RuntimeError(f"F4 second plan was {second_action}, expected NOOP") + payload["f4"] = { + "second_plan_action": second_action, + "inflight_policy": "WAIT", + "ambiguous_policy": "FAIL_CLOSED", + } + + retained_before = query_optional( + "pool.dataset.query", [["id", "=", EXPECTED_FIXTURE_DATASET]] + ) + if retained_before is None: + raise RuntimeError("F5 fixture dataset absent before retain-data delete") + wait_job(call("app.delete", [EXPECTED_APP_NAME, { + "remove_images": False, + "remove_ix_volumes": False, + "force_remove_custom_app": False, + }]), "retain-data app.delete") + app_owned = False + if app_query() is not None: + raise RuntimeError("F5 app remained after retain-data delete") + if query_optional( + "pool.dataset.query", [["id", "=", EXPECTED_FIXTURE_DATASET]] + ) is None: + raise RuntimeError("F5 external fixture dataset was not retained") + assert_persistent_state("F5 retained-data") + + wait_job(call("app.create", [{ + "app_name": EXPECTED_APP_NAME, + "custom_app": True, + "custom_compose_config": runtime_compose, + }]), "reinstall app.create") + app_owned = True + reinstall_app = wait_state("RUNNING") + if canonical_sha256(normalized_live_compose()) != retained_sha: + raise RuntimeError("F5 reinstall app.config drifted") + reinstall_state = assert_persistent_state("F5 reinstall") + reinstall_status = https_ui_probe(a.service_port, timeout=a.timeout) + if not 200 <= reinstall_status < 400: + raise RuntimeError(f"F5 reinstalled GARM UI returned HTTP {reinstall_status}") + post_reinstall_action = reconciliation_action( + reinstall_app.get("state"), + normalized_live_compose(), + retained_compose, + ) + if post_reinstall_action != "NOOP": + raise RuntimeError( + f"F5 post-reinstall plan was {post_reinstall_action}, expected NOOP" + ) + payload["f5"] = { + "external_dataset_retained_across_app_delete": True, + "persistent_state_after_reinstall": reinstall_state, + "https_ui_status": reinstall_status, + "post_reinstall_action": post_reinstall_action, + } + + wait_job(call("app.delete", [EXPECTED_APP_NAME, { + "remove_images": False, + "remove_ix_volumes": False, + "force_remove_custom_app": False, + }]), "final app.delete") + app_owned = False + deleted = call( + "pool.dataset.delete", + [EXPECTED_FIXTURE_DATASET, {"recursive": True, "force": False}], + ) + if deleted is not True: + raise RuntimeError("final pool.dataset.delete did not return true") + dataset_owned = False + if app_query() is not None: + raise RuntimeError("final GARM app residue remains") + if query_optional( + "pool.dataset.query", [["id", "=", EXPECTED_FIXTURE_DATASET]] + ) is not None: + raise RuntimeError("final GARM fixture dataset residue remains") + mountpoint_absent = False + try: + call("filesystem.stat", [EXPECTED_FIXTURE_ROOT]) + except RuntimeError: + mountpoint_absent = True + if not mountpoint_absent: + raise RuntimeError("final GARM fixture mountpoint residue remains") + + payload["cleanup"] = { + "app_absent": True, + "fixture_dataset_absent": True, + "fixture_mountpoint_absent": True, + "zero_residue": True, + } + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["f0_f5_complete"] = True + payload["detail"] = ( + "exact-target Foundry GARM controller completed F0-F5: exact " + "profile/materialization identity, create/readback, product health and persistent " + "state, stop/start/update/redeploy, second-plan NOOP with fail-closed reconciliation, " + "retain-data delete/reinstall/post-reinstall NOOP, and final zero-residue cleanup" + ) + except Exception as exc: + payload["detail"] = f"{type(exc).__name__}: {exc}" + cleanup = {"attempted": bool(app_owned or dataset_owned), "errors": []} + if ws is not None: + if app_owned: + try: + current = query_optional("app.query", [["id", "=", EXPECTED_APP_NAME]]) + if current is not None: + wait_job(call("app.delete", [EXPECTED_APP_NAME, { + "remove_images": False, + "remove_ix_volumes": False, + "force_remove_custom_app": False, + }]), "failure cleanup app.delete") + cleanup["app_absent"] = ( + query_optional("app.query", [["id", "=", EXPECTED_APP_NAME]]) + is None + ) + except Exception as cleanup_exc: + cleanup["errors"].append( + f"app: {type(cleanup_exc).__name__}: {cleanup_exc}" + ) + if dataset_owned: + try: + current = query_optional( + "pool.dataset.query", [["id", "=", EXPECTED_FIXTURE_DATASET]] + ) + if current is not None: + deleted = call( + "pool.dataset.delete", + [EXPECTED_FIXTURE_DATASET, {"recursive": True, "force": False}], + ) + if deleted is not True: + raise RuntimeError("cleanup dataset delete did not return true") + cleanup["fixture_dataset_absent"] = ( + query_optional( + "pool.dataset.query", + [["id", "=", EXPECTED_FIXTURE_DATASET]], + ) + is None + ) + except Exception as cleanup_exc: + cleanup["errors"].append( + f"dataset: {type(cleanup_exc).__name__}: {cleanup_exc}" + ) + cleanup["zero_residue"] = ( + cleanup["attempted"] + and not cleanup["errors"] + and (cleanup.get("app_absent", not app_owned) is True) + and (cleanup.get("fixture_dataset_absent", not dataset_owned) is True) + ) + payload["cleanup"] = cleanup + payload["cleanup_needed"] = cleanup["attempted"] and not cleanup["zero_residue"] + finally: + if ws is not None: + ws.close() + + payload["elapsed_seconds"] = round(time.time() - started, 3) + pathlib.Path(a.out).write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From aa824bb883447bd52f716cff767357983f4f6fff Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:18:13 +0200 Subject: [PATCH 542/719] rdte: bind exact Foundry profile blobs --- config/truenas-rdte-targets.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-targets.json b/config/truenas-rdte-targets.json index 028b12774..cccf012e2 100644 --- a/config/truenas-rdte-targets.json +++ b/config/truenas-rdte-targets.json @@ -22,7 +22,8 @@ "installer_rpc_path": "/", "installer_rpc_guest_port": 80, "installer_source_ref": "release/25.04.1", - "installer_main_blob_sha": "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f" + "installer_main_blob_sha": "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f", + "foundry_profile_blob": "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" }, { "version": "25.04.2.6", @@ -39,7 +40,8 @@ "authority_issue": 423, "installer_rpc_guest_port": 80, "installer_source_ref": "release/25.04.2.6", - "installer_main_blob_sha": "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f" + "installer_main_blob_sha": "4845a665568fcdb8ed911f3f6a23f6373cbe0b8f", + "foundry_profile_blob": "631467044411c96a0fe6d361688b99a794f07c80" }, { "version": "25.10.7", @@ -56,7 +58,8 @@ "installer_rpc_path": "/ws", "installer_rpc_guest_port": 8080, "installer_source_ref": "release/25.10.7", - "installer_main_blob_sha": "9e4f8b2bd9db497210fe6737c053080137be94d8" + "installer_main_blob_sha": "9e4f8b2bd9db497210fe6737c053080137be94d8", + "foundry_profile_blob": "1320cc849a9cc1e7ff6bd78989fc0e4708397248" }, { "version": "26.0.0-BETA.3", @@ -73,7 +76,8 @@ "installer_rpc_path": "/ws", "installer_rpc_guest_port": 8080, "installer_source_ref": "release/26.0.0-BETA.3", - "installer_main_blob_sha": "9e4f8b2bd9db497210fe6737c053080137be94d8" + "installer_main_blob_sha": "9e4f8b2bd9db497210fe6737c053080137be94d8", + "foundry_profile_blob": "a8ebc533dc84172520eba2237ffeeb52b102dc24" } ] } From d49ee31e096df503630ab119b03822e64f323b89 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:18:17 +0200 Subject: [PATCH 543/719] rdte: expose exact Foundry profile blob identity --- scripts/truenas_rdte_target.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_rdte_target.py b/scripts/truenas_rdte_target.py index 0e0ebe245..363e0c860 100644 --- a/scripts/truenas_rdte_target.py +++ b/scripts/truenas_rdte_target.py @@ -26,11 +26,13 @@ def load_registry(path: pathlib.Path): if not isinstance(version,str) or not version or version in seen: raise TargetError(f"invalid or duplicate target version at index {i}") seen.add(version) - for key in ("system_version","iso_name","iso_url","sha256_url","middleware_ref","middleware_commit","foundry_profile","ha_apps_gate","installer_rpc_path","installer_source_ref","installer_main_blob_sha"): + for key in ("system_version","iso_name","iso_url","sha256_url","middleware_ref","middleware_commit","foundry_profile","foundry_profile_blob","ha_apps_gate","installer_rpc_path","installer_source_ref","installer_main_blob_sha"): if not isinstance(t.get(key),str) or not t[key]: raise TargetError(f"{version}: missing {key}") if not SHA_RE.fullmatch(t["middleware_commit"]): raise TargetError(f"{version}: middleware_commit must be exact 40-hex") + if not SHA_RE.fullmatch(t["foundry_profile_blob"]): + raise TargetError(f"{version}: foundry_profile_blob must be exact 40-hex") if t["installer_rpc_path"] not in {"/", "/ws"}: raise TargetError(f"{version}: unsupported installer_rpc_path") if not isinstance(t.get("installer_rpc_guest_port"), int) or not (1 <= t["installer_rpc_guest_port"] <= 65535): @@ -54,6 +56,7 @@ def shell(target): "MIDDLEWARE_REF":target["middleware_ref"], "MIDDLEWARE_COMMIT":target["middleware_commit"], "FOUNDRY_PROFILE":target["foundry_profile"], + "FOUNDRY_PROFILE_BLOB":target["foundry_profile_blob"], "HA_APPS_GATE":target["ha_apps_gate"], "INSTALLER_RPC_PATH":target["installer_rpc_path"], "INSTALLER_RPC_GUEST_PORT":str(target["installer_rpc_guest_port"]), From 677268402876126f1fafc1977123d2f266d4c220 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:18:20 +0200 Subject: [PATCH 544/719] test: lock GARM controller F0-F5 exact target binding --- tests/test_truenas_rdte_target.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 1754039bb..196dc56ac 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -17,6 +17,11 @@ def test_exact_source_anchors(self): self.assertEqual(self.targets["25.04.2.6"]["middleware_commit"], "244b717370fe35fb9eefc3096acfbc25f12b57b6") self.assertEqual(self.targets["25.10.7"]["middleware_commit"], "8ede398839710e56893d88ce85088139d8fab18e") self.assertEqual(self.targets["26.0.0-BETA.3"]["middleware_commit"], "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3") + def test_exact_foundry_profile_blobs(self): + expected = {"25.04.1":"800aca038ded29e6a91bfc4c1b1fe2d2199182e8","25.04.2.6":"631467044411c96a0fe6d361688b99a794f07c80","25.10.7":"1320cc849a9cc1e7ff6bd78989fc0e4708397248","26.0.0-BETA.3":"a8ebc533dc84172520eba2237ffeeb52b102dc24"} + for version, blob in expected.items(): + self.assertEqual(self.targets[version]["foundry_profile_blob"], blob) + def test_vendor_media_is_exact_and_has_digest_sidecar(self): for version,target in self.targets.items(): self.assertIn(version, target["iso_name"]) @@ -66,11 +71,13 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","official-catalog","foliorelay"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","official-catalog","foliorelay"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": self.assertEqual(request.get("consumer_authority_issue"), 277) + if request.get("product") == "garm-controller-f0f5": + self.assertEqual(request.get("consumer_authority_issue"), 282) if request.get("product") == "garm-provider-g2": self.assertEqual(request.get("consumer_authority_issue"), 39) if request.get("product") == "garm-provider-g3": From d3db5ba9be51e5127220f57d8a11963216fa724e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:19:04 +0200 Subject: [PATCH 545/719] rdte: wire exact-target GARM controller F0-F5 selector --- scripts/gha_kvm_truenas_rdte.sh | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index f0561ce43..3ae0a1c3e 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -18,7 +18,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" } OUT="" @@ -72,10 +72,10 @@ if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then exit 2 fi if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" ]]; then + if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -163,7 +163,7 @@ write_receipt() { export R_TARGET_VERSION="$VERSION" R_EXPECTED_SYSTEM_VERSION="$EXPECTED_SYSTEM_VERSION" export R_ISO_NAME="$ISO_NAME" R_ISO_URL="$ISO_URL" R_SHA_URL="$SHA_URL" export R_MIDDLEWARE_REF="$MIDDLEWARE_REF" R_MIDDLEWARE_COMMIT="$MIDDLEWARE_COMMIT" - export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_HA_APPS_GATE="$HA_APPS_GATE" R_INSTALLER_RPC_PATH="$INSTALLER_RPC_PATH" R_INSTALLER_RPC_GUEST_PORT="$INSTALLER_RPC_GUEST_PORT" R_INSTALLER_SOURCE_REF="$INSTALLER_SOURCE_REF" R_INSTALLER_MAIN_BLOB_SHA="$INSTALLER_MAIN_BLOB_SHA" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" + export R_FOUNDRY_PROFILE="$FOUNDRY_PROFILE" R_FOUNDRY_PROFILE_BLOB="$FOUNDRY_PROFILE_BLOB" R_HA_APPS_GATE="$HA_APPS_GATE" R_INSTALLER_RPC_PATH="$INSTALLER_RPC_PATH" R_INSTALLER_RPC_GUEST_PORT="$INSTALLER_RPC_GUEST_PORT" R_INSTALLER_SOURCE_REF="$INSTALLER_SOURCE_REF" R_INSTALLER_MAIN_BLOB_SHA="$INSTALLER_MAIN_BLOB_SHA" R_AUTHORITY_ISSUE="$AUTHORITY_ISSUE" export R_RUNG="$RUNG" R_T6_PRODUCT="$T6_PRODUCT" R_COMPUTE_FIXTURE="$COMPUTE_FIXTURE" R_T0="$T0_OBSERVED" R_RPC_HOSTFWD="$RPC_HOSTFWD_ACCEPTED" export R_VCPUS="$VCPUS" R_RAM_MIB="$RAM_MIB" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" @@ -199,7 +199,7 @@ payload = { else {"name": "garm-provider-g5-version-row", "count": 1, "image": "ghcr.io/actions/actions-runner:2.336.0@sha256:0cfdcc701ce933c6d243c6b0b2da767366dc9f2e99961d4c3754b0b78084cdda"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm-provider-g5" else {"name": "rdte-t6-garm", "image": "ghcr.io/sempersupra/garm-appliance@sha256:1af67841ddd4589e3798dcda8be49230565c849d07ab57fd05899432dcdabca9"} - if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "garm" + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") in {"garm", "garm-controller-f0f5"} else {"name": "rdte-t6-wow-sidecar", "image": "ghcr.io/sempersupra/wow-sidecar@sha256:6b700ce7ba5ae44116b240ccbb54fb3b60dc952a9b4072ca1314b6f311bc5376"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" else {"name": "rdte-t6-catalog-ntfy", "catalog_app": "ntfy", "catalog_version": "1.1.21"} @@ -221,6 +221,7 @@ payload = { "middleware_ref": os.environ["R_MIDDLEWARE_REF"], "middleware_commit": os.environ["R_MIDDLEWARE_COMMIT"], "foundry_profile": os.environ["R_FOUNDRY_PROFILE"], + "foundry_profile_blob": os.environ["R_FOUNDRY_PROFILE_BLOB"], "ha_apps_gate": os.environ["R_HA_APPS_GATE"], "installer_rpc_path": os.environ["R_INSTALLER_RPC_PATH"], "installer_rpc_guest_port": int(os.environ["R_INSTALLER_RPC_GUEST_PORT"]), @@ -630,7 +631,7 @@ PY )" LITELLM_HOSTFWD=",hostfwd=tcp:127.0.0.1:${LITELLM_HOST_PORT}-:30401" fi -if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "garm" ]]; then +if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" ) ]]; then GARM_HOST_PORT="$(python3 - <<'PY' import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() @@ -1064,6 +1065,19 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then --target-version "$VERSION" \ --expected-middleware-commit "$MIDDLEWARE_COMMIT" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "garm-controller-f0f5" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_garm_f0_f5_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$GARM_HOST_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" \ + --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" \ + --target-version "$VERSION" \ + --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ + --expected-middleware-commit "$MIDDLEWARE_COMMIT" \ + --expected-profile-path "$FOUNDRY_PROFILE" \ + --expected-profile-blob "$FOUNDRY_PROFILE_BLOB" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "garm" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" --service-port "$GARM_HOST_PORT" \ @@ -1125,6 +1139,8 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider realized two source-derived runner Apps concurrently, exact Compose and fresh-process Get/List reconciliation passed for both, active deletion failed closed, both retirement orders reached empty inventory with zero residue, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then write_receipt SUPPORTED true foundry-materialization "exact G5 TrueNAS source-matrix row matched the RDTE target and the fixed packaged provider completed the capacity-one Create/read-back/Get/List/inactive/Delete/absence/empty-inventory/zero-residue lifecycle without runtime support inheritance or GitHub/JIT/private/physical/capacity claims" +elif [[ "$T6_PRODUCT" == "garm-controller-f0f5" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact-target Foundry GARM controller completed F0-F5: exact profile/materialization identity, create/read-back, product health and persistent state, stop/start/update/redeploy, second-plan NOOP with WAIT/fail-closed reconciliation, retain-data delete/reinstall/post-reinstall NOOP, and final zero-residue cleanup" elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then From 8be971b224720e115b2f564f2be68ff58c488b71 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:19:23 +0200 Subject: [PATCH 546/719] test: lock GARM controller F0-F5 reconciliation contract --- tests/test_truenas_garm_controller_f0_f5.py | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tests/test_truenas_garm_controller_f0_f5.py diff --git a/tests/test_truenas_garm_controller_f0_f5.py b/tests/test_truenas_garm_controller_f0_f5.py new file mode 100644 index 000000000..5494438b6 --- /dev/null +++ b/tests/test_truenas_garm_controller_f0_f5.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +import importlib.util +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "truenas_middleware_garm_f0_f5_probe", + ROOT / "scripts" / "truenas_middleware_garm_f0_f5_probe.py", +) +MOD = importlib.util.module_from_spec(SPEC) +assert SPEC.loader +SPEC.loader.exec_module(MOD) + + +class GarmControllerF0F5Tests(unittest.TestCase): + def test_reconciliation_actions_are_fail_closed(self): + desired = {"services": {"garm": {"image": "x"}}} + self.assertEqual(MOD.reconciliation_action(None, None, desired), "CREATE") + for state in ("DEPLOYING", "STOPPING"): + self.assertEqual( + MOD.reconciliation_action(state, desired, desired), "WAIT" + ) + for state in ("CRASHED", "ERROR", "UNKNOWN"): + self.assertEqual( + MOD.reconciliation_action(state, desired, desired), "FAIL_CLOSED" + ) + self.assertEqual( + MOD.reconciliation_action("RUNNING", None, desired), "FAIL_CLOSED" + ) + self.assertEqual( + MOD.reconciliation_action("RUNNING", desired, desired), "NOOP" + ) + self.assertEqual( + MOD.reconciliation_action( + "RUNNING", {"services": {"garm": {"image": "drift"}}}, desired + ), + "UPDATE", + ) + + def test_required_methods_cover_f0_f5_mutations_and_readback(self): + expected = { + "system.version", + "app.query", + "app.config", + "app.create", + "app.update", + "app.redeploy", + "app.stop", + "app.start", + "app.delete", + "pool.dataset.query", + "pool.dataset.create", + "pool.dataset.delete", + "filesystem.mkdir", + "filesystem.stat", + } + self.assertTrue(expected.issubset(MOD.REQUIRED_METHODS)) + + def test_secret_normalization_is_stable(self): + compose = { + "configs": { + MOD.BOOTSTRAP_CONFIG: {"content": "secret"}, + MOD.TLS_CERT_CONFIG: {"content": "cert"}, + MOD.TLS_KEY_CONFIG: {"content": "key"}, + } + } + normalized = MOD.normalize_secret_configs(compose) + self.assertEqual( + normalized["configs"][MOD.BOOTSTRAP_CONFIG]["content"], + MOD.BOOTSTRAP_PLACEHOLDER, + ) + self.assertEqual( + normalized["configs"][MOD.TLS_CERT_CONFIG]["content"], + MOD.TLS_PREFIX + MOD.TLS_CERT_CONFIG, + ) + self.assertEqual( + normalized["configs"][MOD.TLS_KEY_CONFIG]["content"], + MOD.TLS_PREFIX + MOD.TLS_KEY_CONFIG, + ) + + +if __name__ == "__main__": + unittest.main() From e580b71c665a07493c42dfe24cc2a7e1d30d90a1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:19:48 +0200 Subject: [PATCH 547/719] rdte: consume merged exact-target GARM F0-F5 producer --- .github/workflows/gha-kvm-system-rdte.yml | 25 ++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 73f2eb013..1b5fb23d1 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -212,7 +212,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_t6_foliorelay_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_garm_controller_f0_f5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_t6_foliorelay_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -241,6 +241,16 @@ jobs: permissions: contents: read + foundry-garm-f0-f5-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-controller-f0f5' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-garm-f0-f5-control.yml@bfbf2158c3f679c3422e6035bdb67e7874a5c07a + with: + foundry_ref: bfbf2158c3f679c3422e6035bdb67e7874a5c07a + target_version: ${{ needs.changes.outputs.truenas_version }} + permissions: + contents: read + foundry-official-catalog-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' @@ -396,7 +406,7 @@ jobs: group: agent-dispatch-heavyweight-rdte queue: max cancel-in-progress: false - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] if: >- always() && needs.contract.result == 'success' && @@ -407,6 +417,7 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'litellm' && needs['foundry-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'wow-sidecar' && needs['foundry-wow-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm' && needs['foundry-garm-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'garm-controller-f0f5' && needs['foundry-garm-f0-f5-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'foliorelay' && needs['foundry-foliorelay-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || @@ -438,6 +449,12 @@ jobs: with: name: garm-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact GARM Foundry F0-F5 control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-controller-f0f5' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-truenas-f0-f5-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Download exact official-catalog Foundry T6 control if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'official-catalog' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c @@ -525,7 +542,9 @@ jobs: --g3-fixture-producer "e92d4024e6e60c0e2f8bcf5301c11fccb14eaaab" ) else - if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then + if [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-controller-f0f5" ]]; then + foundry_commit="bfbf2158c3f679c3422e6035bdb67e7874a5c07a" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm" ]]; then foundry_commit="7fd0d2474248b61cf8e290fc80f40b761cea5e98" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "wow-sidecar" ]]; then foundry_commit="9ac2eef97581caff5b0de740560c55a778fa3f1c" From 6fc8cbd699ae4aa0da898d65425bb2b1d9c6420b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:20:48 +0200 Subject: [PATCH 548/719] rdte: admit controller F0-F5 selector at workflow edges --- .github/workflows/gha-kvm-system-rdte.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 1b5fb23d1..da9648385 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -47,6 +47,7 @@ on: - litellm - wow-sidecar - garm + - garm-controller-f0f5 - garm-provider-g2 - garm-provider-g3 - garm-provider-g4 @@ -183,7 +184,7 @@ jobs: fi truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ From ba3ffa9f3af4fcf4960e82bf7ffc1e355481f5e0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:20:51 +0200 Subject: [PATCH 549/719] test: guard exact GARM controller F0-F5 routing --- tests/test_gha_kvm_system_rdte.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 006e20533..79a267bf4 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -338,7 +338,7 @@ def test_g5_routing_is_source_exact_cross_version_and_public_safe(self): self.assertIn("truenas_middleware_garm_provider_g5_probe.py", text) self.assertIn('G5_MATRIX_PRODUCER', text) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', text, ) g5_probe = (ROOT / "scripts" / "truenas_middleware_garm_provider_g5_probe.py").read_text( @@ -347,6 +347,28 @@ def test_g5_routing_is_source_exact_cross_version_and_public_safe(self): self.assertNotIn("GITHUB_TOKEN", g5_probe) self.assertNotIn("GH_TOKEN", g5_probe) + def test_garm_controller_f0_f5_route_is_exact_cross_version_and_public_safe(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + script = TRUENAS.read_text(encoding="utf-8") + probe = (ROOT / "scripts" / "truenas_middleware_garm_f0_f5_probe.py").read_text( + encoding="utf-8" + ) + self.assertIn("garm-controller-f0f5", workflow) + self.assertIn( + "export-garm-f0-f5-control.yml@bfbf2158c3f679c3422e6035bdb67e7874a5c07a", + workflow, + ) + self.assertIn( + 'foundry_commit="bfbf2158c3f679c3422e6035bdb67e7874a5c07a"', + workflow, + ) + self.assertIn("garm-truenas-f0-f5-control", workflow) + self.assertIn("truenas_middleware_garm_f0_f5_probe.py", script) + self.assertIn('--expected-profile-blob "$FOUNDRY_PROFILE_BLOB"', script) + self.assertIn('"foundry_profile_blob": os.environ["R_FOUNDRY_PROFILE_BLOB"]', script) + self.assertNotIn("GITHUB_TOKEN", probe) + self.assertNotIn("GH_TOKEN", probe) + def test_no_literal_escaped_shell_parameter_expansions(self): needle = chr(92) + "$" + "{" for path in (PVE, TRUENAS): From bf6eac5a15593addf61d63daedd44bea25ed0463 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:24:53 +0200 Subject: [PATCH 550/719] test: admit GARM controller cross-version selector --- tests/test_truenas_t6_official_catalog_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py index 5aa37d101..25f811ac1 100644 --- a/tests/test_truenas_t6_official_catalog_control.py +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -182,7 +182,7 @@ def test_workflow_and_harness_bind_exact_official_catalog_producer(self): self.assertIn('"official-catalog"', harness) self.assertIn("truenas_middleware_official_catalog_t6_probe.py", harness) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', harness, ) From 1365a6de4ee314c4c13b732a38ae841db35271d0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:24:57 +0200 Subject: [PATCH 551/719] rdte: keep core method discovery as bootstrap oracle --- scripts/truenas_middleware_garm_f0_f5_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_garm_f0_f5_probe.py b/scripts/truenas_middleware_garm_f0_f5_probe.py index 059ebcdec..5b7d1b218 100644 --- a/scripts/truenas_middleware_garm_f0_f5_probe.py +++ b/scripts/truenas_middleware_garm_f0_f5_probe.py @@ -33,7 +33,7 @@ BOOTSTRAP_PLACEHOLDER = "__EPHEMERAL_NESTED_GARM_CONFIG__" TLS_PREFIX = "__EPHEMERAL_NESTED_TLS__:" REQUIRED_METHODS = { - "system.version", "core.get_methods", + "system.version", "app.query", "app.config", "app.create", "app.update", "app.redeploy", "app.stop", "app.start", "app.delete", "pool.dataset.query", "pool.dataset.create", "pool.dataset.delete", From 85a6f45eafc597ceab5d443ec5dd436d85d82410 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:27:19 +0200 Subject: [PATCH 552/719] request: GARM controller BETA.3 F0-F5 row --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 6ec51c570..e041a8fae 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-foliorelay-001-avahi-hostpath-f0-f5", - "version": "25.04.1", + "request_id": "tn-26-beta3-t6-garm-controller-f0-f5-001", + "version": "26.0.0-BETA.3", "rung": "t6", - "product": "foliorelay", - "authority_issue": 411, - "consumer_authority_issue": 273, - "purpose": "Run the exact final FolioRelay F0-F5 qualification row on TrueNAS 25.04.1 using the earned cooperative-Avahi architecture, RFC 6762 legacy-unicast independent observer for 25.x, and merged Foundry host-path contract 6494dbd336260b549772ffa5c398d7a104fabf15 consumed by Agent Dispatch. Require immutable control sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d and CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814 from signed publication run 37457117549/artifact 11410550658, exact 25.04.1 profile/source identity, exact Avahi command/user/read-only system-bus coupling, provider-owned path permissions from Foundry metadata, same-instance DNS-SD, PDF/URF exact-source Inbox preservation, restart/update/redeploy persistence, F4 second-plan NOOP with WAIT/fail-closed ambiguity, F5 retain-data delete/reinstall persistence, and final zero residue. No support is inherited from 25.04.2.6, 25.10.7, BETA.3, or generic T0-T5. Acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." + "product": "garm-controller-f0f5", + "authority_issue": 396, + "consumer_authority_issue": 282, + "purpose": "Run the first exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 26.0.0-BETA.3 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. Require exact profile blob a8ebc533dc84172520eba2237ffeeb52b102dc24 and middleware commit 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." } From 00472177ac268f7858689e8181c87437a7a02c87 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:28:40 +0200 Subject: [PATCH 553/719] test: admit GARM controller request selector --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index c47414c56..7055e4ea5 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "official-catalog", "foliorelay"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "official-catalog", "foliorelay"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From cb4d00f57a831328329c53ee41452b5d52f8a345 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:13:01 +0200 Subject: [PATCH 554/719] Request GARM controller 25.10.7 F0-F5 row --- config/truenas-rdte-run-request.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e041a8fae..36def28a6 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-controller-f0-f5-001", - "version": "26.0.0-BETA.3", + "request_id": "tn-25-10-7-t6-garm-controller-f0-f5-001", + "version": "25.10.7", "rung": "t6", "product": "garm-controller-f0f5", "authority_issue": 396, "consumer_authority_issue": 282, - "purpose": "Run the first exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 26.0.0-BETA.3 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. Require exact profile blob a8ebc533dc84172520eba2237ffeeb52b102dc24 and middleware commit 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." + "purpose": "Run the next exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.10.7 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 is independently accepted and matrix-promoted via Foundry #105 / merge 623d9aefb3c8d508c5461286ae6250ab67daf87e; do not inherit that support. Require exact profile blob 1320cc849a9cc1e7ff6bd78989fc0e4708397248 and middleware commit 8ede398839710e56893d88ce85088139d8fab18e; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." } From 10058e0397a2744dc3e327906eebb27b5e9e7394 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:19:20 +0200 Subject: [PATCH 555/719] Fix 25.10.7 GARM controller target authority --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 36def28a6..3a0433045 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -4,7 +4,7 @@ "version": "25.10.7", "rung": "t6", "product": "garm-controller-f0f5", - "authority_issue": 396, + "authority_issue": 410, "consumer_authority_issue": 282, - "purpose": "Run the next exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.10.7 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 is independently accepted and matrix-promoted via Foundry #105 / merge 623d9aefb3c8d508c5461286ae6250ab67daf87e; do not inherit that support. Require exact profile blob 1320cc849a9cc1e7ff6bd78989fc0e4708397248 and middleware commit 8ede398839710e56893d88ce85088139d8fab18e; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." + "purpose": "Run the corrected exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.10.7 under registered target authority 410 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 is independently accepted and matrix-promoted via Foundry #105 / merge 623d9aefb3c8d508c5461286ae6250ab67daf87e; do not inherit that support. Require exact profile blob 1320cc849a9cc1e7ff6bd78989fc0e4708397248 and middleware commit 8ede398839710e56893d88ce85088139d8fab18e; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." } From f861c79cca2ef048a16c29a3bde863a627d1a940 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:20:57 +0200 Subject: [PATCH 556/719] Launch corrected 25.10.7 GARM controller rep 002 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3a0433045..abf15bc01 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-garm-controller-f0-f5-001", + "request_id": "tn-25-10-7-t6-garm-controller-f0-f5-002-authority-corrected", "version": "25.10.7", "rung": "t6", "product": "garm-controller-f0f5", "authority_issue": 410, "consumer_authority_issue": 282, - "purpose": "Run the corrected exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.10.7 under registered target authority 410 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 is independently accepted and matrix-promoted via Foundry #105 / merge 623d9aefb3c8d508c5461286ae6250ab67daf87e; do not inherit that support. Require exact profile blob 1320cc849a9cc1e7ff6bd78989fc0e4708397248 and middleware commit 8ede398839710e56893d88ce85088139d8fab18e; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." + "purpose": "Run corrected rep 002 of the exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.10.7 under registered target authority 410 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 is independently accepted and matrix-promoted via Foundry #105 / merge 623d9aefb3c8d508c5461286ae6250ab67daf87e; do not inherit that support. Require exact profile blob 1320cc849a9cc1e7ff6bd78989fc0e4708397248 and middleware commit 8ede398839710e56893d88ce85088139d8fab18e; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." } From ad26a75c0f321ec8df19b793bd49a63a16bd5048 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:03:16 +0200 Subject: [PATCH 557/719] Request GARM controller 25.04.2.6 F0-F5 row --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index abf15bc01..d02ee8eaf 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-garm-controller-f0-f5-002-authority-corrected", - "version": "25.10.7", + "request_id": "tn-25-04-2-6-t6-garm-controller-f0-f5-001", + "version": "25.04.2.6", "rung": "t6", "product": "garm-controller-f0f5", - "authority_issue": 410, + "authority_issue": 423, "consumer_authority_issue": 282, - "purpose": "Run corrected rep 002 of the exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.10.7 under registered target authority 410 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 is independently accepted and matrix-promoted via Foundry #105 / merge 623d9aefb3c8d508c5461286ae6250ab67daf87e; do not inherit that support. Require exact profile blob 1320cc849a9cc1e7ff6bd78989fc0e4708397248 and middleware commit 8ede398839710e56893d88ce85088139d8fab18e; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." + "purpose": "Run the exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.04.2.6 under registered target authority 423 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 and 25.10.7 are independently accepted and matrix-promoted, with 25.10.7 promoted via Foundry #106 / merge b40705b419cd4b1941d268d4e5588b90ca6113c0; do not inherit support. Require exact profile blob 631467044411c96a0fe6d361688b99a794f07c80 and middleware commit 244b717370fe35fb9eefc3096acfbc25f12b57b6; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." } From a9abde50b886b814d79ce2210b4b38f18b0115db Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:03:26 +0200 Subject: [PATCH 558/719] Request final GARM controller 25.04.1 F0-F5 row --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index d02ee8eaf..1d64016e8 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-garm-controller-f0-f5-001", - "version": "25.04.2.6", + "request_id": "tn-25-04-1-t6-garm-controller-f0-f5-001", + "version": "25.04.1", "rung": "t6", "product": "garm-controller-f0f5", - "authority_issue": 423, + "authority_issue": 411, "consumer_authority_issue": 282, - "purpose": "Run the exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.04.2.6 under registered target authority 423 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3 and 25.10.7 are independently accepted and matrix-promoted, with 25.10.7 promoted via Foundry #106 / merge b40705b419cd4b1941d268d4e5588b90ca6113c0; do not inherit support. Require exact profile blob 631467044411c96a0fe6d361688b99a794f07c80 and middleware commit 244b717370fe35fb9eefc3096acfbc25f12b57b6; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." + "purpose": "Run the final exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.04.1 under registered target authority 411 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3, 25.10.7, and 25.04.2.6 are independently accepted and matrix-promoted, with 25.04.2.6 promoted via Foundry #107 / merge b6e9bc91ebecc0a2c464fd25952a7dd7209e714b; do not inherit support. Require exact profile blob 800aca038ded29e6a91bfc4c1b1fe2d2199182e8 and middleware commit 74ab5a2d373be4097dece257d00e1086376333ba; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." } From e7388b1e9e7b72a705d6c92b1fb6a9b41fc99efe Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:36:28 +0200 Subject: [PATCH 559/719] RDTE: consume exact GARM Container pre-B4 fixture (#356) * RDTE: add GARM Container pre-B4 runtime oracle * Test GARM Container pre-B4 consumer contract * RDTE: route GARM Container pre-B4 capsule * RDTE: consume exact Container pre-B4 fixture * RDTE: expose Container pre-B4 dispatch selector * RDTE: keep Container pre-B4 outside four-vCPU envelope * RDTE: include Container pre-B4 contract in static gate * RDTE: fix Container fixture workflow wiring * RDTE: require explicit token-file absence evidence * Test fail-closed token absence classification --- .github/workflows/gha-kvm-system-rdte.yml | 31 +- .github/workflows/prep-rdte-static.yml | 3 + scripts/gha_kvm_truenas_rdte.sh | 24 +- ..._middleware_garm_container_pre_b4_probe.py | 381 ++++++++++++++++++ tests/test_truenas_garm_container_pre_b4.py | 71 ++++ 5 files changed, 504 insertions(+), 6 deletions(-) create mode 100644 scripts/truenas_middleware_garm_container_pre_b4_probe.py create mode 100644 tests/test_truenas_garm_container_pre_b4.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index da9648385..8299a0ec3 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -52,6 +52,7 @@ on: - garm-provider-g3 - garm-provider-g4 - garm-provider-g5 + - garm-provider-container-pre-b4 - official-catalog - foliorelay proxmox_compute_fixture: @@ -184,7 +185,7 @@ jobs: fi truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ @@ -213,7 +214,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_garm_controller_f0_f5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_t6_foliorelay_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_garm_container_pre_b4.py tests/test_truenas_garm_controller_f0_f5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_t6_foliorelay_control.py tests/test_truenas_rdte_target.py foundry-t6-control: needs: [changes, contract] @@ -317,6 +318,15 @@ jobs: permissions: contents: read + provider-container-pre-b4-fixture: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-container-pre-b4' + uses: SemperSupra/garm-provider-truenas/.github/workflows/export-container-b0-b5-fixture.yml@ae9d3bf99a010fb962775a185941ed1b732f54c3 + with: + provider_ref: ae9d3bf99a010fb962775a185941ed1b732f54c3 + permissions: + contents: read + qemu-t3-topology: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.qemu_topology == 'true' @@ -407,7 +417,7 @@ jobs: group: agent-dispatch-heavyweight-rdte queue: max cancel-in-progress: false - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix, provider-container-pre-b4-fixture] if: >- always() && needs.contract.result == 'success' && @@ -424,7 +434,8 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') || - (needs.changes.outputs.truenas_t6_product == 'garm-provider-g5' && needs['provider-g5-runner-fixture'].result == 'success' && needs['provider-g5-version-matrix'].result == 'success') + (needs.changes.outputs.truenas_t6_product == 'garm-provider-g5' && needs['provider-g5-runner-fixture'].result == 'success' && needs['provider-g5-version-matrix'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'garm-provider-container-pre-b4' && needs['provider-container-pre-b4-fixture'].result == 'success') ) runs-on: ubuntu-26.04 timeout-minutes: 50 @@ -498,6 +509,12 @@ jobs: with: name: garm-provider-truenas-g5-version-matrix path: ${{ runner.temp }}/gha-kvm-system-rdte/g5-matrix + - name: Download exact GARM Container pre-B4 fixture + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-container-pre-b4' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: garm-provider-truenas-container-pre-b4-fixture + path: ${{ runner.temp }}/gha-kvm-system-rdte/container-pre-b4-fixture - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -534,6 +551,12 @@ jobs: --g4-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/g4-fixture" --g4-fixture-producer "e5fc7c50780de54f7579f64990c87b2e16482613" ) + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-provider-container-pre-b4" ]]; then + args+=( + --t6-product "garm-provider-container-pre-b4" + --container-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/container-pre-b4-fixture" + --container-fixture-producer "ae9d3bf99a010fb962775a185941ed1b732f54c3" + ) elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-provider-g5" ]]; then args+=( --t6-product "garm-provider-g5" diff --git a/.github/workflows/prep-rdte-static.yml b/.github/workflows/prep-rdte-static.yml index 1cf627474..2d7f3789e 100644 --- a/.github/workflows/prep-rdte-static.yml +++ b/.github/workflows/prep-rdte-static.yml @@ -89,6 +89,9 @@ jobs: if [[ -f tests/test_truenas_garm_provider_g5.py ]]; then tests+=(tests/test_truenas_garm_provider_g5.py) fi + if [[ -f tests/test_truenas_garm_container_pre_b4.py ]]; then + tests+=(tests/test_truenas_garm_container_pre_b4.py) + fi if [[ -f tests/test_truenas_rdte_target.py ]]; then tests+=(tests/test_truenas_rdte_target.py) fi diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 3ae0a1c3e..337452625 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -18,7 +18,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" } OUT="" @@ -36,6 +36,8 @@ G4_FIXTURE_DIR="" G4_FIXTURE_PRODUCER="" G5_MATRIX_DIR="" G5_MATRIX_PRODUCER="" +CONTAINER_FIXTURE_DIR="" +CONTAINER_FIXTURE_PRODUCER="" SESSION_MANIFEST="" while [[ $# -gt 0 ]]; do case "$1" in @@ -55,6 +57,8 @@ while [[ $# -gt 0 ]]; do --g4-fixture-producer) G4_FIXTURE_PRODUCER="$2"; shift 2 ;; --g5-matrix-dir) G5_MATRIX_DIR="$2"; shift 2 ;; --g5-matrix-producer) G5_MATRIX_PRODUCER="$2"; shift 2 ;; + --container-fixture-dir) CONTAINER_FIXTURE_DIR="$2"; shift 2 ;; + --container-fixture-producer) CONTAINER_FIXTURE_PRODUCER="$2"; shift 2 ;; --session-manifest) SESSION_MANIFEST="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; @@ -75,7 +79,7 @@ if [[ "$RUNG" == "t6" ]]; then if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "garm-provider-container-pre-b4" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -88,6 +92,10 @@ if [[ "$RUNG" == "t6" ]]; then [[ -n "$G4_FIXTURE_DIR" && -d "$G4_FIXTURE_DIR" ]] || { echo "garm-provider-g4 requires --g4-fixture-dir" >&2; exit 2; } [[ "$G4_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g4 requires exact --g4-fixture-producer SHA" >&2; exit 2; } G4_FIXTURE_DIR="$(realpath "$G4_FIXTURE_DIR")" + elif [[ "$T6_PRODUCT" == "garm-provider-container-pre-b4" ]]; then + [[ -n "$CONTAINER_FIXTURE_DIR" && -d "$CONTAINER_FIXTURE_DIR" ]] || { echo "garm-provider-container-pre-b4 requires --container-fixture-dir" >&2; exit 2; } + [[ "$CONTAINER_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-container-pre-b4 requires exact --container-fixture-producer SHA" >&2; exit 2; } + CONTAINER_FIXTURE_DIR="$(realpath "$CONTAINER_FIXTURE_DIR")" elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then [[ -n "$G5_MATRIX_DIR" && -d "$G5_MATRIX_DIR" ]] || { echo "garm-provider-g5 requires --g5-matrix-dir" >&2; exit 2; } [[ "$G5_MATRIX_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g5 requires exact --g5-matrix-producer SHA" >&2; exit 2; } @@ -340,6 +348,9 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G4 client" command -v docker >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: docker" command -v openssl >/dev/null 2>&1 || fail_evidence ENVIRONMENT_FAILURE preflight "missing prerequisite: openssl" + elif [[ "$T6_PRODUCT" == "garm-provider-container-pre-b4" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_garm_container_pre_b4_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM Container pre-B4 client" elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_garm_provider_g5_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS GARM provider G5 client" @@ -1054,6 +1065,13 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then --fixture-dir "$G4_FIXTURE_DIR" \ --fixture-producer-commit "$G4_FIXTURE_PRODUCER" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "garm-provider-container-pre-b4" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_garm_container_pre_b4_probe.py" \ + --host 127.0.0.1 --http-port "$HTTP_PORT" --middleware-port "$MIDDLEWARE_PORT" \ + --password-file "$PASSWORD_FILE" --pool "$DATA_POOL_NAME" \ + --fixture-dir "$CONTAINER_FIXTURE_DIR" \ + --fixture-producer-commit "$CONTAINER_FIXTURE_PRODUCER" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 180 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_provider_g5_probe.py" \ --host 127.0.0.1 --http-port "$HTTP_PORT" --https-port "$HTTPS_PORT" \ @@ -1137,6 +1155,8 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g3" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider CreateInstance realized the source-derived fixed runner App profile, exact Compose read-back and provider Get/List reconciliation passed, supported middleware moved the experiment App inactive, provider DeleteInstance retired it, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then write_receipt SUPPORTED true foundry-materialization "exact packaged GARM TrueNAS provider realized two source-derived runner Apps concurrently, exact Compose and fresh-process Get/List reconciliation passed for both, active deletion failed closed, both retirement orders reached empty inventory with zero residue, and GitHub/JIT/private workload/physical/capacity claims remained unexercised" +elif [[ "$T6_PRODUCT" == "garm-provider-container-pre-b4" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact BETA.3 GARM Container pre-B4 fixture completed supported container.* creation, rootfs staging, bootstrap execution markers, one-shot credential-file consumption, desired init/env scrub, stop/delete, and zero-residue absence; callback/JIT/runtime admission remain separate gates" elif [[ "$T6_PRODUCT" == "garm-provider-g5" ]]; then write_receipt SUPPORTED true foundry-materialization "exact G5 TrueNAS source-matrix row matched the RDTE target and the fixed packaged provider completed the capacity-one Create/read-back/Get/List/inactive/Delete/absence/empty-inventory/zero-residue lifecycle without runtime support inheritance or GitHub/JIT/private/physical/capacity claims" elif [[ "$T6_PRODUCT" == "garm-controller-f0f5" ]]; then diff --git a/scripts/truenas_middleware_garm_container_pre_b4_probe.py b/scripts/truenas_middleware_garm_container_pre_b4_probe.py new file mode 100644 index 000000000..ffa04c326 --- /dev/null +++ b/scripts/truenas_middleware_garm_container_pre_b4_probe.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +"""Exact BETA.3 GARM Container pre-B4 runtime oracle. + +Proves supported TrueNAS container.* realization, rootfs staging, one-shot init +execution, credential-file consumption, desired-state scrub, and zero residue. +It deliberately does not claim callback/JIT or GitHub registration. +""" +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import pathlib +import secrets +import time +import urllib.request + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + + +EXPECTED_SCHEMA = "semper-supra.garm-provider-truenas-container-pre-b4-fixture/3" +EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" + + +class ProbeError(RuntimeError): + pass + + +def is_explicit_not_found(exc: Exception) -> bool: + text = str(exc).lower() + return '"error": 2' in text or "enoent" in text or "not found" in text + + +def load_fixture(directory: pathlib.Path, producer: str) -> dict: + p = directory / "garm-provider-container-pre-b4-fixture.json" + doc = json.loads(p.read_text(encoding="utf-8")) + if doc.get("schema") != EXPECTED_SCHEMA: + raise ProbeError("unexpected Container fixture schema") + if doc.get("producer_source") != producer: + raise ProbeError("Container fixture producer drifted") + target = doc.get("target") or {} + if target.get("system_version") != EXPECTED_VERSION: + raise ProbeError("fixture target drifted") + if target.get("driver") != "container-v1" or target.get("control_surface") != "container.*": + raise ProbeError("fixture driver drifted") + if target.get("status") != "OPEN": + raise ProbeError("fixture must remain non-admitted before runtime evidence") + if (doc.get("source_oracles") or {}).get("runtime_admission_claimed") is not False: + raise ProbeError("fixture illegally claims runtime admission") + return doc + + +def multipart_upload(host: str, port: int, password: str, remote_path: str, content: bytes, mode: int) -> int: + boundary = "----SemperSupra" + secrets.token_hex(12) + data = json.dumps({ + "method": "filesystem.put", + "params": [remote_path, {"append": False, "mode": mode}], + }).encode() + body = bytearray() + def add(name: str, value: bytes, filename: str | None = None, content_type: str | None = None): + body.extend(f"--{boundary}\r\n".encode()) + disp = f'Content-Disposition: form-data; name="{name}"' + if filename is not None: + disp += f'; filename="{filename}"' + body.extend((disp + "\r\n").encode()) + if content_type: + body.extend((f"Content-Type: {content_type}\r\n").encode()) + body.extend(b"\r\n") + body.extend(value) + body.extend(b"\r\n") + add("data", data) + add("file", content, filename="payload", content_type="application/octet-stream") + body.extend(f"--{boundary}--\r\n".encode()) + auth = base64.b64encode(f"truenas_admin:{password}".encode()).decode() + req = urllib.request.Request( + f"http://{host}:{port}/_upload/", + data=bytes(body), + method="POST", + headers={ + "Authorization": f"Basic {auth}", + "Content-Type": f"multipart/form-data; boundary={boundary}", + "Accept": "application/json", + "User-Agent": "SemperSupra-GARM-Container-RDTE/1", + }, + ) + with urllib.request.urlopen(req, timeout=60) as resp: + result = json.loads(resp.read().decode()) + job_id = result.get("job_id") + if isinstance(job_id, bool) or not isinstance(job_id, int): + raise ProbeError(f"filesystem.put upload did not return job id: {result!r}") + return job_id + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--http-port", type=int, required=True) + p.add_argument("--middleware-port", type=int, required=True) + p.add_argument("--password-file", type=pathlib.Path, required=True) + p.add_argument("--fixture-dir", type=pathlib.Path, required=True) + p.add_argument("--fixture-producer-commit", required=True) + p.add_argument("--pool", required=True) + p.add_argument("--out", type=pathlib.Path, required=True) + p.add_argument("--timeout", type=float, default=8.0) + p.add_argument("--job-timeout", type=float, default=600.0) + p.add_argument("--state-timeout", type=float, default=180.0) + a = p.parse_args() + + started = time.time() + receipt = { + "schema": "truenas-garm-container-pre-b4-runtime/v1", + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "b0_b3_b5_pre_b4_complete": False, + "bootstrap_execution_observed": False, + "github_jit_boundary_claimed": False, + "github_registration_exercised": False, + "private_repository_execution": False, + "physical_truenas_mutation": False, + "per_runner_memory_isolation_claimed": False, + "cleanup": {"attempted": False, "absent": False}, + } + ws = None + container_id = None + container_name = None + token = f"container-public-{secrets.token_hex(16)}" + + def emit(code: int) -> int: + receipt["elapsed_seconds"] = round(time.time() - started, 3) + a.out.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(receipt, indent=2, sort_keys=True)) + return code + + try: + fixture = load_fixture(a.fixture_dir, a.fixture_producer_commit) + receipt["fixture_producer_source"] = fixture["producer_source"] + receipt["fixture_schema"] = fixture["schema"] + receipt["target"] = fixture["target"] + receipt["profile"] = fixture["profile"] + container_name = fixture["expected_name"] + + password = a.password_file.read_text(encoding="utf-8").strip() + if not password: + raise ProbeError("password file is empty") + + ws = WebSocket(a.host, a.middleware_port, timeout=a.timeout, tls=False) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) + if connected.get("msg") != "connected": + raise ProbeError(f"DDP connection failed: {connected!r}") + request_id = 1 + def call(method: str, params: list): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + + auth = call("auth.login_ex", [{ + "mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", + "password": password, + }]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise ProbeError("authentication did not return SUCCESS") + + version = call("system.version", []) + receipt["system_version"] = version + if version != EXPECTED_VERSION: + raise ProbeError(f"exact version mismatch: {version!r}") + + methods = call("core.get_methods", []) + required = set((fixture.get("target") or {}).get("required_methods") or []) + missing = sorted(m for m in required if m not in methods) + if missing: + raise ProbeError(f"required middleware methods missing: {missing}") + receipt["required_methods_present"] = True + + existing = call("container.query", [[["name", "=", container_name]]]) + if existing: + raise ProbeError("preexisting named container blocks ownership-safe apply") + + images = call("container.image.query_registry", []) + family = fixture["image_family"] + image_row = next((x for x in images if isinstance(x, dict) and x.get("name") == family), None) + if not image_row: + raise ProbeError(f"image family unavailable: {family}") + versions = [x.get("version") for x in (image_row.get("versions") or []) if isinstance(x, dict) and x.get("version")] + if not versions: + raise ProbeError("image family returned no exact versions") + image_version = versions[-1] + receipt["image"] = {"name": family, "version": image_version} + + desired = fixture["desired_create"] + create_payload = { + "name": container_name, + "description": desired["description"], + "autostart": False, + "idmap": {"type": desired["idmap_type"]}, + "capabilities_policy": desired["capabilities_policy"], + "init": desired["init"], + "initenv": dict(desired["initenv"]), + "pool": a.pool, + "image": {"name": family, "version": image_version}, + } + if token in json.dumps(create_payload): + raise ProbeError("runtime credential leaked into container.create payload") + + create_job = call("container.create", [create_payload]) + if isinstance(create_job, bool) or not isinstance(create_job, int): + raise ProbeError(f"container.create did not return job id: {create_job!r}") + + def wait_job(job_id: int, label: str): + deadline = time.monotonic() + a.job_timeout + while time.monotonic() < deadline: + job = call("core.get_jobs", [[["id", "=", job_id]], {"get": True}]) + if job: + state = job.get("state") + if state == "SUCCESS": + return job + if state in {"FAILED", "ABORTED"}: + raise ProbeError(f"{label} {state}: {job.get('error') or job.get('exception')}") + time.sleep(1) + raise ProbeError(f"{label} timed out") + + wait_job(create_job, "container.create") + rows = call("container.query", [[["name", "=", container_name]]]) + if len(rows) != 1: + raise ProbeError("created container did not read back exactly once") + row = rows[0] + container_id = row.get("id") + if not isinstance(container_id, int): + raise ProbeError("created container has no integer id") + if (row.get("status") or {}).get("state") != "STOPPED": + raise ProbeError("container was not STOPPED before staging") + if row.get("init") != desired["init"] or row.get("initenv") != desired["initenv"]: + raise ProbeError("bootstrap desired state did not read back exactly") + + dataset = row.get("dataset") + if not isinstance(dataset, str) or not dataset: + raise ProbeError("container dataset identity missing") + ds_rows = call("pool.dataset.query", [[["id", "=", dataset]]]) + if not isinstance(ds_rows, list) or len(ds_rows) != 1: + raise ProbeError("container dataset did not resolve exactly once") + mountpoint = ds_rows[0].get("mountpoint") + if not isinstance(mountpoint, str) or not mountpoint.startswith(f"/mnt/{a.pool}/"): + raise ProbeError(f"unsafe container mountpoint: {mountpoint!r}") + receipt["dataset"] = dataset + receipt["mountpoint"] = mountpoint + + staged = [] + for spec in fixture["staged_files"]: + rel = spec["path"] + remote = mountpoint.rstrip("/") + rel + contains_secret = bool(spec["contains_secret"]) + content = token.encode() if contains_secret else spec["content"].encode() + if not contains_secret: + got_sha = hashlib.sha256(content).hexdigest() + if got_sha != spec["sha256"]: + raise ProbeError(f"fixture staged-file digest drift: {rel}") + job_id = multipart_upload(a.host, a.http_port, password, remote, content, int(spec["mode"])) + wait_job(job_id, f"filesystem.put {rel}") + stat = call("filesystem.stat", [remote]) + if stat.get("size") != len(content) or (int(stat.get("mode", 0)) & 0o777) != int(spec["mode"]): + raise ProbeError(f"staged-file readback drift: {rel}") + staged.append({"path": rel, "mode": int(spec["mode"]), "contains_secret": contains_secret}) + receipt["staged_files"] = staged + + call("container.start", [container_id]) + deadline = time.monotonic() + a.state_timeout + while time.monotonic() < deadline: + rows = call("container.query", [[["id", "=", container_id]]]) + if rows and (rows[0].get("status") or {}).get("state") == "RUNNING": + break + time.sleep(1) + else: + raise ProbeError("container did not reach RUNNING") + + marker_stats = {} + for marker in fixture["execution_markers"]: + remote = mountpoint.rstrip("/") + marker + deadline = time.monotonic() + 45 + stat = None + while time.monotonic() < deadline: + try: + stat = call("filesystem.stat", [remote]) + break + except Exception: + time.sleep(1) + if not stat: + raise ProbeError(f"execution marker not observed: {marker}") + marker_stats[marker] = {"size": stat.get("size"), "mode": stat.get("mode")} + receipt["execution_markers"] = marker_stats + receipt["bootstrap_execution_observed"] = True + + token_path = mountpoint.rstrip("/") + "/var/lib/garm-container/bootstrap-instance-token" + try: + call("filesystem.stat", [token_path]) + except RuntimeError as exc: + if not is_explicit_not_found(exc): + raise ProbeError(f"credential-file absence could not be proven: {exc}") from exc + else: + raise ProbeError("one-shot credential file remained after wrapper consumption") + receipt["credential_file_deleted"] = True + + expected_post = fixture["expected_post_start"] + updated = call("container.update", [container_id, { + "init": expected_post["init"], + "initenv": expected_post["initenv"], + }]) + if not isinstance(updated, dict): + rows = call("container.query", [[["id", "=", container_id]]]) + updated = rows[0] if rows else None + if not isinstance(updated, dict) or updated.get("init") != expected_post["init"] or updated.get("initenv") != expected_post["initenv"]: + raise ProbeError("post-start desired-state scrub did not read back") + receipt["desired_state_scrubbed"] = True + + stop_job = call("container.stop", [container_id, {"force_after_timeout": True}]) + if isinstance(stop_job, int) and not isinstance(stop_job, bool): + wait_job(stop_job, "container.stop") + call("container.delete", [container_id]) + deadline = time.monotonic() + a.state_timeout + while time.monotonic() < deadline: + rows = call("container.query", [[["id", "=", container_id]]]) + if not rows: + break + time.sleep(1) + rows = call("container.query", [[["name", "=", container_name]]]) + if rows: + raise ProbeError("container remains after delete") + receipt["cleanup"] = {"attempted": True, "absent": True} + + receipt.update({ + "classification": "SUPPORTED", + "oracleSatisfied": True, + "b0_b3_b5_pre_b4_complete": True, + "detail": ( + "exact BETA.3 container.* candidate completed source/method validation, " + "stopped creation, supported rootfs staging, wrapper/bootstrap-child execution " + "markers, one-shot credential-file consumption, desired init/env scrub, " + "stop/delete and zero-residue absence; callback/JIT remains a separate B4 gate" + ), + }) + return emit(0) + except Exception as exc: + receipt["detail"] = f"{type(exc).__name__}: {str(exc).replace(token, '')}" + if ws is not None and container_id is not None: + receipt["cleanup"]["attempted"] = True + try: + request_id = locals().get("request_id", 1) + def cleanup_call(method: str, params: list): + nonlocal request_id + result = ddp_call(ws, str(request_id), method, params) + request_id += 1 + return result + rows = cleanup_call("container.query", [[["id", "=", container_id]]]) + if rows and (rows[0].get("status") or {}).get("state") == "RUNNING": + result = cleanup_call("container.stop", [container_id, {"force": True}]) + if isinstance(result, int) and not isinstance(result, bool): + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + job = cleanup_call("core.get_jobs", [[["id", "=", result]], {"get": True}]) + if job and job.get("state") in {"SUCCESS", "FAILED", "ABORTED"}: + break + time.sleep(1) + cleanup_call("container.delete", [container_id]) + rows = cleanup_call("container.query", [[["name", "=", container_name]]]) + receipt["cleanup"]["absent"] = not rows + except Exception as cleanup_exc: + receipt["cleanup"]["error"] = f"{type(cleanup_exc).__name__}: {cleanup_exc}" + return emit(2) + finally: + if ws is not None: + try: + ws.close() + except Exception: + pass + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_truenas_garm_container_pre_b4.py b/tests/test_truenas_garm_container_pre_b4.py new file mode 100644 index 000000000..93fc7e8be --- /dev/null +++ b/tests/test_truenas_garm_container_pre_b4.py @@ -0,0 +1,71 @@ +import json +import pathlib +import sys +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import truenas_middleware_garm_container_pre_b4_probe as probe + + +class ContainerPreB4Tests(unittest.TestCase): + def fixture(self, producer="a" * 40): + return { + "schema": probe.EXPECTED_SCHEMA, + "producer_source": producer, + "target": { + "system_version": probe.EXPECTED_VERSION, + "driver": "container-v1", + "control_surface": "container.*", + "status": "OPEN", + "required_methods": ["system.version", "container.query"], + }, + "profile": "truenas-container-linux-general", + "image_family": "ubuntu:noble:amd64:default", + "expected_name": "garm-test", + "source_oracles": { + "runtime_admission_claimed": False, + "github_jit_boundary_claimed": False, + }, + } + + def test_not_found_classifier_is_fail_closed(self): + self.assertTrue(probe.is_explicit_not_found(RuntimeError('filesystem.stat: {"error": 2, "reason": "Path not found"}'))) + self.assertTrue(probe.is_explicit_not_found(RuntimeError("ENOENT"))) + self.assertFalse(probe.is_explicit_not_found(RuntimeError("authentication failed"))) + self.assertFalse(probe.is_explicit_not_found(RuntimeError("transport timeout"))) + + def test_load_accepts_exact_non_admitted_fixture(self): + with tempfile.TemporaryDirectory() as td: + root = pathlib.Path(td) + doc = self.fixture() + (root / "garm-provider-container-pre-b4-fixture.json").write_text( + json.dumps(doc), encoding="utf-8" + ) + self.assertEqual(probe.load_fixture(root, "a" * 40)["profile"], "truenas-container-linux-general") + + def test_load_rejects_runtime_admission_claim(self): + with tempfile.TemporaryDirectory() as td: + root = pathlib.Path(td) + doc = self.fixture() + doc["source_oracles"]["runtime_admission_claimed"] = True + (root / "garm-provider-container-pre-b4-fixture.json").write_text( + json.dumps(doc), encoding="utf-8" + ) + with self.assertRaises(probe.ProbeError): + probe.load_fixture(root, "a" * 40) + + def test_load_rejects_producer_drift(self): + with tempfile.TemporaryDirectory() as td: + root = pathlib.Path(td) + (root / "garm-provider-container-pre-b4-fixture.json").write_text( + json.dumps(self.fixture()), encoding="utf-8" + ) + with self.assertRaises(probe.ProbeError): + probe.load_fixture(root, "b" * 40) + + +if __name__ == "__main__": + unittest.main() From 6f4078fda6a7edd62df910f0f22e395d4705afde Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:37:16 +0200 Subject: [PATCH 560/719] Request GARM Container BETA.3 pre-B4 system rep --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 1d64016e8..51facbbbd 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-garm-controller-f0-f5-001", - "version": "25.04.1", + "request_id": "tn-26-beta3-t6-garm-container-pre-b4-001", + "version": "26.0.0-BETA.3", "rung": "t6", - "product": "garm-controller-f0f5", - "authority_issue": 411, - "consumer_authority_issue": 282, - "purpose": "Run the final exact GARM controller generic Foundry F0-F5 matrix row on TrueNAS 25.04.1 under registered target authority 411 using merged Agent Dispatch consumer 5e97ed4af269f7848771801bd9de1485a3502790 and exact merged Foundry producer bfbf2158c3f679c3422e6035bdb67e7874a5c07a. BETA.3, 25.10.7, and 25.04.2.6 are independently accepted and matrix-promoted, with 25.04.2.6 promoted via Foundry #107 / merge b6e9bc91ebecc0a2c464fd25952a7dd7209e714b; do not inherit support. Require exact profile blob 800aca038ded29e6a91bfc4c1b1fe2d2199182e8 and middleware commit 74ab5a2d373be4097dece257d00e1086376333ba; exact GARM appliance/source/materialization identity; create/readback and HTTPS/config/db health; stop/start/update/redeploy persistence; F4 second-plan NOOP with WAIT/fail-closed ambiguity; F5 external-config retention across App delete/reinstall, post-reinstall NOOP, and final zero residue. No GitHub/JIT, physical TrueNAS, capacity, Container, or VM backend claim." + "product": "garm-provider-container-pre-b4", + "authority_issue": 396, + "consumer_authority_issue": 43, + "purpose": "Run the first exact BETA.3 GARM TrueNAS Container backend pre-B4 real-system capsule under registered target authority 396. Bind merged provider source ae9d3bf99a010fb962775a185941ed1b732f54c3 and merged Agent Dispatch consumer e7388b1e9e7b72a705d6c92b1fb6a9b41fc99efe. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; supported container.* create/readback; exact Ubuntu Noble image resolution; provider-owned root-dataset mountpoint; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This is B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." } From be6f1f98296ca0e4aaca70443cb2daa3f62a3f99 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:38:14 +0200 Subject: [PATCH 561/719] RDTE: admit bounded GARM Container request selector --- tests/test_truenas_rdte_target.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 196dc56ac..6a0532b95 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -71,7 +71,7 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","official-catalog","foliorelay"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","garm-provider-container-pre-b4","official-catalog","foliorelay"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": @@ -82,6 +82,8 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request.get("consumer_authority_issue"), 39) if request.get("product") == "garm-provider-g3": self.assertEqual(request.get("consumer_authority_issue"), 40) + if request.get("product") == "garm-provider-container-pre-b4": + self.assertEqual(request.get("consumer_authority_issue"), 43) if request.get("product") == "foliorelay": self.assertEqual(request.get("consumer_authority_issue"), 273) self.assertNotIn("latest", request["version"].lower()) From 2f3d8070675814eafaf9e309a58802358d91242f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:38:17 +0200 Subject: [PATCH 562/719] RDTE: keep WOW request allowlist aligned --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 7055e4ea5..3fd568308 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "official-catalog", "foliorelay"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "garm-provider-container-pre-b4", "official-catalog", "foliorelay"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From 65540814b97d114b2f3055aa6262ba557eeb4e24 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:39:00 +0200 Subject: [PATCH 563/719] Launch corrected GARM Container pre-B4 rep 002 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 51facbbbd..25f1ebdd7 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-container-pre-b4-001", + "request_id": "tn-26-beta3-t6-garm-container-pre-b4-002", "version": "26.0.0-BETA.3", "rung": "t6", "product": "garm-provider-container-pre-b4", "authority_issue": 396, "consumer_authority_issue": 43, - "purpose": "Run the first exact BETA.3 GARM TrueNAS Container backend pre-B4 real-system capsule under registered target authority 396. Bind merged provider source ae9d3bf99a010fb962775a185941ed1b732f54c3 and merged Agent Dispatch consumer e7388b1e9e7b72a705d6c92b1fb6a9b41fc99efe. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; supported container.* create/readback; exact Ubuntu Noble image resolution; provider-owned root-dataset mountpoint; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This is B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." + "purpose": "Run the first exact BETA.3 GARM TrueNAS Container backend pre-B4 real-system capsule under registered target authority 396. Bind merged provider source ae9d3bf99a010fb962775a185941ed1b732f54c3 and merged Agent Dispatch consumer e7388b1e9e7b72a705d6c92b1fb6a9b41fc99efe with request-contract allowlist corrections through 2f3d8070675814eafaf9e309a58802358d91242f. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; supported container.* create/readback; exact Ubuntu Noble image resolution; provider-owned root-dataset mountpoint; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This is B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." } From a1fc1b25510a0bb2709a8ba3c408ce6cefa361e1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:56:12 +0200 Subject: [PATCH 564/719] RDTE: fix Container pre-B4 middleware TLS (#357) * RDTE: honor middleware TLS for Container pre-B4 probe * RDTE: propagate installed middleware TLS to Container probe * Test Container pre-B4 TLS propagation * RDTE: add explicit TLS CLI switch to Container probe --- scripts/gha_kvm_truenas_rdte.sh | 1 + scripts/truenas_middleware_garm_container_pre_b4_probe.py | 3 ++- tests/test_truenas_garm_container_pre_b4.py | 7 +++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 337452625..06835c057 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -1068,6 +1068,7 @@ elif [[ "$T6_PRODUCT" == "garm-provider-g4" ]]; then elif [[ "$T6_PRODUCT" == "garm-provider-container-pre-b4" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_garm_container_pre_b4_probe.py" \ --host 127.0.0.1 --http-port "$HTTP_PORT" --middleware-port "$MIDDLEWARE_PORT" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ --password-file "$PASSWORD_FILE" --pool "$DATA_POOL_NAME" \ --fixture-dir "$CONTAINER_FIXTURE_DIR" \ --fixture-producer-commit "$CONTAINER_FIXTURE_PRODUCER" \ diff --git a/scripts/truenas_middleware_garm_container_pre_b4_probe.py b/scripts/truenas_middleware_garm_container_pre_b4_probe.py index ffa04c326..7eb266a2a 100644 --- a/scripts/truenas_middleware_garm_container_pre_b4_probe.py +++ b/scripts/truenas_middleware_garm_container_pre_b4_probe.py @@ -97,6 +97,7 @@ def main() -> int: p.add_argument("--host", default="127.0.0.1") p.add_argument("--http-port", type=int, required=True) p.add_argument("--middleware-port", type=int, required=True) + p.add_argument("--tls", action="store_true") p.add_argument("--password-file", type=pathlib.Path, required=True) p.add_argument("--fixture-dir", type=pathlib.Path, required=True) p.add_argument("--fixture-producer-commit", required=True) @@ -144,7 +145,7 @@ def emit(code: int) -> int: if not password: raise ProbeError("password file is empty") - ws = WebSocket(a.host, a.middleware_port, timeout=a.timeout, tls=False) + ws = WebSocket(a.host, a.middleware_port, timeout=a.timeout, tls=a.tls) ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) connected = wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}) if connected.get("msg") != "connected": diff --git a/tests/test_truenas_garm_container_pre_b4.py b/tests/test_truenas_garm_container_pre_b4.py index 93fc7e8be..3428312fb 100644 --- a/tests/test_truenas_garm_container_pre_b4.py +++ b/tests/test_truenas_garm_container_pre_b4.py @@ -11,6 +11,13 @@ class ContainerPreB4Tests(unittest.TestCase): + def test_probe_and_harness_propagate_tls(self): + script = (ROOT / "scripts" / "truenas_middleware_garm_container_pre_b4_probe.py").read_text(encoding="utf-8") + harness = (ROOT / "scripts" / "gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + self.assertIn('p.add_argument("--tls", action="store_true")', script) + self.assertIn("tls=a.tls", script) + self.assertIn('"${MIDDLEWARE_TLS_ARG[@]}"', harness) + def fixture(self, producer="a" * 40): return { "schema": probe.EXPECTED_SCHEMA, From 5280e852023ddbfb0c1d742ba13a70e8cf1e80c2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:56:42 +0200 Subject: [PATCH 565/719] Launch GARM Container pre-B4 rep 003 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 25f1ebdd7..93416924f 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-container-pre-b4-002", + "request_id": "tn-26-beta3-t6-garm-container-pre-b4-003", "version": "26.0.0-BETA.3", "rung": "t6", "product": "garm-provider-container-pre-b4", "authority_issue": 396, "consumer_authority_issue": 43, - "purpose": "Run the first exact BETA.3 GARM TrueNAS Container backend pre-B4 real-system capsule under registered target authority 396. Bind merged provider source ae9d3bf99a010fb962775a185941ed1b732f54c3 and merged Agent Dispatch consumer e7388b1e9e7b72a705d6c92b1fb6a9b41fc99efe with request-contract allowlist corrections through 2f3d8070675814eafaf9e309a58802358d91242f. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; supported container.* create/readback; exact Ubuntu Noble image resolution; provider-owned root-dataset mountpoint; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This is B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." + "purpose": "Retry the exact BETA.3 GARM TrueNAS Container backend pre-B4 real-system capsule after the rep-002 WSS transport defect was corrected. Bind merged provider source ae9d3bf99a010fb962775a185941ed1b732f54c3 and corrected Agent Dispatch consumer a1fc1b25510a0bb2709a8ba3c408ce6cefa361e1. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; authenticated WSS middleware transport; supported container.* create/readback; exact Ubuntu Noble image resolution; provider-owned root-dataset mountpoint; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This is B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." } From 62c94e38fdc140a0a9c32add1fc19725aeebbe2d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:26:18 +0200 Subject: [PATCH 566/719] prep(foliorelay): admit dedicated HTTPS TLS host path --- scripts/truenas_middleware_foliorelay_t6_probe.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index b74f1138a..466a9f086 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -120,6 +120,14 @@ def validate_host_path_requirements(control: dict, target_version: str): ROOT+"/secrets":("directory","0700"), TOKEN_PATH:("file","0400"), } + scheme=(runtime.get("management_scheme") or "http").lower() + if scheme=="https": + tls_root=runtime.get("management_tls_root") + if tls_root!=ROOT+"/tls": + raise RuntimeError("HTTPS management TLS root drifted") + expected[tls_root]=("directory","0700") + elif scheme!="http": + raise RuntimeError(f"unsupported management scheme {scheme!r}") by_path={} for item in requirements: if not isinstance(item,dict): From 45ee54933236488764ab0b87754530eaba1c1262 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:26:34 +0200 Subject: [PATCH 567/719] prep(foliorelay): validate management transport isolation --- .../truenas_middleware_foliorelay_t6_probe.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 466a9f086..46bf52072 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -190,8 +190,36 @@ def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): if not expected.issubset(required): raise RuntimeError("required FolioRelay oracle contract drifted") validate_host_path_requirements(control,target_version) + validate_management_transport(control, services) return control, compose +def validate_management_transport(control: dict, services: dict) -> str: + runtime=control.get("runtime") or {} + scheme=(runtime.get("management_scheme") or "http").lower() + if scheme=="http": + return scheme + if scheme!="https": + raise RuntimeError(f"unsupported management scheme {scheme!r}") + if runtime.get("management_tls_root")!=ROOT+"/tls": + raise RuntimeError("HTTPS management TLS root drifted") + if runtime.get("management_tls_state")!="/var/lib/foliorelay-tls": + raise RuntimeError("HTTPS management TLS state target drifted") + required=set(control.get("required_oracles") or []) + for oracle in ("management-tls-ready","management-tls-identity-persistent"): + if oracle not in required: + raise RuntimeError(f"HTTPS control missing required oracle {oracle}") + tls_root=runtime["management_tls_root"] + tls_target=runtime["management_tls_state"] + control_service=services.get("control") or {} + mounts=[m for m in (control_service.get("volumes") or []) if isinstance(m,dict)] + if not any(m.get("source")==tls_root and m.get("target")==tls_target and m.get("read_only") is not True for m in mounts): + raise RuntimeError("HTTPS control missing writable dedicated TLS mount") + for service_name in ("cups","discovery"): + for mount in (services.get(service_name) or {}).get("volumes") or []: + if isinstance(mount,dict) and (mount.get("source")==tls_root or mount.get("target")==tls_target): + raise RuntimeError(f"{service_name} must not receive management TLS state") + return scheme + def multipart_upload(host, port, tls, username, password, remote_path, content, mode, timeout): boundary = "----semper-supra-foliorelay-t6" data = json.dumps({"method":"filesystem.put","params":[remote_path,{"append":False,"mode":mode}]}, separators=(",",":")) From ba5f0a08f2354c74dafd9c5d91e726e8c6a92414 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:26:49 +0200 Subject: [PATCH 568/719] prep(foliorelay): support HTTPS management requests --- scripts/truenas_middleware_foliorelay_t6_probe.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 46bf52072..ad9959471 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -250,10 +250,17 @@ def multipart_upload(host, port, tls, username, password, remote_path, content, raise RuntimeError("filesystem.put did not return job_id") return job_id -def http_bytes(host, port, path, token=None, method="GET", timeout=8.0): - req=urllib.request.Request(f"http://{host}:{port}{path}", method=method) +def http_bytes(host, port, path, token=None, method="GET", timeout=8.0, tls=False): + scheme="https" if tls else "http" + req=urllib.request.Request(f"{scheme}://{host}:{port}{path}", method=method) if token: req.add_header("Authorization", f"Bearer {token}") + if tls: + ctx=ssl.create_default_context() + ctx.check_hostname=False + ctx.verify_mode=ssl.CERT_NONE + with urllib.request.urlopen(req, timeout=timeout, context=ctx) as r: + return r.read() with urllib.request.urlopen(req, timeout=timeout) as r: return r.read() @@ -272,11 +279,11 @@ def middleware_http_bytes(host, port, tls, path, timeout=8.0): raise RuntimeError(f"middleware download HTTP {resp.status}") return payload -def wait_http(host, port, path, timeout_s): +def wait_http(host, port, path, timeout_s, tls=False): deadline=time.monotonic()+timeout_s while time.monotonic() Date: Wed, 7 Oct 2026 23:27:15 +0200 Subject: [PATCH 569/719] prep(foliorelay): fingerprint HTTPS management identity --- .../truenas_middleware_foliorelay_t6_probe.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index ad9959471..27b469cb3 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -264,6 +264,33 @@ def http_bytes(host, port, path, token=None, method="GET", timeout=8.0, tls=Fals with urllib.request.urlopen(req, timeout=timeout) as r: return r.read() +def management_tls_fingerprint(host: str, port: int, server_name: str, timeout: float) -> str: + ctx=ssl.create_default_context() + ctx.check_hostname=False + ctx.verify_mode=ssl.CERT_NONE + with socket.create_connection((host,port),timeout=timeout) as raw: + with ctx.wrap_socket(raw,server_hostname=server_name) as conn: + der=conn.getpeercert(binary_form=True) + if not der: + raise RuntimeError("management TLS peer returned no certificate") + pem=ssl.DER_cert_to_PEM_cert(der) + cert_path=None + try: + with tempfile.NamedTemporaryFile("w",encoding="utf-8",delete=False) as handle: + handle.write(pem) + cert_path=handle.name + checked=subprocess.run( + ["openssl","x509","-in",cert_path,"-noout","-checkhost",server_name], + text=True,capture_output=True, + ) + if checked.returncode!=0: + detail=(checked.stdout+"\n"+checked.stderr).strip() + raise RuntimeError(f"management TLS certificate host mismatch: {detail}") + finally: + if cert_path: + pathlib.Path(cert_path).unlink(missing_ok=True) + return hashlib.sha256(der).hexdigest() + def middleware_http_bytes(host, port, tls, path, timeout=8.0): if tls: ctx=ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE From 672fcb1f67f1a446d393b7f417a8825169e5868b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:27:29 +0200 Subject: [PATCH 570/719] prep(foliorelay): derive management scheme from Foundry control --- scripts/truenas_middleware_foliorelay_t6_probe.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 27b469cb3..13f33070a 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -578,7 +578,10 @@ def main(): } try: control,compose=load_control(a.control_dir,a.foundry_commit,a.target_version) - payload["materialization"]={"schema":control["schema"],"foundry_ref":control["foundry_ref"],"compose_canonical_sha256":canonical_sha256(compose),"control_image":EXPECTED_CONTROL,"cups_image":EXPECTED_CUPS} + management_scheme=validate_management_transport(control,compose.get("services") or {}) + management_tls=management_scheme=="https" + management_tls_initial=None + payload["materialization"]={"schema":control["schema"],"foundry_ref":control["foundry_ref"],"compose_canonical_sha256":canonical_sha256(compose),"control_image":EXPECTED_CONTROL,"cups_image":EXPECTED_CUPS,"management_scheme":management_scheme} password=pathlib.Path(a.password_file).read_text().strip() ws=WebSocket(a.host,a.port,timeout=a.timeout,tls=a.tls); ws.send_json({"msg":"connect","version":"1","support":["1"]}) if wait_for(ws,lambda m:m.get("msg") in {"connected","failed"}).get("msg")!="connected": raise RuntimeError("DDP connection failed") From 3d8a0943906e26c706d70f9e3955ebf1aa5b43f6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:28:04 +0200 Subject: [PATCH 571/719] prep(foliorelay): exercise HTTPS across T6 lifecycle --- .../truenas_middleware_foliorelay_t6_probe.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 13f33070a..c08e48a75 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -697,9 +697,11 @@ def wait_state(name,state): if needed not in exact: raise RuntimeError(f"container identity mismatch: {needed[0]}") readback=call("app.config",[EXPECTED_APP_NAME]) if canonical_sha256(readback)!=canonical_sha256(compose): raise RuntimeError("app.config readback drifted") - if not wait_http(a.host,a.control_port,"/readyz",a.state_timeout): raise RuntimeError("control readyz failed") + if not wait_http(a.host,a.control_port,"/readyz",a.state_timeout,tls=management_tls): raise RuntimeError("control readyz failed") + if management_tls: + management_tls_initial=management_tls_fingerprint(a.host,a.control_port,PUBLIC_HOST,a.timeout) tok=TOKEN.decode().strip() - printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout)) + printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout,tls=management_tls)) uuid=(printer.get("identity") or {}).get("printer_uuid"); uri=printer.get("public_uri") if not isinstance(uuid,str) or not uuid or uri!=PUBLIC_URI: raise RuntimeError("control canonical printer identity drifted") with tempfile.TemporaryDirectory() as td: @@ -716,7 +718,7 @@ def wait_state(name,state): expected={"application/pdf":sha256_bytes(pdf.read_bytes()),"image/urf":sha256_bytes(urf.read_bytes())} deadline=time.monotonic()+60; jobs=None while time.monotonic() Date: Wed, 7 Oct 2026 23:31:38 +0200 Subject: [PATCH 572/719] prep(foliorelay): retain management TLS identity evidence --- scripts/truenas_middleware_foliorelay_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index c08e48a75..c01edae71 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -873,7 +873,7 @@ def wait_state(name,state): payload.update({ "classification":"SUPPORTED","oracleSatisfied":True, "identity":{"printer_uuid":uuid,"public_uri":PUBLIC_URI,"control_cups_uuid_match":True,"dnssd_uuid_match":True,"dnssd_public_uri_match":True}, - "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP","second_plan_noop":True,"retain_data_reinstall":True}, + "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"management_scheme":management_scheme,"management_tls_identity_persistent":(True if management_tls else None),"management_tls_fingerprint_sha256":management_tls_initial,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP","second_plan_noop":True,"retain_data_reinstall":True}, "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True,"service_instance":observed.get("service_instance"),"srv_target":PUBLIC_HOST,"srv_port":PUBLIC_IPP_PORT,"resource_path":PUBLIC_RESOURCE_PATH}, "cleanup":{"apps_absent":True,"fixture_dataset_absent":True,"fixture_mountpoint_absent":True,"zero_residue":True}, "reconciliation":{"second_plan_action":second_plan_action,"post_reinstall_action":reinstall_plan_action,"inflight_policy":"WAIT","ambiguous_policy":"FAIL_CLOSED"}, From 1a0aa42d0ec9063bfd88d0f62234647a322308ad Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:32:00 +0200 Subject: [PATCH 573/719] test(foliorelay): qualify HTTPS consumer contract --- tests/test_truenas_t6_foliorelay_control.py | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 0195b2690..70b44f86d 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -114,6 +114,56 @@ def test_host_path_contract_drives_least_privilege_materialization(self): with self.assertRaisesRegex(RuntimeError,"kind/mode drifted"): MOD.validate_host_path_requirements(widened,"25.10.7") + def test_https_management_contract_is_control_only_and_backward_compatible(self): + self.assertEqual(MOD.validate_management_transport({"runtime":{}},{"control":{},"cups":{},"discovery":{}}),"http") + + tls_root=MOD.ROOT+"/tls" + tls_target="/var/lib/foliorelay-tls" + control={ + "runtime":{ + "management_scheme":"https", + "management_tls_root":tls_root, + "management_tls_state":tls_target, + }, + "required_oracles":["management-tls-ready","management-tls-identity-persistent"], + } + services={ + "control":{"volumes":[{"type":"bind","source":tls_root,"target":tls_target,"read_only":False}]}, + "cups":{"volumes":[]}, + "discovery":{"volumes":[]}, + } + self.assertEqual(MOD.validate_management_transport(control,services),"https") + + leaked=json.loads(json.dumps(services)) + leaked["cups"]["volumes"].append({"type":"bind","source":tls_root,"target":tls_target,"read_only":True}) + with self.assertRaisesRegex(RuntimeError,"cups must not receive"): + MOD.validate_management_transport(control,leaked) + + missing_oracle=json.loads(json.dumps(control)) + missing_oracle["required_oracles"]=["management-tls-ready"] + with self.assertRaisesRegex(RuntimeError,"management-tls-identity-persistent"): + MOD.validate_management_transport(missing_oracle,services) + + def test_https_host_path_contract_adds_private_tls_root(self): + tls_root=MOD.ROOT+"/tls" + requirements=[ + {"path":MOD.ROOT+"/control","kind":"directory","uid":10001,"gid":10001,"mode":"0710"}, + {"path":tls_root,"kind":"directory","uid":10001,"gid":10001,"mode":"0700"}, + {"path":MOD.ROOT+"/artifacts","kind":"directory","uid":10001,"gid":10001,"mode":"0700"}, + {"path":MOD.ROOT+"/cups-state","kind":"directory","uid":10001,"gid":10001,"mode":"0755"}, + {"path":MOD.ROOT+"/cups-spool","kind":"directory","uid":10001,"gid":10001,"mode":"0755"}, + {"path":MOD.ROOT+"/secrets","kind":"directory","uid":10001,"gid":10001,"mode":"0700"}, + {"path":MOD.TOKEN_PATH,"kind":"file","uid":10001,"gid":10001,"mode":"0400"}, + ] + got=MOD.validate_host_path_requirements({ + "runtime":{ + "management_scheme":"https", + "management_tls_root":tls_root, + "host_path_requirements":requirements, + } + },"25.10.7") + self.assertEqual(next(x for x in got if x["path"]==tls_root)["mode"],"0700") + def test_f4_reconciliation_policy_and_f5_retention_contract(self): exact={"services":{"control":{"image":"sha256:exact"}}} drift={"services":{"control":{"image":"sha256:drift"}}} From 7a9b43dbd09caac718657fedd2cde442b9dc895a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:29:47 +0200 Subject: [PATCH 574/719] rdte: pin hardened FolioRelay HTTPS control digest --- scripts/truenas_middleware_foliorelay_t6_probe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index c01edae71..0fafc4682 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -32,7 +32,7 @@ "-dbus-address","unix:path=/run/dbus/system_bus_socket", ] OBSERVER_APP_NAME = "rdte-t6-foliorelay-observer" -EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" +EXPECTED_CONTROL = "ghcr.io/sempersupra/foliorelay-control@sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5" EXPECTED_CUPS = "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" OBSERVER_IMAGE = "docker.io/library/hello-world@sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8" DATASET = "rdtepool/foliorelay-t6" From 9f0d6982c5169c73b249ba7a69c37c1efb0abab4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:29:49 +0200 Subject: [PATCH 575/719] rdte: bind merged FolioRelay HTTPS Foundry authority --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 8299a0ec3..00ef7624b 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -266,9 +266,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@6494dbd336260b549772ffa5c398d7a104fabf15 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e with: - foundry_ref: 6494dbd336260b549772ffa5c398d7a104fabf15 + foundry_ref: 25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -575,7 +575,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="6494dbd336260b549772ffa5c398d7a104fabf15" + foundry_commit="25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 1b40d64bbda9f6d381541df98e654570e3d2d6d1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:31:07 +0200 Subject: [PATCH 576/719] rdte: update FolioRelay exact Foundry binding assertions --- tests/test_truenas_t6_foliorelay_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 70b44f86d..318fcb210 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -296,10 +296,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@6494dbd336260b549772ffa5c398d7a104fabf15", workflow) + self.assertIn("export-foliorelay-t6-control.yml@25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="6494dbd336260b549772ffa5c398d7a104fabf15"', workflow) + self.assertIn('foundry_commit="25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From 43b290499a4bbb21e813ecf68dd440117a3768d7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:32:49 +0200 Subject: [PATCH 577/719] rdte: bind FolioRelay prep workflow to HTTPS authority --- .github/workflows/prep-foliorelay-t6-consumer.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 9b682ae84..e362bcbb2 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@6494dbd336260b549772ffa5c398d7a104fabf15 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e with: - foundry_ref: 6494dbd336260b549772ffa5c398d7a104fabf15 + foundry_ref: 25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e target_version: 25.10.7 permissions: contents: read @@ -126,9 +126,9 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "6494dbd336260b549772ffa5c398d7a104fabf15" and + .foundry_ref == "25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e" and .candidate.truenas_version == "25.10.7" and - .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d" and + .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and .secrets_captured == false ' "$root/control.json" >/dev/null From 50413278de03e59d9cc22da4afe3b08100417acb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:37:29 +0200 Subject: [PATCH 578/719] RDTE: stage FolioRelay BETA.3 WebUI HTTPS F0-F5 --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 93416924f..c123ea2fa 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-container-pre-b4-003", + "request_id": "tn-26-beta3-t6-foliorelay-014-webui-https-f0-f5", "version": "26.0.0-BETA.3", "rung": "t6", - "product": "garm-provider-container-pre-b4", + "product": "foliorelay", "authority_issue": 396, - "consumer_authority_issue": 43, - "purpose": "Retry the exact BETA.3 GARM TrueNAS Container backend pre-B4 real-system capsule after the rep-002 WSS transport defect was corrected. Bind merged provider source ae9d3bf99a010fb962775a185941ed1b732f54c3 and corrected Agent Dispatch consumer a1fc1b25510a0bb2709a8ba3c408ce6cefa361e1. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; authenticated WSS middleware transport; supported container.* create/readback; exact Ubuntu Noble image resolution; provider-owned root-dataset mountpoint; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This is B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." + "consumer_authority_issue": 273, + "purpose": "Requalify the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after the accepted WebUI, product-native HTTPS management, and TLS-key-isolation changes. Bind merged Agent Dispatch consumer 04bdab5fa2bcadd7cfd09f6480f29f7e7ca9e9fd, merged Foundry authority 25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-26.0.0-BETA.3/profile identity; exact three-service readback; host-published HTTPS management on 18443 with private app-network HTTP 18080 not host-published; certificate host binding; stable management TLS leaf fingerprint across restart, update/redeploy, and retained-data reinstall; dedicated 0700 TLS state mounted only into control and absent from CUPS/discovery; canonical printer identity; IPP; PDF and URF exact-source Inbox preservation; same-instance DNS-SD; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data delete/reinstall preserving identity, Inbox, artifact bytes, and TLS identity; and final zero-residue cleanup. Do not inherit the prior c8d578 FolioRelay matrix or infer support from workflow success; acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt with TLS oracles satisfied." } From 0e8ce40f0f82dd29b6b565ef97a1f5ff6c2ca09c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:00:04 +0200 Subject: [PATCH 579/719] rdte: verify TrueNAS FolioRelay Web UI portal readback --- .../truenas_middleware_foliorelay_t6_probe.py | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/scripts/truenas_middleware_foliorelay_t6_probe.py b/scripts/truenas_middleware_foliorelay_t6_probe.py index 0fafc4682..0cb8525aa 100644 --- a/scripts/truenas_middleware_foliorelay_t6_probe.py +++ b/scripts/truenas_middleware_foliorelay_t6_probe.py @@ -181,7 +181,8 @@ def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): raise RuntimeError("control discovery backend disagrees with rendered Compose") required = set(control.get("required_oracles") or []) expected = { - "app-create-running", "config-readback-exact-compose", "portal-ready", + "app-create-running", "config-readback-exact-compose", + "management-endpoint-ready", "truenas-webui-portal-advertised", "ipp-get-printer-attributes", "canonical-uri-coherence", "control-cups-dnssd-uuid-coherence", "dnssd-universal-visible", "pdf-exact-source-inbox", "urf-exact-source-inbox", @@ -191,8 +192,31 @@ def load_control(root: pathlib.Path, foundry_ref: str, target_version: str): raise RuntimeError("required FolioRelay oracle contract drifted") validate_host_path_requirements(control,target_version) validate_management_transport(control, services) + validate_management_portal_contract(control, compose) return control, compose +def validate_management_portal_contract(control: dict, compose: dict) -> str: + runtime=control.get("runtime") or {} + portal=runtime.get("management_portal") + expected={ + "name":"Web UI", + "scheme":"https", + "host":PUBLIC_HOST, + "port":runtime.get("management_port"), + "path":"/", + } + if portal!=expected: + raise RuntimeError("TrueNAS Web UI portal control metadata drifted") + if compose.get("x-portals")!=[expected]: + raise RuntimeError("TrueNAS Web UI portal Compose metadata drifted") + required=set(control.get("required_oracles") or []) + for oracle in ("management-endpoint-ready","truenas-webui-portal-advertised"): + if oracle not in required: + raise RuntimeError(f"portal control missing required oracle {oracle}") + if "portal-ready" in required: + raise RuntimeError("ambiguous legacy portal-ready oracle must not remain") + return f"https://{PUBLIC_HOST}:{expected['port']}{expected['path']}" + def validate_management_transport(control: dict, services: dict) -> str: runtime=control.get("runtime") or {} scheme=(runtime.get("management_scheme") or "http").lower() @@ -581,7 +605,8 @@ def main(): management_scheme=validate_management_transport(control,compose.get("services") or {}) management_tls=management_scheme=="https" management_tls_initial=None - payload["materialization"]={"schema":control["schema"],"foundry_ref":control["foundry_ref"],"compose_canonical_sha256":canonical_sha256(compose),"control_image":EXPECTED_CONTROL,"cups_image":EXPECTED_CUPS,"management_scheme":management_scheme} + management_portal_url=validate_management_portal_contract(control,compose) + payload["materialization"]={"schema":control["schema"],"foundry_ref":control["foundry_ref"],"compose_canonical_sha256":canonical_sha256(compose),"control_image":EXPECTED_CONTROL,"cups_image":EXPECTED_CUPS,"management_scheme":management_scheme,"management_portal_url":management_portal_url} password=pathlib.Path(a.password_file).read_text().strip() ws=WebSocket(a.host,a.port,timeout=a.timeout,tls=a.tls); ws.send_json({"msg":"connect","version":"1","support":["1"]}) if wait_for(ws,lambda m:m.get("msg") in {"connected","failed"}).get("msg")!="connected": raise RuntimeError("DDP connection failed") @@ -691,6 +716,9 @@ def wait_state(name,state): if not isinstance(create,int): raise RuntimeError("app.create did not return job") app_created=True wait_job(create,"app.create"); app=wait_state(EXPECTED_APP_NAME,"RUNNING") + portals=app.get("portals") or {} + if portals!={"Web UI":management_portal_url}: + raise RuntimeError(f"TrueNAS Web UI portal readback drifted: {portals!r}") details=(app.get("active_workloads") or {}).get("container_details") or [] exact={(x.get("service_name"),x.get("image"),x.get("state")) for x in details} for needed in [("control",EXPECTED_CONTROL,"running"),("cups",EXPECTED_CUPS,"running"),("discovery",EXPECTED_CONTROL,"running")]: @@ -813,6 +841,9 @@ def wait_state(name,state): if not wait_http(a.host,a.control_port,"/readyz",a.state_timeout,tls=management_tls): raise RuntimeError("readyz failed after update/redeploy") if management_tls and management_tls_fingerprint(a.host,a.control_port,PUBLIC_HOST,a.timeout)!=management_tls_initial: raise RuntimeError("management TLS identity drifted after update/redeploy") + redeploy_app=wait_state(EXPECTED_APP_NAME,"RUNNING") + if (redeploy_app.get("portals") or {})!={"Web UI":management_portal_url}: + raise RuntimeError("TrueNAS Web UI portal drifted after update/redeploy") redeploy_printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout,tls=management_tls)) redeploy_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout,tls=management_tls)) if (redeploy_printer.get("identity") or {}).get("printer_uuid")!=uuid or redeploy_jobs!=before_jobs: @@ -841,6 +872,9 @@ def wait_state(name,state): raise RuntimeError("readyz failed after retain-data reinstall") if management_tls and management_tls_fingerprint(a.host,a.control_port,PUBLIC_HOST,a.timeout)!=management_tls_initial: raise RuntimeError("management TLS identity drifted after retain-data reinstall") + reinstall_app=wait_state(EXPECTED_APP_NAME,"RUNNING") + if (reinstall_app.get("portals") or {})!={"Web UI":management_portal_url}: + raise RuntimeError("TrueNAS Web UI portal drifted after retain-data reinstall") reinstall_printer=json.loads(http_bytes(a.host,a.control_port,"/api/v1/printer",tok,timeout=a.timeout,tls=management_tls)) reinstall_jobs=json.loads(http_bytes(a.host,a.control_port,"/api/v1/jobs",tok,timeout=a.timeout,tls=management_tls)) if ((reinstall_printer.get("identity") or {}).get("printer_uuid")!=uuid @@ -873,13 +907,13 @@ def wait_state(name,state): payload.update({ "classification":"SUPPORTED","oracleSatisfied":True, "identity":{"printer_uuid":uuid,"public_uri":PUBLIC_URI,"control_cups_uuid_match":True,"dnssd_uuid_match":True,"dnssd_public_uri_match":True}, - "runtime":{"three_services_exact":True,"compose_readback_exact":True,"portal_ready":True,"management_scheme":management_scheme,"management_tls_identity_persistent":(True if management_tls else None),"management_tls_fingerprint_sha256":management_tls_initial,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP","second_plan_noop":True,"retain_data_reinstall":True}, + "runtime":{"three_services_exact":True,"compose_readback_exact":True,"management_endpoint_ready":True,"truenas_webui_portal_advertised":True,"management_portal_url":management_portal_url,"management_portal_persistent":True,"management_scheme":management_scheme,"management_tls_identity_persistent":(True if management_tls else None),"management_tls_fingerprint_sha256":management_tls_initial,"ipp_get_printer_attributes":True,"forwarded_ipp_resource_path_match":True,"pdf_exact_source_inbox":True,"urf_exact_source_inbox":True,"restart_preserved_identity_and_inbox":True,"update_redeploy_preserved_identity_and_inbox":True,"replan_action":"NOOP","second_plan_noop":True,"retain_data_reinstall":True}, "dnssd":{"observer_app":OBSERVER_APP_NAME,"universal_visible":True,"distinct_observer_context":True,"service_instance":observed.get("service_instance"),"srv_target":PUBLIC_HOST,"srv_port":PUBLIC_IPP_PORT,"resource_path":PUBLIC_RESOURCE_PATH}, "cleanup":{"apps_absent":True,"fixture_dataset_absent":True,"fixture_mountpoint_absent":True,"zero_residue":True}, "reconciliation":{"second_plan_action":second_plan_action,"post_reinstall_action":reinstall_plan_action,"inflight_policy":"WAIT","ambiguous_policy":"FAIL_CLOSED"}, "owned_data":{"policy":"RETAIN_EXTERNAL_DATASET_ON_APP_DELETE_THEN_EXPLICIT_FIXTURE_CLEANUP","dataset_retained_across_app_delete":True,"identity_preserved_after_reinstall":True,"inbox_preserved_after_reinstall":True,"artifact_bytes_preserved_after_reinstall":True}, "producer_gate":{"replan_noop_required":True,"runtime_does_not_reconstruct_foundry_control":True}, - "detail":"exact Foundry-exported FolioRelay control realized on TrueNAS; exact three-service identity, portal/IPP, PDF+URF source preservation, independent in-guest DNS-SD observation, restart/update/redeploy persistence, second-plan NOOP reconciliation, retain-data delete/reinstall persistence, and final zero-residue cleanup passed" + "detail":"exact Foundry-exported FolioRelay control realized on TrueNAS; exact three-service identity, HTTPS management endpoint + TrueNAS Web UI portal + IPP, PDF+URF source preservation, independent in-guest DNS-SD observation, restart/update/redeploy persistence, second-plan NOOP reconciliation, retain-data delete/reinstall persistence, and final zero-residue cleanup passed" }) except Exception as exc: payload["detail"]=f"{type(exc).__name__}: {exc}" From c9ad015bf651f7d57ee55b7e7cd688b92380a5ba Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:00:06 +0200 Subject: [PATCH 580/719] rdte: bind portal-aware FolioRelay Foundry authority --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 00ef7624b..76c8dc636 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -266,9 +266,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@5dfa9b430cb83151170c4cc3080e01d605cbcf43 with: - foundry_ref: 25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e + foundry_ref: 5dfa9b430cb83151170c4cc3080e01d605cbcf43 target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -575,7 +575,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e" + foundry_commit="5dfa9b430cb83151170c4cc3080e01d605cbcf43" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 53329bd49b254b20bb4070279d10c2b5ecc21cbb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:00:09 +0200 Subject: [PATCH 581/719] rdte: prep portal-aware FolioRelay control --- .github/workflows/prep-foliorelay-t6-consumer.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index e362bcbb2..3048625e5 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@5dfa9b430cb83151170c4cc3080e01d605cbcf43 with: - foundry_ref: 25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e + foundry_ref: 5dfa9b430cb83151170c4cc3080e01d605cbcf43 target_version: 25.10.7 permissions: contents: read @@ -126,7 +126,7 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e" and + .foundry_ref == "5dfa9b430cb83151170c4cc3080e01d605cbcf43" and .candidate.truenas_version == "25.10.7" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and From 432c44d6cfdd23c69b1cc544a9dbfdf1e0b2dfb1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:00:11 +0200 Subject: [PATCH 582/719] rdte: test TrueNAS Web UI portal oracle --- tests/test_truenas_t6_foliorelay_control.py | 41 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 318fcb210..07761038c 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -35,6 +35,11 @@ def test_probe_preserves_product_oracles_and_zero_residue(self): 'app.update', 'app.redeploy', 'replan_action', '"NOOP"', 'update_redeploy_preserved_identity_and_inbox', 'second_plan_noop', 'retain_data_reinstall', + 'TrueNAS Web UI portal readback drifted', + 'TrueNAS Web UI portal drifted after update/redeploy', + 'TrueNAS Web UI portal drifted after retain-data reinstall', + '"truenas_webui_portal_advertised":True', + '"management_portal_persistent":True', ): self.assertIn(needle, text) observer = (ROOT/"tools"/"foliorelay-observer"/"main.go").read_text(encoding="utf-8") @@ -114,6 +119,38 @@ def test_host_path_contract_drives_least_privilege_materialization(self): with self.assertRaisesRegex(RuntimeError,"kind/mode drifted"): MOD.validate_host_path_requirements(widened,"25.10.7") + def test_true_nas_webui_portal_contract_is_exact(self): + portal={ + "name":"Web UI", + "scheme":"https", + "host":MOD.PUBLIC_HOST, + "port":18443, + "path":"/", + } + control={ + "runtime":{ + "management_port":18443, + "management_portal":portal, + }, + "required_oracles":[ + "management-endpoint-ready", + "truenas-webui-portal-advertised", + ], + } + compose={"x-portals":[portal]} + self.assertEqual( + MOD.validate_management_portal_contract(control,compose), + "https://foliorelay-t6.local:18443/", + ) + drift=json.loads(json.dumps(compose)) + drift["x-portals"][0]["host"]="127.0.0.1" + with self.assertRaisesRegex(RuntimeError,"Compose metadata drifted"): + MOD.validate_management_portal_contract(control,drift) + legacy=json.loads(json.dumps(control)) + legacy["required_oracles"].append("portal-ready") + with self.assertRaisesRegex(RuntimeError,"legacy portal-ready"): + MOD.validate_management_portal_contract(legacy,compose) + def test_https_management_contract_is_control_only_and_backward_compatible(self): self.assertEqual(MOD.validate_management_transport({"runtime":{}},{"control":{},"cups":{},"discovery":{}}),"http") @@ -296,10 +333,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e", workflow) + self.assertIn("export-foliorelay-t6-control.yml@5dfa9b430cb83151170c4cc3080e01d605cbcf43", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e"', workflow) + self.assertIn('foundry_commit="5dfa9b430cb83151170c4cc3080e01d605cbcf43"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From 9104ee6d8356562ec743164ea1700982c183af03 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:38:26 +0200 Subject: [PATCH 583/719] FolioRelay: rebind portal consumer to healthcheck-fixed Foundry --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 76c8dc636..73c2e748d 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -266,9 +266,9 @@ jobs: foundry-foliorelay-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'foliorelay' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@5dfa9b430cb83151170c4cc3080e01d605cbcf43 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@81deb97185760975fd8d3162df42056c77b3c0fd with: - foundry_ref: 5dfa9b430cb83151170c4cc3080e01d605cbcf43 + foundry_ref: 81deb97185760975fd8d3162df42056c77b3c0fd target_version: ${{ needs.changes.outputs.truenas_version }} permissions: contents: read @@ -575,7 +575,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "official-catalog" ]]; then foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then - foundry_commit="5dfa9b430cb83151170c4cc3080e01d605cbcf43" + foundry_commit="81deb97185760975fd8d3162df42056c77b3c0fd" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi From 4854c72cd016958ac86448000398338c1fc0295c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:38:28 +0200 Subject: [PATCH 584/719] FolioRelay: rebind portal consumer to healthcheck-fixed Foundry --- .github/workflows/prep-foliorelay-t6-consumer.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prep-foliorelay-t6-consumer.yml b/.github/workflows/prep-foliorelay-t6-consumer.yml index 3048625e5..ed8ab50cf 100644 --- a/.github/workflows/prep-foliorelay-t6-consumer.yml +++ b/.github/workflows/prep-foliorelay-t6-consumer.yml @@ -27,9 +27,9 @@ permissions: jobs: foundry-control: - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@5dfa9b430cb83151170c4cc3080e01d605cbcf43 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-foliorelay-t6-control.yml@81deb97185760975fd8d3162df42056c77b3c0fd with: - foundry_ref: 5dfa9b430cb83151170c4cc3080e01d605cbcf43 + foundry_ref: 81deb97185760975fd8d3162df42056c77b3c0fd target_version: 25.10.7 permissions: contents: read @@ -126,7 +126,7 @@ jobs: test -s "$root/compose.json" jq -e ' .schema == "semper-supra.foliorelay-truenas-t6-control/1" and - .foundry_ref == "5dfa9b430cb83151170c4cc3080e01d605cbcf43" and + .foundry_ref == "81deb97185760975fd8d3162df42056c77b3c0fd" and .candidate.truenas_version == "25.10.7" and .candidate.control_image == "ghcr.io/sempersupra/foliorelay-control@sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5" and .candidate.cups_image == "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814" and From c2968955667fd6a9485fa9eaa03cf982c939b43d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:38:30 +0200 Subject: [PATCH 585/719] FolioRelay: rebind portal consumer to healthcheck-fixed Foundry --- tests/test_truenas_t6_foliorelay_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 07761038c..4aacd59f0 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -333,10 +333,10 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): workflow=(ROOT/".github"/"workflows"/"gha-kvm-system-rdte.yml").read_text(encoding="utf-8") harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") self.assertIn("truenas_t6_product == 'foliorelay'", workflow) - self.assertIn("export-foliorelay-t6-control.yml@5dfa9b430cb83151170c4cc3080e01d605cbcf43", workflow) + self.assertIn("export-foliorelay-t6-control.yml@81deb97185760975fd8d3162df42056c77b3c0fd", workflow) self.assertIn("target_version: ${{ needs.changes.outputs.truenas_version }}", workflow) self.assertIn("name: foliorelay-t6-control", workflow) - self.assertIn('foundry_commit="5dfa9b430cb83151170c4cc3080e01d605cbcf43"', workflow) + self.assertIn('foundry_commit="81deb97185760975fd8d3162df42056c77b3c0fd"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) From 11ccac51349149fd7d6d6397eb66b7753e6d9756 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:40:27 +0200 Subject: [PATCH 586/719] RDTE: request FolioRelay BETA.3 HTTPS healthfix rep 015 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index c123ea2fa..191c903f4 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-014-webui-https-f0-f5", + "request_id": "tn-26-beta3-t6-foliorelay-015-webui-https-healthfix-f0-f5", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Requalify the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after the accepted WebUI, product-native HTTPS management, and TLS-key-isolation changes. Bind merged Agent Dispatch consumer 04bdab5fa2bcadd7cfd09f6480f29f7e7ca9e9fd, merged Foundry authority 25801ed41ff15f4f68c2878dac8b32c2ee2a2e0e, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-26.0.0-BETA.3/profile identity; exact three-service readback; host-published HTTPS management on 18443 with private app-network HTTP 18080 not host-published; certificate host binding; stable management TLS leaf fingerprint across restart, update/redeploy, and retained-data reinstall; dedicated 0700 TLS state mounted only into control and absent from CUPS/discovery; canonical printer identity; IPP; PDF and URF exact-source Inbox preservation; same-instance DNS-SD; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data delete/reinstall preserving identity, Inbox, artifact bytes, and TLS identity; and final zero-residue cleanup. Do not inherit the prior c8d578 FolioRelay matrix or infer support from workflow success; acceptance requires a retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt with TLS oracles satisfied." + "purpose": "Requalify the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after the post-WebUI HTTPS rep 014 isolated an inherited control-plane healthcheck on the discovery service. Bind merged Agent Dispatch portal/HTTPS consumer 144cd3e4ef375f9957a501b3bdabfcf0724092ec, merged Foundry authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. The sole causal materialization repair from rep 014 disables the inherited control-image HEALTHCHECK on discovery and requires that override in Foundry validation; product image/TLS/CUPS/DNS-SD behavior is unchanged. Require exact TrueNAS-26.0.0-BETA.3/profile identity; exact three-service RUNNING readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; host-published HTTPS management on 18443 with private app-network HTTP 18080 not host-published; stable management TLS leaf fingerprint across restart, update/redeploy, and retained-data reinstall; dedicated 0700 TLS state mounted only into control; canonical printer identity; IPP; PDF and URF exact-source Inbox preservation; same-instance DNS-SD; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data delete/reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; and final zero-residue cleanup. Do not inherit any prior matrix PASS or infer support from workflow success; acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." } From 45f1b60071204397010b3b95ddef4409b21eb558 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:34:46 +0200 Subject: [PATCH 587/719] RDTE: forward FolioRelay HTTPS management port --- scripts/gha_kvm_truenas_rdte.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 06835c057..33a63d56e 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -659,7 +659,7 @@ for _ in range(3): print(" ".join(ports)) PY ) - FOLIORELAY_HOSTFWD=",hostfwd=tcp:127.0.0.1:${FOLIORELAY_CONTROL_HOST_PORT}-:18080,hostfwd=tcp:127.0.0.1:${FOLIORELAY_IPP_HOST_PORT}-:8634,hostfwd=tcp:127.0.0.1:${FOLIORELAY_OBSERVER_HOST_PORT}-:18081" + FOLIORELAY_HOSTFWD=",hostfwd=tcp:127.0.0.1:${FOLIORELAY_CONTROL_HOST_PORT}-:18443,hostfwd=tcp:127.0.0.1:${FOLIORELAY_IPP_HOST_PORT}-:8634,hostfwd=tcp:127.0.0.1:${FOLIORELAY_OBSERVER_HOST_PORT}-:18081" fi : >"$STATE_DIR/serial.log" sudo -n qemu-system-x86_64 \ From 3e215fdd887bc1540a30408920c47163d50ee5fb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:34:50 +0200 Subject: [PATCH 588/719] RDTE: regress FolioRelay HTTPS host forwarding --- tests/test_truenas_t6_foliorelay_control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_truenas_t6_foliorelay_control.py b/tests/test_truenas_t6_foliorelay_control.py index 4aacd59f0..19e651cfd 100644 --- a/tests/test_truenas_t6_foliorelay_control.py +++ b/tests/test_truenas_t6_foliorelay_control.py @@ -339,6 +339,8 @@ def test_workflow_and_harness_route_exact_foliorelay_export(self): self.assertIn('foundry_commit="81deb97185760975fd8d3162df42056c77b3c0fd"', workflow) self.assertIn("truenas_middleware_foliorelay_t6_probe.py", harness) self.assertIn('FOLIORELAY_CONTROL_HOST_PORT', harness) + self.assertIn('hostfwd=tcp:127.0.0.1:${FOLIORELAY_CONTROL_HOST_PORT}-:18443', harness) + self.assertNotIn('hostfwd=tcp:127.0.0.1:${FOLIORELAY_CONTROL_HOST_PORT}-:18080', harness) self.assertIn('FOLIORELAY_IPP_HOST_PORT', harness) self.assertIn('FOLIORELAY_OBSERVER_HOST_PORT', harness) self.assertIn('--observer-port "$FOLIORELAY_OBSERVER_HOST_PORT"', harness) From 90288e94e44027589b3ac5955e77996bc305d779 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:37:10 +0200 Subject: [PATCH 589/719] RDTE: request FolioRelay BETA.3 HTTPS hostfwd rep 016 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 191c903f4..0ec9bc64e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-015-webui-https-healthfix-f0-f5", + "request_id": "tn-26-beta3-t6-foliorelay-016-https-hostfwd-f0-f5", "version": "26.0.0-BETA.3", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Requalify the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after the post-WebUI HTTPS rep 014 isolated an inherited control-plane healthcheck on the discovery service. Bind merged Agent Dispatch portal/HTTPS consumer 144cd3e4ef375f9957a501b3bdabfcf0724092ec, merged Foundry authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. The sole causal materialization repair from rep 014 disables the inherited control-image HEALTHCHECK on discovery and requires that override in Foundry validation; product image/TLS/CUPS/DNS-SD behavior is unchanged. Require exact TrueNAS-26.0.0-BETA.3/profile identity; exact three-service RUNNING readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; host-published HTTPS management on 18443 with private app-network HTTP 18080 not host-published; stable management TLS leaf fingerprint across restart, update/redeploy, and retained-data reinstall; dedicated 0700 TLS state mounted only into control; canonical printer identity; IPP; PDF and URF exact-source Inbox preservation; same-instance DNS-SD; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data delete/reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; and final zero-residue cleanup. Do not inherit any prior matrix PASS or infer support from workflow success; acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." + "purpose": "Requalify the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after rep 015 proved all three product containers RUNNING and exact Compose readback, but isolated stale Agent Dispatch QEMU management forwarding to guest HTTP 18080. Bind merged Agent Dispatch HTTPS/portal consumer plus host-forward correction 11e6caed23512b9e042d0500ef8fcd1e2eef88b5, merged Foundry authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. The sole causal delta from rep 015 is the QEMU management hostfwd now targets published HTTPS guest port 18443 instead of private HTTP 18080; IPP 8634, observer 18081, product, Foundry, TLS state, DNS-SD, and storage semantics are unchanged. Require exact TrueNAS-26.0.0-BETA.3/profile identity; exact three-service RUNNING readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; externally reachable HTTPS readyz/API; stable management TLS leaf fingerprint across restart, update/redeploy, and retained-data reinstall; dedicated 0700 TLS state mounted only into control; canonical printer identity; IPP; PDF and URF exact-source Inbox preservation; same-instance DNS-SD; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data delete/reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; and final zero-residue cleanup. Do not inherit prior matrix PASS or infer support from workflow success; acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." } From ee1290a24851f741bdc05f0b21d3e2a71d139a2e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:39:02 +0200 Subject: [PATCH 590/719] RDTE: request refreshed FolioRelay 25.10.7 F0-F5 --- config/truenas-rdte-run-request.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 0ec9bc64e..4cfce42fc 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-foliorelay-016-https-hostfwd-f0-f5", - "version": "26.0.0-BETA.3", + "request_id": "tn-25-10-7-t6-foliorelay-007-webui-https-f0-f5", + "version": "25.10.7", "rung": "t6", "product": "foliorelay", "authority_issue": 396, "consumer_authority_issue": 273, - "purpose": "Requalify the exact FolioRelay 26.0.0-BETA.3 Foundry F0-F5 matrix cell after rep 015 proved all three product containers RUNNING and exact Compose readback, but isolated stale Agent Dispatch QEMU management forwarding to guest HTTP 18080. Bind merged Agent Dispatch HTTPS/portal consumer plus host-forward correction 11e6caed23512b9e042d0500ef8fcd1e2eef88b5, merged Foundry authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. The sole causal delta from rep 015 is the QEMU management hostfwd now targets published HTTPS guest port 18443 instead of private HTTP 18080; IPP 8634, observer 18081, product, Foundry, TLS state, DNS-SD, and storage semantics are unchanged. Require exact TrueNAS-26.0.0-BETA.3/profile identity; exact three-service RUNNING readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; externally reachable HTTPS readyz/API; stable management TLS leaf fingerprint across restart, update/redeploy, and retained-data reinstall; dedicated 0700 TLS state mounted only into control; canonical printer identity; IPP; PDF and URF exact-source Inbox preservation; same-instance DNS-SD; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data delete/reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; and final zero-residue cleanup. Do not inherit prior matrix PASS or infer support from workflow success; acceptance requires retained SUPPORTED/oracleSatisfied=true complete F0-F5 evidence." + "purpose": "Requalify the exact FolioRelay TrueNAS 25.10.7 F0-F5 matrix cell for the accepted WebUI/product-native HTTPS candidate after refreshed BETA.3 rep 016 independently earned support. Bind Agent Dispatch consumer + HTTPS host-forward authority 11e6caed23512b9e042d0500ef8fcd1e2eef88b5, Foundry 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.10.7/profile identity; cooperative host Avahi discovery with the existing 25.x least-privilege control-root contract and independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; final zero residue. Do not inherit BETA.3 or historical 25.10.7 support; acceptance requires this exact row's retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." } From c0421d7fd9036d1ff53025f5476acb03692fb92c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:40:27 +0200 Subject: [PATCH 591/719] RDTE: bind FolioRelay 25.10.7 request to exact authority --- config/truenas-rdte-run-request.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 4cfce42fc..bef943cd0 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -4,7 +4,7 @@ "version": "25.10.7", "rung": "t6", "product": "foliorelay", - "authority_issue": 396, + "authority_issue": 410, "consumer_authority_issue": 273, "purpose": "Requalify the exact FolioRelay TrueNAS 25.10.7 F0-F5 matrix cell for the accepted WebUI/product-native HTTPS candidate after refreshed BETA.3 rep 016 independently earned support. Bind Agent Dispatch consumer + HTTPS host-forward authority 11e6caed23512b9e042d0500ef8fcd1e2eef88b5, Foundry 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.10.7/profile identity; cooperative host Avahi discovery with the existing 25.x least-privilege control-root contract and independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; final zero residue. Do not inherit BETA.3 or historical 25.10.7 support; acceptance requires this exact row's retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." } From 84be7a83d436758788ab08dedf96815053b6c14b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:51:27 +0200 Subject: [PATCH 592/719] RDTE: consume source-derived GARM Container rootfs projection (#369) * RDTE: derive Container rootfs from exact BETA.3 source projection * Test exact source-derived Container rootfs projection * RDTE: bind corrected Container rootfs fixture source --- .github/workflows/gha-kvm-system-rdte.yml | 6 +-- ..._middleware_garm_container_pre_b4_probe.py | 39 +++++++++++++++---- tests/test_truenas_garm_container_pre_b4.py | 32 ++++++++++++++- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 73c2e748d..763f7efde 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -321,9 +321,9 @@ jobs: provider-container-pre-b4-fixture: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-container-pre-b4' - uses: SemperSupra/garm-provider-truenas/.github/workflows/export-container-b0-b5-fixture.yml@ae9d3bf99a010fb962775a185941ed1b732f54c3 + uses: SemperSupra/garm-provider-truenas/.github/workflows/export-container-b0-b5-fixture.yml@9f3c86169320b6a2330bbb345d25a41c736c73c2 with: - provider_ref: ae9d3bf99a010fb962775a185941ed1b732f54c3 + provider_ref: 9f3c86169320b6a2330bbb345d25a41c736c73c2 permissions: contents: read @@ -555,7 +555,7 @@ jobs: args+=( --t6-product "garm-provider-container-pre-b4" --container-fixture-dir "$RUNNER_TEMP/gha-kvm-system-rdte/container-pre-b4-fixture" - --container-fixture-producer "ae9d3bf99a010fb962775a185941ed1b732f54c3" + --container-fixture-producer "9f3c86169320b6a2330bbb345d25a41c736c73c2" ) elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "garm-provider-g5" ]]; then args+=( diff --git a/scripts/truenas_middleware_garm_container_pre_b4_probe.py b/scripts/truenas_middleware_garm_container_pre_b4_probe.py index 7eb266a2a..20cfda349 100644 --- a/scripts/truenas_middleware_garm_container_pre_b4_probe.py +++ b/scripts/truenas_middleware_garm_container_pre_b4_probe.py @@ -21,6 +21,9 @@ EXPECTED_SCHEMA = "semper-supra.garm-provider-truenas-container-pre-b4-fixture/3" EXPECTED_VERSION = "TrueNAS-26.0.0-BETA.3" +EXPECTED_DATASET_TEMPLATE = "{pool}/.truenas_containers/containers/{name}" +EXPECTED_MOUNTPOINT_TEMPLATE = "/.truenas_containers/{pool}/containers/{name}" +EXPECTED_ROOTFS_SOURCE = "src/middlewared/middlewared/plugins/container/utils.py" class ProbeError(RuntimeError): @@ -46,11 +49,37 @@ def load_fixture(directory: pathlib.Path, producer: str) -> dict: raise ProbeError("fixture driver drifted") if target.get("status") != "OPEN": raise ProbeError("fixture must remain non-admitted before runtime evidence") - if (doc.get("source_oracles") or {}).get("runtime_admission_claimed") is not False: + source_oracles = doc.get("source_oracles") or {} + if source_oracles.get("runtime_admission_claimed") is not False: raise ProbeError("fixture illegally claims runtime admission") + if source_oracles.get("source_derived_rootfs_projection_required") is not True: + raise ProbeError("fixture does not require source-derived rootfs projection") + required = set(target.get("required_methods") or []) + if "pool.dataset.query" in required: + raise ProbeError("fixture illegally reintroduced hidden-dataset query lowering") + projection = doc.get("rootfs_projection") or {} + if projection.get("dataset_template") != EXPECTED_DATASET_TEMPLATE: + raise ProbeError("fixture dataset projection drifted") + if projection.get("mountpoint_template") != EXPECTED_MOUNTPOINT_TEMPLATE: + raise ProbeError("fixture mountpoint projection drifted") + if projection.get("source_path") != EXPECTED_ROOTFS_SOURCE: + raise ProbeError("fixture rootfs source path drifted") return doc +def derive_rootfs_mountpoint(fixture: dict, pool: str, name: str, dataset: str) -> str: + projection = fixture.get("rootfs_projection") or {} + expected_dataset = projection["dataset_template"].replace("{pool}", pool).replace("{name}", name) + if dataset != expected_dataset: + raise ProbeError( + f"container dataset drifted from source projection: got {dataset!r}, expected {expected_dataset!r}" + ) + mountpoint = projection["mountpoint_template"].replace("{pool}", pool).replace("{name}", name) + if not mountpoint.startswith("/.truenas_containers/"): + raise ProbeError(f"unsafe source-derived container mountpoint: {mountpoint!r}") + return mountpoint + + def multipart_upload(host: str, port: int, password: str, remote_path: str, content: bytes, mode: int) -> int: boundary = "----SemperSupra" + secrets.token_hex(12) data = json.dumps({ @@ -240,14 +269,10 @@ def wait_job(job_id: int, label: str): dataset = row.get("dataset") if not isinstance(dataset, str) or not dataset: raise ProbeError("container dataset identity missing") - ds_rows = call("pool.dataset.query", [[["id", "=", dataset]]]) - if not isinstance(ds_rows, list) or len(ds_rows) != 1: - raise ProbeError("container dataset did not resolve exactly once") - mountpoint = ds_rows[0].get("mountpoint") - if not isinstance(mountpoint, str) or not mountpoint.startswith(f"/mnt/{a.pool}/"): - raise ProbeError(f"unsafe container mountpoint: {mountpoint!r}") + mountpoint = derive_rootfs_mountpoint(fixture, a.pool, container_name, dataset) receipt["dataset"] = dataset receipt["mountpoint"] = mountpoint + receipt["rootfs_projection_source"] = fixture["rootfs_projection"]["source_path"] staged = [] for spec in fixture["staged_files"]: diff --git a/tests/test_truenas_garm_container_pre_b4.py b/tests/test_truenas_garm_container_pre_b4.py index 3428312fb..bdd4bb495 100644 --- a/tests/test_truenas_garm_container_pre_b4.py +++ b/tests/test_truenas_garm_container_pre_b4.py @@ -27,7 +27,12 @@ def fixture(self, producer="a" * 40): "driver": "container-v1", "control_surface": "container.*", "status": "OPEN", - "required_methods": ["system.version", "container.query"], + "required_methods": ["system.version", "container.query", "filesystem.put", "filesystem.stat"], + }, + "rootfs_projection": { + "dataset_template": probe.EXPECTED_DATASET_TEMPLATE, + "mountpoint_template": probe.EXPECTED_MOUNTPOINT_TEMPLATE, + "source_path": probe.EXPECTED_ROOTFS_SOURCE, }, "profile": "truenas-container-linux-general", "image_family": "ubuntu:noble:amd64:default", @@ -35,9 +40,34 @@ def fixture(self, producer="a" * 40): "source_oracles": { "runtime_admission_claimed": False, "github_jit_boundary_claimed": False, + "source_derived_rootfs_projection_required": True, }, } + def test_rootfs_projection_is_exact_and_fail_closed(self): + doc = self.fixture() + name = doc["expected_name"] + dataset = f"rdtepool/.truenas_containers/containers/{name}" + self.assertEqual( + probe.derive_rootfs_mountpoint(doc, "rdtepool", name, dataset), + f"/.truenas_containers/rdtepool/containers/{name}", + ) + with self.assertRaises(probe.ProbeError): + probe.derive_rootfs_mountpoint( + doc, "rdtepool", name, f"rdtepool/foreign_layout/{name}" + ) + + def test_load_rejects_hidden_dataset_query_reintroduction(self): + with tempfile.TemporaryDirectory() as td: + root = pathlib.Path(td) + doc = self.fixture() + doc["target"]["required_methods"].append("pool.dataset.query") + (root / "garm-provider-container-pre-b4-fixture.json").write_text( + json.dumps(doc), encoding="utf-8" + ) + with self.assertRaises(probe.ProbeError): + probe.load_fixture(root, "a" * 40) + def test_not_found_classifier_is_fail_closed(self): self.assertTrue(probe.is_explicit_not_found(RuntimeError('filesystem.stat: {"error": 2, "reason": "Path not found"}'))) self.assertTrue(probe.is_explicit_not_found(RuntimeError("ENOENT"))) From d8872c2d66848cd19d687bef422d3417308c2a9e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 15:52:43 +0200 Subject: [PATCH 593/719] Prepare GARM Container BETA.3 pre-B4 rep 004 --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index bef943cd0..b5d2afde1 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-foliorelay-007-webui-https-f0-f5", - "version": "25.10.7", + "request_id": "tn-26-beta3-t6-garm-container-pre-b4-004", + "version": "26.0.0-BETA.3", "rung": "t6", - "product": "foliorelay", - "authority_issue": 410, - "consumer_authority_issue": 273, - "purpose": "Requalify the exact FolioRelay TrueNAS 25.10.7 F0-F5 matrix cell for the accepted WebUI/product-native HTTPS candidate after refreshed BETA.3 rep 016 independently earned support. Bind Agent Dispatch consumer + HTTPS host-forward authority 11e6caed23512b9e042d0500ef8fcd1e2eef88b5, Foundry 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.10.7/profile identity; cooperative host Avahi discovery with the existing 25.x least-privilege control-root contract and independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; final zero residue. Do not inherit BETA.3 or historical 25.10.7 support; acceptance requires this exact row's retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." + "product": "garm-provider-container-pre-b4", + "authority_issue": 396, + "consumer_authority_issue": 43, + "purpose": "Run corrected BETA.3 GARM TrueNAS Container pre-B4 real-system rep 004 after rep 003 proved WSS, exact image resolution, container.create and cleanup but failed on a stale hidden-dataset query assumption. Bind merged provider source 9f3c86169320b6a2330bbb345d25a41c736c73c2 and merged Agent Dispatch consumer 84be7a83d436758788ab08dedf96815053b6c14b. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; supported container.* create/readback; exact Ubuntu Noble image resolution; exact source-derived dataset /.truenas_containers/containers/ and rootfs /.truenas_containers//containers/ with no pool.dataset.query dependency; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This remains B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." } From 4d0ecb2fbd4a1e2df80d9429de9ab5f54895bdd3 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:20:57 +0200 Subject: [PATCH 594/719] RDTE: add fail-closed exact BETA.3 GARM VM static fixture consumer --- scripts/truenas_garm_vm_pre_b4_fixture.py | 188 ++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 scripts/truenas_garm_vm_pre_b4_fixture.py diff --git a/scripts/truenas_garm_vm_pre_b4_fixture.py b/scripts/truenas_garm_vm_pre_b4_fixture.py new file mode 100644 index 000000000..7967c911b --- /dev/null +++ b/scripts/truenas_garm_vm_pre_b4_fixture.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Fail-closed, source-only consumer for the exact TrueNAS BETA.3 GARM VM fixture. + +This module does not contact TrueNAS, create a guest, or admit runtime support. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import re + +SCHEMA = "semper-supra.garm-provider-truenas-vm-pre-b4-fixture/1" +TARGET = "TrueNAS-26.0.0-BETA.3" +MIDDLEWARE = "81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3" +TEMPLATE_NAME = "garm_tpl_ubuntu_2404_20260926_amd64" +IMAGE_URL = "https://cloud-images.ubuntu.com/releases/noble/release-20260926/ubuntu-24.04-server-cloudimg-amd64.img" +IMAGE_SHA256 = "6a81c37564db9b1ee84e141922625e1d7c5b389b99bb3c572e0243607d5bb4d2" +TOKEN_PLACEHOLDER = "__RUN_LOCAL_INSTANCE_TOKEN__" +MARKER_PREFIX = "GARM_VM_BOOTSTRAP_CONSUMED_V1:" +REQUIRED_METHODS = { + "system.version", "vm.query", "vm.create", "vm.update", + "vm.clone", "vm.delete", "vm.start", "vm.stop", + "vm.poweroff", "vm.status", "vm.device.query", + "vm.device.create", "vm.device.delete", +} +REQUIRED_TRUE = { + "exact_version_required", + "25_04_1_not_admitted", + "foreign_ownership_rejected", + "classic_vm_name_safe", + "fixed_cpu_memory_profile_required", + "autostart_forbidden", + "zvol_template_clone_required", + "supported_filesystem_put_required", + "stock_template_source_exact", + "self_contained_nocloud_bootstrap", + "unprivileged_runner_required", + "owned_seed_dataset_required", + "owned_cdrom_required", + "active_seed_detach_forbidden", + "bootstrap_consumption_signal_required", + "console_consumption_marker_required", + "seed_device_absence_required", + "seed_dataset_absence_required", + "final_vm_absence_required", +} +REQUIRED_FALSE = { + "runtime_admission_claimed", + "guest_boot_claimed", + "bootstrap_consumption_claimed", + "github_jit_boundary_claimed", + "docker_or_container_actions_claimed", + "windows_or_gpu_claimed", +} + + +class FixtureError(ValueError): + pass + + +def require(ok: bool, reason: str) -> None: + if not ok: + raise FixtureError(reason) + + +def validate_fixture(doc: dict, producer: str) -> dict: + """Return only non-secret source/static evidence. Never log user-data.""" + require(re.fullmatch(r"[0-9a-f]{40}", producer) is not None, "invalid producer SHA") + require(isinstance(doc, dict), "fixture must be an object") + require(doc.get("schema") == SCHEMA, "fixture schema drift") + require(doc.get("producer_source") == producer, "producer ref drift") + require(doc.get("authority") == "SemperSupra/garm-provider-truenas-private#43", "owner authority drift") + + target = doc.get("target") or {} + require(target.get("version") == "26.0.0-BETA.3", "version drift") + require(target.get("system_version") == TARGET, "system version drift") + require(target.get("middleware_commit") == MIDDLEWARE, "middleware source drift") + require(target.get("driver") == "vm-v1", "driver drift") + require(target.get("control_surface") == "vm.*", "VM API drift") + require(target.get("status") == "OPEN", "unearned runtime admission") + require(REQUIRED_METHODS.issubset(set(target.get("required_methods") or [])), "missing VM API methods") + + require(doc.get("profile") == "truenas-vm-linux-general", "profile drift") + require(doc.get("template_family") == "ubuntu-24.04-amd64-template", "template family drift") + require(doc.get("template_version") == "ubuntu-24.04-release-20260926-amd64", "template version drift") + require(doc.get("template_runtime_name") == TEMPLATE_NAME, "runtime template name drift") + require(doc.get("template_source_url") == IMAGE_URL, "stock image URL drift") + require(doc.get("template_source_sha256") == IMAGE_SHA256, "stock image digest drift") + + name = doc.get("expected_name") + require(isinstance(name, str) and re.fullmatch(r"[a-zA-Z0-9_]{1,150}", name) is not None, + "classic VM name illegal") + owner = doc.get("expected_ownership") or {} + require(owner.get("schema") == "semper-supra.garm-vm-owner/1", "ownership schema drift") + require(owner.get("managed_by") == "garm-provider-truenas", "foreign management") + require(owner.get("profile") == doc["profile"], "ownership profile drift") + for key in ("controller_id", "pool_id", "runner_name"): + require(isinstance(owner.get(key), str) and bool(owner[key]), "missing ownership field " + key) + + clone = doc.get("clone") or {} + require(clone.get("vcpus") == 4 and type(clone.get("vcpus")) is int, "CPU profile drift") + require(clone.get("memory_bytes") == 8 * 1024**3 and type(clone.get("memory_bytes")) is int, + "memory profile drift") + require(clone.get("autostart") is False, "autostart forbidden") + require(isinstance(clone.get("description"), str) and owner["managed_by"] in clone["description"], + "ownership not encoded in description") + + seed = doc.get("seed") or {} + require(seed.get("run_local_token_placeholder") == TOKEN_PLACEHOLDER, "token placeholder drift") + require(seed.get("transport") == "provider-owned child dataset + public filesystem.put input pipe", + "seed transport drift") + require(seed.get("attach_device") == "owned vm.device CDROM", "seed attachment drift") + require(seed.get("retain_while_active") is True and seed.get("delete_only_when_stopped") is True, + "unsafe active seed retirement") + require(seed.get("consumption_signal_required") is True, "missing consumption signal") + marker = MARKER_PREFIX + name + require(seed.get("consumption_marker") == marker, "console consumption marker drift") + user_data = seed.get("user_data") + require(isinstance(user_data, str) and TOKEN_PLACEHOLDER in user_data and marker in user_data, + "NoCloud bootstrap material incomplete") + for needle in ("path: /usr/local/libexec/garm-runner-bootstrap", + "path: /usr/local/libexec/garm-bootstrap", + "name: garm-runner", "/usr/sbin/runuser -u garm-runner", + 'rm -f "$env_file"', "/dev/console"): + require(needle in user_data, "NoCloud self-contained/bootstrap invariant absent") + require(TOKEN_PLACEHOLDER not in str(seed.get("meta_data") or ""), "token leaked to meta-data") + require(TOKEN_PLACEHOLDER not in clone["description"], "token leaked to ownership") + + flags = doc.get("source_oracles") or {} + for flag in REQUIRED_TRUE: + require(flags.get(flag) is True, "unproven source invariant: " + flag) + for flag in REQUIRED_FALSE: + require(flags.get(flag) is False, "forbidden support claim: " + flag) + retirement = doc.get("retirement") or [] + require(isinstance(retirement, list) and len(retirement) >= 7, "missing retirement steps") + require(any("vm.stop" in s for s in retirement), "VM stop absent") + require(any("seed dataset absence" in s for s in retirement), "seed cleanup absent") + require(any("final VM absence" in s for s in retirement), "final VM cleanup absent") + + runner = doc.get("runner") or {} + require(re.fullmatch(r"[0-9a-f]{64}", str(runner.get("tool_sha256", ""))) is not None, + "unpinned runner tool") + require(str(runner.get("tool_url", "")).startswith("https://github.com/actions/runner/releases/download/"), + "unexpected runner tool origin") + return { + "schema": "truenas-garm-vm-pre-b4-consumer-static/v1", + "classification": "STATIC_CONTRACT_PASS", + "oracleSatisfied": True, + "source_only": True, + "producer_source": producer, + "target": TARGET, + "driver": "vm-v1", + "template_runtime_name": TEMPLATE_NAME, + "expected_name": name, + "vm_runtime_admitted": False, + "guest_boot_observed": False, + "bootstrap_consumption_observed": False, + "github_jit_exercised": False, + "zero_residue_runtime_proven": False, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--fixture", type=pathlib.Path, required=True) + parser.add_argument("--producer-commit", required=True) + parser.add_argument("--out", type=pathlib.Path, required=True) + args = parser.parse_args() + try: + doc = json.loads(args.fixture.read_text(encoding="utf-8")) + receipt = validate_fixture(doc, args.producer_commit) + except (OSError, ValueError, TypeError, KeyError) as exc: + receipt = { + "schema": "truenas-garm-vm-pre-b4-consumer-static/v1", + "classification": "STATIC_CONTRACT_FAILURE", + "oracleSatisfied": False, + "source_only": True, + "reason": str(exc)[:180], + "vm_runtime_admitted": False, + } + args.out.write_text(json.dumps(receipt, sort_keys=True, indent=2) + "\n", encoding="utf-8") + print(json.dumps({k: v for k, v in receipt.items() if k != "reason"}, sort_keys=True)) + return 0 if receipt["oracleSatisfied"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From 005f59344e6b22f4c09eb217985ae466106a123a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:21:25 +0200 Subject: [PATCH 595/719] RDTE: adversarial VM exact-template/ownership/claim-boundary fixture tests --- tests/test_truenas_garm_vm_pre_b4.py | 165 +++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 tests/test_truenas_garm_vm_pre_b4.py diff --git a/tests/test_truenas_garm_vm_pre_b4.py b/tests/test_truenas_garm_vm_pre_b4.py new file mode 100644 index 000000000..f22364984 --- /dev/null +++ b/tests/test_truenas_garm_vm_pre_b4.py @@ -0,0 +1,165 @@ +import copy +import pathlib +import sys +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import truenas_garm_vm_pre_b4_fixture as vm + +PRODUCER = "5944d1dc62b4da2a247dda9754cfe674caedf5cf" + + +def fixture(): + name = "garm_nested_vm_01" + owner = { + "schema": "semper-supra.garm-vm-owner/1", + "managed_by": "garm-provider-truenas", + "controller_id": "controller", + "pool_id": "pool", + "runner_name": "runner", + "profile": "truenas-vm-linux-general", + } + marker = vm.MARKER_PREFIX + name + flags = {key: True for key in vm.REQUIRED_TRUE} + flags.update({key: False for key in vm.REQUIRED_FALSE}) + return { + "schema": vm.SCHEMA, + "producer_source": PRODUCER, + "authority": "SemperSupra/garm-provider-truenas-private#43", + "target": { + "version": "26.0.0-BETA.3", + "system_version": vm.TARGET, + "driver": "vm-v1", + "control_surface": "vm.*", + "middleware_commit": vm.MIDDLEWARE, + "required_methods": sorted(vm.REQUIRED_METHODS), + "status": "OPEN", + }, + "profile": "truenas-vm-linux-general", + "template_family": "ubuntu-24.04-amd64-template", + "template_version": "ubuntu-24.04-release-20260926-amd64", + "template_runtime_name": vm.TEMPLATE_NAME, + "template_source_url": vm.IMAGE_URL, + "template_source_sha256": vm.IMAGE_SHA256, + "expected_name": name, + "expected_ownership": owner, + "clone": { + "description": "garm-provider-truenas managed VM", + "vcpus": 4, + "memory_bytes": 8 * 1024**3, + "autostart": False, + }, + "seed": { + "run_local_token_placeholder": vm.TOKEN_PLACEHOLDER, + "transport": "provider-owned child dataset + public filesystem.put input pipe", + "attach_device": "owned vm.device CDROM", + "retain_while_active": True, + "delete_only_when_stopped": True, + "consumption_signal_required": True, + "consumption_marker": marker, + "meta_data": "instance-id: owned-1", + "user_data": "\n".join([ + vm.TOKEN_PLACEHOLDER, + marker, + "path: /usr/local/libexec/garm-runner-bootstrap", + "path: /usr/local/libexec/garm-bootstrap", + "name: garm-runner", + "/usr/sbin/runuser -u garm-runner", + 'rm -f "$env_file"', + "/dev/console", + ]), + }, + "source_oracles": flags, + "retirement": [ + "stop VM through vm.stop", "verify STOPPED", + "delete owned NoCloud CDROM through vm.device.delete", + "verify seed device absence", "delete provider-owned seed dataset", + "verify seed dataset absence", + "delete provider-owned VM and cloned boot ZVOL", + "verify final VM absence", + ], + "runner": { + "tool_url": "https://github.com/actions/runner/releases/download/v2.336.0/test.tar.gz", + "tool_sha256": "a" * 64, + }, + } + + +class VMStaticContractTests(unittest.TestCase): + def test_exact_fixture_is_static_only(self): + receipt = vm.validate_fixture(fixture(), PRODUCER) + self.assertEqual(receipt["classification"], "STATIC_CONTRACT_PASS") + self.assertTrue(receipt["source_only"]) + self.assertFalse(receipt["vm_runtime_admitted"]) + self.assertFalse(receipt["bootstrap_consumption_observed"]) + self.assertFalse(receipt["github_jit_exercised"]) + self.assertFalse(receipt["zero_residue_runtime_proven"]) + self.assertNotIn(vm.TOKEN_PLACEHOLDER, str(receipt)) + + def test_rejects_exact_version_and_producer_drift(self): + original = fixture() + for key, value in [ + ("system_version", "TrueNAS-25.10.7"), + ("middleware_commit", "0" * 40), + ("status", "PASS"), + ("driver", "virt-instance-vm"), + ]: + doc = copy.deepcopy(original) + doc["target"][key] = value + with self.subTest(key=key), self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + with self.assertRaises(vm.FixtureError): + vm.validate_fixture(original, "0" * 40) + + def test_rejects_template_and_vm_name_drift(self): + original = fixture() + for key, value in [ + ("template_runtime_name", "ubuntu_current"), + ("template_source_sha256", "0" * 64), + ("expected_name", "garm-runner-with-hyphens"), + ]: + doc = copy.deepcopy(original) + doc[key] = value + with self.subTest(key=key), self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + + def test_rejects_unearned_claims(self): + for flag in vm.REQUIRED_FALSE: + doc = fixture() + doc["source_oracles"][flag] = True + with self.subTest(flag=flag), self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + + def test_rejects_ownership_and_privilege_drift(self): + doc = fixture() + doc["expected_ownership"]["managed_by"] = "foreign-controller" + with self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + doc = fixture() + doc["clone"]["autostart"] = True + with self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + doc = fixture() + doc["seed"]["user_data"] = "missing bootstrap" + with self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + doc = fixture() + doc["seed"]["meta_data"] = vm.TOKEN_PLACEHOLDER + with self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + + def test_rejects_cleanup_and_missing_methods(self): + doc = fixture() + doc["target"]["required_methods"].remove("vm.device.delete") + with self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + doc = fixture() + doc["retirement"] = [] + with self.assertRaises(vm.FixtureError): + vm.validate_fixture(doc, PRODUCER) + + +if __name__ == "__main__": + unittest.main() From 0630dc105c41655ab1e6774edacafa51209435fe Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:22:09 +0200 Subject: [PATCH 596/719] CI: validate exact GARM VM fixture in cheap static PR lane --- .github/workflows/garm-vm-pre-b4-static.yml | 69 +++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/garm-vm-pre-b4-static.yml diff --git a/.github/workflows/garm-vm-pre-b4-static.yml b/.github/workflows/garm-vm-pre-b4-static.yml new file mode 100644 index 000000000..0f0a36caa --- /dev/null +++ b/.github/workflows/garm-vm-pre-b4-static.yml @@ -0,0 +1,69 @@ +name: GARM VM pre-B4 static consumer + +on: + pull_request: + paths: + - .github/workflows/garm-vm-pre-b4-static.yml + - scripts/truenas_garm_vm_pre_b4_fixture.py + - tests/test_truenas_garm_vm_pre_b4.py + +permissions: + contents: read + +jobs: + vm-fixture-static: + runs-on: ubuntu-24.04 + timeout-minutes: 12 + steps: + - name: Checkout Agent Dispatch consumer + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Compile and adversarial source tests + run: | + set -euo pipefail + python3 -m py_compile scripts/truenas_garm_vm_pre_b4_fixture.py tests/test_truenas_garm_vm_pre_b4.py + python3 -m unittest -v tests/test_truenas_garm_vm_pre_b4.py + - name: Export exact public GARM VM source fixture + env: + PROVIDER_REF: 5944d1dc62b4da2a247dda9754cfe674caedf5cf + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/garm-vm-provider" "$RUNNER_TEMP/garm-vm-evidence" + git -C "$RUNNER_TEMP/garm-vm-provider" init -q + git -C "$RUNNER_TEMP/garm-vm-provider" fetch -q --depth=1 \ + https://github.com/SemperSupra/garm-provider-truenas.git "$PROVIDER_REF" + git -C "$RUNNER_TEMP/garm-vm-provider" checkout -q --detach FETCH_HEAD + test "$(git -C "$RUNNER_TEMP/garm-vm-provider" rev-parse HEAD)" = "$PROVIDER_REF" + ( + cd "$RUNNER_TEMP/garm-vm-provider" + GARM_VM_FIXTURE_OUT="$RUNNER_TEMP/garm-vm-evidence/garm-provider-vm-pre-b4-fixture.json" \ + GARM_VM_FIXTURE_SOURCE="$PROVIDER_REF" \ + go test ./internal/vmbackend -run '^TestExportNestedVMPreB4Fixture$' -count=1 + ) + - name: Independently validate exact exported fixture without runtime mutation + run: | + set -euo pipefail + python3 scripts/truenas_garm_vm_pre_b4_fixture.py \ + --fixture "$RUNNER_TEMP/garm-vm-evidence/garm-provider-vm-pre-b4-fixture.json" \ + --producer-commit 5944d1dc62b4da2a247dda9754cfe674caedf5cf \ + --out "$RUNNER_TEMP/garm-vm-evidence/consumer-static-receipt.json" + python3 - "$RUNNER_TEMP/garm-vm-evidence/consumer-static-receipt.json" <<'PY' + import json, sys + r = json.load(open(sys.argv[1])) + assert r["classification"] == "STATIC_CONTRACT_PASS" + assert r["source_only"] is True + assert r["vm_runtime_admitted"] is False + assert r["guest_boot_observed"] is False + assert r["bootstrap_consumption_observed"] is False + assert r["github_jit_exercised"] is False + assert r["zero_residue_runtime_proven"] is False + PY + - name: Retain only sanitized static consumer receipt + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: garm-vm-pre-b4-static-receipt + path: ${{ runner.temp }}/garm-vm-evidence/consumer-static-receipt.json + if-no-files-found: warn + retention-days: 7 From b837d3d40a5225244309461271d8c07bdac5d235 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:52:49 +0200 Subject: [PATCH 597/719] RDTE: add bounded read-only VM template preflight without support admission --- scripts/truenas_garm_vm_template_preflight.py | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 scripts/truenas_garm_vm_template_preflight.py diff --git a/scripts/truenas_garm_vm_template_preflight.py b/scripts/truenas_garm_vm_template_preflight.py new file mode 100644 index 000000000..e9623e39a --- /dev/null +++ b/scripts/truenas_garm_vm_template_preflight.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Bounded, read-only VM template preflight for the exact GARM TrueNAS BETA.3 cell. + +Never creates/clones/starts/deletes VM, media, or datasets; never claims runtime +admission. VM name/readback is not proof of stock-image SHA or Foundry provenance. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +from typing import Any, Callable + +from truenas_garm_vm_pre_b4_fixture import ( + TARGET, REQUIRED_METHODS, validate_fixture, FixtureError, +) + +SNAPSHOT_SCHEMA = "truenas-garm-vm-template-readonly-snapshot/v1" +RECEIPT_SCHEMA = "truenas-garm-vm-template-readonly-preflight/v1" +READ_METHODS = frozenset({ + "system.version", "core.get_methods", "vm.query", "vm.status", "vm.device.query" +}) + + +class PreflightError(ValueError): + pass + + +def _base_receipt() -> dict[str, Any]: + return { + "schema": RECEIPT_SCHEMA, + "classification": "BLOCKED", + "oracleSatisfied": False, + "read_only": True, + "runtime_mutation_performed": False, + "runtime_support_admitted": False, + "template_source_provenance_verified": False, + "guest_boot_observed": False, + "github_jit_exercised": False, + "zero_residue_runtime_proven": False, + } + + +def collect_snapshot(call: Callable[[str, list], Any], template_name: str) -> dict[str, Any]: + """Issue only explicit supported middleware reads, returning a narrow snapshot. + + Authentication is performed outside this function. Raw VM descriptions, API + errors, credentials and guest console text are never retained as evidence. + """ + if not isinstance(template_name, str) or not template_name.startswith("garm_tpl_"): + raise PreflightError("unexpected template identity") + observed_methods: list[str] = [] + + def read(method: str, args: list) -> Any: + if method not in READ_METHODS: + raise PreflightError("non-read method forbidden") + observed_methods.append(method) + return call(method, args) + + version = read("system.version", []) + methods = read("core.get_methods", []) + if not isinstance(methods, dict): + raise PreflightError("method census not a map") + rows = read("vm.query", [[["name", "=", template_name]]]) + if not isinstance(rows, list): + raise PreflightError("vm.query did not return a list") + + template_rows: list[dict[str, Any]] = [] + status = None + devices: list[dict[str, Any]] = [] + for row in rows: + if not isinstance(row, dict): + raise PreflightError("invalid vm.query row") + if row.get("name") != template_name: + raise PreflightError("vm.query returned an unrelated VM") + ident = row.get("id") + if not isinstance(ident, int) or isinstance(ident, bool) or ident <= 0: + raise PreflightError("missing VM ID") + template_rows.append({"name": row["name"], "id": ident}) + + if len(template_rows) == 1: + ident = template_rows[0]["id"] + status_raw = read("vm.status", [ident]) + if not isinstance(status_raw, dict): + raise PreflightError("vm.status response not a map") + status = status_raw.get("state") + device_rows = read("vm.device.query", [[["vm", "=", ident]]]) + if not isinstance(device_rows, list): + raise PreflightError("vm.device.query did not return a list") + for row in device_rows: + if not isinstance(row, dict): + raise PreflightError("VM device row not a map") + attrs = row.get("attributes") or {} + if not isinstance(attrs, dict): + raise PreflightError("VM device attributes not a map") + # No opaque device configuration or sensitive paths in evidence. + path = attrs.get("path") + devices.append({ + "dtype": attrs.get("dtype"), + "zvol_backed": isinstance(path, str) and path.startswith("/dev/zvol/"), + "raw_backed": attrs.get("dtype") == "RAW", + }) + + return { + "schema": SNAPSHOT_SCHEMA, + "version": version, + "available_methods": sorted(methods.keys()), + "template_rows": template_rows, + "template_state": status, + "template_devices": devices, + "read_methods_used": observed_methods, + "source_provenance_receipt": None, # deliberately not fabricated + } + + +def evaluate(fixture: dict[str, Any], producer: str, snapshot: dict[str, Any]) -> dict[str, Any]: + """Evaluate an independently collected observation; never infer template SHA.""" + static = validate_fixture(fixture, producer) + out = _base_receipt() + out["producer_source"] = producer + out["target"] = TARGET + out["template_runtime_name"] = static["template_runtime_name"] + + def block(reason: str) -> dict[str, Any]: + out["reason_code"] = reason + return out + + if not isinstance(snapshot, dict) or snapshot.get("schema") != SNAPSHOT_SCHEMA: + return block("INVALID_SNAPSHOT_SCHEMA") + if snapshot.get("version") != TARGET: + return block("EXACT_TARGET_MISMATCH") + methods = snapshot.get("available_methods") + if not isinstance(methods, list) or not all(isinstance(m, str) for m in methods): + return block("INVALID_METHOD_CENSUS") + if not REQUIRED_METHODS.issubset(set(methods)): + return block("VM_METHODS_ABSENT") + sequence = snapshot.get("read_methods_used") + if not isinstance(sequence, list) or any(m not in READ_METHODS for m in sequence): + return block("UNTRUSTED_OBSERVER") + if sequence[:3] != ["system.version", "core.get_methods", "vm.query"]: + return block("INCOMPLETE_OBSERVER") + rows = snapshot.get("template_rows") + if not isinstance(rows, list): + return block("INVALID_VM_QUERY") + if len(rows) == 0: + return block("TEMPLATE_MISSING") + if len(rows) != 1: + return block("TEMPLATE_AMBIGUOUS") + row = rows[0] + if not isinstance(row, dict) or row.get("name") != static["template_runtime_name"]: + return block("TEMPLATE_NAME_DRIFT") + if not isinstance(row.get("id"), int) or isinstance(row["id"], bool) or row["id"] <= 0: + return block("INVALID_VM_ID") + if sequence[-2:] != ["vm.status", "vm.device.query"]: + return block("INCOMPLETE_TEMPLATE_READBACK") + if snapshot.get("template_state") != "STOPPED": + return block("TEMPLATE_NOT_STOPPED") + devices = snapshot.get("template_devices") + if not isinstance(devices, list): + return block("INVALID_DEVICE_READBACK") + if any(not isinstance(dev, dict) for dev in devices): + return block("INVALID_DEVICE_ROW") + if any(d.get("raw_backed") is True or d.get("dtype") == "RAW" for d in devices): + return block("RAW_DISK_NOT_CLONEABLE") + boot = [d for d in devices if d.get("dtype") == "DISK"] + if len(boot) != 1 or boot[0].get("zvol_backed") is not True: + return block("ZVOL_BOOT_DISK_NOT_EXACT") + # This is a *preflight observation*, not an immutable-image verification. + # vm.query/name/status/device data alone cannot establish a specific + # downloaded cloud image SHA, boot-disk content or bootstrap consumption. + out["classification"] = "TEMPLATE_OBSERVED_SOURCE_UNVERIFIED" + out["oracleSatisfied"] = True + out["reason_code"] = "STOCK_IMAGE_PROVENANCE_SEPARATELY_REQUIRED" + return out + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--fixture", type=pathlib.Path, required=True) + ap.add_argument("--producer-commit", required=True) + ap.add_argument("--snapshot", type=pathlib.Path, required=True) + ap.add_argument("--out", type=pathlib.Path, required=True) + args = ap.parse_args() + try: + fixture = json.loads(args.fixture.read_text(encoding="utf-8")) + snapshot = json.loads(args.snapshot.read_text(encoding="utf-8")) + result = evaluate(fixture, args.producer_commit, snapshot) + except (OSError, json.JSONDecodeError, FixtureError, PreflightError, TypeError, KeyError): + result = _base_receipt() + result["reason_code"] = "INPUT_OR_FIXTURE_VALIDATION_FAILED" + args.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(result, sort_keys=True)) + return 0 if result.get("oracleSatisfied") is True else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From 163cb5c64db62bca4b245a3f654076913a8a5948 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:53:46 +0200 Subject: [PATCH 598/719] RDTE: add VM template read-only preflight falsification tests --- ...test_truenas_garm_vm_template_preflight.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/test_truenas_garm_vm_template_preflight.py diff --git a/tests/test_truenas_garm_vm_template_preflight.py b/tests/test_truenas_garm_vm_template_preflight.py new file mode 100644 index 000000000..1d8374d3f --- /dev/null +++ b/tests/test_truenas_garm_vm_template_preflight.py @@ -0,0 +1,94 @@ +import pathlib +import sys +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) +sys.path.insert(0, str(ROOT / "tests")) +import truenas_garm_vm_template_preflight as pre +from test_truenas_garm_vm_pre_b4 import PRODUCER, fixture + +def snapshot(): + return { + "schema": pre.SNAPSHOT_SCHEMA, + "version": pre.TARGET, + "available_methods": sorted(pre.REQUIRED_METHODS), + "template_rows": [{"name": pre.validate_fixture(fixture(), PRODUCER)["template_runtime_name"], "id": 42}], + "template_state": "STOPPED", + "template_devices": [ + {"dtype": "DISK", "zvol_backed": True, "raw_backed": False}, + {"dtype": "NIC", "zvol_backed": False, "raw_backed": False}, + ], + "read_methods_used": ["system.version", "core.get_methods", "vm.query", + "vm.status", "vm.device.query"], + } + +class VMReadOnlyPreflightTests(unittest.TestCase): + def test_suitable_observation_does_not_prove_source_or_support(self): + result = pre.evaluate(fixture(), PRODUCER, snapshot()) + self.assertEqual(result["classification"], "TEMPLATE_OBSERVED_SOURCE_UNVERIFIED") + self.assertTrue(result["oracleSatisfied"]) + for key in ("runtime_mutation_performed", "runtime_support_admitted", + "template_source_provenance_verified", "guest_boot_observed", + "zero_residue_runtime_proven"): + self.assertFalse(result[key]) + + def test_observer_uses_only_reads_and_redacts_opaque_fields(self): + name = pre.validate_fixture(fixture(), PRODUCER)["template_runtime_name"] + calls = [] + def rpc(method, params): + calls.append(method) + if method == "system.version": return pre.TARGET + if method == "core.get_methods": return {m: {} for m in pre.REQUIRED_METHODS} + if method == "vm.query": return [{"name": name, "id": 42, "opaque": "not-for-receipt"}] + if method == "vm.status": return {"state": "STOPPED"} + if method == "vm.device.query": + return [{"attributes": {"dtype": "DISK", "path": "/dev/zvol/rdtepool/base", "opaque": "not-for-receipt"}}] + raise AssertionError(method) + result = pre.collect_snapshot(rpc, name) + self.assertEqual(calls, ["system.version", "core.get_methods", "vm.query", + "vm.status", "vm.device.query"]) + self.assertNotIn("not-for-receipt", str(result)) + self.assertNotIn("/dev/zvol/rdtepool/base", str(result)) + self.assertEqual(pre.evaluate(fixture(), PRODUCER, result)["classification"], + "TEMPLATE_OBSERVED_SOURCE_UNVERIFIED") + + def test_absent_wrong_version_or_methods_fail_closed(self): + s = snapshot() + s["template_rows"] = [] + s["read_methods_used"] = s["read_methods_used"][:3] + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "TEMPLATE_MISSING") + s = snapshot() + s["version"] = "TrueNAS-25.10.7" + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "EXACT_TARGET_MISMATCH") + s = snapshot() + s["available_methods"].remove("vm.clone") + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "VM_METHODS_ABSENT") + + def test_state_disk_shape_and_ownership(self): + s = snapshot() + s["template_state"] = "RUNNING" + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "TEMPLATE_NOT_STOPPED") + s = snapshot() + s["template_devices"][0] = {"dtype": "RAW", "raw_backed": True} + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "RAW_DISK_NOT_CLONEABLE") + s = snapshot() + s["template_devices"][0]["zvol_backed"] = False + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "ZVOL_BOOT_DISK_NOT_EXACT") + s = snapshot() + s["template_rows"][0]["name"] = "foreign_vm" + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "TEMPLATE_NAME_DRIFT") + s = snapshot() + s["template_rows"].append(s["template_rows"][0].copy()) + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "TEMPLATE_AMBIGUOUS") + + def test_untrusted_observer_and_unsubstantiated_provenance(self): + s = snapshot() + s["read_methods_used"].append("vm.start") + self.assertEqual(pre.evaluate(fixture(), PRODUCER, s)["reason_code"], "UNTRUSTED_OBSERVER") + s = snapshot() + s["source_provenance_receipt"] = {"claimed": True} + self.assertFalse(pre.evaluate(fixture(), PRODUCER, s)["template_source_provenance_verified"]) + +if __name__ == "__main__": + unittest.main() From ae3e193cf30680122394f2f61279da4578faf36c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:54:02 +0200 Subject: [PATCH 599/719] CI: gate read-only VM template preflight and adversarial tests --- .github/workflows/garm-vm-pre-b4-static.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/garm-vm-pre-b4-static.yml b/.github/workflows/garm-vm-pre-b4-static.yml index 0f0a36caa..c947e9bfe 100644 --- a/.github/workflows/garm-vm-pre-b4-static.yml +++ b/.github/workflows/garm-vm-pre-b4-static.yml @@ -6,6 +6,8 @@ on: - .github/workflows/garm-vm-pre-b4-static.yml - scripts/truenas_garm_vm_pre_b4_fixture.py - tests/test_truenas_garm_vm_pre_b4.py + - scripts/truenas_garm_vm_template_preflight.py + - tests/test_truenas_garm_vm_template_preflight.py permissions: contents: read @@ -22,8 +24,8 @@ jobs: - name: Compile and adversarial source tests run: | set -euo pipefail - python3 -m py_compile scripts/truenas_garm_vm_pre_b4_fixture.py tests/test_truenas_garm_vm_pre_b4.py - python3 -m unittest -v tests/test_truenas_garm_vm_pre_b4.py + python3 -m py_compile scripts/truenas_garm_vm_pre_b4_fixture.py tests/test_truenas_garm_vm_pre_b4.py scripts/truenas_garm_vm_template_preflight.py tests/test_truenas_garm_vm_template_preflight.py + python3 -m unittest -v tests/test_truenas_garm_vm_pre_b4.py tests/test_truenas_garm_vm_template_preflight.py - name: Export exact public GARM VM source fixture env: PROVIDER_REF: 5944d1dc62b4da2a247dda9754cfe674caedf5cf From ee066dd72a5ab04bcda499f06e3c66e8b3307ecd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:55:19 +0200 Subject: [PATCH 600/719] RDTE: request refreshed FolioRelay 25.04.2.6 F0-F5 successor --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index bef943cd0..c1f876c17 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-10-7-t6-foliorelay-007-webui-https-f0-f5", - "version": "25.10.7", + "request_id": "tn-25-04-2-6-t6-foliorelay-003-webui-https-f0-f5", + "version": "25.04.2.6", "rung": "t6", "product": "foliorelay", - "authority_issue": 410, + "authority_issue": 423, "consumer_authority_issue": 273, - "purpose": "Requalify the exact FolioRelay TrueNAS 25.10.7 F0-F5 matrix cell for the accepted WebUI/product-native HTTPS candidate after refreshed BETA.3 rep 016 independently earned support. Bind Agent Dispatch consumer + HTTPS host-forward authority 11e6caed23512b9e042d0500ef8fcd1e2eef88b5, Foundry 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.10.7/profile identity; cooperative host Avahi discovery with the existing 25.x least-privilege control-root contract and independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; final zero residue. Do not inherit BETA.3 or historical 25.10.7 support; acceptance requires this exact row's retained SUPPORTED/oracleSatisfied=true complete F0-F5 receipt." + "purpose": "Requalify the exact FolioRelay TrueNAS 25.04.2.6 F0-F5 matrix cell for the accepted WebUI/product-native HTTPS candidate after refreshed BETA.3 and 25.10.7 independently earned support. Bind the current Agent Dispatch execution authority at 0d1f5c1d8808b0bedba5a509e61d4c703938bf63, exact Foundry T6 runtime/materialization authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.04.2.6/profile authority issue 423; cooperative host Avahi discovery and 25.x least-privilege control-root contract; independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; final zero residue. Do not inherit support from BETA.3, 25.10.7, or the historical pre-WebUI 25.04.2.6 receipt." } From 04c1be26b03dbf8c81d6994e34e870c06733cb4c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:12:43 +0200 Subject: [PATCH 601/719] RDTE: source-bind FolioRelay requested-shape images --- scripts/gha_kvm_truenas_rdte.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 33a63d56e..3a807f817 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -176,8 +176,23 @@ write_receipt() { export R_VCPUS="$VCPUS" R_RAM_MIB="$RAM_MIB" export R_RPC_OK="$RPC_DISCOVERY_OK" R_RPC_DISCOVERY="$RPC_DISCOVERY_JSON" R_QEMU_ALIVE="$QEMU_ALIVE_AT_GATE" export R_INSTALL_RESULT="$INSTALL_RESULT_JSON" R_MIDDLEWARE_RESULT="$MIDDLEWARE_RESULT_JSON" R_POOL_RESULT="$POOL_RESULT_JSON" R_COMPUTE_RESULT="$COMPUTE_RESULT_JSON" R_APP_RESULT="$APP_RESULT_JSON" R_LIFECYCLE_RESULT="$LIFECYCLE_RESULT_JSON" R_FOUNDRY_RESULT="$FOUNDRY_RESULT_JSON" R_SESSION_RESULT="$SESSION_RESULT_JSON" + export R_FOUNDRY_CONTROL_DIR="$FOUNDRY_CONTROL_DIR" python3 - <<'PY' import json, os, pathlib + +def exact_foliorelay_requested_images(): + if os.environ.get("R_RUNG") != "t6" or os.environ.get("R_T6_PRODUCT") != "foliorelay": + return None + control_dir = pathlib.Path(os.environ.get("R_FOUNDRY_CONTROL_DIR") or "") + try: + control = json.loads((control_dir / "control.json").read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return None + candidate = control.get("candidate") or {} + images = [candidate.get("control_image"), candidate.get("cups_image")] + return images if all(isinstance(v, str) and "@sha256:" in v for v in images) else None + +foliorelay_images = exact_foliorelay_requested_images() payload = { "contract": "gha-kvm-system-lab/v1", "target": { @@ -212,7 +227,7 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "wow-sidecar" else {"name": "rdte-t6-catalog-ntfy", "catalog_app": "ntfy", "catalog_version": "1.1.21"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" - else {"name": "rdte-t6-foliorelay", "images": ["ghcr.io/sempersupra/foliorelay-control@sha256:c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d", "ghcr.io/sempersupra/foliorelay-cups@sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814"]} + else {"name": "rdte-t6-foliorelay", "images": foliorelay_images} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" From 16ac8c3888cb5f340b0e91aec91405e2682a931f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:12:50 +0200 Subject: [PATCH 602/719] RDTE: regress stale FolioRelay receipt image metadata --- tests/test_truenas_rdte_target.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 6a0532b95..471d7469b 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -56,6 +56,13 @@ def test_installer_rpc_endpoint_is_exact_version_profiled(self): self.assertEqual(target["installer_source_ref"], source_ref) self.assertEqual(target["installer_main_blob_sha"], main_blob) + def test_foliorelay_requested_shape_is_source_bound_not_stale_literal(self): + harness=(ROOT/"scripts"/"gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + self.assertNotIn("c8d5787162db919f84e9607d13f368995138861355f3fa269cbb10561f24d80d", harness) + self.assertIn('export R_FOUNDRY_CONTROL_DIR="$FOUNDRY_CONTROL_DIR"', harness) + self.assertIn('control = json.loads((control_dir / "control.json").read_text', harness) + self.assertIn('"images": foliorelay_images', harness) + def test_unknown_exact_version_fails_closed(self): with self.assertRaises(MOD.TargetError): targets=self.targets From fd97e7a4ccce4ea1e2faa6ab27229da79d510c35 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:15:28 +0200 Subject: [PATCH 603/719] RDTE: prepare final refreshed FolioRelay 25.04.1 F0-F5 --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index c1f876c17..c0dfb10c2 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-foliorelay-003-webui-https-f0-f5", - "version": "25.04.2.6", + "request_id": "tn-25-04-1-t6-foliorelay-003-webui-https-f0-f5", + "version": "25.04.1", "rung": "t6", "product": "foliorelay", - "authority_issue": 423, + "authority_issue": 411, "consumer_authority_issue": 273, - "purpose": "Requalify the exact FolioRelay TrueNAS 25.04.2.6 F0-F5 matrix cell for the accepted WebUI/product-native HTTPS candidate after refreshed BETA.3 and 25.10.7 independently earned support. Bind the current Agent Dispatch execution authority at 0d1f5c1d8808b0bedba5a509e61d4c703938bf63, exact Foundry T6 runtime/materialization authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.04.2.6/profile authority issue 423; cooperative host Avahi discovery and 25.x least-privilege control-root contract; independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; final zero residue. Do not inherit support from BETA.3, 25.10.7, or the historical pre-WebUI 25.04.2.6 receipt." + "purpose": "Final refreshed FolioRelay TrueNAS 25.04.1 F0-F5 matrix row for the accepted WebUI/product-native HTTPS candidate. Bind exact Agent Dispatch execution authority c96defde83ee2c0220b1cd549b8f9e7ef5bbf73a with source-bound requested-shape evidence, exact Foundry T6 runtime/materialization authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.04.1/profile authority issue 411 and profile blob 800aca038ded29e6a91bfc4c1b1fe2d2199182e8; cooperative host Avahi discovery and 25.x least-privilege 0710 control-root traversal; independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; top-level requested_shape image identities source-bound to the exact admitted control bundle; final zero residue. Do not inherit support from any other row or historical pre-WebUI 25.04.1 receipt." } From 502c51b9b794e22546e578ff89420125354b42b7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:50:02 +0200 Subject: [PATCH 604/719] feat(rdte): add bounded native Paper lifecycle and status/world oracle on disposable TrueNAS --- .../truenas_middleware_minecraft_t6_probe.py | 375 ++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 scripts/truenas_middleware_minecraft_t6_probe.py diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py new file mode 100644 index 000000000..1a816d0b9 --- /dev/null +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python3 +"""Native Minecraft Paper T6 on one disposable TrueNAS RDTE target. + +This is a product oracle over the existing TrueNAS Apps management API, not +another app-controller or a physical TrueNAS deployment operator. +""" +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +import pathlib +import re +import subprocess +import sys +import time + +from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for + +SCHEMA = "semper-supra.minecraft-paper-truenas-t6-control/1" +EXPECTED_TARGET = "25.04.1" +EXPECTED_PROFILE = "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" +EXPECTED_APPS_COMMIT = "532236882212e8c5e51d5b49cfe2c6ce3dd5675e" +EXPECTED_MIDDLEWARE = "74ab5a2d373be4097dece257d00e1086376333ba" +APP = "rdte-t6-minecraft-paper" +DATASET = "rdtepool/minecraft-paper-t6-world" +WORLD = "/mnt/rdtepool/minecraft-paper-t6-world" +PORT = 25580 +WORLD_LEVEL = WORLD + "/level.dat" +REQUIRED_OWNER = { + "SUPRACRAFT_COLLAGE_MANAGED": "true", + "SUPRACRAFT_COLLAGE_SCHEMA": "1", + "SUPRACRAFT_COLLAGE_MANAGER_ID": "manager-rdte", + "SUPRACRAFT_COLLAGE_FLEET_ID": "rdte", + "SUPRACRAFT_COLLAGE_SERVICE_ID": "11111111-1111-4111-8111-111111111111", + "SUPRACRAFT_COLLAGE_WORLD_ID": "world-rdte-001", +} + + +class MinecraftT6Error(RuntimeError): + pass + + +def canonical_sha256(doc: dict) -> str: + return hashlib.sha256(json.dumps(doc, sort_keys=True, separators=(",", ":")).encode()).hexdigest() + + +def load_control(path: pathlib.Path, expected_commit: str) -> dict: + control = json.loads((path / "control.json").read_text(encoding="utf-8")) + if not isinstance(control, dict) or control.get("schema") != SCHEMA: + raise MinecraftT6Error("Minecraft Foundry control schema drift") + if not re.fullmatch("[0-9a-f]{40}", expected_commit): + raise MinecraftT6Error("producer SHA is not immutable") + if control.get("foundry_ref") != expected_commit: + raise MinecraftT6Error("Foundry producer identity drift") + signed = control.copy() + claimed = signed.pop("control_sha256", None) + if claimed != canonical_sha256(signed): + raise MinecraftT6Error("Foundry control canonical digest drift") + target = control.get("target") or {} + if (target.get("truenas_version") != EXPECTED_TARGET or + target.get("expected_system_version") != "TrueNAS-" + EXPECTED_TARGET or + target.get("profile_git_blob_sha") != EXPECTED_PROFILE or + target.get("middleware_commit") != EXPECTED_MIDDLEWARE): + raise MinecraftT6Error("exact TrueNAS target/profile drift") + source = control.get("source") or {} + if (source.get("apps_commit") != EXPECTED_APPS_COMMIT or + source.get("app") != "community/minecraft" or + source.get("library_version") != "2.3.4" or + not re.fullmatch("[0-9a-f]{64}", str(source.get("source_compose_sha256", ""))) or + not re.fullmatch("[0-9a-f]{64}", str(source.get("values_raw_sha256", "")))): + raise MinecraftT6Error("pinned native Minecraft catalog source drift") + runtime = control.get("runtime") or {} + if (runtime.get("app_name") != APP or runtime.get("catalog_app") != "minecraft" or + runtime.get("train") != "community" or runtime.get("catalog_version_candidate") != "1.15.24" or + runtime.get("paper_version") != "1.21.11" or + runtime.get("required_owned_world_dataset") != DATASET or + runtime.get("status_oracle") != "minecraft-java-status-oracle/v1"): + raise MinecraftT6Error("Minecraft product/source identity drift") + values = runtime.get("values_template") + if not isinstance(values, dict): + raise MinecraftT6Error("missing native values template") + minecraft = values.get("minecraft") or {} + if (minecraft.get("type") != "PAPER" or minecraft.get("version") != "1.21.11" or + minecraft.get("eula") is not True or minecraft.get("online_mode") is not True or + minecraft.get("enable_rcon") is not False): + raise MinecraftT6Error("Paper/EULA/network mode contract drift") + owner = minecraft.get("additional_envs") or [] + if len(owner) != len(REQUIRED_OWNER) or { + i.get("name"): i.get("value") for i in owner if isinstance(i, dict) + } != REQUIRED_OWNER: + raise MinecraftT6Error("exact owner metadata drift") + storage = values.get("storage") or {} + if (storage.get("data") or {}).get("type") != "ix_volume": + raise MinecraftT6Error("base data must use independent App ixVolume") + mounts = storage.get("additional_storage") or [] + if (len(mounts) != 1 or mounts[0].get("type") != "host_path" or + mounts[0].get("mount_path") != "/data/world" or + mounts[0].get("read_only") is not False): + raise MinecraftT6Error("independent writable world mount contract drift") + if ((mounts[0].get("host_path_config") or {}).get("path") != "/opt/tests/collage-world-only"): + raise MinecraftT6Error("source host-path identity drift") + network = values.get("network") or {} + if (network.get("host_network") is not False or + (network.get("server_port") or {}).get("port_number") != 25565 or + (network.get("server_port") or {}).get("bind_mode") != "published"): + raise MinecraftT6Error("source published ingress contract drift") + if (not isinstance(control.get("evidence"), dict) or + control["evidence"].get("source_render_pass") is not True or + any(control["evidence"].get(flag) is not False for flag in ( + "live_catalog_version_verified", "live_minecraft_server_qualified", + "world_retention_qualified", "physical_truenas_qualified")) or + control.get("secrets_captured") is not False): + raise MinecraftT6Error("source candidate incorrectly promotes runtime or secrets") + return control + + +def runtime_values(control: dict) -> dict: + """Lower ONLY the experiment's world dataset and published guest port.""" + values = copy.deepcopy(control["runtime"]["values_template"]) + values["storage"]["additional_storage"][0]["host_path_config"]["path"] = WORLD + values["network"]["server_port"]["port_number"] = PORT + return values + + +def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, + expected_version: str = "1.21.11") -> dict: + if not script.is_file() or script.name != "minecraft_java_status_oracle.py": + raise MinecraftT6Error("exact Minecraft product oracle file absent") + cmd = [sys.executable, str(script), "--port", str(port), + "--expected-version", expected_version, "--timeout", "5", + "--out", str(out)] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=12) + if result.returncode != 0 or not out.is_file(): + raise MinecraftT6Error("Minecraft Java status oracle did not pass") + verdict = json.loads(out.read_text(encoding="utf-8")) + if (verdict.get("schema") != "minecraft-java-status-oracle/v1" or + verdict.get("classification") != "SUPPORTED" or + verdict.get("oracleSatisfied") is not True or + verdict.get("java_version") != expected_version or + verdict.get("status_packet_valid") is not True): + raise MinecraftT6Error("Minecraft Java status receipt invalid") + return verdict + + +def owned_config(config: dict, wanted: dict) -> bool: + if not isinstance(config, dict): + return False + minecraft = config.get("minecraft") or {} + env = minecraft.get("additional_envs") or [] + return ( + minecraft.get("type") == "PAPER" + and minecraft.get("version") == "1.21.11" + and {v.get("name"): v.get("value") for v in env if isinstance(v, dict)} == REQUIRED_OWNER + and (config.get("storage") or {}).get("additional_storage") == wanted["storage"]["additional_storage"] + and (config.get("network") or {}).get("server_port") == wanted["network"]["server_port"] + ) + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--host", default="127.0.0.1") + p.add_argument("--port", required=True, type=int) + p.add_argument("--status-port", required=True, type=int) + p.add_argument("--status-oracle", required=True, type=pathlib.Path) + p.add_argument("--password-file", required=True, type=pathlib.Path) + p.add_argument("--control-dir", required=True, type=pathlib.Path) + p.add_argument("--foundry-commit", required=True) + p.add_argument("--expected-system-version", required=True) + p.add_argument("--out", required=True, type=pathlib.Path) + p.add_argument("--tls", action="store_true") + p.add_argument("--timeout", type=float, default=8) + p.add_argument("--job-timeout", type=float, default=600) + p.add_argument("--state-timeout", type=float, default=480) + a = p.parse_args() + started = time.monotonic() + payload = { + "schema": "truenas-minecraft-paper-t6/v1", + "classification": "ORACLE_FAILURE", "oracleSatisfied": False, + "product": "paper-upstream-1.21.11", "target": EXPECTED_TARGET, + "cleanup": {"attempted": False, "zero_residue": False}, + "secret_values_captured": False, + } + ws = None + dataset_created = False + app_created = False + try: + control = load_control(a.control_dir, a.foundry_commit) + if a.expected_system_version != "TrueNAS-" + EXPECTED_TARGET: + raise MinecraftT6Error("requested exact system version drift") + desired = runtime_values(control) + payload["foundry_ref"] = a.foundry_commit + payload["control_sha256"] = control["control_sha256"] + if not a.status_oracle.is_file(): + raise MinecraftT6Error("qualified status oracle source absent") + password = a.password_file.read_text(encoding="utf-8").strip() + ws = WebSocket(a.host, a.port, timeout=a.timeout, tls=a.tls) + ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) + if wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}).get("msg") != "connected": + raise MinecraftT6Error("DDP connection failed") + ident = 1 + def call(method, params): + nonlocal ident + result = ddp_call(ws, str(ident), method, params) + ident += 1 + return result + def one(method, filters): + rows = call(method, [filters]) + if not isinstance(rows, list) or len(rows) > 1: + raise MinecraftT6Error(method + " returned ambiguous query") + return rows[0] if rows else None + def wait_job(job, label): + if not isinstance(job, int): + raise MinecraftT6Error(label + " did not return job ID") + deadline = time.monotonic() + a.job_timeout + while time.monotonic() < deadline: + state = call("core.get_jobs", [[["id", "=", job]], {"get": True}]) + if isinstance(state, dict) and state.get("state") == "SUCCESS": + return + if isinstance(state, dict) and state.get("state") in {"FAILED", "ABORTED"}: + raise MinecraftT6Error(label + " returned terminal failed job") + time.sleep(2) + raise MinecraftT6Error(label + " job timeout") + def getapp(): + return one("app.query", [["id", "=", APP]]) + def wait_app(): + deadline = time.monotonic() + a.state_timeout + while time.monotonic() < deadline: + app = getapp() + if isinstance(app, dict) and app.get("state") == "RUNNING": + cfg = call("app.config", [APP]) + if owned_config(cfg, desired): + return app + raise MinecraftT6Error("RUNNING App read-back did not match owner") + if isinstance(app, dict) and app.get("state") in {"CRASHED", "ERROR"}: + raise MinecraftT6Error("Minecraft App reported terminal runtime failure") + time.sleep(3) + raise MinecraftT6Error("Minecraft App never reached verified RUNNING") + def stat_level(): + try: + item = call("filesystem.stat", [WORLD_LEVEL]) + except Exception: + return None + if isinstance(item, dict) and isinstance(item.get("size"), int) and item["size"] > 0: + return {"size": item["size"], "type": item.get("type")} + return None + def wait_status(label): + deadline = time.monotonic() + a.state_timeout + last = None + while time.monotonic() < deadline: + receipt = a.out.with_name(a.out.stem + "-status-" + label + ".json") + try: + verdict = safe_status_oracle(a.status_oracle, a.status_port, receipt) + level = stat_level() + if level is not None: + payload.setdefault("readiness", {})[label] = {"protocol": verdict, "level": level} + return + except (OSError, ValueError, MinecraftT6Error, subprocess.TimeoutExpired) as exc: + last = type(exc).__name__ + time.sleep(4) + raise MinecraftT6Error("Minecraft runtime status+world did not converge: " + str(last)) + def delete_app(label): + nonlocal app_created + wait_job(call("app.delete", [APP, { + "remove_images": False, + "remove_ix_volumes": True, + "force_remove_custom_app": False, + }]), label) + app_created = False + if getapp() is not None: + raise MinecraftT6Error("App survived " + label) + + auth = call("auth.login_ex", [{"mechanism": "PASSWORD_PLAIN", + "username": "truenas_admin", "password": password}]) + if not isinstance(auth, dict) or auth.get("response_type") != "SUCCESS": + raise MinecraftT6Error("middleware authentication failed") + if call("system.version", []) != a.expected_system_version: + raise MinecraftT6Error("installed TrueNAS exact identity mismatch") + methods = call("core.get_methods", []) + required = {"app.query", "app.config", "app.create", "app.delete", "app.start", + "app.stop", "app.redeploy", "pool.dataset.query", "pool.dataset.create", + "pool.dataset.delete", "filesystem.stat", "filesystem.setperm"} + if not isinstance(methods, dict) or not required.issubset(methods): + raise MinecraftT6Error("required native TrueNAS methods missing") + if getapp() is not None or one("pool.dataset.query", [["id", "=", DATASET]]) is not None: + raise MinecraftT6Error("refusing to adopt pre-existing app or ZFS world dataset") + catalog = call("catalog.get_app_details", ["minecraft", {"train": "community"}]) + versions = (catalog or {}).get("versions") or {} + if not isinstance(versions, dict) or control["runtime"]["catalog_version_candidate"] not in versions: + raise MinecraftT6Error("exact native Minecraft catalog version unavailable") + payload["catalog_version_verified"] = True + + dataset = call("pool.dataset.create", [{"name": DATASET, "type": "FILESYSTEM", + "share_type": "GENERIC", "comments": "disposable Minecraft Paper T6"}]) + if not isinstance(dataset, dict) or dataset.get("id") != DATASET: + raise MinecraftT6Error("created dataset identity mismatch") + dataset_created = True + wait_job(call("filesystem.setperm", [{"path": WORLD, "uid": 568, "gid": 568, + "mode": "775", "options": {"stripacl": True, "recursive": False, "traverse": False}}]), + "world directory owner") + create_payload = {"app_name": APP, "catalog_app": "minecraft", "train": "community", + "version": control["runtime"]["catalog_version_candidate"], + "values": desired} + app_created = True # ownership was clean immediately before mutating request + wait_job(call("app.create", [create_payload]), "Minecraft first create") + wait_app() + wait_status("first") + first_level = payload["readiness"]["first"]["level"]["size"] + + wait_job(call("app.stop", [APP]), "Minecraft stop") + wait_job(call("app.start", [APP]), "Minecraft start") + wait_app() + wait_status("restart") + wait_job(call("app.redeploy", [APP]), "Minecraft redeploy") + wait_app() + wait_status("redeploy") + delete_app("Minecraft retain-world delete") + if one("pool.dataset.query", [["id", "=", DATASET]]) is None: + raise MinecraftT6Error("world dataset destroyed by App delete") + if stat_level() is None: + raise MinecraftT6Error("world level.dat absent after App deletion") + payload["world_retained_on_delete"] = True + app_created = True + wait_job(call("app.create", [create_payload]), "Minecraft reinstall") + wait_app() + wait_status("reinstall") + if first_level <= 0: + raise MinecraftT6Error("world level.dat size invalid") + payload["world_rebound_after_reinstall"] = True + delete_app("Minecraft final delete") + if stat_level() is None: + raise MinecraftT6Error("world not retained before explicit owned cleanup") + result = call("pool.dataset.delete", [DATASET, {"recursive": True, "force": False}]) + if result is not True: + raise MinecraftT6Error("owned ZFS dataset delete was not confirmed") + dataset_created = False + if one("pool.dataset.query", [["id", "=", DATASET]]) is not None: + raise MinecraftT6Error("world dataset cleanup left residue") + payload["cleanup"] = {"attempted": True, "zero_residue": True, "app_absent": True, + "owned_world_dataset_absent": True} + payload["classification"] = "SUPPORTED" + payload["oracleSatisfied"] = True + payload["detail"] = "native Paper Minecraft status/world lifecycle verified in disposable TrueNAS; not vanilla 26.3 or physical site" + except Exception as exc: + payload["failure_type"] = type(exc).__name__ + payload["detail"] = str(exc) if isinstance(exc, MinecraftT6Error) else "runtime exception; inspect bounded guest diagnostics" + if ws is not None and (app_created or dataset_created): + cleanup = {"attempted": True, "zero_residue": False} + try: + if app_created and getapp() is not None: + delete_app("failure cleanup App") + cleanup["app_absent"] = getapp() is None + if dataset_created and cleanup["app_absent"] and one("pool.dataset.query", [["id", "=", DATASET]]) is not None: + # Destroy only own dataset, never any unknown/foreign dataset. + removed = call("pool.dataset.delete", [DATASET, {"recursive": True, "force": False}]) + if removed is not True: + raise MinecraftT6Error("failure cleanup dataset refused") + cleanup["owned_world_dataset_absent"] = one("pool.dataset.query", [["id", "=", DATASET]]) is None + cleanup["zero_residue"] = cleanup["app_absent"] and cleanup["owned_world_dataset_absent"] + except Exception as cexc: + cleanup["cleanup_error_type"] = type(cexc).__name__ + payload["cleanup"] = cleanup + finally: + if ws is not None: + ws.close() + payload["elapsed_seconds"] = round(time.monotonic() - started, 2) + a.out.parent.mkdir(parents=True, exist_ok=True) + a.out.write_text(json.dumps(payload, sort_keys=True, indent=2) + "\n", encoding="utf-8") + print(json.dumps(payload, sort_keys=True)) + return 0 if payload["oracleSatisfied"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) From cb363fe4a9fada816d8d6f5be945470d0f9a69e8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:50:31 +0200 Subject: [PATCH 605/719] test(rdte): falsify Minecraft source, ownership and world-data boundaries --- tests/test_truenas_t6_minecraft_control.py | 171 +++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tests/test_truenas_t6_minecraft_control.py diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py new file mode 100644 index 000000000..8f048b3dd --- /dev/null +++ b/tests/test_truenas_t6_minecraft_control.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Cheap falsification of exact Minecraft/TrueNAS source/control semantics.""" +from __future__ import annotations + +import copy +import importlib.util +import json +import pathlib +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "minecraft_t6", ROOT / "scripts/truenas_middleware_minecraft_t6_probe.py" +) +mc = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(mc) + +SOURCE_SHA = "a" * 40 + + +def fixture() -> dict: + values = { + "resources": {"limits": {"cpus": 2, "memory": 4096}}, + "minecraft": { + "type": "PAPER", "version": "1.21.11", "eula": True, "online_mode": True, + "enable_rcon": False, "additional_envs": [ + {"name": k, "value": v} for k, v in mc.REQUIRED_OWNER.items() + ], + }, + "storage": { + "data": {"type": "ix_volume"}, + "additional_storage": [{ + "type": "host_path", "mount_path": "/data/world", + "read_only": False, + "host_path_config": {"path": "/opt/tests/collage-world-only"}, + }], + }, + "network": {"host_network": False, + "server_port": {"port_number": 25565, "bind_mode": "published"}}, + } + return { + "schema": mc.SCHEMA, + "foundry_ref": SOURCE_SHA, + "target": { + "truenas_version": "25.04.1", + "expected_system_version": "TrueNAS-25.04.1", + "profile_git_blob_sha": mc.EXPECTED_PROFILE, + "middleware_commit": mc.EXPECTED_MIDDLEWARE, + }, + "source": { + "apps_commit": mc.EXPECTED_APPS_COMMIT, + "app": "community/minecraft", + "library_version": "2.3.4", + "source_compose_sha256": "1"*64, + "values_raw_sha256": "2"*64, + }, + "runtime": { + "app_name": mc.APP, "catalog_app": "minecraft", + "train": "community", "catalog_version_candidate": "1.15.24", + "paper_version": "1.21.11", "required_owned_world_dataset": mc.DATASET, + "status_oracle": "minecraft-java-status-oracle/v1", + "values_template": values, + }, + "evidence": { + "source_render_pass": True, + "live_catalog_version_verified": False, + "live_minecraft_server_qualified": False, + "world_retention_qualified": False, + "physical_truenas_qualified": False, + }, + "secrets_captured": False, + } + + +def admit(doc, expected=SOURCE_SHA): + doc["control_sha256"] = mc.canonical_sha256(doc) + with tempfile.TemporaryDirectory() as td: + (pathlib.Path(td) / "control.json").write_text(json.dumps(doc), encoding="utf8") + return mc.load_control(pathlib.Path(td), expected) + + +class TestNativeMinecraftControl(unittest.TestCase): + def test_good_control_lowering_changes_only_world_and_port(self): + control = admit(fixture()) + initial = copy.deepcopy(control["runtime"]["values_template"]) + lowered = mc.runtime_values(control) + self.assertEqual(lowered["storage"]["additional_storage"][0]["host_path_config"]["path"], mc.WORLD) + self.assertEqual(lowered["network"]["server_port"]["port_number"], mc.PORT) + self.assertEqual(initial["storage"]["additional_storage"][0]["host_path_config"]["path"], + "/opt/tests/collage-world-only") + self.assertEqual(initial["network"]["server_port"]["port_number"], 25565) + self.assertEqual(mc.WORLD, "/mnt/rdtepool/minecraft-paper-t6-world") + self.assertTrue(mc.owned_config(lowered, lowered)) + + def test_wrong_producer_rejected(self): + with self.assertRaisesRegex(mc.MinecraftT6Error, "producer identity"): + admit(fixture(), expected="b"*40) + + def test_invalid_producer_ref_rejected(self): + with self.assertRaisesRegex(mc.MinecraftT6Error, "not immutable"): + admit(fixture(), expected="main") + + def test_control_digest_tamper_rejected(self): + with tempfile.TemporaryDirectory() as td: + doc = fixture() + doc["control_sha256"] = "f"*64 + (pathlib.Path(td) / "control.json").write_text(json.dumps(doc)) + with self.assertRaisesRegex(mc.MinecraftT6Error, "canonical digest"): + mc.load_control(pathlib.Path(td), SOURCE_SHA) + + def test_wrong_target_rejected(self): + doc = fixture() + doc["target"]["truenas_version"] = "25.10.7" + with self.assertRaisesRegex(mc.MinecraftT6Error, "target/profile"): + admit(doc) + + def test_wrong_upstream_source_rejected(self): + doc = fixture() + doc["source"]["apps_commit"] = "b"*40 + with self.assertRaisesRegex(mc.MinecraftT6Error, "catalog source"): + admit(doc) + + def test_paper_is_not_vanilla(self): + doc = fixture() + doc["runtime"]["paper_version"] = "26.3" + with self.assertRaisesRegex(mc.MinecraftT6Error, "product/source"): + admit(doc) + + def test_preclaimed_live_pass_rejected(self): + doc = fixture() + doc["evidence"]["live_minecraft_server_qualified"] = True + with self.assertRaisesRegex(mc.MinecraftT6Error, "promotes runtime"): + admit(doc) + + def test_base_world_same_storage_rejected(self): + doc = fixture() + doc["runtime"]["values_template"]["storage"]["data"]["type"] = "host_path" + with self.assertRaisesRegex(mc.MinecraftT6Error, "App ixVolume"): + admit(doc) + + def test_readonly_external_world_rejected(self): + doc = fixture() + doc["runtime"]["values_template"]["storage"]["additional_storage"][0]["read_only"] = True + with self.assertRaisesRegex(mc.MinecraftT6Error, "writable world"): + admit(doc) + + def test_ownership_drift_rejected(self): + doc = fixture() + doc["runtime"]["values_template"]["minecraft"]["additional_envs"][0]["value"] = "false" + with self.assertRaisesRegex(mc.MinecraftT6Error, "owner metadata"): + admit(doc) + + def test_foreign_config_not_adopted(self): + control = admit(fixture()) + values = mc.runtime_values(control) + self.assertFalse(mc.owned_config({"minecraft": {"type": "PAPER", "version": "1.21.11"}}, values)) + bad = copy.deepcopy(values) + bad["minecraft"]["additional_envs"] = [] + self.assertFalse(mc.owned_config(bad, values)) + + def test_status_script_must_be_exact_named(self): + with tempfile.TemporaryDirectory() as td: + other = pathlib.Path(td) / "anything.py" + other.write_text("print('ok')", encoding="utf-8") + with self.assertRaisesRegex(mc.MinecraftT6Error, "oracle file absent"): + mc.safe_status_oracle(other, 25565, pathlib.Path(td) / "status.json") + + +if __name__ == "__main__": + unittest.main() From fc57bf301891e871cab77570c0b928a1117ecce4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:50:40 +0200 Subject: [PATCH 606/719] test(rdte): resolve existing sibling DDP module from repo source --- tests/test_truenas_t6_minecraft_control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index 8f048b3dd..9c0033232 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -6,10 +6,12 @@ import importlib.util import json import pathlib +import sys import tempfile import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) SPEC = importlib.util.spec_from_file_location( "minecraft_t6", ROOT / "scripts/truenas_middleware_minecraft_t6_probe.py" ) From 7b7e4ea39c52ff9bed5d204a6dca9a8a6523a0fb Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:50:51 +0200 Subject: [PATCH 607/719] ci(rdte): add separate cheap validation for Minecraft TrueNAS consumer --- .github/workflows/minecraft-t6-static.yml | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/minecraft-t6-static.yml diff --git a/.github/workflows/minecraft-t6-static.yml b/.github/workflows/minecraft-t6-static.yml new file mode 100644 index 000000000..7c120fe57 --- /dev/null +++ b/.github/workflows/minecraft-t6-static.yml @@ -0,0 +1,41 @@ +name: Minecraft T6 consumer cheap qualification + +on: + pull_request: + paths: + - ".github/workflows/minecraft-t6-static.yml" + - "scripts/truenas_middleware_minecraft_t6_probe.py" + - "tests/test_truenas_t6_minecraft_control.py" + - "scripts/gha_kvm_truenas_rdte.sh" + - ".github/workflows/gha-kvm-system-rdte.yml" + - "config/truenas-capsule-providers.json" + - "config/truenas-session-minecraft-paper-25-04-1.json" + workflow_dispatch: + +permissions: + contents: read + +jobs: + cheap: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Checkout exact feature revision + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - name: Compile product and middleware consumer + run: | + python3 -m py_compile scripts/truenas_middleware_minecraft_t6_probe.py + python3 -m py_compile scripts/truenas_middleware_ddp_probe.py + bash -n scripts/gha_kvm_truenas_rdte.sh + - name: Execute source and fail-closed negatives + run: python3 -m unittest -v tests/test_truenas_t6_minecraft_control.py + - name: Verify session manifest exact contract + run: | + python3 scripts/truenas_session_manifest.py \ + --manifest config/truenas-session-minecraft-paper-25-04-1.json \ + --targets config/truenas-rdte-targets.json \ + --providers config/truenas-capsule-providers.json + python3 scripts/truenas_single_capsule_adapter.py \ + --manifest config/truenas-session-minecraft-paper-25-04-1.json \ + --targets config/truenas-rdte-targets.json \ + --providers config/truenas-capsule-providers.json From ec7f1776c0d8ab4b4bb4e77e598df419a71ba1a2 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:51:18 +0200 Subject: [PATCH 608/719] feat(rdte): stage immutable single-capsule Minecraft 25.04.1 candidate without launch --- ...uenas-session-minecraft-paper-25-04-1.json | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 config/truenas-session-minecraft-paper-25-04-1.json diff --git a/config/truenas-session-minecraft-paper-25-04-1.json b/config/truenas-session-minecraft-paper-25-04-1.json new file mode 100644 index 000000000..833075e77 --- /dev/null +++ b/config/truenas-session-minecraft-paper-25-04-1.json @@ -0,0 +1,66 @@ +{ + "schema": "truenas-session/v1", + "session_id": "tn-25-04-1-minecraft-paper-t6-001", + "version": "25.04.1", + "authority": { + "repository": "SemperSupra/agent-dispatch-private", + "issue": 480 + }, + "budget": { + "guest_memory_mib": 8192, + "download_mib": 2048, + "timeout_minutes": 48 + }, + "pool": { + "name": "rdtepool", + "data_disks": 2 + }, + "capsules": [ + { + "schema": "truenas-capsule/v1", + "id": "minecraft-paper-world-retention-001", + "kind": "foundry-product", + "provider": "minecraft-paper-t6", + "authority": { + "repository": "SupraCraft/mc-server-images", + "issue": 53 + }, + "exact": { + "foundry_producer_ref": "db54c26a245b81952930819ef29ab8e2c6851f87", + "foundry_artifact_sha256": "cebe2f54dc87875ac276b1b6f46286372488db64b00c376b6ca7923f9430121a", + "minecraft_product_oracle_ref": "d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8", + "minecraft_version": "1.21.11", + "native_catalog_candidate": "1.15.24", + "truenas_target_profile_blob": "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" + }, + "namespace": "rdte-minecraft-paper-25-04-1", + "ports": [ + 25580 + ], + "requirements": [ + "apps", + "zfs-pool", + "native-catalog", + "minecraft-java-status" + ], + "resources": { + "memory_mib": 4096, + "download_mib": 1024, + "estimated_minutes": 35 + }, + "phases": { + "setup": true, + "apply": true, + "verify": true, + "cleanup": true + }, + "oracle": "Minecraft Java status, level.dat world survival after App ixVolume delete and reinstall, zero owned residue", + "receipt_contract": "truenas-minecraft-paper-t6/v1", + "retry_policy": "read-only-reconcile", + "mutating": true, + "cleanup_required": true, + "dependencies": [] + } + ], + "manifest_sha256": "735f2daa94877da895c08a8ad690df2ef57f0225317ff68d46228adca17cdaa6" +} From 72fc556afdab3ba3d1c8cda4fe42b141bc65dd1a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:51:23 +0200 Subject: [PATCH 609/719] feat(rdte): register exact Minecraft Paper T6 provider under existing capsule registry --- config/truenas-capsule-providers.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/truenas-capsule-providers.json b/config/truenas-capsule-providers.json index dd9f38ea1..58a5ef457 100644 --- a/config/truenas-capsule-providers.json +++ b/config/truenas-capsule-providers.json @@ -44,6 +44,14 @@ "probe": "scripts/truenas_middleware_foliorelay_t6_probe.py", "control_artifact": "foliorelay-t6-control", "existing_selector": "foliorelay" + }, + { + "id": "minecraft-paper-t6", + "kind": "foundry-product", + "product": "minecraft-paper", + "probe": "scripts/truenas_middleware_minecraft_t6_probe.py", + "control_artifact": "minecraft-paper-truenas-t6-control", + "existing_selector": "minecraft-paper" } ] } From b4820fe6f05d8b13265f0117b6cf5091d11c18e7 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:52:15 +0200 Subject: [PATCH 610/719] feat(rdte): wire source-bound Minecraft T6 producer and exact Java oracle into existing KVM lane --- .github/workflows/gha-kvm-system-rdte.yml | 40 +++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 763f7efde..e8931bed4 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -55,6 +55,7 @@ on: - garm-provider-container-pre-b4 - official-catalog - foliorelay + - minecraft-paper proxmox_compute_fixture: description: "Optional native PVE REST compute fixture" required: true @@ -185,7 +186,7 @@ jobs: fi truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay" or . == "minecraft-paper")' "$RUN_REQUEST_PATH")" truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ @@ -273,6 +274,16 @@ jobs: permissions: contents: read + foundry-minecraft-paper-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-paper-t6-control.yml@db54c26a245b81952930819ef29ab8e2c6851f87 + with: + foundry_ref: db54c26a245b81952930819ef29ab8e2c6851f87 + target_version: "25.04.1" + permissions: + contents: read + provider-g2-fixtures: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' @@ -417,7 +428,7 @@ jobs: group: agent-dispatch-heavyweight-rdte queue: max cancel-in-progress: false - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix, provider-container-pre-b4-fixture] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, foundry-minecraft-paper-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix, provider-container-pre-b4-fixture] if: >- always() && needs.contract.result == 'success' && @@ -431,6 +442,7 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'garm-controller-f0f5' && needs['foundry-garm-f0-f5-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'foliorelay' && needs['foundry-foliorelay-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' && needs['foundry-minecraft-paper-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') || @@ -479,6 +491,25 @@ jobs: with: name: foliorelay-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact Minecraft Paper Foundry control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: minecraft-paper-truenas-t6-control + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Checkout exact Minecraft product status oracle + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SupraCraft/mc-server-images + ref: d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 + path: .deps/minecraft-product + persist-credentials: false + - name: Prove exact Minecraft product source identity + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + run: | + test "$(git -C .deps/minecraft-product rev-parse HEAD)" = d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 + python3 -m unittest -v .deps/minecraft-product/tools/test_minecraft_java_status_oracle.py - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c @@ -576,6 +607,8 @@ jobs: foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then foundry_commit="81deb97185760975fd8d3162df42056c77b3c0fd" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then + foundry_commit="db54c26a245b81952930819ef29ab8e2c6851f87" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi @@ -586,6 +619,9 @@ jobs: ) fi fi + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then + args+=(--session-manifest config/truenas-session-minecraft-paper-25-04-1.json) + fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" - name: Preserve TrueNAS harness failure evidence From 733409a2fc3b488bc1fed07437c0c40217ff14f3 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:52:52 +0200 Subject: [PATCH 611/719] feat(rdte): bind Minecraft Paper T6 to disposable QEMU forwarding and existing session harness --- scripts/gha_kvm_truenas_rdte.sh | 41 +++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 3a807f817..caeedd9d4 100755 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -18,7 +18,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" } OUT="" @@ -76,10 +76,10 @@ if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then exit 2 fi if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then + if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "garm-provider-container-pre-b4" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "garm-provider-container-pre-b4" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" || "$T6_PRODUCT" == "minecraft-paper" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -229,6 +229,8 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" else {"name": "rdte-t6-foliorelay", "images": foliorelay_images} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" + else {"name": "rdte-t6-minecraft-paper", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.24", "paper_version": "1.21.11"} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "minecraft-paper" else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} @@ -377,6 +379,11 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 WOW Sidecar control client" + elif [[ "$T6_PRODUCT" == "minecraft-paper" ]]; then + [[ -f "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing Minecraft T6 product probe" + [[ -f "$REPO_ROOT/.deps/minecraft-product/tools/minecraft_java_status_oracle.py" ]] || + fail_evidence HARNESS_FAILURE preflight "missing pinned Minecraft product oracle" elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 FolioRelay control client" @@ -649,6 +656,16 @@ FOLIORELAY_CONTROL_HOST_PORT="" FOLIORELAY_IPP_HOST_PORT="" FOLIORELAY_OBSERVER_HOST_PORT="" FOLIORELAY_HOSTFWD="" +MINECRAFT_HOST_PORT="" +MINECRAFT_HOSTFWD="" +if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "minecraft-paper" ]]; then + MINECRAFT_HOST_PORT="$(python3 - <<'PY' +import socket +s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() +PY +)" + MINECRAFT_HOSTFWD=",hostfwd=tcp:127.0.0.1:${MINECRAFT_HOST_PORT}-:25580" +fi if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "litellm" ]]; then LITELLM_HOST_PORT="$(python3 - <<'PY' import socket @@ -683,7 +700,7 @@ sudo -n qemu-system-x86_64 \ -device "virtio-blk-pci,drive=rdteboot,id=rdte-boot,addr=0x4,bootindex=1" \ "${DATA_DRIVE_ARGS[@]}" \ -boot strict=on \ - -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443${LITELLM_HOSTFWD}${GARM_HOSTFWD}${FOLIORELAY_HOSTFWD}" \ + -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$HTTP_PORT-:80,hostfwd=tcp:127.0.0.1:$HTTPS_PORT-:443${LITELLM_HOSTFWD}${GARM_HOSTFWD}${FOLIORELAY_HOSTFWD}${MINECRAFT_HOSTFWD}" \ -device "virtio-net-pci,netdev=net0,mac=$NIC_MAC,addr=0x3" \ -display none -monitor none \ -serial "file:$STATE_DIR/serial.log" \ @@ -969,6 +986,7 @@ if [[ -n "$SESSION_MANIFEST" ]]; then export SCTX_PASSWORD_FILE="$PASSWORD_FILE" SCTX_CONTROL_DIR="$FOUNDRY_CONTROL_DIR" SCTX_FOUNDRY_COMMIT="$FOUNDRY_COMMIT" export SCTX_LITELLM_PORT="$LITELLM_HOST_PORT" SCTX_GARM_PORT="$GARM_HOST_PORT" export SCTX_FOLIO_CONTROL="$FOLIORELAY_CONTROL_HOST_PORT" SCTX_FOLIO_IPP="$FOLIORELAY_IPP_HOST_PORT" SCTX_FOLIO_OBSERVER="$FOLIORELAY_OBSERVER_HOST_PORT" + export SCTX_MINECRAFT_STATUS_PORT="$MINECRAFT_HOST_PORT" SCTX_MINECRAFT_ORACLE="$REPO_ROOT/.deps/minecraft-product/tools/minecraft_java_status_oracle.py" SCTX_TARGET_SYSTEM="$EXPECTED_SYSTEM_VERSION" if (( ${#MIDDLEWARE_TLS_ARG[@]} )); then SCTX_TLS=true; else SCTX_TLS=false; fi export SCTX_TLS python3 - "$SESSION_CONTEXT" <<'PY' @@ -980,10 +998,12 @@ provider_id={ "garm":"garm-t6", "official-catalog":"official-catalog-t6", "foliorelay":"foliorelay-t6", + "minecraft-paper":"minecraft-paper-t6", }[selector] ports={} if selector=="litellm": ports["service"]=int(os.environ["SCTX_LITELLM_PORT"]) elif selector=="garm": ports["service"]=int(os.environ["SCTX_GARM_PORT"]) +elif selector=="minecraft-paper": ports["status"]=int(os.environ["SCTX_MINECRAFT_STATUS_PORT"]) elif selector=="foliorelay": ports={ "control":int(os.environ["SCTX_FOLIO_CONTROL"]), @@ -1001,6 +1021,8 @@ payload={ "control_dir":os.environ["SCTX_CONTROL_DIR"], "foundry_commit":os.environ["SCTX_FOUNDRY_COMMIT"], "ports":ports, + **({"status_oracle":os.environ["SCTX_MINECRAFT_ORACLE"], + "expected_system_version":os.environ["SCTX_TARGET_SYSTEM"]} if selector=="minecraft-paper" else {}), } }, } @@ -1128,6 +1150,15 @@ elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then --control-dir "$FOUNDRY_CONTROL_DIR" \ --foundry-commit "$FOUNDRY_COMMIT" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "minecraft-paper" ]]; then + python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + --status-port "$MINECRAFT_HOST_PORT" \ + --status-oracle "$REPO_ROOT/.deps/minecraft-product/tools/minecraft_java_status_oracle.py" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ @@ -1181,6 +1212,8 @@ elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported WOW Sidecar control passed immutable image/config read-back, permissions/seed metadata, public-fixture worker runtime, restart persistence, and zero-residue cleanup" +elif [[ "$T6_PRODUCT" == "minecraft-paper" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact native TrueNAS Minecraft Paper server passed protocol status + world level.dat persistence across App stop/start, redeploy, retain-world delete/reinstall and owned cleanup; no vanilla 26.3 or physical TrueNAS claim" elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported FolioRelay three-service control passed exact image/config read-back, portal and IPP reachability, canonical identity, PDF/URF exact-source Inbox preservation, independent in-guest DNS-SD observation, restart persistence, and zero-residue cleanup" else From 07e0f1c7c13e9ffca62c2e52514fe67a59a863f5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:53:02 +0200 Subject: [PATCH 612/719] feat(rdte): extend existing capsule executor with exact Minecraft status probe binding --- .../truenas_existing_probe_capsule_executor.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_existing_probe_capsule_executor.py b/scripts/truenas_existing_probe_capsule_executor.py index 58a3710ed..06ac20e29 100644 --- a/scripts/truenas_existing_probe_capsule_executor.py +++ b/scripts/truenas_existing_probe_capsule_executor.py @@ -19,6 +19,7 @@ "wow-sidecar-t6": {}, "garm-t6": {"service": "--service-port"}, "official-catalog-t6": {}, + "minecraft-paper-t6": {"status": "--status-port"}, "foliorelay-t6": { "control": "--control-port", "ipp": "--ipp-port", @@ -31,6 +32,7 @@ "wow-sidecar-t6": 240, "garm-t6": 240, "official-catalog-t6": 240, + "minecraft-paper-t6": 480, "foliorelay-t6": 300, } @@ -134,11 +136,22 @@ def build_probe_command( raise SessionError(f"{provider_id}.ports must be an object") for name, flag in PROVIDER_PORT_ARGS[provider_id].items(): command += [flag, str(positive_port(ports.get(name), f"{provider_id}.ports.{name}"))] + if provider_id == "minecraft-paper-t6": + status_oracle = pathlib.Path(nonempty(binding.get("status_oracle"), "minecraft.status_oracle")) + if not status_oracle.is_file() or status_oracle.name != "minecraft_java_status_oracle.py": + raise SessionError("pinned Minecraft product oracle file is absent") + expected_system_version = nonempty(binding.get("expected_system_version"), "minecraft.expected_system_version") + if expected_system_version != "TrueNAS-25.04.1": + raise SessionError("Minecraft native product target version drift") + command += [ + "--status-oracle", str(status_oracle), + "--expected-system-version", expected_system_version, + ] command += [ "--out", str(provider_receipt), "--timeout", "8", - "--job-timeout", "300", + "--job-timeout", "600" if provider_id == "minecraft-paper-t6" else "300", "--state-timeout", str(STATE_TIMEOUT[provider_id]), ] return command From f94e54488afa52dbdc310fa03f3a1436d19f48f1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:53:39 +0200 Subject: [PATCH 613/719] fix(rdte): invoke exact product oracle synthetic test as standalone script --- .github/workflows/gha-kvm-system-rdte.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index e8931bed4..84b79d0b6 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -509,7 +509,7 @@ jobs: if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' run: | test "$(git -C .deps/minecraft-product rev-parse HEAD)" = d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 - python3 -m unittest -v .deps/minecraft-product/tools/test_minecraft_java_status_oracle.py + python3 .deps/minecraft-product/tools/test_minecraft_java_status_oracle.py - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c From cf98c75610943b0f7e9dbc7ea39564bb65242a14 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:54:12 +0200 Subject: [PATCH 614/719] feat(rdte): adopt existing middleware byte-transfer for exact world sentinel comparison --- scripts/truenas_middleware_minecraft_t6_probe.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 1a816d0b9..b8a3ac409 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -17,6 +17,7 @@ import time from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for +from truenas_middleware_foliorelay_t6_probe import middleware_http_bytes, multipart_upload SCHEMA = "semper-supra.minecraft-paper-truenas-t6-control/1" EXPECTED_TARGET = "25.04.1" @@ -28,6 +29,7 @@ WORLD = "/mnt/rdtepool/minecraft-paper-t6-world" PORT = 25580 WORLD_LEVEL = WORLD + "/level.dat" +SENTINEL_PATH = WORLD + "/.supracraft-paper-t6-sentinel" REQUIRED_OWNER = { "SUPRACRAFT_COLLAGE_MANAGED": "true", "SUPRACRAFT_COLLAGE_SCHEMA": "1", @@ -190,6 +192,7 @@ def main() -> int: if a.expected_system_version != "TrueNAS-" + EXPECTED_TARGET: raise MinecraftT6Error("requested exact system version drift") desired = runtime_values(control) + sentinel_bytes = ("SUPRACRAFT-PAPER-T6:" + control["control_sha256"] + "\n").encode("ascii") payload["foundry_ref"] = a.foundry_commit payload["control_sha256"] = control["control_sha256"] if not a.status_oracle.is_file(): @@ -237,6 +240,15 @@ def wait_app(): raise MinecraftT6Error("Minecraft App reported terminal runtime failure") time.sleep(3) raise MinecraftT6Error("Minecraft App never reached verified RUNNING") + def read_sentinel(): + row = call("core.download", ["filesystem.get", [SENTINEL_PATH], "paper-t6-sentinel.txt", True]) + if not isinstance(row, list) or len(row) != 2 or not isinstance(row[0], int): + raise MinecraftT6Error("sentinel download response invalid") + wait_job(row[0], "world sentinel download") + content = middleware_http_bytes(a.host, a.port, a.tls, row[1], timeout=a.timeout) + if content != sentinel_bytes: + raise MinecraftT6Error("world sentinel bytes changed or disappeared") + return hashlib.sha256(content).hexdigest() def stat_level(): try: item = call("filesystem.stat", [WORLD_LEVEL]) From 18e35585f13917d29c9e2b8f5932625f8e08db4e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:54:21 +0200 Subject: [PATCH 615/719] feat(rdte): enforce same exact world sentinel bytes across native app lifecycle --- scripts/truenas_middleware_minecraft_t6_probe.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index b8a3ac409..985e7b294 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -311,6 +311,10 @@ def delete_app(label): wait_job(call("filesystem.setperm", [{"path": WORLD, "uid": 568, "gid": 568, "mode": "775", "options": {"stripacl": True, "recursive": False, "traverse": False}}]), "world directory owner") + upload = multipart_upload(a.host, a.port, a.tls, "truenas_admin", password, + SENTINEL_PATH, sentinel_bytes, 0o644, a.timeout) + wait_job(upload, "world sentinel write") + payload["sentinel_sha256"] = read_sentinel() create_payload = {"app_name": APP, "catalog_app": "minecraft", "train": "community", "version": control["runtime"]["catalog_version_candidate"], "values": desired} @@ -318,6 +322,7 @@ def delete_app(label): wait_job(call("app.create", [create_payload]), "Minecraft first create") wait_app() wait_status("first") + read_sentinel() first_level = payload["readiness"]["first"]["level"]["size"] wait_job(call("app.stop", [APP]), "Minecraft stop") @@ -332,17 +337,20 @@ def delete_app(label): raise MinecraftT6Error("world dataset destroyed by App delete") if stat_level() is None: raise MinecraftT6Error("world level.dat absent after App deletion") + read_sentinel() payload["world_retained_on_delete"] = True app_created = True wait_job(call("app.create", [create_payload]), "Minecraft reinstall") wait_app() wait_status("reinstall") + read_sentinel() if first_level <= 0: raise MinecraftT6Error("world level.dat size invalid") payload["world_rebound_after_reinstall"] = True delete_app("Minecraft final delete") if stat_level() is None: raise MinecraftT6Error("world not retained before explicit owned cleanup") + read_sentinel() result = call("pool.dataset.delete", [DATASET, {"recursive": True, "force": False}]) if result is not True: raise MinecraftT6Error("owned ZFS dataset delete was not confirmed") From 11ba16448c5a776b73db0ed283c406ec81f7e1ab Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:54:42 +0200 Subject: [PATCH 616/719] test(rdte): cover exact Minecraft capsule argv with existing executor tests --- tests/test_truenas_existing_probe_capsule_executor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_truenas_existing_probe_capsule_executor.py b/tests/test_truenas_existing_probe_capsule_executor.py index e863c92c1..c71fed604 100644 --- a/tests/test_truenas_existing_probe_capsule_executor.py +++ b/tests/test_truenas_existing_probe_capsule_executor.py @@ -29,11 +29,14 @@ def fixture(self, provider_id): password.write_text("test\n", encoding="utf-8") control = root / "control" control.mkdir() + oracle = root / "minecraft_java_status_oracle.py" + oracle.write_text("# pinned synthetic oracle\n", encoding="utf-8") ports = { "litellm-t6": {"service": 18100}, "wow-sidecar-t6": {}, "garm-t6": {"service": 18101}, "official-catalog-t6": {}, + "minecraft-paper-t6": {"status": 18105}, "foliorelay-t6": {"control": 18102, "ipp": 18103, "observer": 18104}, }[provider_id] descriptor = { @@ -61,6 +64,8 @@ def fixture(self, provider_id): "control_dir": str(control), "foundry_commit": "1" * 40, "ports": ports, + **({"status_oracle": str(oracle), + "expected_system_version": "TrueNAS-25.04.1"} if provider_id == "minecraft-paper-t6" else {}), } }, } @@ -72,6 +77,11 @@ def test_existing_provider_commands_preserve_current_probe_arguments(self): "wow-sidecar-t6": ["--state-timeout", "240"], "garm-t6": ["--service-port", "18101", "--state-timeout", "240"], "official-catalog-t6": ["--state-timeout", "240"], + "minecraft-paper-t6": [ + "--status-port", "18105", "--status-oracle", + "--expected-system-version", "TrueNAS-25.04.1", + "--job-timeout", "600", "--state-timeout", "480" + ], "foliorelay-t6": [ "--control-port", "18102", "--ipp-port", "18103", From d4517faff4bf2c2d1bc5a7391d2081f263ed6b8b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:54:49 +0200 Subject: [PATCH 617/719] ci(rdte): preserve existing single-capsule equivalence and manifest regressions --- .github/workflows/minecraft-t6-static.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/minecraft-t6-static.yml b/.github/workflows/minecraft-t6-static.yml index 7c120fe57..ea1463e2e 100644 --- a/.github/workflows/minecraft-t6-static.yml +++ b/.github/workflows/minecraft-t6-static.yml @@ -28,7 +28,10 @@ jobs: python3 -m py_compile scripts/truenas_middleware_ddp_probe.py bash -n scripts/gha_kvm_truenas_rdte.sh - name: Execute source and fail-closed negatives - run: python3 -m unittest -v tests/test_truenas_t6_minecraft_control.py + run: | + python3 -m unittest -v tests/test_truenas_t6_minecraft_control.py + python3 -m unittest -v tests/test_truenas_existing_probe_capsule_executor.py + python3 -m unittest -v tests/test_truenas_session_manifest.py - name: Verify session manifest exact contract run: | python3 scripts/truenas_session_manifest.py \ From a6c799eb7d5b6d5504c262c6f50177b7b3acfff0 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:55:23 +0200 Subject: [PATCH 618/719] test(rdte): include registered Minecraft consumer in exact known-probe inventory --- tests/test_truenas_session_manifest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_session_manifest.py b/tests/test_truenas_session_manifest.py index 7f0b996a4..ed9209237 100644 --- a/tests/test_truenas_session_manifest.py +++ b/tests/test_truenas_session_manifest.py @@ -156,7 +156,8 @@ def test_provider_registry_wraps_existing_probes(self): "wow-sidecar-t6": "scripts/truenas_middleware_wow_sidecar_t6_probe.py", "garm-t6": "scripts/truenas_middleware_garm_t6_probe.py", "official-catalog-t6": "scripts/truenas_middleware_official_catalog_t6_probe.py", - "foliorelay-t6": "scripts/truenas_middleware_foliorelay_t6_probe.py" + "foliorelay-t6": "scripts/truenas_middleware_foliorelay_t6_probe.py", + "minecraft-paper-t6": "scripts/truenas_middleware_minecraft_t6_probe.py" } actual = {item["id"]: item["probe"] for item in registry["providers"]} self.assertEqual(actual, expected) From 080275c1d4b65e29da6046d568e769c23115d727 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:55:53 +0200 Subject: [PATCH 619/719] test(rdte): preserve 26-BETA.3 product gate with exact Minecraft 25.04.1 exception --- tests/test_gha_kvm_system_rdte.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 79a267bf4..2e7200cee 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -338,7 +338,7 @@ def test_g5_routing_is_source_exact_cross_version_and_public_safe(self): self.assertIn("truenas_middleware_garm_provider_g5_probe.py", text) self.assertIn('G5_MATRIX_PRODUCER', text) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', text, ) g5_probe = (ROOT / "scripts" / "truenas_middleware_garm_provider_g5_probe.py").read_text( From d33fd5c2e9eef5584c57b0f90419ad2e92438e02 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:55:55 +0200 Subject: [PATCH 620/719] test(rdte): preserve 26-BETA.3 product gate with exact Minecraft 25.04.1 exception --- tests/test_truenas_t6_official_catalog_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py index 25f811ac1..6ad15c2ab 100644 --- a/tests/test_truenas_t6_official_catalog_control.py +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -182,7 +182,7 @@ def test_workflow_and_harness_bind_exact_official_catalog_producer(self): self.assertIn('"official-catalog"', harness) self.assertIn("truenas_middleware_official_catalog_t6_probe.py", harness) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', harness, ) From c9caed1df514bff0f52c8c3733a024d39d868b54 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:56:45 +0200 Subject: [PATCH 621/719] fix(rdte): require owned ZFS permissions and bounded middleware-only sentinel download --- scripts/truenas_middleware_minecraft_t6_probe.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 985e7b294..105e9c58e 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -244,6 +244,8 @@ def read_sentinel(): row = call("core.download", ["filesystem.get", [SENTINEL_PATH], "paper-t6-sentinel.txt", True]) if not isinstance(row, list) or len(row) != 2 or not isinstance(row[0], int): raise MinecraftT6Error("sentinel download response invalid") + if not isinstance(row[1], str) or not row[1].startswith("/_download/") or ".." in row[1]: + raise MinecraftT6Error("sentinel download path is outside middleware") wait_job(row[0], "world sentinel download") content = middleware_http_bytes(a.host, a.port, a.tls, row[1], timeout=a.timeout) if content != sentinel_bytes: @@ -308,9 +310,16 @@ def delete_app(label): if not isinstance(dataset, dict) or dataset.get("id") != DATASET: raise MinecraftT6Error("created dataset identity mismatch") dataset_created = True - wait_job(call("filesystem.setperm", [{"path": WORLD, "uid": 568, "gid": 568, - "mode": "775", "options": {"stripacl": True, "recursive": False, "traverse": False}}]), - "world directory owner") + perms = call("filesystem.setperm", [{"path": WORLD, "uid": 568, "gid": 568, + "mode": "775", "options": {"stripacl": True, "recursive": False, "traverse": False}}]) + if isinstance(perms, int) and not isinstance(perms, bool): + wait_job(perms, "world directory owner") + elif perms not in (None, True): + raise MinecraftT6Error("unexpected world permission result") + observed_perms = call("filesystem.stat", [WORLD]) + if (not isinstance(observed_perms, dict) or observed_perms.get("uid") != 568 + or observed_perms.get("gid") != 568): + raise MinecraftT6Error("writable world ownership readback mismatch") upload = multipart_upload(a.host, a.port, a.tls, "truenas_admin", password, SENTINEL_PATH, sentinel_bytes, 0o644, a.timeout) wait_job(upload, "world sentinel write") From 7655e68778903631baa068299f378719f8191a5c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:57:06 +0200 Subject: [PATCH 622/719] fix(rdte): reject unexpected Paper image and keep immutable runtime digest explicitly unqualified --- scripts/truenas_middleware_minecraft_t6_probe.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 105e9c58e..16604857b 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -23,6 +23,7 @@ EXPECTED_TARGET = "25.04.1" EXPECTED_PROFILE = "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" EXPECTED_APPS_COMMIT = "532236882212e8c5e51d5b49cfe2c6ce3dd5675e" +EXPECTED_IMAGE_FRAGMENT = "ghcr.io/itzg/minecraft-server:2026.8.2-java21" EXPECTED_MIDDLEWARE = "74ab5a2d373be4097dece257d00e1086376333ba" APP = "rdte-t6-minecraft-paper" DATASET = "rdtepool/minecraft-paper-t6-world" @@ -232,6 +233,11 @@ def wait_app(): while time.monotonic() < deadline: app = getapp() if isinstance(app, dict) and app.get("state") == "RUNNING": + images = ((app.get("active_workloads") or {}).get("images") or []) + if not isinstance(images, list) or not any(EXPECTED_IMAGE_FRAGMENT in str(i) for i in images): + raise MinecraftT6Error("native Minecraft App runtime image differs from exact source candidate") + payload["observed_image_reference"] = EXPECTED_IMAGE_FRAGMENT + payload["image_digest_qualification"] = "NOT_YET_EARNED" cfg = call("app.config", [APP]) if owned_config(cfg, desired): return app From 31855885fd23545d94fde8b77dca8344319b8e48 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:07:36 +0200 Subject: [PATCH 623/719] run(rdte): request first source-bound Minecraft Paper nested TrueNAS 25.04.1 T6 oracle rep --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index c0dfb10c2..935ded160 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-foliorelay-003-webui-https-f0-f5", + "request_id": "tn-25-04-1-t6-minecraft-paper-001-native-boot-world-reinstall", "version": "25.04.1", "rung": "t6", - "product": "foliorelay", + "product": "minecraft-paper", "authority_issue": 411, - "consumer_authority_issue": 273, - "purpose": "Final refreshed FolioRelay TrueNAS 25.04.1 F0-F5 matrix row for the accepted WebUI/product-native HTTPS candidate. Bind exact Agent Dispatch execution authority c96defde83ee2c0220b1cd549b8f9e7ef5bbf73a with source-bound requested-shape evidence, exact Foundry T6 runtime/materialization authority 81deb97185760975fd8d3162df42056c77b3c0fd, immutable control sha256:d0ba6d1efbed0d9f84b20d374eeb44ee28ab0874a683396e628850f159193cf5, and unchanged CUPS sha256:0997ad2054ca5e57f34291372aed55f549eee9ff201f171b430436b0655c0814. Require exact TrueNAS-25.04.1/profile authority issue 411 and profile blob 800aca038ded29e6a91bfc4c1b1fe2d2199182e8; cooperative host Avahi discovery and 25.x least-privilege 0710 control-root traversal; independent legacy-unicast observer; exact three-service RUNNING/readback; TrueNAS Web UI portal https://foliorelay-t6.local:18443/; HTTPS management and stable TLS leaf identity across restart/update/redeploy/retained-data reinstall; IPP; PDF/URF exact-source Inbox preservation; canonical printer identity; F4 second-plan NOOP with WAIT/fail-closed reconciliation; F5 retained-data reinstall preserving identity, Inbox, artifact bytes, portal metadata, and TLS identity; top-level requested_shape image identities source-bound to the exact admitted control bundle; final zero residue. Do not inherit support from any other row or historical pre-WebUI 25.04.1 receipt." + "consumer_authority_issue": 588, + "purpose": "FIRST disposable native TrueNAS 25.04.1 nested KVM Minecraft Paper application qualification. User authorized nested evaluation before any local TrueNAS deployment. Source producer SemperSupra/truenas-app-foundry@db54c26a245b81952930819ef29ab8e2c6851f87 (source artifact control, schema semper-supra.minecraft-paper-truenas-t6-control/1, run 38096741189) and product oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 (9/9 protocol tests CI 38096489436) consumed by Agent Dispatch PR #382 experiment merge 3b7aa4642dcee498ea42a0c56260f0b5a6468eed. Exact native community/minecraft Paper 1.21.11 catalog candidate 1.15.24 must be independently checked on guest; source image tag candidate does not establish digest. Bounded 8GiB guest, shared heavyweight slot; verify app native lifecycle, loopback-only Java status, independent world ZFS and unchanged sentinel across stop/start/redeploy/delete/reinstall, final owned cleanup. Fail closed on native catalog drift or runtime failure; preserve evidence and do not change user production TrueNAS, do not claim vanilla 26.3 or Two Rivers runtime support. Independent Jules PR #591 was empty; classify review inconclusive, use live disposable oracle for falsification only, keep production deployment hold." } From f5dadf12eb4a0797121da1eab5bd3d05279dfd0e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:08:14 +0200 Subject: [PATCH 624/719] test(rdte): admit exact source-qualified Minecraft Paper in existing product inventory assertions --- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 3fd568308..9d03b68cd 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "garm-provider-container-pre-b4", "official-catalog", "foliorelay"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "garm-provider-container-pre-b4", "official-catalog", "foliorelay", "minecraft-paper"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From 758c90b6d113f60bfa6c58bdadd959298bbf9c50 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:08:20 +0200 Subject: [PATCH 625/719] test(rdte): admit exact source-qualified Minecraft Paper in existing product inventory assertions --- tests/test_truenas_rdte_target.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 471d7469b..72f841457 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -78,7 +78,7 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","garm-provider-container-pre-b4","official-catalog","foliorelay"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","garm-provider-container-pre-b4","official-catalog","foliorelay","minecraft-paper"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": From d2df68ad8209216cbce7cc9862d0b1477bd67473 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:08:52 +0200 Subject: [PATCH 626/719] run(rdte): admit Minecraft Paper nested TrueNAS T6 after complete contract green --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 935ded160..59a812129 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-001-native-boot-world-reinstall", + "request_id": "tn-25-04-1-t6-minecraft-paper-002-native-boot-world-reinstall", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "FIRST disposable native TrueNAS 25.04.1 nested KVM Minecraft Paper application qualification. User authorized nested evaluation before any local TrueNAS deployment. Source producer SemperSupra/truenas-app-foundry@db54c26a245b81952930819ef29ab8e2c6851f87 (source artifact control, schema semper-supra.minecraft-paper-truenas-t6-control/1, run 38096741189) and product oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 (9/9 protocol tests CI 38096489436) consumed by Agent Dispatch PR #382 experiment merge 3b7aa4642dcee498ea42a0c56260f0b5a6468eed. Exact native community/minecraft Paper 1.21.11 catalog candidate 1.15.24 must be independently checked on guest; source image tag candidate does not establish digest. Bounded 8GiB guest, shared heavyweight slot; verify app native lifecycle, loopback-only Java status, independent world ZFS and unchanged sentinel across stop/start/redeploy/delete/reinstall, final owned cleanup. Fail closed on native catalog drift or runtime failure; preserve evidence and do not change user production TrueNAS, do not claim vanilla 26.3 or Two Rivers runtime support. Independent Jules PR #591 was empty; classify review inconclusive, use live disposable oracle for falsification only, keep production deployment hold." + "purpose": "FIRST disposable native TrueNAS 25.04.1 nested KVM Minecraft Paper application qualification. User authorized nested evaluation before any local TrueNAS deployment. Source producer SemperSupra/truenas-app-foundry@db54c26a245b81952930819ef29ab8e2c6851f87 (source artifact control, schema semper-supra.minecraft-paper-truenas-t6-control/1, run 38096741189) and product oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 (9/9 protocol tests CI 38096489436) consumed by Agent Dispatch PR #382 experiment merge 3b7aa4642dcee498ea42a0c56260f0b5a6468eed. Exact native community/minecraft Paper 1.21.11 catalog candidate 1.15.24 must be independently checked on guest; source image tag candidate does not establish digest. Bounded 8GiB guest, shared heavyweight slot; verify app native lifecycle, loopback-only Java status, independent world ZFS and unchanged sentinel across stop/start/redeploy/delete/reinstall, final owned cleanup. Fail closed on native catalog drift or runtime failure; preserve evidence and do not change user production TrueNAS, do not claim vanilla 26.3 or Two Rivers runtime support. Independent Jules PR #591 was empty; classify review inconclusive, use live disposable oracle for falsification only, keep production deployment hold. R2: retry after correcting exactly two legacy product inventory test literals; exact source contract prior no-request run 38150028030 SUCCESS (149 tests, 1 skipped). Original request 38149980587 was stopped pre-VM by fail-closed contract, no nested guest boot." } From c844e7a719e2e9b75911452a2c7e6d54b2f27995 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:11:20 +0200 Subject: [PATCH 627/719] fix(rdte): invoke nonexecutable Python capsule adapter through interpreter; gate Minecraft workflow on full guest receipt --- .github/workflows/gha-kvm-system-rdte.yml | 7 ++- scripts/assert_minecraft_t6_receipt.py | 68 +++++++++++++++++++++++ scripts/truenas_session_runner.py | 1 + tests/test_minecraft_t6_receipt_gate.py | 54 ++++++++++++++++++ tests/test_truenas_session_runner.py | 13 +++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 scripts/assert_minecraft_t6_receipt.py create mode 100644 tests/test_minecraft_t6_receipt_gate.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 84b79d0b6..90c60c496 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -215,7 +215,7 @@ jobs: with: persist-credentials: false - name: Validate public-safe contract - run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_garm_container_pre_b4.py tests/test_truenas_garm_controller_f0_f5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_t6_foliorelay_control.py tests/test_truenas_rdte_target.py + run: python3 -m unittest -v tests/test_gha_kvm_system_rdte.py tests/test_truenas_future_protocols.py tests/test_truenas_t6_litellm_control.py tests/test_truenas_t6_wow_sidecar_control.py tests/test_truenas_t6_garm_control.py tests/test_truenas_garm_provider_g2.py tests/test_truenas_garm_provider_g3.py tests/test_truenas_garm_provider_g4.py tests/test_truenas_garm_provider_g5.py tests/test_truenas_garm_container_pre_b4.py tests/test_truenas_garm_controller_f0_f5.py tests/test_truenas_t6_official_catalog_control.py tests/test_truenas_t6_foliorelay_control.py tests/test_truenas_rdte_target.py tests/test_truenas_session_runner.py tests/test_minecraft_t6_receipt_gate.py foundry-t6-control: needs: [changes, contract] @@ -624,6 +624,11 @@ jobs: fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + # A green job is not a Minecraft product pass without full guest evidence. + # Failure still uploads the receipt through always() below. + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then + python3 scripts/assert_minecraft_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + fi - name: Preserve TrueNAS harness failure evidence if: always() env: diff --git a/scripts/assert_minecraft_t6_receipt.py b/scripts/assert_minecraft_t6_receipt.py new file mode 100644 index 000000000..958a61d3b --- /dev/null +++ b/scripts/assert_minecraft_t6_receipt.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Fail closed unless nested Minecraft T6 proved protocol, persistence and cleanup.""" +import argparse +import json +import pathlib +import sys + + +def check(r): + def required(ok, message): + if not ok: + raise ValueError(message) + required(isinstance(r, dict) and r.get("contract") == "gha-kvm-system-lab/v1", "root schema") + target = r.get("target") or {} + required(target.get("product") == "truenas" and target.get("version") == "25.04.1" + and target.get("rung") == "T6", "wrong exact guest") + required(r.get("classification") == "SUPPORTED" and r.get("oracleSatisfied") is True, + "guest/root oracle failure") + s = r.get("session_execution") or {} + required(s.get("schema") == "truenas-session-execution/v1" + and s.get("classification") == "SESSION_CLEAN" and s.get("session_clean") is True, + "unclean capsule session") + capsules = s.get("capsules") or [] + required(isinstance(capsules, list) and len(capsules) == 1, "capsule count") + c = capsules[0] + required(c.get("provider") == "minecraft-paper-t6" and c.get("verdict") == "SUPPORTED" + and c.get("cleanup_satisfied") is True, "provider verdict/cleanup") + p = c.get("provider_receipt") or {} + f = r.get("foundry_materialization") or {} + required(p.get("schema") == "truenas-minecraft-paper-t6/v1" + and p.get("classification") == "SUPPORTED" and p.get("oracleSatisfied") is True, + "product oracle failure") + required(f.get("schema") == p.get("schema") and + f.get("control_sha256") == p.get("control_sha256"), + "materialization/provenance mismatch") + required(p.get("catalog_version_verified") is True, "catalog not confirmed") + required(p.get("world_retained_on_delete") is True + and p.get("world_rebound_after_reinstall") is True, "world not persistent") + cleanup = p.get("cleanup") or {} + required(all(cleanup.get(k) is True for k in + ("zero_residue", "app_absent", "owned_world_dataset_absent")), "cleanup failed") + readiness = p.get("readiness") or {} + for phase in ("first", "restart", "redeploy", "reinstall"): + stage = readiness.get(phase) or {} + o = stage.get("protocol") or {} + level = stage.get("level") or {} + required(o.get("schema") == "minecraft-java-status-oracle/v1" + and o.get("oracleSatisfied") is True and o.get("java_version") == "1.21.11" + and o.get("status_packet_valid") is True, "status missing: " + phase) + required(isinstance(level.get("size"), int) and level["size"] > 0, + "world level.dat absent: " + phase) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--receipt", type=pathlib.Path, required=True) + args = parser.parse_args() + try: + check(json.loads(args.receipt.read_text(encoding="utf-8"))) + except (OSError, ValueError) as exc: + print("Minecraft T6 acceptance FAILED: " + str(exc), file=sys.stderr) + return 2 + print("Minecraft T6 acceptance SUPPORTED: protocol/world/cleanup") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/truenas_session_runner.py b/scripts/truenas_session_runner.py index c2f95f616..4bd581d4a 100644 --- a/scripts/truenas_session_runner.py +++ b/scripts/truenas_session_runner.py @@ -181,6 +181,7 @@ def execute_capsule( try: cp = subprocess.run( [ + sys.executable, str(executor), "--capsule", str(descriptor_path), diff --git a/tests/test_minecraft_t6_receipt_gate.py b/tests/test_minecraft_t6_receipt_gate.py new file mode 100644 index 000000000..840ee3419 --- /dev/null +++ b/tests/test_minecraft_t6_receipt_gate.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import copy +import pathlib +import sys +import unittest +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) +from assert_minecraft_t6_receipt import check + + +def supported(): + o={"schema":"minecraft-java-status-oracle/v1","oracleSatisfied":True, + "java_version":"1.21.11","status_packet_valid":True} + p={"schema":"truenas-minecraft-paper-t6/v1","classification":"SUPPORTED", + "oracleSatisfied":True,"control_sha256":"a"*64,"catalog_version_verified":True, + "world_retained_on_delete":True,"world_rebound_after_reinstall":True, + "readiness":{k:{"protocol":o,"level":{"size":128}} for k in ("first","restart","redeploy","reinstall")}, + "cleanup":{"zero_residue":True,"app_absent":True,"owned_world_dataset_absent":True}} + c={"provider":"minecraft-paper-t6","verdict":"SUPPORTED","cleanup_satisfied":True,"provider_receipt":p} + return {"contract":"gha-kvm-system-lab/v1","classification":"SUPPORTED","oracleSatisfied":True, + "target":{"product":"truenas","version":"25.04.1","rung":"T6"}, + "session_execution":{"schema":"truenas-session-execution/v1","classification":"SESSION_CLEAN", + "session_clean":True,"capsules":[c]}, + "foundry_materialization":copy.deepcopy(p)} + + +class GateTests(unittest.TestCase): + def test_accepts_complete_synthetic(self): + check(supported()) + + def test_workflow_checks_exact_guest_receipt(self): + y=(ROOT/".github/workflows/gha-kvm-system-rdte.yml").read_text() + self.assertIn("python3 scripts/assert_minecraft_t6_receipt.py --receipt",y) + + def test_no_green_laundering(self): + for mutation in ( + lambda x:x.update({"classification":"HARNESS_FAILURE"}), + lambda x:x.update({"oracleSatisfied":False}), + lambda x:x.update({"session_execution":None}), + lambda x:x["session_execution"]["capsules"][0].update({"verdict":"ORACLE_FAILURE"}), + lambda x:x["session_execution"]["capsules"][0]["provider_receipt"].update({"catalog_version_verified":False}), + lambda x:x["session_execution"]["capsules"][0]["provider_receipt"].update({"world_rebound_after_reinstall":False}), + lambda x:x["session_execution"]["capsules"][0]["provider_receipt"]["cleanup"].update({"zero_residue":False}), + lambda x:x["session_execution"]["capsules"][0]["provider_receipt"]["readiness"].pop("reinstall"), + lambda x:x.update({"foundry_materialization":None}), + ): + d=supported() + mutation(d) + with self.assertRaises(ValueError): + check(d) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_truenas_session_runner.py b/tests/test_truenas_session_runner.py index bd72b0429..80518a6fa 100644 --- a/tests/test_truenas_session_runner.py +++ b/tests/test_truenas_session_runner.py @@ -162,6 +162,19 @@ def test_failed_dependency_skips_only_dependent_capsule(self): self.assertEqual(verdicts["independent"], "SUPPORTED") self.assertEqual(result["classification"], "SESSION_CLEAN") + def test_nonexecutable_python_adapter_uses_interpreter(self): + doc = manifest([capsule("first", "official-catalog-t6", 18080)]) + root = pathlib.Path(tempfile.mkdtemp()) + self.addCleanup(lambda: __import__("shutil").rmtree(root, ignore_errors=True)) + executable = self.executor() + executable.chmod(0o644) + self.assertFalse(executable.stat().st_mode & stat.S_IXUSR) + result = runner.run_session( + self.temp_json(doc), TARGETS, PROVIDERS, executable, + self.temp_json({"first": {"verdict": "SUPPORTED"}}), root / "receipts" + ) + self.assertEqual(result["capsules"][0]["verdict"], "SUPPORTED") + def test_executor_without_receipt_fails_closed(self): doc = manifest([ capsule("first", "official-catalog-t6", 18080), From 9b28e8b081cf1a07d7fc50a21bf732f4b57a27ab Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:12:23 +0200 Subject: [PATCH 628/719] fix(rdte): dispatch Python adapters via interpreter while preserving existing executable shell providers --- scripts/truenas_session_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/truenas_session_runner.py b/scripts/truenas_session_runner.py index 4bd581d4a..73124dff5 100644 --- a/scripts/truenas_session_runner.py +++ b/scripts/truenas_session_runner.py @@ -181,7 +181,7 @@ def execute_capsule( try: cp = subprocess.run( [ - sys.executable, + *([sys.executable] if executor.suffix == ".py" else []), str(executor), "--capsule", str(descriptor_path), From 96771d9c5a84e8b2cff051876adbe55693d69be9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:13:41 +0200 Subject: [PATCH 629/719] run(rdte): request Minecraft Paper T6 rep 003 after nonexecutable capsule and receipt gate fixes --- config/truenas-rdte-run-request.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index b5d2afde1..44102b0d1 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-26-beta3-t6-garm-container-pre-b4-004", - "version": "26.0.0-BETA.3", + "request_id": "tn-25-04-1-t6-minecraft-paper-003-interpreter-receipt-gate", + "version": "25.04.1", "rung": "t6", - "product": "garm-provider-container-pre-b4", - "authority_issue": 396, - "consumer_authority_issue": 43, - "purpose": "Run corrected BETA.3 GARM TrueNAS Container pre-B4 real-system rep 004 after rep 003 proved WSS, exact image resolution, container.create and cleanup but failed on a stale hidden-dataset query assumption. Bind merged provider source 9f3c86169320b6a2330bbb345d25a41c736c73c2 and merged Agent Dispatch consumer 84be7a83d436758788ab08dedf96815053b6c14b. Require exact TrueNAS-26.0.0-BETA.3 / middleware 81e1265a86083888ba94a2bdfc02ff5c9c5ef6a3; supported container.* create/readback; exact Ubuntu Noble image resolution; exact source-derived dataset /.truenas_containers/containers/ and rootfs /.truenas_containers//containers/ with no pool.dataset.query dependency; filesystem.put/stat staging of exact non-secret scripts plus one run-local 0600 credential file; observed init-wrapper and bootstrap-child markers; explicit credential-file deletion; persisted init/initenv scrub to /sbin/init plus empty environment; supported stop/delete; and final named-container absence. This remains B0-B3/B5 pre-B4 evidence only. Do not claim callback/JIT, GitHub registration, runtime admission, per-runner memory isolation, Docker/container-actions, physical TrueNAS readiness, or capacity promotion." + "product": "minecraft-paper", + "authority_issue": 411, + "consumer_authority_issue": 588, + "purpose": "Disposable nested TrueNAS 25.04.1 Minecraft Paper 1.21.11 native App T6 rep 003, explicitly authorized before any local/physical deployment. Correct first real rep 38150065146 HARNESS_FAILURE/false-green workflow: session runner directly executed Python adapter at Git mode 100644, returning EACCES and no session receipt; sealed source fix PR #384 merged at 93916e2279a341dbedf43f7942ddaafbc1cc961e. Require terminal public cheap runs 38158673452 and 38158673454 SUCCESS. Run only the existing serialized public GHA heavy TrueNAS lane. Exact Foundry producer db54c26a245b81952930819ef29ab8e2c6851f87 source control and product Java status oracle d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. Independently verify live native catalog Minecraft 1.15.24, application identity, loopback protocol 1.21.11, level.dat, stable owned world sentinel through restart/redeploy/remove_ix_volumes delete and reinstall, and final owned cleanup. Mandatory postrun receipt admission gate must yield nonzero workflow on any missing/failed runtime oracle; preserve failure artifact. Do not use local TrueNAS or private GHA minutes. No claims for vanilla 26.3, VanillaCord, Two Rivers, physical HIL, or immutable image digest." } From b413d0eaad881d84644e88fa8440caa4b1576020 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:45:23 +0200 Subject: [PATCH 630/719] fix(minecraft): match current TrueNAS native catalog r1.15.29 and immutable image digest; report discovered catalog revisions --- .../truenas_middleware_minecraft_t6_probe.py | 29 ++++++++++++++----- tests/test_truenas_t6_minecraft_control.py | 17 ++++++++++- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 16604857b..d2794d348 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -22,8 +22,8 @@ SCHEMA = "semper-supra.minecraft-paper-truenas-t6-control/1" EXPECTED_TARGET = "25.04.1" EXPECTED_PROFILE = "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" -EXPECTED_APPS_COMMIT = "532236882212e8c5e51d5b49cfe2c6ce3dd5675e" -EXPECTED_IMAGE_FRAGMENT = "ghcr.io/itzg/minecraft-server:2026.8.2-java21" +EXPECTED_APPS_COMMIT = "bb96ef6fd8c2708765012a4a3ca3739370a4f92c" +EXPECTED_IMAGE_FRAGMENT = "ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa" EXPECTED_MIDDLEWARE = "74ab5a2d373be4097dece257d00e1086376333ba" APP = "rdte-t6-minecraft-paper" DATASET = "rdtepool/minecraft-paper-t6-world" @@ -70,13 +70,14 @@ def load_control(path: pathlib.Path, expected_commit: str) -> dict: source = control.get("source") or {} if (source.get("apps_commit") != EXPECTED_APPS_COMMIT or source.get("app") != "community/minecraft" or + source.get("image_reference") != EXPECTED_IMAGE_FRAGMENT or source.get("library_version") != "2.3.4" or not re.fullmatch("[0-9a-f]{64}", str(source.get("source_compose_sha256", ""))) or not re.fullmatch("[0-9a-f]{64}", str(source.get("values_raw_sha256", "")))): raise MinecraftT6Error("pinned native Minecraft catalog source drift") runtime = control.get("runtime") or {} if (runtime.get("app_name") != APP or runtime.get("catalog_app") != "minecraft" or - runtime.get("train") != "community" or runtime.get("catalog_version_candidate") != "1.15.24" or + runtime.get("train") != "community" or runtime.get("catalog_version_candidate") != "1.15.29" or runtime.get("paper_version") != "1.21.11" or runtime.get("required_owned_world_dataset") != DATASET or runtime.get("status_oracle") != "minecraft-java-status-oracle/v1"): @@ -234,10 +235,12 @@ def wait_app(): app = getapp() if isinstance(app, dict) and app.get("state") == "RUNNING": images = ((app.get("active_workloads") or {}).get("images") or []) - if not isinstance(images, list) or not any(EXPECTED_IMAGE_FRAGMENT in str(i) for i in images): - raise MinecraftT6Error("native Minecraft App runtime image differs from exact source candidate") - payload["observed_image_reference"] = EXPECTED_IMAGE_FRAGMENT - payload["image_digest_qualification"] = "NOT_YET_EARNED" + matching_image = next((str(i) for i in images if + isinstance(i, str) and i == EXPECTED_IMAGE_FRAGMENT), None) if isinstance(images, list) else None + if matching_image is None: + raise MinecraftT6Error("native Minecraft App runtime image differs from digest-pinned source") + payload["observed_image_reference"] = matching_image + payload["image_digest_qualification"] = "SOURCE_AND_GUEST_REF_MATCH" cfg = call("app.config", [APP]) if owned_config(cfg, desired): return app @@ -307,7 +310,17 @@ def delete_app(label): raise MinecraftT6Error("refusing to adopt pre-existing app or ZFS world dataset") catalog = call("catalog.get_app_details", ["minecraft", {"train": "community"}]) versions = (catalog or {}).get("versions") or {} - if not isinstance(versions, dict) or control["runtime"]["catalog_version_candidate"] not in versions: + expected_catalog = control["runtime"]["catalog_version_candidate"] + # Record bounded public-safe discovered revision identities, not catalog secrets. + payload["catalog_discovery"] = { + "catalog_app": (catalog or {}).get("name"), + "latest_version": (catalog or {}).get("latest_version"), + "healthy": (catalog or {}).get("healthy"), + "visible_versions": sorted(str(v) for v in versions)[:32] if isinstance(versions, dict) else [], + "requested_version": expected_catalog, + "upstream_apps_commit": EXPECTED_APPS_COMMIT, + } + if not isinstance(versions, dict) or expected_catalog not in versions: raise MinecraftT6Error("exact native Minecraft catalog version unavailable") payload["catalog_version_verified"] = True diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index 9c0033232..78a61d156 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -53,13 +53,14 @@ def fixture() -> dict: "source": { "apps_commit": mc.EXPECTED_APPS_COMMIT, "app": "community/minecraft", + "image_reference": mc.EXPECTED_IMAGE_FRAGMENT, "library_version": "2.3.4", "source_compose_sha256": "1"*64, "values_raw_sha256": "2"*64, }, "runtime": { "app_name": mc.APP, "catalog_app": "minecraft", - "train": "community", "catalog_version_candidate": "1.15.24", + "train": "community", "catalog_version_candidate": "1.15.29", "paper_version": "1.21.11", "required_owned_world_dataset": mc.DATASET, "status_oracle": "minecraft-java-status-oracle/v1", "values_template": values, @@ -93,6 +94,8 @@ def test_good_control_lowering_changes_only_world_and_port(self): "/opt/tests/collage-world-only") self.assertEqual(initial["network"]["server_port"]["port_number"], 25565) self.assertEqual(mc.WORLD, "/mnt/rdtepool/minecraft-paper-t6-world") + self.assertIn("@sha256:", mc.EXPECTED_IMAGE_FRAGMENT) + self.assertEqual(control["source"]["apps_commit"], mc.EXPECTED_APPS_COMMIT) self.assertTrue(mc.owned_config(lowered, lowered)) def test_wrong_producer_rejected(self): @@ -117,6 +120,18 @@ def test_wrong_target_rejected(self): with self.assertRaisesRegex(mc.MinecraftT6Error, "target/profile"): admit(doc) + def test_digest_pinned_minecraft_image_required(self): + doc = fixture() + doc["source"]["image_reference"] = "ghcr.io/itzg/minecraft-server:latest" + with self.assertRaisesRegex(mc.MinecraftT6Error, "catalog source"): + admit(doc) + + def test_live_catalog_version_mismatch_rejected(self): + doc = fixture() + doc["runtime"]["catalog_version_candidate"] = "1.15.24" + with self.assertRaisesRegex(mc.MinecraftT6Error, "product/source"): + admit(doc) + def test_wrong_upstream_source_rejected(self): doc = fixture() doc["source"]["apps_commit"] = "b"*40 From 7a3e11164c0ecfcccd5c6cf3de5100f5e35e152c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:47:50 +0200 Subject: [PATCH 631/719] fix(minecraft): bind source-proven catalog 1.15.29 and digest-pinned native app lifecycle to Foundry exporter 2cc7e1fe --- .github/workflows/gha-kvm-system-rdte.yml | 6 +++--- ...truenas-session-minecraft-paper-25-04-1.json | 12 +++++++----- .../truenas_middleware_minecraft_t6_probe.py | 17 ++++++++++++++--- tests/test_truenas_t6_minecraft_control.py | 13 +++++++++++++ 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 90c60c496..a5e5fe1ec 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -277,9 +277,9 @@ jobs: foundry-minecraft-paper-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-paper-t6-control.yml@db54c26a245b81952930819ef29ab8e2c6851f87 + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-paper-t6-control.yml@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e with: - foundry_ref: db54c26a245b81952930819ef29ab8e2c6851f87 + foundry_ref: 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e target_version: "25.04.1" permissions: contents: read @@ -608,7 +608,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then foundry_commit="81deb97185760975fd8d3162df42056c77b3c0fd" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then - foundry_commit="db54c26a245b81952930819ef29ab8e2c6851f87" + foundry_commit="2cc7e1fe3772f8c532e933b3d7832e6cf985db4e" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi diff --git a/config/truenas-session-minecraft-paper-25-04-1.json b/config/truenas-session-minecraft-paper-25-04-1.json index 833075e77..4281b5da4 100644 --- a/config/truenas-session-minecraft-paper-25-04-1.json +++ b/config/truenas-session-minecraft-paper-25-04-1.json @@ -26,12 +26,14 @@ "issue": 53 }, "exact": { - "foundry_producer_ref": "db54c26a245b81952930819ef29ab8e2c6851f87", - "foundry_artifact_sha256": "cebe2f54dc87875ac276b1b6f46286372488db64b00c376b6ca7923f9430121a", + "foundry_producer_ref": "2cc7e1fe3772f8c532e933b3d7832e6cf985db4e", + "foundry_artifact_sha256": "cd249c0a97069ff958475b65dbf2143769fe87534c8879b02912de9e146ab525", "minecraft_product_oracle_ref": "d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8", "minecraft_version": "1.21.11", - "native_catalog_candidate": "1.15.24", - "truenas_target_profile_blob": "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" + "native_catalog_candidate": "1.15.29", + "truenas_target_profile_blob": "800aca038ded29e6a91bfc4c1b1fe2d2199182e8", + "native_image_reference": "ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa", + "upstream_apps_commit": "bb96ef6fd8c2708765012a4a3ca3739370a4f92c" }, "namespace": "rdte-minecraft-paper-25-04-1", "ports": [ @@ -62,5 +64,5 @@ "dependencies": [] } ], - "manifest_sha256": "735f2daa94877da895c08a8ad690df2ef57f0225317ff68d46228adca17cdaa6" + "manifest_sha256": "85a740366843255ff2cee80ee0da3550429c911614b91d27325badf58a9d3152" } diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index d2794d348..c1306e63b 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -45,6 +45,19 @@ class MinecraftT6Error(RuntimeError): pass +def qualified_native_image(app: dict) -> str | None: + """Accept only the digest-pinned OCI reference, with optional tag normalization.""" + work = app.get("active_workloads") or {} + candidates = list(work.get("images") or []) + for item in work.get("container_details") or []: + if isinstance(item, dict) and isinstance(item.get("image"), str): + candidates.append(item["image"]) + repository_tag, digest = EXPECTED_IMAGE_FRAGMENT.split("@sha256:", 1) + repository = repository_tag.rsplit(":", 1)[0] + valid = {EXPECTED_IMAGE_FRAGMENT, repository + "@sha256:" + digest} + return next((v for v in candidates if isinstance(v, str) and v in valid), None) + + def canonical_sha256(doc: dict) -> str: return hashlib.sha256(json.dumps(doc, sort_keys=True, separators=(",", ":")).encode()).hexdigest() @@ -234,9 +247,7 @@ def wait_app(): while time.monotonic() < deadline: app = getapp() if isinstance(app, dict) and app.get("state") == "RUNNING": - images = ((app.get("active_workloads") or {}).get("images") or []) - matching_image = next((str(i) for i in images if - isinstance(i, str) and i == EXPECTED_IMAGE_FRAGMENT), None) if isinstance(images, list) else None + matching_image = qualified_native_image(app) if matching_image is None: raise MinecraftT6Error("native Minecraft App runtime image differs from digest-pinned source") payload["observed_image_reference"] = matching_image diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index 78a61d156..a7b68253b 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -176,6 +176,19 @@ def test_foreign_config_not_adopted(self): bad["minecraft"]["additional_envs"] = [] self.assertFalse(mc.owned_config(bad, values)) + def test_runtime_image_matches_digest_with_or_without_tag(self): + ref = mc.EXPECTED_IMAGE_FRAGMENT + app = {"active_workloads": {"images": [ref]}} + self.assertEqual(mc.qualified_native_image(app), ref) + repo, digest = ref.split("@sha256:", 1) + normalized = repo.rsplit(":", 1)[0] + "@sha256:" + digest + app = {"active_workloads": {"container_details": [{"image": normalized}]}} + self.assertEqual(mc.qualified_native_image(app), normalized) + app = {"active_workloads": {"images": [ref + ".evil"]}} + self.assertIsNone(mc.qualified_native_image(app)) + app = {"active_workloads": {"images": ["ghcr.io/itzg/minecraft-server:latest"]}} + self.assertIsNone(mc.qualified_native_image(app)) + def test_status_script_must_be_exact_named(self): with tempfile.TemporaryDirectory() as td: other = pathlib.Path(td) / "anything.py" From 1771faa395df6d186f0f46549cd55ae6b3e1c5c9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:49:53 +0200 Subject: [PATCH 632/719] run(rdte): admit Minecraft Paper live catalog 1.15.29 T6 disposable TrueNAS rep 004 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 44102b0d1..7837bcccb 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-003-interpreter-receipt-gate", + "request_id": "tn-25-04-1-t6-minecraft-paper-004-catalog-r29-sha-image", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "Disposable nested TrueNAS 25.04.1 Minecraft Paper 1.21.11 native App T6 rep 003, explicitly authorized before any local/physical deployment. Correct first real rep 38150065146 HARNESS_FAILURE/false-green workflow: session runner directly executed Python adapter at Git mode 100644, returning EACCES and no session receipt; sealed source fix PR #384 merged at 93916e2279a341dbedf43f7942ddaafbc1cc961e. Require terminal public cheap runs 38158673452 and 38158673454 SUCCESS. Run only the existing serialized public GHA heavy TrueNAS lane. Exact Foundry producer db54c26a245b81952930819ef29ab8e2c6851f87 source control and product Java status oracle d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. Independently verify live native catalog Minecraft 1.15.24, application identity, loopback protocol 1.21.11, level.dat, stable owned world sentinel through restart/redeploy/remove_ix_volumes delete and reinstall, and final owned cleanup. Mandatory postrun receipt admission gate must yield nonzero workflow on any missing/failed runtime oracle; preserve failure artifact. Do not use local TrueNAS or private GHA minutes. No claims for vanilla 26.3, VanillaCord, Two Rivers, physical HIL, or immutable image digest." + "purpose": "Disposable nested TrueNAS 25.04.1 Minecraft Paper native App T6 rep 004 (user-authorized, pre-local). Correct live rep 003 38158758653 ORACLE_FAILURE: pinned catalog 1.15.24 unavailable. Exact current truenas/apps@bb96ef6fd8c2708765012a4a3ca3739370a4f92c exposes community/minecraft 1.15.29 with OCI ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa. Source producer SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e qualified via exact source world-render run 38160798858, export run 38160798843 artifact 11710720580 SHA cd249c0a97069ff958475b65dbf2143769fe87534c8879b02912de9e146ab525 and security run 38160798828. Agent Dispatch consumer PR #385 merged at 8f31715a294d467cc4af16bad3291e3d093df938, static 38161156313 and Minecraft 38161156283 pass. Reuse single public heavy guest lane, exact current native catalog and immutable source image. Require TrueNAS live app catalog/version+OCI digest read-back, Java status Paper 1.21.11 and level.dat at first/restart/redeploy/reinstall, owned ZFS world/sentinel retained across App ixVolume delete/reinstall, zero cleanup residue, terminal CI nonzero on any incomplete runtime acceptance. Preserve failure receipt and catalog discovery on refusal. No physical/local TrueNAS changes or private GHA minutes; no vanilla 26.3/Two Rivers claims." } From 22dae9faeab516ef9b855ca47ddd2efe679e0337 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:29:34 +0200 Subject: [PATCH 633/719] fix(minecraft): preserve redacted native app.create job errors and independently verify failure cleanup Make terminal core.get_jobs failures diagnosable in bounded public receipts. Distinguish product oracle failure with proven owned cleanup and read-only middleware health from truly contaminated sessions. Preserve fail-closed CI and physical HOLD. --- ...truenas_existing_probe_capsule_executor.py | 10 +++++ .../truenas_middleware_minecraft_t6_probe.py | 39 +++++++++++++++++++ ...truenas_existing_probe_capsule_executor.py | 25 ++++++++++++ tests/test_truenas_t6_minecraft_control.py | 13 +++++++ 4 files changed, 87 insertions(+) diff --git a/scripts/truenas_existing_probe_capsule_executor.py b/scripts/truenas_existing_probe_capsule_executor.py index 06ac20e29..d27f2a0cb 100644 --- a/scripts/truenas_existing_probe_capsule_executor.py +++ b/scripts/truenas_existing_probe_capsule_executor.py @@ -180,6 +180,16 @@ def wrap_receipt( verdict = observed if observed in KNOWN_FAILURES else "HARNESS_FAILURE" cleanup_satisfied = False platform_healthy = False + # Minecraft T6 provides independent, post-failure read-only cleanup + # and platform witnesses. Do not promote the failed product oracle. + if provider["id"] == "minecraft-paper-t6" and provider_receipt: + evidence = provider_receipt.get("cleanup") or {} + cleanup_satisfied = (evidence.get("attempted") is True + and evidence.get("zero_residue") is True + and evidence.get("app_absent") is True + and evidence.get("owned_world_dataset_absent") is True) + platform_healthy = (cleanup_satisfied + and provider_receipt.get("post_failure_platform_healthy") is True) reason = ( (provider_receipt or {}).get("detail") or stderr.strip() diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index c1306e63b..35c9c8e7d 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -58,6 +58,29 @@ def qualified_native_image(app: dict) -> str | None: return next((v for v in candidates if isinstance(v, str) and v in valid), None) +def safe_job_error(state: dict, password: str) -> dict: + """Bounded public-safe error evidence. Never emit traceback or unredacted secrets.""" + raw = state.get("error") + raw = raw if isinstance(raw, str) else "" + if password: + raw = raw.replace(password, "[redacted]") + raw = re.sub( + r"(?i)(password|passphrase|token|secret|authorization|credential|api[_-]?key)\\s*[:=]\\s*[^,;\\s]+", + r"\\1=[redacted]", raw, + ) + raw = re.sub(r"[A-Za-z0-9_+/=-]{48,}", "[opaque]", raw) + raw = "".join(c if 32 <= ord(c) <= 126 else " " for c in raw) + info = state.get("exc_info") or {} + kind = info.get("type") if isinstance(info, dict) else None + return { + "job_state": state.get("state") if state.get("state") in ("FAILED", "ABORTED") else "UNKNOWN", + "error_excerpt": raw[:480], + "error_present": bool(raw), + "exception_kind": str(kind)[:100] if isinstance(kind, str) else None, + "truncated": len(raw) > 480, + } + + def canonical_sha256(doc: dict) -> str: return hashlib.sha256(json.dumps(doc, sort_keys=True, separators=(",", ":")).encode()).hexdigest() @@ -237,6 +260,9 @@ def wait_job(job, label): if isinstance(state, dict) and state.get("state") == "SUCCESS": return if isinstance(state, dict) and state.get("state") in {"FAILED", "ABORTED"}: + payload["failed_job"] = { + "phase": label, "job_id": job, **safe_job_error(state, password) + } raise MinecraftT6Error(label + " returned terminal failed job") time.sleep(2) raise MinecraftT6Error(label + " job timeout") @@ -420,6 +446,19 @@ def delete_app(label): except Exception as cexc: cleanup["cleanup_error_type"] = type(cexc).__name__ payload["cleanup"] = cleanup + # A failed product oracle need not mean contaminated guest or failed cleanup. + # Prove the guest middleware is still coherent using read-only endpoints. + if ws is not None and payload["cleanup"].get("zero_residue") is True: + try: + system_ok = call("system.version", []) == a.expected_system_version + docker = call("docker.config", []) + payload["post_failure_platform_healthy"] = bool( + system_ok and isinstance(docker, dict) + and getapp() is None + and one("pool.dataset.query", [["id", "=", DATASET]]) is None + ) + except Exception: + payload["post_failure_platform_healthy"] = False finally: if ws is not None: ws.close() diff --git a/tests/test_truenas_existing_probe_capsule_executor.py b/tests/test_truenas_existing_probe_capsule_executor.py index c71fed604..dc9df691c 100644 --- a/tests/test_truenas_existing_probe_capsule_executor.py +++ b/tests/test_truenas_existing_probe_capsule_executor.py @@ -141,6 +141,31 @@ def test_failed_provider_receipt_stops_fail_closed_until_membrane_exists(self): self.assertFalse(receipt["cleanup_satisfied"]) self.assertFalse(receipt["platform_healthy"]) + def test_minecraft_oracle_failure_with_verified_cleanup_is_not_contamination(self): + _, descriptor, _ = self.fixture("minecraft-paper-t6") + receipt = exe.wrap_receipt(descriptor, { + "classification": "ORACLE_FAILURE", + "oracleSatisfied": False, + "detail": "app.create job failed", + "post_failure_platform_healthy": True, + "cleanup": {"attempted": True, "zero_residue": True, + "app_absent": True, "owned_world_dataset_absent": True} + }, 2, "") + self.assertEqual(receipt["verdict"], "ORACLE_FAILURE") + self.assertTrue(receipt["cleanup_satisfied"]) + self.assertTrue(receipt["platform_healthy"]) + + def test_minecraft_partial_cleanup_still_stops_mutation(self): + _, descriptor, _ = self.fixture("minecraft-paper-t6") + receipt = exe.wrap_receipt(descriptor, { + "classification": "ORACLE_FAILURE", "oracleSatisfied": False, + "post_failure_platform_healthy": True, + "cleanup": {"attempted": True, "zero_residue": False, + "app_absent": True, "owned_world_dataset_absent": False} + }, 2, "") + self.assertFalse(receipt["cleanup_satisfied"]) + self.assertFalse(receipt["platform_healthy"]) + def test_missing_provider_receipt_is_harness_failure(self): _, descriptor, _ = self.fixture("official-catalog-t6") receipt = exe.wrap_receipt(descriptor, None, 7, "crashed") diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index a7b68253b..9657a1fdc 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -189,6 +189,19 @@ def test_runtime_image_matches_digest_with_or_without_tag(self): app = {"active_workloads": {"images": ["ghcr.io/itzg/minecraft-server:latest"]}} self.assertIsNone(mc.qualified_native_image(app)) + def test_job_error_receipt_redacts_secrets_and_bounds_detail(self): + state = {"state": "FAILED", + "error": "AppValidation: host_path invalid; password=my-secret-123; " + "TOKEN=supersecret0987; credential=abcd; literal=known-pass"} + cleaned = mc.safe_job_error(state, "known-pass") + self.assertEqual(cleaned["job_state"], "FAILED") + self.assertTrue(cleaned["error_present"]) + for forbidden in ("my-secret-123", "supersecret0987", "abcd", "known-pass"): + self.assertNotIn(forbidden, cleaned["error_excerpt"]) + self.assertIn("host_path invalid", cleaned["error_excerpt"]) + large = mc.safe_job_error({"state": "FAILED", "error": "x" * 700}, "secret") + self.assertLessEqual(len(large["error_excerpt"]), 480) + def test_status_script_must_be_exact_named(self): with tempfile.TemporaryDirectory() as td: other = pathlib.Path(td) / "anything.py" From ec39809dd13dedac2c1f478a3cd6c91eeb55551f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:30:03 +0200 Subject: [PATCH 634/719] fix(minecraft): make credential-redaction regex consume whitespace and capture groups correctly --- scripts/truenas_middleware_minecraft_t6_probe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 35c9c8e7d..313195c0e 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -65,8 +65,8 @@ def safe_job_error(state: dict, password: str) -> dict: if password: raw = raw.replace(password, "[redacted]") raw = re.sub( - r"(?i)(password|passphrase|token|secret|authorization|credential|api[_-]?key)\\s*[:=]\\s*[^,;\\s]+", - r"\\1=[redacted]", raw, + r"(?i)(password|passphrase|token|secret|authorization|credential|api[_-]?key)\s*[:=]\s*[^,;\s]+", + r"\1=[redacted]", raw, ) raw = re.sub(r"[A-Za-z0-9_+/=-]{48,}", "[opaque]", raw) raw = "".join(c if 32 <= ord(c) <= 126 else " " for c in raw) From be699f27619d631a962ae3e2a9a15d9b1abca317 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:31:21 +0200 Subject: [PATCH 635/719] run(rdte): request isolated Minecraft native app.create diagnostic rep 005 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 7837bcccb..9ad75137b 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-004-catalog-r29-sha-image", + "request_id": "tn-25-04-1-t6-minecraft-paper-005-native-create-job-error", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "Disposable nested TrueNAS 25.04.1 Minecraft Paper native App T6 rep 004 (user-authorized, pre-local). Correct live rep 003 38158758653 ORACLE_FAILURE: pinned catalog 1.15.24 unavailable. Exact current truenas/apps@bb96ef6fd8c2708765012a4a3ca3739370a4f92c exposes community/minecraft 1.15.29 with OCI ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa. Source producer SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e qualified via exact source world-render run 38160798858, export run 38160798843 artifact 11710720580 SHA cd249c0a97069ff958475b65dbf2143769fe87534c8879b02912de9e146ab525 and security run 38160798828. Agent Dispatch consumer PR #385 merged at 8f31715a294d467cc4af16bad3291e3d093df938, static 38161156313 and Minecraft 38161156283 pass. Reuse single public heavy guest lane, exact current native catalog and immutable source image. Require TrueNAS live app catalog/version+OCI digest read-back, Java status Paper 1.21.11 and level.dat at first/restart/redeploy/reinstall, owned ZFS world/sentinel retained across App ixVolume delete/reinstall, zero cleanup residue, terminal CI nonzero on any incomplete runtime acceptance. Preserve failure receipt and catalog discovery on refusal. No physical/local TrueNAS changes or private GHA minutes; no vanilla 26.3/Two Rivers claims." + "purpose": "Disposable nested TrueNAS 25.04.1 / Minecraft Paper 1.21.11 T6 diagnostic rep 005. Prior true guest rep 004 [38161276550] established live native catalog community/minecraft 1.15.29 healthy, source-bound digest image, owned world dataset and sentinel, but app.create job terminal FAILED without reason; guest cleanup of owned app and dataset zero-residue was independently observed, but capsule envelope formerly mislabeled it contaminated. PR #386 merged experimental consumer at exact sha 45ab52b0c7a9fa31b2a87b53b37e62c14df3dfe4, after static 38164086126 and Minecraft 38164086114 terminal success. Exact producer SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, upstream apps bb96ef6fd8c2708765012a4a3ca3739370a4f92c native revision 1.15.29, image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. One serialized public heavyweight lane only. Preserve bounded redacted core.get_jobs state/error on FAILED app.create, independently verify guest platform health and zero-residue cleanup with read-only middleware, maintain fail-closed CI if Paper status/world lifecycle not qualified. If app.create is corrected and launches, require status 1.21.11, level.dat and world sentinel across first/restart/redeploy/reinstall, final cleanup. Never mutate local/physical TrueNAS, never treat Paper as vanilla Java 26.3 / Two Rivers, never promote on partial evidence." } From a360e59eff62a495d27b7dfdc2e9e688abba3fff Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:04:55 +0200 Subject: [PATCH 636/719] fix(minecraft): omit absent optional seed from native TrueNAS app.create values Exact guest rep 005 produced [EINVAL] values.minecraft.seed: null not allowed. Preserve immutable Foundry source control and all other values, drop only seed=null at runtime, and reject accidental rewrites of explicit seeds through regression checks. --- .../truenas_middleware_minecraft_t6_probe.py | 9 +++++++- tests/test_truenas_t6_minecraft_control.py | 21 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 313195c0e..2ca40da4e 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -157,10 +157,17 @@ def load_control(path: pathlib.Path, expected_commit: str) -> dict: def runtime_values(control: dict) -> dict: - """Lower ONLY the experiment's world dataset and published guest port.""" + """Lower exact site bindings and remove only an absent optional native seed. + + Foundry source-render accepts seed: null; middleware app.create on + TrueNAS 25.04.1 rejects that field with [EINVAL] ...seed: null not allowed. + Omission means catalog default, not a synthesized world seed. + """ values = copy.deepcopy(control["runtime"]["values_template"]) values["storage"]["additional_storage"][0]["host_path_config"]["path"] = WORLD values["network"]["server_port"]["port_number"] = PORT + if "seed" in values["minecraft"] and values["minecraft"]["seed"] is None: + del values["minecraft"]["seed"] return values diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index 9657a1fdc..d059c9de8 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -26,7 +26,7 @@ def fixture() -> dict: "resources": {"limits": {"cpus": 2, "memory": 4096}}, "minecraft": { "type": "PAPER", "version": "1.21.11", "eula": True, "online_mode": True, - "enable_rcon": False, "additional_envs": [ + "enable_rcon": False, "seed": None, "additional_envs": [ {"name": k, "value": v} for k, v in mc.REQUIRED_OWNER.items() ], }, @@ -84,10 +84,14 @@ def admit(doc, expected=SOURCE_SHA): class TestNativeMinecraftControl(unittest.TestCase): - def test_good_control_lowering_changes_only_world_and_port(self): + def test_good_control_lowering_changes_site_bindings_and_omits_null_optional_seed(self): control = admit(fixture()) initial = copy.deepcopy(control["runtime"]["values_template"]) lowered = mc.runtime_values(control) + self.assertIn("seed", initial["minecraft"]) + self.assertIsNone(initial["minecraft"]["seed"]) + self.assertNotIn("seed", lowered["minecraft"]) + self.assertIsNone(control["runtime"]["values_template"]["minecraft"]["seed"]) self.assertEqual(lowered["storage"]["additional_storage"][0]["host_path_config"]["path"], mc.WORLD) self.assertEqual(lowered["network"]["server_port"]["port_number"], mc.PORT) self.assertEqual(initial["storage"]["additional_storage"][0]["host_path_config"]["path"], @@ -98,6 +102,19 @@ def test_good_control_lowering_changes_only_world_and_port(self): self.assertEqual(control["source"]["apps_commit"], mc.EXPECTED_APPS_COMMIT) self.assertTrue(mc.owned_config(lowered, lowered)) + def test_explicit_non_null_seed_is_not_implicitly_changed(self): + doc = fixture() + doc["runtime"]["values_template"]["minecraft"]["seed"] = "43-meaningful-seed" + lowered = mc.runtime_values(admit(doc)) + self.assertEqual(lowered["minecraft"]["seed"], "43-meaningful-seed") + + def test_unrelated_optional_nulls_do_not_get_stripped(self): + doc = fixture() + doc["runtime"]["values_template"]["minecraft"]["motd"] = None + lowered = mc.runtime_values(admit(doc)) + self.assertIsNone(lowered["minecraft"]["motd"]) + self.assertNotIn("seed", lowered["minecraft"]) + def test_wrong_producer_rejected(self): with self.assertRaisesRegex(mc.MinecraftT6Error, "producer identity"): admit(fixture(), expected="b"*40) From 80556e0458b326b225cfec22a5d3b9472b2d6758 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:06:20 +0200 Subject: [PATCH 637/719] run(rdte): admit disposable Minecraft Paper native seed-null fix T6 rep 006 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 9ad75137b..c884fa9a6 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-005-native-create-job-error", + "request_id": "tn-25-04-1-t6-minecraft-paper-006-seed-null-validation-fix", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "Disposable nested TrueNAS 25.04.1 / Minecraft Paper 1.21.11 T6 diagnostic rep 005. Prior true guest rep 004 [38161276550] established live native catalog community/minecraft 1.15.29 healthy, source-bound digest image, owned world dataset and sentinel, but app.create job terminal FAILED without reason; guest cleanup of owned app and dataset zero-residue was independently observed, but capsule envelope formerly mislabeled it contaminated. PR #386 merged experimental consumer at exact sha 45ab52b0c7a9fa31b2a87b53b37e62c14df3dfe4, after static 38164086126 and Minecraft 38164086114 terminal success. Exact producer SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, upstream apps bb96ef6fd8c2708765012a4a3ca3739370a4f92c native revision 1.15.29, image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. One serialized public heavyweight lane only. Preserve bounded redacted core.get_jobs state/error on FAILED app.create, independently verify guest platform health and zero-residue cleanup with read-only middleware, maintain fail-closed CI if Paper status/world lifecycle not qualified. If app.create is corrected and launches, require status 1.21.11, level.dat and world sentinel across first/restart/redeploy/reinstall, final cleanup. Never mutate local/physical TrueNAS, never treat Paper as vanilla Java 26.3 / Two Rivers, never promote on partial evidence." + "purpose": "Disposable nested TrueNAS 25.04.1 Minecraft Paper 1.21.11 T6 rep 006. Exact guest rep 005 [38164170973], created at be699f27619d631a962ae3e2a9a15d9b1abca317, proved live community/minecraft catalog 1.15.29, owned world sentinel and clean guest, but native app.create job 77 returned [EINVAL] values.minecraft.seed: null not allowed. Fail-closed correctly rejected. PR #387 merged experimental consumer at exact sha 10bb1c1fb6b846585547c987ff3949f9d30eaed3, now removes ONLY optional seed=None before sending native app.create, never modifies immutable source control, does not rewrite explicit seed or unrelated nulls. Cheap public Minecraft [38170715830] and general static [38170715835] tests terminal SUCCESS. Exact source Foundry SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, upstream truenas/apps@bb96ef6fd8c2708765012a4a3ca3739370a4f92c, native revision 1.15.29, OCI ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, status oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. Execute only one shared serialized public runner exact guest. Require full native app.create RUNNING identity, exact 1.21.11 Java status, level.dat, owned world sentinel retained across restart/redeploy/native remove_ix_volumes delete/reinstall and final zero owned residue. On any failure preserve bounded diagnostic receipts and independent guest cleanup witness; fail workflow, no local/physical TrueNAS or private GH actions minutes. Paper scope does not qualify vanilla Java 26.3 or Two Rivers." } From 5f00ccc04d269d1a0af5ddb77d54b57206b65ef3 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:09:20 +0200 Subject: [PATCH 638/719] fix(minecraft): reconcile root requested-shape catalog revision with native 1.15.29 Correct descriptive root receipt metadata; live product/Foundry authority already pinned 1.15.29. No fixture or native deployment semantics change. Add cheap explicit consistency regression. --- scripts/gha_kvm_truenas_rdte.sh | 2 +- tests/test_truenas_t6_minecraft_control.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) mode change 100755 => 100644 scripts/gha_kvm_truenas_rdte.sh diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh old mode 100755 new mode 100644 index caeedd9d4..238a6caa3 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -229,7 +229,7 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" else {"name": "rdte-t6-foliorelay", "images": foliorelay_images} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" - else {"name": "rdte-t6-minecraft-paper", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.24", "paper_version": "1.21.11"} + else {"name": "rdte-t6-minecraft-paper", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.29", "paper_version": "1.21.11"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "minecraft-paper" else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index d059c9de8..c2a2c3513 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -115,6 +115,11 @@ def test_unrelated_optional_nulls_do_not_get_stripped(self): self.assertIsNone(lowered["minecraft"]["motd"]) self.assertNotIn("seed", lowered["minecraft"]) + def test_root_requested_shape_uses_same_native_catalog_revision(self): + harness = (ROOT / "scripts/gha_kvm_truenas_rdte.sh").read_text(encoding="utf-8") + self.assertIn('"catalog_version_candidate": "1.15.29", "paper_version": "1.21.11"', harness) + self.assertNotIn('"catalog_version_candidate": "1.15.24"', harness) + def test_wrong_producer_rejected(self): with self.assertRaisesRegex(mc.MinecraftT6Error, "producer identity"): admit(fixture(), expected="b"*40) From c924518d1ac1da793568b8d4c0378a2aa05925cd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:27:26 +0200 Subject: [PATCH 639/719] fix(rdte): retain sanitized live TrueNAS ownership mismatch diagnostics * fix(rdte): retain sanitized live Minecraft owner-readback mismatch witnesses * test(rdte): ensure live readback witnesses contain booleans only --- .../truenas_middleware_minecraft_t6_probe.py | 32 ++++++++++++------- tests/test_truenas_t6_minecraft_control.py | 20 ++++++++++++ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 2ca40da4e..749c1bedd 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -191,18 +191,22 @@ def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, return verdict +def owner_readback_vector(config: dict, wanted: dict) -> dict[str, bool]: + """Only boolean equality witnesses. Never emit app.config values or owner envs.""" + minecraft = (config.get("minecraft") or {}) if isinstance(config, dict) else {} + env = minecraft.get("additional_envs") or [] if isinstance(minecraft, dict) else [] + found_owner = {v.get("name"): v.get("value") for v in env if isinstance(v, dict)} if isinstance(env, list) else {} + return { + "minecraft_type": minecraft.get("type") == "PAPER" if isinstance(minecraft, dict) else False, + "minecraft_version": minecraft.get("version") == "1.21.11" if isinstance(minecraft, dict) else False, + "owner_environment": found_owner == REQUIRED_OWNER, + "additional_storage": (config.get("storage") or {}).get("additional_storage") == wanted["storage"]["additional_storage"] if isinstance(config, dict) and isinstance(config.get("storage") or {}, dict) else False, + "server_port": (config.get("network") or {}).get("server_port") == wanted["network"]["server_port"] if isinstance(config, dict) and isinstance(config.get("network") or {}, dict) else False, + } + + def owned_config(config: dict, wanted: dict) -> bool: - if not isinstance(config, dict): - return False - minecraft = config.get("minecraft") or {} - env = minecraft.get("additional_envs") or [] - return ( - minecraft.get("type") == "PAPER" - and minecraft.get("version") == "1.21.11" - and {v.get("name"): v.get("value") for v in env if isinstance(v, dict)} == REQUIRED_OWNER - and (config.get("storage") or {}).get("additional_storage") == wanted["storage"]["additional_storage"] - and (config.get("network") or {}).get("server_port") == wanted["network"]["server_port"] - ) + return all(owner_readback_vector(config, wanted).values()) def main() -> int: @@ -286,8 +290,12 @@ def wait_app(): payload["observed_image_reference"] = matching_image payload["image_digest_qualification"] = "SOURCE_AND_GUEST_REF_MATCH" cfg = call("app.config", [APP]) - if owned_config(cfg, desired): + match_vector = owner_readback_vector(cfg, desired) + if all(match_vector.values()): return app + # Emit only field-level match booleans; no actual user values, + # storage paths, environment variables or credentials. + payload["owner_readback_match"] = match_vector raise MinecraftT6Error("RUNNING App read-back did not match owner") if isinstance(app, dict) and app.get("state") in {"CRASHED", "ERROR"}: raise MinecraftT6Error("Minecraft App reported terminal runtime failure") diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index d059c9de8..76182e24c 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -193,6 +193,26 @@ def test_foreign_config_not_adopted(self): bad["minecraft"]["additional_envs"] = [] self.assertFalse(mc.owned_config(bad, values)) + def test_live_owner_mismatch_report_contains_only_boolean_witnesses(self): + wanted = mc.runtime_values(admit(fixture())) + exact = mc.owner_readback_vector(copy.deepcopy(wanted), wanted) + self.assertEqual(set(exact), {"minecraft_type", "minecraft_version", + "owner_environment", "additional_storage", "server_port"}) + self.assertTrue(all(exact.values())) + corrupted = copy.deepcopy(wanted) + corrupted["storage"]["additional_storage"][0]["host_path_config"]["path"] = "/private/secret-owner-path" + corrupted["network"]["server_port"]["port_number"] = 9999 + result = mc.owner_readback_vector(corrupted, wanted) + self.assertFalse(result["additional_storage"]) + self.assertFalse(result["server_port"]) + self.assertTrue(result["minecraft_type"]) + self.assertFalse(mc.owned_config(corrupted, wanted)) + serialized = json.dumps(result) + self.assertNotIn("private", serialized) + self.assertNotIn("/mnt/", serialized) + self.assertTrue(all(isinstance(value, bool) for value in result.values())) + self.assertTrue(all(not isinstance(value, str) for value in result.values())) + def test_runtime_image_matches_digest_with_or_without_tag(self): ref = mc.EXPECTED_IMAGE_FRAGMENT app = {"active_workloads": {"images": [ref]}} From edfb1b2baa3cbff5928221df8e705f013a00577b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:29:36 +0200 Subject: [PATCH 640/719] request(rdte): admit exact TrueNAS Paper rep 007 live readback diagnosis --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index c884fa9a6..20cd14283 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-006-seed-null-validation-fix", + "request_id": "tn-25-04-1-t6-minecraft-paper-007-owner-readback-vector", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "Disposable nested TrueNAS 25.04.1 Minecraft Paper 1.21.11 T6 rep 006. Exact guest rep 005 [38164170973], created at be699f27619d631a962ae3e2a9a15d9b1abca317, proved live community/minecraft catalog 1.15.29, owned world sentinel and clean guest, but native app.create job 77 returned [EINVAL] values.minecraft.seed: null not allowed. Fail-closed correctly rejected. PR #387 merged experimental consumer at exact sha 10bb1c1fb6b846585547c987ff3949f9d30eaed3, now removes ONLY optional seed=None before sending native app.create, never modifies immutable source control, does not rewrite explicit seed or unrelated nulls. Cheap public Minecraft [38170715830] and general static [38170715835] tests terminal SUCCESS. Exact source Foundry SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, upstream truenas/apps@bb96ef6fd8c2708765012a4a3ca3739370a4f92c, native revision 1.15.29, OCI ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, status oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. Execute only one shared serialized public runner exact guest. Require full native app.create RUNNING identity, exact 1.21.11 Java status, level.dat, owned world sentinel retained across restart/redeploy/native remove_ix_volumes delete/reinstall and final zero owned residue. On any failure preserve bounded diagnostic receipts and independent guest cleanup witness; fail workflow, no local/physical TrueNAS or private GH actions minutes. Paper scope does not qualify vanilla Java 26.3 or Two Rivers." + "purpose": "Fresh one-slot disposable live TrueNAS 25.04.1 Minecraft Paper 1.21.11 T6 rep 007, under agent-dispatch-private#480/#588 and source-qualified diagnostic PR #390 squash c924518d1ac1da793568b8d4c0378a2aa05925cd. Previous actual guest rep 006 [38170800682] verified live native community/minecraft 1.15.29, immutable expected Paper image, clean guest and no owned residue, but failed strict app.config RUNNING ownership readback after app.create. Do not assume which owner field normalized. New source adds ONLY a sanitized five-boolean owner_readback_match vector on mismatch (minecraft type/version, additional owner env, storage, server-port), preserving refusal, secret privacy and cleanup, with independent exact-head Jules PASS report agent-dispatch-private#606 and public cheap runs 38174331190/38174331300. This is NOT a rerun of unchanged request 006: diagnose live middleware normalization using genuine QEMU/KVM TrueNAS 25.04.1, accepted immutable Foundry product control 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e and upstream truenas/apps bb96ef6fd8c2708765012a4a3ca3739370a4f92c. Source/product status oracle SupraCraft/mc-server-images d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8, image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa. Require strict native app config and genuine protocol, world preservation across stop/redeploy/delete/reinstall, final zero owned residue. On failure emit bounded boolean diagnostic, keep ORACLE_FAILURE with independent post-failure cleanup/platform-health witness. One heavyweight job, public free runner, no physical TrueNAS, private-hosted GHA, release, or vanilla 26.x claim." } From 2fe074d242c2aed7b55707add9db404abc150dfc Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:51:25 +0200 Subject: [PATCH 641/719] fix(minecraft): tolerate only catalog-default empty host_ips on port read-back Live TrueNAS rep 007 isolated server_port as sole mismatch. Native community/minecraft 1.15.29 questions.yaml has host_ips default empty. Preserve strict port_number, bind_mode and absent-or-empty host_ips, reject nonempty/extra bindings, expose boolean-only failure probes. --- .../truenas_middleware_minecraft_t6_probe.py | 33 +++++++++++++++- tests/test_truenas_t6_minecraft_control.py | 39 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 749c1bedd..5df4cbed9 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -191,6 +191,32 @@ def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, return verdict +def native_server_port_matches(actual: object, desired: object) -> bool: + """Allow only the upstream 1.15.29 catalog's default empty host_ips addition.""" + if not isinstance(actual, dict) or not isinstance(desired, dict): + return False + if actual == desired: + return True + if "host_ips" in desired or set(actual) != set(desired) | {"host_ips"}: + return False + return actual.get("host_ips") == [] and all( + actual.get(key) == value for key, value in desired.items() + ) + + +def port_readback_diagnostics(actual: object, desired: object) -> dict[str, bool]: + """Only boolean comparison; no site network data or secrets in public receipts.""" + if not isinstance(actual, dict) or not isinstance(desired, dict): + return {"port_number": False, "bind_mode": False, + "host_ips_empty_or_absent": False, "only_expected_keys": False} + return { + "port_number": "port_number" in actual and actual.get("port_number") == desired.get("port_number"), + "bind_mode": "bind_mode" in actual and actual.get("bind_mode") == desired.get("bind_mode"), + "host_ips_empty_or_absent": "host_ips" not in actual or actual["host_ips"] == [], + "only_expected_keys": set(actual) <= set(desired) | {"host_ips"}, + } + + def owner_readback_vector(config: dict, wanted: dict) -> dict[str, bool]: """Only boolean equality witnesses. Never emit app.config values or owner envs.""" minecraft = (config.get("minecraft") or {}) if isinstance(config, dict) else {} @@ -201,7 +227,7 @@ def owner_readback_vector(config: dict, wanted: dict) -> dict[str, bool]: "minecraft_version": minecraft.get("version") == "1.21.11" if isinstance(minecraft, dict) else False, "owner_environment": found_owner == REQUIRED_OWNER, "additional_storage": (config.get("storage") or {}).get("additional_storage") == wanted["storage"]["additional_storage"] if isinstance(config, dict) and isinstance(config.get("storage") or {}, dict) else False, - "server_port": (config.get("network") or {}).get("server_port") == wanted["network"]["server_port"] if isinstance(config, dict) and isinstance(config.get("network") or {}, dict) else False, + "server_port": native_server_port_matches((config.get("network") or {}).get("server_port"), wanted["network"]["server_port"]) if isinstance(config, dict) and isinstance(config.get("network") or {}, dict) else False, } @@ -296,6 +322,11 @@ def wait_app(): # Emit only field-level match booleans; no actual user values, # storage paths, environment variables or credentials. payload["owner_readback_match"] = match_vector + network = (cfg.get("network") or {}) if isinstance(cfg, dict) else {} + payload["server_port_readback_match"] = port_readback_diagnostics( + network.get("server_port") if isinstance(network, dict) else None, + desired["network"]["server_port"], + ) raise MinecraftT6Error("RUNNING App read-back did not match owner") if isinstance(app, dict) and app.get("state") in {"CRASHED", "ERROR"}: raise MinecraftT6Error("Minecraft App reported terminal runtime failure") diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index c1b6c1b26..4631aaae8 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -198,6 +198,45 @@ def test_foreign_config_not_adopted(self): bad["minecraft"]["additional_envs"] = [] self.assertFalse(mc.owned_config(bad, values)) + def test_native_server_port_exact_or_documented_default_only(self): + desired = {"bind_mode": "published", "port_number": mc.PORT} + self.assertTrue(mc.native_server_port_matches(dict(desired), desired)) + self.assertTrue(mc.native_server_port_matches(dict(desired, host_ips=[]), desired)) + for forbidden in [ + dict(desired, host_ips=["127.0.0.1"]), + dict(desired, port_number=mc.PORT+1), + dict(desired, bind_mode="exposed"), + dict(desired, unknown="bad"), + {"port_number": mc.PORT, "host_ips": []}, + None, {}, + ]: + with self.subTest(value=str(forbidden)): + self.assertFalse(mc.native_server_port_matches(forbidden, desired)) + self.assertFalse(mc.native_server_port_matches(dict(desired, host_ips=[]), + dict(desired, host_ips=["0.0.0.0"]))) + + def test_native_port_default_preserves_full_ownership_gate(self): + desired = mc.runtime_values(admit(fixture())) + got = copy.deepcopy(desired) + got["network"]["server_port"]["host_ips"] = [] + self.assertTrue(mc.owned_config(got, desired)) + got["network"]["server_port"]["host_ips"] = ["0.0.0.0"] + self.assertFalse(mc.owned_config(got, desired)) + got["network"]["server_port"]["host_ips"] = [] + got["minecraft"]["additional_envs"].pop() + self.assertFalse(mc.owned_config(got, desired)) + + def test_port_readback_diagnostics_remain_boolean_and_private(self): + desired = {"bind_mode": "published", "port_number": mc.PORT} + diag = mc.port_readback_diagnostics( + dict(desired, host_ips=["192.0.2.8"], secret="redacted"), desired) + self.assertTrue(diag["port_number"]) + self.assertTrue(diag["bind_mode"]) + self.assertFalse(diag["host_ips_empty_or_absent"]) + self.assertFalse(diag["only_expected_keys"]) + self.assertTrue(all(type(v) is bool for v in diag.values())) + self.assertNotIn("192.0.2.8", json.dumps(diag)) + def test_live_owner_mismatch_report_contains_only_boolean_witnesses(self): wanted = mc.runtime_values(admit(fixture())) exact = mc.owner_readback_vector(copy.deepcopy(wanted), wanted) From 0ada2d7d2d03929f138f7fc3087f15c7a6499e5d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:53:37 +0200 Subject: [PATCH 642/719] run(rdte): admit disposable Minecraft Paper native port-normalization T6 rep 008 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 20cd14283..9f90e583b 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-007-owner-readback-vector", + "request_id": "tn-25-04-1-t6-minecraft-paper-008-port-empty-host-ips-normalization", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "Fresh one-slot disposable live TrueNAS 25.04.1 Minecraft Paper 1.21.11 T6 rep 007, under agent-dispatch-private#480/#588 and source-qualified diagnostic PR #390 squash c924518d1ac1da793568b8d4c0378a2aa05925cd. Previous actual guest rep 006 [38170800682] verified live native community/minecraft 1.15.29, immutable expected Paper image, clean guest and no owned residue, but failed strict app.config RUNNING ownership readback after app.create. Do not assume which owner field normalized. New source adds ONLY a sanitized five-boolean owner_readback_match vector on mismatch (minecraft type/version, additional owner env, storage, server-port), preserving refusal, secret privacy and cleanup, with independent exact-head Jules PASS report agent-dispatch-private#606 and public cheap runs 38174331190/38174331300. This is NOT a rerun of unchanged request 006: diagnose live middleware normalization using genuine QEMU/KVM TrueNAS 25.04.1, accepted immutable Foundry product control 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e and upstream truenas/apps bb96ef6fd8c2708765012a4a3ca3739370a4f92c. Source/product status oracle SupraCraft/mc-server-images d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8, image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa. Require strict native app config and genuine protocol, world preservation across stop/redeploy/delete/reinstall, final zero owned residue. On failure emit bounded boolean diagnostic, keep ORACLE_FAILURE with independent post-failure cleanup/platform-health witness. One heavyweight job, public free runner, no physical TrueNAS, private-hosted GHA, release, or vanilla 26.x claim." + "purpose": "User-authorized disposable native Minecraft Paper TrueNAS 25.04.1 T6 qualification rep 008 in ONE serialized public/free heavyweight lane. Rep 007 [38184480046] exact edfb1b2baa3cbff5928221df8e705f013a00577b returned ORACLE_FAILURE after native RUNNING app with expected SHA-pinned image, owner vector type/version/env/storage true, only server_port false; independently clean guest and zero owned residue. Pinned upstream truenas/apps@bb96ef6fd8c2708765012a4a3ca3739370a4f92c Minecraft 1.15.29 questions.yaml defines optional network.server_port.host_ips default []. This branch contains narrowly scoped PR #392 merged exact c1f3b21fa4233c04fabf43390eac4a50333b6fd7: accept only empty host_ips addition; all unexpected/missing keys, nonempty bindings, changed port/bind mode and foreign ownership are rejected. Public Minecraft cheap CI 38185854358 SUCCESS, static CI 38185854412 SUCCESS; root receipt metadata PR #388 also merged; merge-triggered #38185893127 completed without running T6. Exact producer SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, immutable image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, Minecraft oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. Run REAL disposable guest; require native catalog + image identity + owner config, exact Java 1.21.11 protocol and level.dat first/restart/redeploy/reinstall, independent world sentinel retained after native App delete remove_ix_volumes=true and reinstall, final zero owned dataset/App residue; persist bounded diagnostic and fail closed on any failure. No local/physical TrueNAS APPLY or private GitHub Actions minutes; Paper does not qualify Vanilla 26.3, Two Rivers or the whole Velocity hosting stack." } From 0257b87bcd9e0bfe77fec9a97e1ee74ce7770060 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:23:25 +0200 Subject: [PATCH 643/719] fix(minecraft): split live Java-status vs world-file diagnostic evidence Run 008 timed out after native App RUNNING without distinguishing protocol, world creation or middleware liveness; cleanup later lost DDP EOF. Capture bounded allowlisted status oracle failure category, independently sample level.dat, record App read-only state/reachability and preserve fail-closed cleanup. Add privacy and negative tests. No acceptance policy changes. --- .../truenas_middleware_minecraft_t6_probe.py | 82 ++++++++++++++++++- tests/test_truenas_t6_minecraft_control.py | 44 ++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 5df4cbed9..a71cad994 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -191,6 +191,44 @@ def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, return verdict +def classify_java_status_receipt(path: pathlib.Path) -> str: + """Map the trusted, bounded *local oracle result* to an allowlisted cause. + + Do not propagate raw Minecraft status payloads, MOTD, IPs, user names, + private paths or arbitrary exception strings into public receipts. + """ + try: + if not path.is_file() or path.stat().st_size > 4096: + return "missing_or_oversized_receipt" + row = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError): + return "unreadable_receipt" + if not isinstance(row, dict) or row.get("schema") != "minecraft-java-status-oracle/v1": + return "invalid_receipt" + if row.get("classification") == "SUPPORTED" and row.get("oracleSatisfied") is True: + return "status_supported" + if row.get("classification") != "ORACLE_FAILURE" or row.get("oracleSatisfied") is not False: + return "invalid_receipt" + reason = row.get("failure_reason") + if reason == "transport failure": + return "transport_failure" + if reason == "server version mismatch": + return "version_mismatch" + if reason == "server protocol is absent or invalid": + return "protocol_version_invalid" + if reason in { + "truncated VarInt", "VarInt overflow", "VarInt too long", + "packet exceeds bound", "outbound packet exceeds bound", + "truncated packet", "status frame length invalid", + "wrong status packet id", "status JSON length mismatch", + "malformed status JSON", "status payload not an object", + "players status absent", "players counters invalid", + "player counter bounds invalid", + }: + return "invalid_or_truncated_status" + return "other_oracle_failure" + + def native_server_port_matches(actual: object, desired: object) -> bool: """Allow only the upstream 1.15.29 catalog's default empty host_ips addition.""" if not isinstance(actual, dict) or not isinstance(desired, dict): @@ -354,18 +392,60 @@ def stat_level(): def wait_status(label): deadline = time.monotonic() + a.state_timeout last = None + attempts = 0 + level_stat_attempted = False + level_ever_observed = False + status_ever_satisfied = False + status_cause = "not_probed" while time.monotonic() < deadline: + attempts += 1 receipt = a.out.with_name(a.out.stem + "-status-" + label + ".json") try: verdict = safe_status_oracle(a.status_oracle, a.status_port, receipt) + status_ever_satisfied = True + status_cause = "status_supported" + level_stat_attempted = True level = stat_level() + level_ever_observed = level_ever_observed or (level is not None) if level is not None: payload.setdefault("readiness", {})[label] = {"protocol": verdict, "level": level} return except (OSError, ValueError, MinecraftT6Error, subprocess.TimeoutExpired) as exc: last = type(exc).__name__ + # The oracle writes a bounded local failure receipt even on a + # failed connection. Only copy an allowlisted category. + status_cause = classify_java_status_receipt(receipt) + # Sample world independently of protocol success; a server may + # create level.dat while no TCP status listener is accessible. + # A negative stat is not a proof that the world is absent. + if attempts == 1 or attempts % 8 == 0: + level_stat_attempted = True + level_ever_observed = level_ever_observed or (stat_level() is not None) time.sleep(4) - raise MinecraftT6Error("Minecraft runtime status+world did not converge: " + str(last)) + snapshot = { + "attempts": attempts, + "status_oracle_ever_satisfied": status_ever_satisfied, + "level_stat_attempted": level_stat_attempted, + "level_dat_ever_observed": level_ever_observed, + "last_status_cause": status_cause, + "last_exception_kind": last if last in { + "OSError", "ValueError", "MinecraftT6Error", "TimeoutExpired" + } else "none_or_other", + } + # Middleware reachability and App lifecycle state are separate + # evidence, never substitutes for the Java/status+level.dat oracle. + try: + app = getapp() + state = app.get("state") if isinstance(app, dict) else None + snapshot["middleware_app_query_succeeded"] = True + snapshot["app_state"] = state if state in { + "RUNNING", "DEPLOYING", "CRASHED", "STOPPED", "UNKNOWN" + } else "other_or_absent" + except Exception: + snapshot["middleware_app_query_succeeded"] = False + snapshot["app_state"] = "query_failed" + payload.setdefault("readiness_diagnostics", {})[label] = snapshot + raise MinecraftT6Error("Minecraft runtime status+world did not converge") def delete_app(label): nonlocal app_created wait_job(call("app.delete", [APP, { diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index 4631aaae8..1c0ebfb11 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -283,6 +283,50 @@ def test_job_error_receipt_redacts_secrets_and_bounds_detail(self): large = mc.safe_job_error({"state": "FAILED", "error": "x" * 700}, "secret") self.assertLessEqual(len(large["error_excerpt"]), 480) + def test_status_diagnostic_receipt_only_emits_allowed_failure_category(self): + with tempfile.TemporaryDirectory() as td: + p = pathlib.Path(td) / "status.json" + variants = [ + ({"schema": "minecraft-java-status-oracle/v1", + "classification": "ORACLE_FAILURE", "oracleSatisfied": False, + "failure_type": "ConnectionRefusedError", + "failure_reason": "transport failure", + "sensitive": "192.0.2.20 aPrivateName"}, "transport_failure"), + ({"schema": "minecraft-java-status-oracle/v1", + "classification": "ORACLE_FAILURE", "oracleSatisfied": False, + "failure_type": "OracleFailure", + "failure_reason": "server version mismatch"}, "version_mismatch"), + ({"schema": "minecraft-java-status-oracle/v1", + "classification": "ORACLE_FAILURE", "oracleSatisfied": False, + "failure_reason": "malformed status JSON"}, "invalid_or_truncated_status"), + ({"schema": "minecraft-java-status-oracle/v1", + "classification": "ORACLE_FAILURE", "oracleSatisfied": False, + "failure_reason": "ATTACKER_PASSWORD=supersecret"}, "other_oracle_failure"), + ({"schema": "minecraft-java-status-oracle/v1", + "classification": "SUPPORTED", "oracleSatisfied": True}, "status_supported"), + ({"schema": "invalid", "failure_reason": "transport failure"}, "invalid_receipt"), + ] + for row, expected in variants: + with self.subTest(expected=expected): + p.write_text(json.dumps(row)) + result = mc.classify_java_status_receipt(p) + self.assertEqual(result, expected) + self.assertNotIn("192.0.2.20", result) + self.assertNotIn("supersecret", result) + p.write_bytes(b"x" * 5000) + self.assertEqual(mc.classify_java_status_receipt(p), "missing_or_oversized_receipt") + p.unlink() + self.assertEqual(mc.classify_java_status_receipt(p), "missing_or_oversized_receipt") + p.write_text("{broken") + self.assertEqual(mc.classify_java_status_receipt(p), "unreadable_receipt") + + def test_readiness_source_probes_level_even_when_protocol_fails(self): + source = (ROOT / "scripts/truenas_middleware_minecraft_t6_probe.py").read_text() + self.assertIn("level_ever_observed = level_ever_observed or (stat_level() is not None)", source) + self.assertIn('"status_oracle_ever_satisfied": status_ever_satisfied', source) + self.assertIn('"middleware_app_query_succeeded" ', source) + self.assertNotIn('payload["raw_status_json"]', source) + def test_status_script_must_be_exact_named(self): with tempfile.TemporaryDirectory() as td: other = pathlib.Path(td) / "anything.py" From c4b590e2d35cfb6981232f19d94905b6e53c9282 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:23:40 +0200 Subject: [PATCH 644/719] test(minecraft): assert actual status-diagnostic middleware witness assignment --- tests/test_truenas_t6_minecraft_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index 1c0ebfb11..2644fb500 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -324,7 +324,7 @@ def test_readiness_source_probes_level_even_when_protocol_fails(self): source = (ROOT / "scripts/truenas_middleware_minecraft_t6_probe.py").read_text() self.assertIn("level_ever_observed = level_ever_observed or (stat_level() is not None)", source) self.assertIn('"status_oracle_ever_satisfied": status_ever_satisfied', source) - self.assertIn('"middleware_app_query_succeeded" ', source) + self.assertIn('snapshot["middleware_app_query_succeeded"] = True', source) self.assertNotIn('payload["raw_status_json"]', source) def test_status_script_must_be_exact_named(self): From 1dc86b120b73f571fda0b5ac353a1155a4a4cd67 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:25:25 +0200 Subject: [PATCH 645/719] run(rdte): request serialized native Minecraft diagnostic 009 protocol versus level.dat cause --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 9f90e583b..3e4edbd1e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-008-port-empty-host-ips-normalization", + "request_id": "tn-25-04-1-t6-minecraft-paper-009-protocol-world-cause-split", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "User-authorized disposable native Minecraft Paper TrueNAS 25.04.1 T6 qualification rep 008 in ONE serialized public/free heavyweight lane. Rep 007 [38184480046] exact edfb1b2baa3cbff5928221df8e705f013a00577b returned ORACLE_FAILURE after native RUNNING app with expected SHA-pinned image, owner vector type/version/env/storage true, only server_port false; independently clean guest and zero owned residue. Pinned upstream truenas/apps@bb96ef6fd8c2708765012a4a3ca3739370a4f92c Minecraft 1.15.29 questions.yaml defines optional network.server_port.host_ips default []. This branch contains narrowly scoped PR #392 merged exact c1f3b21fa4233c04fabf43390eac4a50333b6fd7: accept only empty host_ips addition; all unexpected/missing keys, nonempty bindings, changed port/bind mode and foreign ownership are rejected. Public Minecraft cheap CI 38185854358 SUCCESS, static CI 38185854412 SUCCESS; root receipt metadata PR #388 also merged; merge-triggered #38185893127 completed without running T6. Exact producer SemperSupra/truenas-app-foundry@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, immutable image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, Minecraft oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. Run REAL disposable guest; require native catalog + image identity + owner config, exact Java 1.21.11 protocol and level.dat first/restart/redeploy/reinstall, independent world sentinel retained after native App delete remove_ix_volumes=true and reinstall, final zero owned dataset/App residue; persist bounded diagnostic and fail closed on any failure. No local/physical TrueNAS APPLY or private GitHub Actions minutes; Paper does not qualify Vanilla 26.3, Two Rivers or the whole Velocity hosting stack." + "purpose": "One bounded disposable real nested TrueNAS 25.04.1 / Minecraft Paper 1.21.11 diagnostic rep 009 using public free-tier shared KVM lane. Prior rep 008 [38185968934] at 0ada2d7d2d03929f138f7fc3087f15c7a6499e5d reached native RUNNING app and immutable image/owner config (community/minecraft 1.15.29), then first Minecraft Java status + level.dat readiness failed after 480s with generic error and failure cleanup DDP EOF; zero residue UNPROVEN, session SESSION_CONTAMINATED, no acceptance. Cause unknown (Java server bootstrap vs loopback/QEMU TCP hostfwd vs Minecraft version/status framing vs world file), do not guess. PR #393 merged at 0397a5c868a66eef6def290f12a2019d154103a0, public cheap Minecraft 38187847086 and RDTE static 38187847126 terminal success; pure diagnostic change ONLY: categorize exact pinned Java oracle typed local failure receipt into predefined privacy-safe values, independently sample positive level.dat proof even if status fails, read-only middleware app-state/reachability on timeout. All native create/owner/image/ingress + server status, world retention, and cleanup gates remain fail-closed. Public Foundry producer exact 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, truenas/apps upstream bb96ef6fd8c2708765012a4a3ca3739370a4f92c community/minecraft 1.15.29, image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, source pinned status oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. One real KVM nested guest only, no duplicate/other heavyweight, no private-minute use, no physical TrueNAS mutation; physical HOLD. Record bounded typed cause and cleanup uncertainty accurately, do not infer Paper/vanilla 26.3/Two Rivers acceptance without exact protocol+level.dat first/restart/redeploy/reinstall/cleanup." } From 8d39e5153956ba74a8d486ddce27f640ad076c08 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:37:14 +0200 Subject: [PATCH 646/719] fix(rdte): pin source-qualified Paper status oracle and exact protocol 774 Consume public product PR #61/main at 1b2227847d56007cdf8b668c00845326827e5fd5; require literal Paper 1.21.11 / protocol 774; generic vanilla remains unmodified. Reject malformed implementation evidence and preserve bounded protocol mismatch classification. Prior live 009 proved Paper status transport and level.dat but unbranded name false-negative. --- .github/workflows/gha-kvm-system-rdte.yml | 4 +- .../truenas_middleware_minecraft_t6_probe.py | 10 +++- tests/test_truenas_t6_minecraft_control.py | 54 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index a5e5fe1ec..38543277a 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -502,13 +502,13 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: repository: SupraCraft/mc-server-images - ref: d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 + ref: 1b2227847d56007cdf8b668c00845326827e5fd5 path: .deps/minecraft-product persist-credentials: false - name: Prove exact Minecraft product source identity if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' run: | - test "$(git -C .deps/minecraft-product rev-parse HEAD)" = d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8 + test "$(git -C .deps/minecraft-product rev-parse HEAD)" = 1b2227847d56007cdf8b668c00845326827e5fd5 python3 .deps/minecraft-product/tools/test_minecraft_java_status_oracle.py - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index a71cad994..874639c56 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -176,8 +176,9 @@ def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, if not script.is_file() or script.name != "minecraft_java_status_oracle.py": raise MinecraftT6Error("exact Minecraft product oracle file absent") cmd = [sys.executable, str(script), "--port", str(port), - "--expected-version", expected_version, "--timeout", "5", - "--out", str(out)] + "--expected-version", expected_version, + "--implementation", "paper", "--expected-protocol", "774", + "--timeout", "5", "--out", str(out)] result = subprocess.run(cmd, capture_output=True, text=True, timeout=12) if result.returncode != 0 or not out.is_file(): raise MinecraftT6Error("Minecraft Java status oracle did not pass") @@ -186,6 +187,9 @@ def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, verdict.get("classification") != "SUPPORTED" or verdict.get("oracleSatisfied") is not True or verdict.get("java_version") != expected_version or + verdict.get("server_implementation") != "paper" or + verdict.get("status_name_contract") != "paper_brand_exact" or + verdict.get("server_protocol") != 774 or verdict.get("status_packet_valid") is not True): raise MinecraftT6Error("Minecraft Java status receipt invalid") return verdict @@ -216,6 +220,8 @@ def classify_java_status_receipt(path: pathlib.Path) -> str: return "version_mismatch" if reason == "server protocol is absent or invalid": return "protocol_version_invalid" + if reason == "server protocol mismatch": + return "protocol_mismatch" if reason in { "truncated VarInt", "VarInt overflow", "VarInt too long", "packet exceeds bound", "outbound packet exceeds bound", diff --git a/tests/test_truenas_t6_minecraft_control.py b/tests/test_truenas_t6_minecraft_control.py index 2644fb500..30d1b4bd9 100644 --- a/tests/test_truenas_t6_minecraft_control.py +++ b/tests/test_truenas_t6_minecraft_control.py @@ -283,6 +283,17 @@ def test_job_error_receipt_redacts_secrets_and_bounds_detail(self): large = mc.safe_job_error({"state": "FAILED", "error": "x" * 700}, "secret") self.assertLessEqual(len(large["error_excerpt"]), 480) + def test_protocol_mismatch_is_an_allowlisted_status_cause(self): + with tempfile.TemporaryDirectory() as td: + p = pathlib.Path(td) / "oracle.json" + p.write_text(json.dumps({ + "schema": "minecraft-java-status-oracle/v1", + "classification": "ORACLE_FAILURE", "oracleSatisfied": False, + "failure_reason": "server protocol mismatch", + "raw_response": "do-not-copy-this-name" + })) + self.assertEqual(mc.classify_java_status_receipt(p), "protocol_mismatch") + def test_status_diagnostic_receipt_only_emits_allowed_failure_category(self): with tempfile.TemporaryDirectory() as td: p = pathlib.Path(td) / "status.json" @@ -327,6 +338,49 @@ def test_readiness_source_probes_level_even_when_protocol_fails(self): self.assertIn('snapshot["middleware_app_query_succeeded"] = True', source) self.assertNotIn('payload["raw_status_json"]', source) + def test_paper_status_invocation_and_receipt_gate_are_exact(self): + import unittest.mock as mock + control = mc.runtime_values(admit(fixture())) + with tempfile.TemporaryDirectory() as td: + script = pathlib.Path(td) / "minecraft_java_status_oracle.py" + script.write_text("# product oracle placeholder; subprocess instrumented") + out = pathlib.Path(td) / "status.json" + valid = { + "schema": "minecraft-java-status-oracle/v1", + "classification": "SUPPORTED", "oracleSatisfied": True, + "java_version": "1.21.11", "server_implementation": "paper", + "status_name_contract": "paper_brand_exact", + "server_protocol": 774, "status_packet_valid": True, + } + def mock_run(cmd, **_kwargs): + self.assertIn("--implementation", cmd) + self.assertEqual(cmd[cmd.index("--implementation") + 1], "paper") + self.assertEqual(cmd[cmd.index("--expected-protocol") + 1], "774") + out.write_text(json.dumps(valid)) + return type("Process", (), {"returncode": 0})() + with mock.patch.object(mc.subprocess, "run", side_effect=mock_run): + got = mc.safe_status_oracle(script, mc.PORT, out) + self.assertEqual(got["server_protocol"], 774) + for key, value in ( + ("server_protocol", 773), + ("server_implementation", "vanilla"), + ("status_name_contract", "vanilla_exact"), + ("java_version", "1.21.10"), + ): + invalid = dict(valid, **{key: value}) + def mock_bad(cmd, **_kwargs): + out.write_text(json.dumps(invalid)) + return type("Process", (), {"returncode": 0})() + with mock.patch.object(mc.subprocess, "run", side_effect=mock_bad): + with self.assertRaisesRegex(mc.MinecraftT6Error, "receipt invalid"): + mc.safe_status_oracle(script, mc.PORT, out) + + def test_source_pin_now_uses_approved_paper_oracle(self): + workflow = (ROOT / ".github/workflows/gha-kvm-system-rdte.yml").read_text() + pin = "1b2227847d56007cdf8b668c00845326827e5fd5" + self.assertEqual(workflow.count(pin), 2) + self.assertNotIn("d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8", workflow) + def test_status_script_must_be_exact_named(self): with tempfile.TemporaryDirectory() as td: other = pathlib.Path(td) / "anything.py" From 50d6aa75835727997395156d4b44c3ce668b38f4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 05:40:21 +0200 Subject: [PATCH 647/719] run(rdte): admit exact Paper 1.21.11 branded ping/protocol 774 T6 live rep 010 --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 3e4edbd1e..1a9ffaa5c 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-009-protocol-world-cause-split", + "request_id": "tn-25-04-1-t6-minecraft-paper-010-exact-paper-ping-protocol-774", "version": "25.04.1", "rung": "t6", "product": "minecraft-paper", "authority_issue": 411, "consumer_authority_issue": 588, - "purpose": "One bounded disposable real nested TrueNAS 25.04.1 / Minecraft Paper 1.21.11 diagnostic rep 009 using public free-tier shared KVM lane. Prior rep 008 [38185968934] at 0ada2d7d2d03929f138f7fc3087f15c7a6499e5d reached native RUNNING app and immutable image/owner config (community/minecraft 1.15.29), then first Minecraft Java status + level.dat readiness failed after 480s with generic error and failure cleanup DDP EOF; zero residue UNPROVEN, session SESSION_CONTAMINATED, no acceptance. Cause unknown (Java server bootstrap vs loopback/QEMU TCP hostfwd vs Minecraft version/status framing vs world file), do not guess. PR #393 merged at 0397a5c868a66eef6def290f12a2019d154103a0, public cheap Minecraft 38187847086 and RDTE static 38187847126 terminal success; pure diagnostic change ONLY: categorize exact pinned Java oracle typed local failure receipt into predefined privacy-safe values, independently sample positive level.dat proof even if status fails, read-only middleware app-state/reachability on timeout. All native create/owner/image/ingress + server status, world retention, and cleanup gates remain fail-closed. Public Foundry producer exact 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, truenas/apps upstream bb96ef6fd8c2708765012a4a3ca3739370a4f92c community/minecraft 1.15.29, image ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, source pinned status oracle SupraCraft/mc-server-images@d383c657b6accc11fe587f4e9bcd6e2c1a4aa7a8. One real KVM nested guest only, no duplicate/other heavyweight, no private-minute use, no physical TrueNAS mutation; physical HOLD. Record bounded typed cause and cleanup uncertainty accurately, do not infer Paper/vanilla 26.3/Two Rivers acceptance without exact protocol+level.dat first/restart/redeploy/reinstall/cleanup." + "purpose": "One serialized FREE PUBLIC real nested TrueNAS 25.04.1 Minecraft Paper 1.21.11 T6 qualification rep 010; not a blind unchanged rerun. Previous exact guest rep 009 public GHA #38187943219 verified running native community/minecraft 1.15.29, digest-locked ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, correct ownership, *positive level.dat*, Java TCP status responding but status version_mismatch to naive unbranded 1.21.11, 119 retries / 480s; final DDP EOF cleanup unknown and red session. Actual source PaperMC/Paper ver/1.21.11@c5eb0790f199da6c38d0a650e1e5cd5415b28185 proves status name `Paper 1.21.11` (getServerModName + space + server version; brand Paper), protocol release 774. Public source status oracle in SupraCraft/mc-server-images PR #61 merged exact 1b2227847d56007cdf8b668c00845326827e5fd5 permits only exact branded status under explicit --implementation paper and --expected-protocol 774; leaves vanilla exact plain name and rejects drift. Public source CI 38200346452/38200346521/38200346558 SUCCESS. Experimental Agent Dispatch PR #395 merged exact 99bf7078378ea7a1fa6dea27f0a08868456ce4ba pinning product source checksum via exact HEAD check in workflow and verifying both branded mode and exact protocol as independent receipt. Consumer public CI 38200492184 and prep static 38200492170 SUCCESS; merge-triggered 38200562936 SUCCESS with NO heavyweight guest. Foundry producer exact 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, truenas/apps upstream bb96ef6fd8c2708765012a4a3ca3739370a4f92c. Require truly live guest status response implementing Paper Java 1.21.11 protocol 774, level.dat first boot and after stop/start/redeploy/delete+reinstall, owned world sentinel and separate external ZFS world retention, app final delete and explicit world dataset removal, no residual. Maintain fail-close on any mismatch, report independent causal status/level/backend and cleanup witnesses and never claim physical host/vanilla 26.3 or Two Rivers. Shared heavyweight one-slot public GHA only, no local/physical TrueNAS mutation, no private GHA or paid runners; physical APPLY HOLD remains. If failure, preserve full bounded evidence and do not repeat unchanged rep." } From 6017846980559e5d8a6e76ed4e7ca15a1acb0179 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:37:02 +0200 Subject: [PATCH 648/719] RDTE: execute COLLAGE read-only native App join inside disposable TrueNAS guest (#397) * feat(rdte): add disposable APPS_READ credential and TLS-only COLLAGE native App observer * feat(rdte): qualify COLLAGE native readback during real native Paper App first-start window * feat(rdte): admit distinct COLLAGE Paper readback T6 selector without altering baseline Paper behavior * feat(rdte): gate separate public COLLAGE P2 TrueNAS KVM path behind cheap pinned source tests * test(rdte): require independent TLS native readback plus complete disposable Paper lifecycle * test(rdte): source gates for scoped guest observer and exact fail-closed receipt * ci(rdte): qualify bounded COLLAGE P2 adapter without starting heavyweight KVM * fix(rdte): preserve historical product target guard and add exact TrueNAS 25.04.1 COLLAGE exception --- .github/workflows/gha-kvm-system-rdte.yml | 44 +++- .../workflows/qualify-collage-p2-source.yml | 43 ++++ scripts/assert_collage_p2_receipt.py | 86 ++++++++ scripts/gha_kvm_truenas_rdte.sh | 31 ++- .../truenas_middleware_collage_readback_p2.py | 194 ++++++++++++++++++ .../truenas_middleware_minecraft_t6_probe.py | 24 +++ tests/test_collage_p2_native.py | 97 +++++++++ 7 files changed, 503 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/qualify-collage-p2-source.yml create mode 100644 scripts/assert_collage_p2_receipt.py create mode 100644 scripts/truenas_middleware_collage_readback_p2.py create mode 100644 tests/test_collage_p2_native.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 38543277a..1bfcb29b3 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -56,6 +56,7 @@ on: - official-catalog - foliorelay - minecraft-paper + - collage-native-paper proxmox_compute_fixture: description: "Optional native PVE REST compute fixture" required: true @@ -186,7 +187,7 @@ jobs: fi truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay" or . == "minecraft-paper")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay" or . == "minecraft-paper" or . == "collage-native-paper")' "$RUN_REQUEST_PATH")" truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ @@ -276,7 +277,7 @@ jobs: foundry-minecraft-paper-t6-control: needs: [changes, contract] - if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-paper-t6-control.yml@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e with: foundry_ref: 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e @@ -442,7 +443,7 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'garm-controller-f0f5' && needs['foundry-garm-f0-f5-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'foliorelay' && needs['foundry-foliorelay-t6-control'].result == 'success') || - (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' && needs['foundry-minecraft-paper-t6-control'].result == 'success') || + ((needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') && needs['foundry-minecraft-paper-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') || @@ -492,13 +493,13 @@ jobs: name: foliorelay-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Download exact Minecraft Paper Foundry control - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + if: needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: minecraft-paper-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Checkout exact Minecraft product status oracle - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + if: needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: repository: SupraCraft/mc-server-images @@ -506,10 +507,34 @@ jobs: path: .deps/minecraft-product persist-credentials: false - name: Prove exact Minecraft product source identity - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' + if: needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') run: | test "$(git -C .deps/minecraft-product rev-parse HEAD)" = 1b2227847d56007cdf8b668c00845326827e5fd5 python3 .deps/minecraft-product/tools/test_minecraft_java_status_oracle.py + - name: Checkout exact accepted COLLAGE read-only source + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SupraCraft/collage + ref: fbda94f3fea0a216ddd9c35b38a09d325bed3640 + path: .deps/collage-source + persist-credentials: false + - name: Set up pinned Go toolchain for COLLAGE read-only probe + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version-file: .deps/collage-source/tools/truenas-readonly-probe/go.mod + cache: false + - name: Prequalify exact COLLAGE source, Go probe and policy before KVM + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + run: | + set -euo pipefail + test "$(git -C .deps/collage-source rev-parse HEAD)" = fbda94f3fea0a216ddd9c35b38a09d325bed3640 + (cd .deps/collage-source && + python3 -m unittest discover -s tests -p 'test_collage_*.py' -v) + (cd .deps/collage-source/tools/truenas-readonly-probe && + go mod download && go mod verify && + go test ./... && go build -o /dev/null .) - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c @@ -607,7 +632,7 @@ jobs: foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then foundry_commit="81deb97185760975fd8d3162df42056c77b3c0fd" - elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" || "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then foundry_commit="2cc7e1fe3772f8c532e933b3d7832e6cf985db4e" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" @@ -626,9 +651,12 @@ jobs: python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" # A green job is not a Minecraft product pass without full guest evidence. # Failure still uploads the receipt through always() below. - if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" || "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then python3 scripts/assert_minecraft_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" fi + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then + python3 scripts/assert_collage_p2_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + fi - name: Preserve TrueNAS harness failure evidence if: always() env: diff --git a/.github/workflows/qualify-collage-p2-source.yml b/.github/workflows/qualify-collage-p2-source.yml new file mode 100644 index 000000000..86e0f9711 --- /dev/null +++ b/.github/workflows/qualify-collage-p2-source.yml @@ -0,0 +1,43 @@ +name: Qualify COLLAGE P2 nested TrueNAS source + +on: + pull_request: + branches: + - experiment/gha-kvm-system-rdte + paths: + - 'scripts/truenas_middleware_collage_readback_p2.py' + - 'scripts/truenas_middleware_minecraft_t6_probe.py' + - 'scripts/assert_collage_p2_receipt.py' + - 'scripts/gha_kvm_truenas_rdte.sh' + - 'tests/test_collage_p2_native.py' + - 'tests/test_truenas_t6_minecraft_control.py' + - '.github/workflows/gha-kvm-system-rdte.yml' + - '.github/workflows/qualify-collage-p2-source.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + cheap-source: + runs-on: ubuntu-24.04 + timeout-minutes: 8 + steps: + - name: Checkout exact candidate + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Syntax, receipt, existing product and negative source checks + run: | + set -euo pipefail + python3 -m py_compile \ + scripts/truenas_middleware_collage_readback_p2.py \ + scripts/truenas_middleware_minecraft_t6_probe.py \ + scripts/assert_collage_p2_receipt.py \ + tests/test_collage_p2_native.py + python3 -m unittest -v \ + tests.test_collage_p2_native \ + tests.test_truenas_t6_minecraft_control + - name: Declare no live-system result + run: | + echo 'SOURCE_ONLY: no TrueNAS guest boot or authenticated API readback. The new distinct request ID requires a separate accepted request-edge after this gate.' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/assert_collage_p2_receipt.py b/scripts/assert_collage_p2_receipt.py new file mode 100644 index 000000000..ad8940be2 --- /dev/null +++ b/scripts/assert_collage_p2_receipt.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Exact product receipt gate for real disposable COLLAGE native readback P2. + +Requires full native Paper lifecycle AND TLS-verified provider read-only match. +Cannot pass from a synthetic observation file or a green runner job alone. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys + +EXPECTED_SOURCE = "fbda94f3fea0a216ddd9c35b38a09d325bed3640" +EXPECTED_SERVICE = "11111111-1111-4111-8111-111111111111" +EXPECTED_APP = "rdte-t6-minecraft-paper" + + +def check(row: dict) -> None: + def need(cond, reason): + if not cond: + raise ValueError(reason) + need(type(row) is dict and row.get("contract") == "gha-kvm-system-lab/v1", + "root contract") + target = row.get("target") or {} + need(target.get("version") == "25.04.1" + and target.get("rung") == "T6" + and target.get("product") == "truenas", "wrong target") + need(row.get("classification") == "SUPPORTED" + and row.get("oracleSatisfied") is True, "root failed") + p = row.get("foundry_materialization") or {} + need(p.get("schema") == "truenas-minecraft-paper-t6/v1" + and p.get("classification") == "SUPPORTED" + and p.get("oracleSatisfied") is True, "Paper guest failed") + need(p.get("catalog_version_verified") is True + and p.get("world_retained_on_delete") is True + and p.get("world_rebound_after_reinstall") is True, + "native Paper lifecycle did not qualify") + stages = p.get("readiness") or {} + for label in ("first", "restart", "redeploy", "reinstall"): + stage = stages.get(label) or {} + status = stage.get("protocol") or {} + level = stage.get("level") or {} + need(status.get("oracleSatisfied") is True + and status.get("server_protocol") == 774 + and status.get("server_implementation") == "paper" + and type(level.get("size")) is int and level["size"] > 0, + "exact Java status/world stage absent: " + label) + cleanup = p.get("cleanup") or {} + need(all(cleanup.get(k) is True for k in + ("zero_residue", "app_absent", "owned_world_dataset_absent")), + "native cleanup not exact") + observation = p.get("collage_readback") or {} + need(observation.get("schema") == "semper-supra.collage-native-readback-p2/1" + and observation.get("classification") == "PROVIDER_READBACK_MATCH" + and observation.get("source_sha") == EXPECTED_SOURCE + and observation.get("native_app") == EXPECTED_APP + and observation.get("service_id") == EXPECTED_SERVICE + and observation.get("ownership_fields_matched") == 6 + and observation.get("credential_scope") == "APPS_READ" + and observation.get("transport") == "isolated_guest_tls_verified" + and observation.get("observation_mutation_count") == 0 + and observation.get("world_activated") is False + and observation.get("cloudnet_adoption_executed") is False + and observation.get("physical_true_nas_mutated") is False, + "real guest read-only provider observation missing") + need(isinstance(observation.get("tls_cert_sha256"), str) + and len(observation["tls_cert_sha256"]) == 64, + "TLS certificate anchor receipt missing") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--receipt", type=pathlib.Path, required=True) + args = parser.parse_args() + try: + check(json.loads(args.receipt.read_text(encoding="utf-8"))) + except (OSError, ValueError) as exc: + print("COLLAGE P2 native guest acceptance FAILED: " + str(exc), file=sys.stderr) + return 2 + print("COLLAGE P2 real guest acceptance SUPPORTED: Paper lifecycle + native TLS observation") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 238a6caa3..8b5c87726 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -18,7 +18,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper|collage-native-paper] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" } OUT="" @@ -77,9 +77,13 @@ if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then fi if [[ "$RUNG" == "t6" ]]; then if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then - [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } + if [[ "$T6_PRODUCT" == "collage-native-paper" ]]; then + [[ "$VERSION" == "25.04.1" ]] || { echo "COLLAGE P2 admitted only on isolated TrueNAS 25.04.1" >&2; exit 2; } + else + [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } + fi fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "garm-provider-container-pre-b4" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" || "$T6_PRODUCT" == "minecraft-paper" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "garm-provider-container-pre-b4" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" || "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -230,7 +234,7 @@ payload = { else {"name": "rdte-t6-foliorelay", "images": foliorelay_images} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" else {"name": "rdte-t6-minecraft-paper", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.29", "paper_version": "1.21.11"} - if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "minecraft-paper" + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") in {"minecraft-paper", "collage-native-paper"} else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} if os.environ.get("R_RUNG") == "t6" else {"name": "rdte-t4-probe", "image": "nginx@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10"} @@ -379,7 +383,7 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 WOW Sidecar control client" - elif [[ "$T6_PRODUCT" == "minecraft-paper" ]]; then + elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing Minecraft T6 product probe" [[ -f "$REPO_ROOT/.deps/minecraft-product/tools/minecraft_java_status_oracle.py" ]] || @@ -658,7 +662,7 @@ FOLIORELAY_OBSERVER_HOST_PORT="" FOLIORELAY_HOSTFWD="" MINECRAFT_HOST_PORT="" MINECRAFT_HOSTFWD="" -if [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "minecraft-paper" ]]; then +if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ) ]]; then MINECRAFT_HOST_PORT="$(python3 - <<'PY' import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() @@ -1150,7 +1154,17 @@ elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then --control-dir "$FOUNDRY_CONTROL_DIR" \ --foundry-commit "$FOUNDRY_COMMIT" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 300 --state-timeout 240 >/dev/null 2>&1 || true -elif [[ "$T6_PRODUCT" == "minecraft-paper" ]]; then +elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ]]; then + COLLAGE_P2_ARGS=() + if [[ "$T6_PRODUCT" == "collage-native-paper" ]]; then + [[ -d "$REPO_ROOT/.deps/collage-source" ]] || + fail_evidence HARNESS_FAILURE foundry-materialization "pinned COLLAGE P2 checkout missing" + COLLAGE_P2_ARGS=( + --collage-source-dir "$REPO_ROOT/.deps/collage-source" + --collage-https-port "$HTTPS_PORT" + --collage-source-ref "fbda94f3fea0a216ddd9c35b38a09d325bed3640" + ) + fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ --status-port "$MINECRAFT_HOST_PORT" \ @@ -1158,6 +1172,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-paper" ]]; then "${MIDDLEWARE_TLS_ARG[@]}" \ --password-file "$PASSWORD_FILE" --control-dir "$FOUNDRY_CONTROL_DIR" \ --foundry-commit "$FOUNDRY_COMMIT" --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ + "${COLLAGE_P2_ARGS[@]}" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ @@ -1212,7 +1227,7 @@ elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported WOW Sidecar control passed immutable image/config read-back, permissions/seed metadata, public-fixture worker runtime, restart persistence, and zero-residue cleanup" -elif [[ "$T6_PRODUCT" == "minecraft-paper" ]]; then +elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ]]; then write_receipt SUPPORTED true foundry-materialization "exact native TrueNAS Minecraft Paper server passed protocol status + world level.dat persistence across App stop/start, redeploy, retain-world delete/reinstall and owned cleanup; no vanilla 26.3 or physical TrueNAS claim" elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported FolioRelay three-service control passed exact image/config read-back, portal and IPP reachability, canonical identity, PDF/URF exact-source Inbox preservation, independent in-guest DNS-SD observation, restart persistence, and zero-residue cleanup" diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py new file mode 100644 index 000000000..1773a5249 --- /dev/null +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""COLLAGE P2: independent native TrueNAS App readback in one disposable guest. + +This helper only runs after the existing Paper T6 proof has installed a native +App. Credential/certificate setup mutates the *disposable guest* before the +observation phase. Only the Go observer reads provider state. Never use on a +physical TrueNAS host, with an existing site's credentials, or in operations. +""" +from __future__ import annotations + +import datetime as dt +import hashlib +import json +import os +import pathlib +import re +import ssl +import subprocess +import sys +import tempfile + +SCHEMA = "semper-supra.collage-native-readback-p2/1" +REALM = "d68a14b9-2a1c-4e83-a452-e3b569cdf93e" +SERVICE_UUID = "11111111-1111-4111-8111-111111111111" +NAME = "collage_p2_observer" +ROLE_NAME = "collage-p2-apps-read" +OWNER = "rdte-t6-minecraft-paper" +WORLD_ID = "world-rdte-001" +DEFAULT_SOURCE_SHA = "fbda94f3fea0a216ddd9c35b38a09d325bed3640" + + +class ObservationError(RuntimeError): + pass + + +def require(value: bool, code: str) -> None: + if not value: + raise ObservationError(code) + + +def provision_guest_readonly(call) -> str: + """Called on *already isolated* disposable TrueNAS 25.04.1 via admin DDP. + + Explicit provisioning is *outside* the zero-mutation observation phase. + No API keys, returned user/group/privilege records, or raw errors in logs. + """ + require(call("user.query", [[["username", "=", NAME]]]) == [], + "PREEXISTING_READONLY_USER_REFUSED") + require(call("privilege.query", [[["name", "=", ROLE_NAME]]]) == [], + "PREEXISTING_PRIVILEGE_REFUSED") + roles = call("privilege.roles", []) + require(isinstance(roles, dict) and "APPS_READ" in roles, + "APPS_READ_ROLE_NOT_AVAILABLE") + user = call("user.create", [{ + "username": NAME, "full_name": "Disposable COLLAGE P2 observer", + "group_create": True, "password_disabled": True, + "ssh_password_enabled": False, "smb": False, + }]) + require(isinstance(user, dict) and user.get("username") == NAME, + "READONLY_ACCOUNT_CREATION_REFUSED") + group = user.get("group") + require(isinstance(group, dict) and type(group.get("id")) is int, + "READONLY_ACCOUNT_GROUP_UNAVAILABLE") + privilege = call("privilege.create", [{ + "name": ROLE_NAME, "local_groups": [group["id"]], + "roles": ["APPS_READ"], "web_shell": False, + }]) + require(isinstance(privilege, dict) and privilege.get("name") == ROLE_NAME, + "APPS_READ_ASSIGNMENT_UNVERIFIED") + user_roles = call("user.query", [[["username", "=", NAME]], {"get": True}]) + require(isinstance(user_roles, dict) + and user_roles.get("roles") == ["APPS_READ"], + "APPS_READ_EFFECTIVE_ROLE_NOT_EXACT") + expiry = (dt.datetime.now(dt.timezone.utc) + dt.timedelta(hours=1)).isoformat() + key = call("api_key.create", [{ + "name": "disposable-collage-p2", "username": NAME, + "expires_at": expiry, + }]) + require(isinstance(key, dict) and isinstance(key.get("key"), str) + and len(key["key"]) > 20, + "DISPOSABLE_READONLY_KEY_NOT_ISSUED") + return key["key"] + + +def discover_tls_anchor(port: int, out: pathlib.Path) -> tuple[str, str]: + """Pin self-signed guest HTTPS cert for private ephemeral RDTE loopback. + + TOFU is limited to the fresh, runner-owned, 127.0.0.1 QEMU forwarded port. + The subsequent Go client MUST verify this pinned cert. No insecure Go TLS. + Operational deployments must instead pin their explicit trusted CA. + """ + require(0 < port < 65536, "INVALID_GUEST_HTTPS_PORT") + try: + pem = ssl.get_server_certificate(("127.0.0.1", port), timeout=12) + der = ssl.PEM_cert_to_DER_cert(pem) + except (OSError, ssl.SSLError): + raise ObservationError("DISPOSABLE_GUEST_TLS_UNAVAILABLE") from None + out.write_text(pem, encoding="ascii") + p = subprocess.run(["openssl", "x509", "-in", str(out), "-noout", + "-ext", "subjectAltName"], text=True, + capture_output=True, timeout=12, check=False) + require(p.returncode == 0, "TLS_CERTIFICATE_SAN_UNAVAILABLE") + sans = p.stdout + if "IP Address:127.0.0.1" in sans: + hostname = "127.0.0.1" + elif "DNS:localhost" in sans: + hostname = "localhost" + else: + # Never override an arbitrary hostname in runner DNS. + names = re.findall(r"DNS:(truenas(?:\.[a-z0-9.-]+)?)", sans, re.I) + require(bool(names), "TLS_CERTIFICATE_HOST_IDENTITY_UNSUPPORTED") + hostname = names[0].lower() + add = subprocess.run(["sudo", "-n", "sh", "-c", + "printf '127.0.0.1 %s\\n' \"$1\" >> /etc/hosts", + "sh", hostname], capture_output=True, timeout=8) + require(add.returncode == 0, "EPHEMERAL_GUEST_HOSTNAME_BINDING_FAILED") + return hostname, hashlib.sha256(der).hexdigest() + + +def run_real_readback(call, *, collage: pathlib.Path, https_port: int, + state_dir: pathlib.Path, expected_source_sha: str + = DEFAULT_SOURCE_SHA) -> dict: + """Return ONLY allowlisted, non-secret independent readback evidence.""" + require(collage.is_dir(), "PINNED_COLLAGE_CHECKOUT_MISSING") + git = subprocess.run(["git", "-C", str(collage), "rev-parse", "HEAD"], + text=True, capture_output=True, timeout=5) + require(git.returncode == 0 and git.stdout.strip() == expected_source_sha, + "PINNED_COLLAGE_COMMIT_DRIFT") + source_plan = collage / "examples" / "source-only-rdte.json" + fixture = collage / "examples" / "contract-only-world.fixture" + require(source_plan.is_file() and fixture.is_file(), + "SOURCE_ONLY_FIXTURE_UNAVAILABLE") + request = json.loads(source_plan.read_text(encoding="utf-8")) + request["request_id"] = "collage-p2-native-paper" + request["target"].update({ + "deployment_id": OWNER, "service_id": SERVICE_UUID, + "world_id": WORLD_ID, "minecraft_version": "1.21.11", + "backend": "paper", "purpose": "generic", + }) + request["artifact"].update({ + "world_id": WORLD_ID, "minecraft_version": "1.21.11", + }) + source = state_dir / "collage-p2-request.json" + source.write_text(json.dumps(request, indent=2, sort_keys=True) + "\n") + key = provision_guest_readonly(call) + trust = state_dir / "collage-p2-guest-cert.pem" + hostname, cert_sha = discover_tls_anchor(https_port, trust) + env = os.environ.copy() + env["TRUENAS_API_KEY"] = key + env["TRUENAS_PORT"] = str(https_port) + env["SSL_CERT_FILE"] = str(trust) + cmd = [sys.executable, str(collage / "tools" / "collage_native_readback.py"), + "--request", str(source), "--artifact-root", str(collage), + "--host", hostname, "--username", NAME, + "--trusted-realm-id", REALM, + "--manager-id", "manager-rdte", "--fleet-id", "rdte"] + try: + p = subprocess.run(cmd, env=env, text=True, capture_output=True, + timeout=180, check=False) + except (OSError, subprocess.TimeoutExpired): + raise ObservationError("NATIVE_READONLY_CLIENT_UNAVAILABLE") from None + # No raw stdout/stderr from clients or API; only extract declared schema. + try: + verdict = json.loads(p.stdout) if len(p.stdout) < 32768 else {} + except ValueError: + verdict = {} + require(p.returncode == 0 and verdict.get("schema") == "collage.native-readback/1" + and verdict.get("classification") == "PROVIDER_READBACK_MATCH" + and verdict.get("mutation_authorized") is False + and verdict.get("ownership_fields_matched") == 6 + and verdict.get("service_id") == SERVICE_UUID + and verdict.get("native_app") == OWNER, + "NATIVE_READONLY_GUEST_JOIN_FAILED") + return { + "schema": SCHEMA, + "classification": "PROVIDER_READBACK_MATCH", + "source_sha": expected_source_sha, + "source_plan_sha256": verdict["source_plan_sha256"], + "native_app": OWNER, + "truenas_version": verdict["truenas_version"], + "service_id": SERVICE_UUID, + "world_id": WORLD_ID, + "ownership_fields_matched": 6, + "tls_cert_sha256": cert_sha, + "transport": "isolated_guest_tls_verified", + "credential_scope": "APPS_READ", + "credential_mutation_phase": "disposable_guest_setup_before_observation", + "observation_mutation_count": 0, + "world_artifact_format_verified": False, + "two_rivers_gameplay_qualified": False, + "world_activated": False, + "cloudnet_adoption_executed": False, + "physical_true_nas_mutated": False, + } diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 874639c56..9472af4c8 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -290,6 +290,9 @@ def main() -> int: p.add_argument("--foundry-commit", required=True) p.add_argument("--expected-system-version", required=True) p.add_argument("--out", required=True, type=pathlib.Path) + p.add_argument("--collage-source-dir", type=pathlib.Path) + p.add_argument("--collage-https-port", type=int) + p.add_argument("--collage-source-ref") p.add_argument("--tls", action="store_true") p.add_argument("--timeout", type=float, default=8) p.add_argument("--job-timeout", type=float, default=600) @@ -520,6 +523,27 @@ def delete_app(label): wait_app() wait_status("first") read_sentinel() + if a.collage_source_dir is not None: + if (a.collage_https_port is None or a.collage_source_ref is None + or a.collage_https_port <= 0): + raise MinecraftT6Error("COLLAGE P2 readback admission incomplete") + from truenas_middleware_collage_readback_p2 import ( + ObservationError, run_real_readback, + ) + try: + payload["collage_readback"] = run_real_readback( + call, + collage=a.collage_source_dir, + https_port=a.collage_https_port, + state_dir=a.out.parent, + expected_source_sha=a.collage_source_ref, + ) + except ObservationError as err: + # The helper emits only a fixed reason code, no raw auth, + # certificate, network or client traces. + raise MinecraftT6Error("COLLAGE P2: " + str(err)) from None + read_sentinel() + first_level = payload["readiness"]["first"]["level"]["size"] wait_job(call("app.stop", [APP]), "Minecraft stop") diff --git a/tests/test_collage_p2_native.py b/tests/test_collage_p2_native.py new file mode 100644 index 000000000..208ca7d66 --- /dev/null +++ b/tests/test_collage_p2_native.py @@ -0,0 +1,97 @@ +"""Cheap source/contract tests for COLLAGE P2 disposable TrueNAS observer. + +These tests DO NOT impersonate a real TrueNAS guest or satisfy the live oracle. +""" +from __future__ import annotations + +import copy +import pathlib +import subprocess +import unittest + +from scripts.assert_collage_p2_receipt import check + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +class CollageP2SourceTests(unittest.TestCase): + def test_success_requires_real_native_guest_receipt(self): + with self.assertRaisesRegex(ValueError, "root contract"): + check({}) + with self.assertRaisesRegex(ValueError, "root failed"): + check({ + "contract": "gha-kvm-system-lab/v1", + "target": {"version": "25.04.1", "rung": "T6", "product": "truenas"}, + "classification": "HARNESS_FAILURE", "oracleSatisfied": False, + }) + + def test_paper_success_is_not_collage_readback_success(self): + native = { + "contract": "gha-kvm-system-lab/v1", + "target": {"version": "25.04.1", "rung": "T6", "product": "truenas"}, + "classification": "SUPPORTED", "oracleSatisfied": True, + "foundry_materialization": { + "schema": "truenas-minecraft-paper-t6/v1", + "classification": "SUPPORTED", "oracleSatisfied": True, + "catalog_version_verified": True, + "world_retained_on_delete": True, + "world_rebound_after_reinstall": True, + "cleanup": {"zero_residue": True, "app_absent": True, + "owned_world_dataset_absent": True}, + "readiness": {key: { + "protocol": {"oracleSatisfied": True, "server_protocol": 774, + "server_implementation": "paper"}, + "level": {"size": 123}, + } for key in ("first", "restart", "redeploy", "reinstall")}, + }, + } + with self.assertRaisesRegex(ValueError, "observation missing"): + check(native) + fake = copy.deepcopy(native) + fake["foundry_materialization"]["collage_readback"] = { + "schema": "semper-supra.collage-native-readback-p2/1", + "classification": "PROVIDER_READBACK_MATCH", + "source_sha": "fbda94f3fea0a216ddd9c35b38a09d325bed3640", + "native_app": "rdte-t6-minecraft-paper", + "service_id": "11111111-1111-4111-8111-111111111111", + "ownership_fields_matched": 6, + "credential_scope": "FULL_ADMIN", + "transport": "isolated_guest_tls_verified", + "observation_mutation_count": 0, + "world_activated": False, + "cloudnet_adoption_executed": False, + "physical_true_nas_mutated": False, + "tls_cert_sha256": "a" * 64, + } + with self.assertRaisesRegex(ValueError, "observation missing"): + check(fake) + fake["foundry_materialization"]["collage_readback"]["credential_scope"] = "APPS_READ" + check(fake) # Only pure receipt predicate; NEVER live product acceptance. + fake["foundry_materialization"]["collage_readback"]["observation_mutation_count"] = 1 + with self.assertRaisesRegex(ValueError, "observation missing"): + check(fake) + + def test_existing_paper_fixture_is_opt_in_unchanged(self): + paper = (ROOT / "scripts/truenas_middleware_minecraft_t6_probe.py").read_text() + shell = (ROOT / "scripts/gha_kvm_truenas_rdte.sh").read_text() + workflow = (ROOT / ".github/workflows/gha-kvm-system-rdte.yml").read_text() + helper = (ROOT / "scripts/truenas_middleware_collage_readback_p2.py").read_text() + self.assertIn("if a.collage_source_dir is not None:", paper) + self.assertIn('if [[ "$T6_PRODUCT" == "collage-native-paper" ]]; then', shell) + self.assertIn('--collage-source-ref "fbda94f3fea0a216ddd9c35b38a09d325bed3640"', shell) + self.assertIn("ref: fbda94f3fea0a216ddd9c35b38a09d325bed3640", workflow) + self.assertIn('"roles": ["APPS_READ"]', helper) + self.assertIn('"web_shell": False', helper) + self.assertIn('env["SSL_CERT_FILE"]', helper) + self.assertIn("observation_mutation_count", helper) + self.assertNotIn("InsecureSkipVerify: true", helper) + + def test_bash_static_syntax(self): + for path in ("scripts/gha_kvm_truenas_rdte.sh",): + p = subprocess.run(["bash", "-n", str(ROOT / path)], + capture_output=True, text=True) + self.assertEqual(p.returncode, 0, p.stderr) + + +if __name__ == "__main__": + unittest.main() From a18595a11b4751c7ac102757e804bc7c11702180 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:37:53 +0200 Subject: [PATCH 649/719] rdte(collage): admit one fresh isolated native TrueNAS P2 read-only guest experiment --- config/truenas-rdte-run-request.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 1a9ffaa5c..2a975c1b7 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-minecraft-paper-010-exact-paper-ping-protocol-774", + "request_id": "tn-25-04-1-t6-collage-native-paper-p2-001-real-app-readonly", "version": "25.04.1", "rung": "t6", - "product": "minecraft-paper", + "product": "collage-native-paper", "authority_issue": 411, - "consumer_authority_issue": 588, - "purpose": "One serialized FREE PUBLIC real nested TrueNAS 25.04.1 Minecraft Paper 1.21.11 T6 qualification rep 010; not a blind unchanged rerun. Previous exact guest rep 009 public GHA #38187943219 verified running native community/minecraft 1.15.29, digest-locked ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa, correct ownership, *positive level.dat*, Java TCP status responding but status version_mismatch to naive unbranded 1.21.11, 119 retries / 480s; final DDP EOF cleanup unknown and red session. Actual source PaperMC/Paper ver/1.21.11@c5eb0790f199da6c38d0a650e1e5cd5415b28185 proves status name `Paper 1.21.11` (getServerModName + space + server version; brand Paper), protocol release 774. Public source status oracle in SupraCraft/mc-server-images PR #61 merged exact 1b2227847d56007cdf8b668c00845326827e5fd5 permits only exact branded status under explicit --implementation paper and --expected-protocol 774; leaves vanilla exact plain name and rejects drift. Public source CI 38200346452/38200346521/38200346558 SUCCESS. Experimental Agent Dispatch PR #395 merged exact 99bf7078378ea7a1fa6dea27f0a08868456ce4ba pinning product source checksum via exact HEAD check in workflow and verifying both branded mode and exact protocol as independent receipt. Consumer public CI 38200492184 and prep static 38200492170 SUCCESS; merge-triggered 38200562936 SUCCESS with NO heavyweight guest. Foundry producer exact 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, truenas/apps upstream bb96ef6fd8c2708765012a4a3ca3739370a4f92c. Require truly live guest status response implementing Paper Java 1.21.11 protocol 774, level.dat first boot and after stop/start/redeploy/delete+reinstall, owned world sentinel and separate external ZFS world retention, app final delete and explicit world dataset removal, no residual. Maintain fail-close on any mismatch, report independent causal status/level/backend and cleanup witnesses and never claim physical host/vanilla 26.3 or Two Rivers. Shared heavyweight one-slot public GHA only, no local/physical TrueNAS mutation, no private GHA or paid runners; physical APPLY HOLD remains. If failure, preserve full bounded evidence and do not repeat unchanged rep." + "consumer_authority_issue": 614, + "purpose": "One genuinely new public/free-tier disposable TrueNAS 25.04.1 KVM COLLAGE P2 live integration experiment. Parent Paper T6 real PASS 38200675099 is unchanged and not being blindly rerun: this rep uniquely tests an independent scoped APPS_READ guest user/key, TLS-verified Go JSON-RPC read-only app.query over guest HTTPS on the actual native RUNNING Paper 1.21.11 community/minecraft 1.15.29 App, exact six provider-neutral owner fields bound to one COLLAGE source-only request/service UUID/realm context, and zero mutation during observation; key provision and guest CA trust bootstrap are separate disposable setup mutations. Pin COLLAGE main fbda94f3fea0a216ddd9c35b38a09d325bed3640, validated by CI #38210962222 (25 source/policy tests), adapter consumer PR #397 merged SHA 6017846980559e5d8a6e76ed4e7ca15a1acb0179, and exact Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e; keep prior Paper server/protocol 774 plus external world retention and zero-residue complete oracle. New specialized P2 receipt required after source and preflight gates; do not claim Two Rivers/vanilla 26.3, CloudNet managed adoption, cryptographic realm identity, operational actor authority, or physical TrueNAS HIL. On failure preserve bounded causal receipt and do NOT automatically rerun unchanged. One shared serialized heavyweight public GHA slot only; physical APPLY HOLD." } From eb1b0ac601d9c9dd108862a1310b000f744572f5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:39:47 +0200 Subject: [PATCH 650/719] fix: admit COLLAGE P2 only under exact T6 product contract (#398) * fix(rdte): register bounded COLLAGE T6 in exact existing target-contract tests * fix(rdte): register bounded COLLAGE T6 in exact existing target-contract tests --- tests/test_truenas_rdte_target.py | 6 +++++- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 72f841457..be6ed5498 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -78,7 +78,11 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","garm-provider-container-pre-b4","official-catalog","foliorelay","minecraft-paper"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","garm-provider-container-pre-b4","official-catalog","foliorelay","minecraft-paper","collage-native-paper"}) + if request.get("product") == "collage-native-paper": + self.assertEqual(request["rung"], "t6") + self.assertEqual(request["version"], "25.04.1") + self.assertEqual(request["consumer_authority_issue"], 614) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 9d03b68cd..0ceee8844 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "garm-provider-container-pre-b4", "official-catalog", "foliorelay", "minecraft-paper"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "garm-provider-container-pre-b4", "official-catalog", "foliorelay", "minecraft-paper", "collage-native-paper"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) From 35623e36e6b57a2b009180671140cf13d7124e32 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:40:36 +0200 Subject: [PATCH 651/719] rdte(collage): fresh gated P2 guest after corrected exact product allowlist --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 2a975c1b7..dac6587e1 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-collage-native-paper-p2-001-real-app-readonly", + "request_id": "tn-25-04-1-t6-collage-native-paper-p2-002-registered-product-contract", "version": "25.04.1", "rung": "t6", "product": "collage-native-paper", "authority_issue": 411, "consumer_authority_issue": 614, - "purpose": "One genuinely new public/free-tier disposable TrueNAS 25.04.1 KVM COLLAGE P2 live integration experiment. Parent Paper T6 real PASS 38200675099 is unchanged and not being blindly rerun: this rep uniquely tests an independent scoped APPS_READ guest user/key, TLS-verified Go JSON-RPC read-only app.query over guest HTTPS on the actual native RUNNING Paper 1.21.11 community/minecraft 1.15.29 App, exact six provider-neutral owner fields bound to one COLLAGE source-only request/service UUID/realm context, and zero mutation during observation; key provision and guest CA trust bootstrap are separate disposable setup mutations. Pin COLLAGE main fbda94f3fea0a216ddd9c35b38a09d325bed3640, validated by CI #38210962222 (25 source/policy tests), adapter consumer PR #397 merged SHA 6017846980559e5d8a6e76ed4e7ca15a1acb0179, and exact Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e; keep prior Paper server/protocol 774 plus external world retention and zero-residue complete oracle. New specialized P2 receipt required after source and preflight gates; do not claim Two Rivers/vanilla 26.3, CloudNet managed adoption, cryptographic realm identity, operational actor authority, or physical TrueNAS HIL. On failure preserve bounded causal receipt and do NOT automatically rerun unchanged. One shared serialized heavyweight public GHA slot only; physical APPLY HOLD." + "purpose": "One genuinely new public/free-tier disposable TrueNAS 25.04.1 KVM COLLAGE P2 live integration experiment. Parent Paper T6 real PASS 38200675099 is unchanged and not being blindly rerun: this rep uniquely tests an independent scoped APPS_READ guest user/key, TLS-verified Go JSON-RPC read-only app.query over guest HTTPS on the actual native RUNNING Paper 1.21.11 community/minecraft 1.15.29 App, exact six provider-neutral owner fields bound to one COLLAGE source-only request/service UUID/realm context, and zero mutation during observation; key provision and guest CA trust bootstrap are separate disposable setup mutations. Pin COLLAGE main fbda94f3fea0a216ddd9c35b38a09d325bed3640, validated by CI #38210962222 (25 source/policy tests), adapter consumer PR #397 merged SHA 6017846980559e5d8a6e76ed4e7ca15a1acb0179, and exact Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e; keep prior Paper server/protocol 774 plus external world retention and zero-residue complete oracle. New specialized P2 receipt required after source and preflight gates; do not claim Two Rivers/vanilla 26.3, CloudNet managed adoption, cryptographic realm identity, operational actor authority, or physical TrueNAS HIL. On failure preserve bounded causal receipt and do NOT automatically rerun unchanged. One shared serialized heavyweight public GHA slot only; physical APPLY HOLD. Correction before heavyweight execution: original request #38231134539 failed cheap contract 2/158 due to old product allowlists, and did not boot a guest. Smallest owning fix PR #398 merged experiment head eb1b0ac601d9c9dd108862a1310b000f744572f5; merge-triggered public workflow #38231334737 contract SUCCESS and heavy guest SKIPPED. This is first actual P2 heavyweight request after corrected source gate; no unchanged blind retry." } From 28720b6fb2be90f21fb97f36f49cd0d3a568d0a5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:56:18 +0200 Subject: [PATCH 652/719] fix: parse TrueNAS 25.04 privilege.roles as array before disposable API-key setup (#400) * fix(rdte): parse TrueNAS 25.04 privilege.roles as verified role-descriptor array * test(rdte): verify TrueNAS 25.04 role vector and mutation-free refusal * ci(rdte): run TrueNAS 25.04 role-shape regression in public cheap preflight --- .../workflows/qualify-collage-p2-source.yml | 5 ++- .../truenas_middleware_collage_readback_p2.py | 24 +++++++++-- tests/test_collage_p2_roles.py | 43 +++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 tests/test_collage_p2_roles.py diff --git a/.github/workflows/qualify-collage-p2-source.yml b/.github/workflows/qualify-collage-p2-source.yml index 86e0f9711..9d7c83e02 100644 --- a/.github/workflows/qualify-collage-p2-source.yml +++ b/.github/workflows/qualify-collage-p2-source.yml @@ -10,6 +10,7 @@ on: - 'scripts/assert_collage_p2_receipt.py' - 'scripts/gha_kvm_truenas_rdte.sh' - 'tests/test_collage_p2_native.py' + - 'tests/test_collage_p2_roles.py' - 'tests/test_truenas_t6_minecraft_control.py' - '.github/workflows/gha-kvm-system-rdte.yml' - '.github/workflows/qualify-collage-p2-source.yml' @@ -34,9 +35,11 @@ jobs: scripts/truenas_middleware_collage_readback_p2.py \ scripts/truenas_middleware_minecraft_t6_probe.py \ scripts/assert_collage_p2_receipt.py \ - tests/test_collage_p2_native.py + tests/test_collage_p2_native.py \ + tests/test_collage_p2_roles.py python3 -m unittest -v \ tests.test_collage_p2_native \ + tests.test_collage_p2_roles \ tests.test_truenas_t6_minecraft_control - name: Declare no live-system result run: | diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 1773a5249..950173655 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -38,6 +38,25 @@ def require(value: bool, code: str) -> None: raise ObservationError(code) +def available_role_names(roles: object) -> set[str]: + """TrueNAS 25.04 privilege.roles -> list of role descriptors, not a map. + + Reject malformed or ambiguous inventories before any guest mutation. Only + report whether the exact role is offered; never infer broader privilege. + """ + require(isinstance(roles, list), "PRIVILEGE_ROLES_SHAPE_DRIFT") + names: set[str] = set() + for item in roles: + require(isinstance(item, dict) and isinstance(item.get("name"), str) + and isinstance(item.get("includes"), list) + and all(isinstance(x, str) for x in item["includes"]), + "PRIVILEGE_ROLES_SHAPE_DRIFT") + name = item["name"] + require(name not in names, "DUPLICATE_PRIVILEGE_ROLE") + names.add(name) + return names + + def provision_guest_readonly(call) -> str: """Called on *already isolated* disposable TrueNAS 25.04.1 via admin DDP. @@ -48,9 +67,8 @@ def provision_guest_readonly(call) -> str: "PREEXISTING_READONLY_USER_REFUSED") require(call("privilege.query", [[["name", "=", ROLE_NAME]]]) == [], "PREEXISTING_PRIVILEGE_REFUSED") - roles = call("privilege.roles", []) - require(isinstance(roles, dict) and "APPS_READ" in roles, - "APPS_READ_ROLE_NOT_AVAILABLE") + roles = available_role_names(call("privilege.roles", [])) + require("APPS_READ" in roles, "APPS_READ_ROLE_NOT_AVAILABLE") user = call("user.create", [{ "username": NAME, "full_name": "Disposable COLLAGE P2 observer", "group_create": True, "password_disabled": True, diff --git a/tests/test_collage_p2_roles.py b/tests/test_collage_p2_roles.py new file mode 100644 index 000000000..d2dd6aaac --- /dev/null +++ b/tests/test_collage_p2_roles.py @@ -0,0 +1,43 @@ +"""Source-only checks for the TrueNAS 25.04 privilege.roles response contract.""" +import unittest + +from scripts.truenas_middleware_collage_readback_p2 import ( + ObservationError, available_role_names, provision_guest_readonly, +) + + +class RolesTests(unittest.TestCase): + def test_role_descriptors_are_an_array(self): + rows = [ + {"name": "APPS_READ", "includes": [], "builtin": False, "stig": None}, + {"name": "READONLY_ADMIN", "includes": ["APPS_READ"], + "builtin": True, "stig": None}, + ] + self.assertEqual(available_role_names(rows), {"APPS_READ", "READONLY_ADMIN"}) + + def test_reject_schema_drift(self): + for rows in ({"APPS_READ": {}}, None, [{"name": "APPS_READ"}], + [{"name": "APPS_READ", "includes": "string"}], + [{"name": "APPS_READ", "includes": []}, + {"name": "APPS_READ", "includes": []}]): + with self.subTest(rows=rows), self.assertRaises(ObservationError): + available_role_names(rows) + + def test_unknown_role_does_not_provision(self): + methods = [] + + def only_discovery(method, params): + methods.append(method) + if method == "privilege.roles": + return [{"name": "POOL_READ", "includes": []}] + if method in ("user.query", "privilege.query"): + return [] + raise AssertionError("unexpected method") + with self.assertRaisesRegex(ObservationError, "APPS_READ_ROLE_NOT_AVAILABLE"): + provision_guest_readonly(only_discovery) + self.assertEqual(methods, + ["user.query", "privilege.query", "privilege.roles"]) + + +if __name__ == "__main__": + unittest.main() From d39703ddee181838edf4697375d7bb6d1a1a8f83 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 16:57:49 +0200 Subject: [PATCH 653/719] rdte(collage): bounded unique P2-003 guest rep for corrected TrueNAS APPS_READ roles array --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index dac6587e1..d1ad291fa 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-collage-native-paper-p2-002-registered-product-contract", + "request_id": "tn-25-04-1-t6-collage-native-paper-p2-003-array-roles-observer", "version": "25.04.1", "rung": "t6", "product": "collage-native-paper", "authority_issue": 411, "consumer_authority_issue": 614, - "purpose": "One genuinely new public/free-tier disposable TrueNAS 25.04.1 KVM COLLAGE P2 live integration experiment. Parent Paper T6 real PASS 38200675099 is unchanged and not being blindly rerun: this rep uniquely tests an independent scoped APPS_READ guest user/key, TLS-verified Go JSON-RPC read-only app.query over guest HTTPS on the actual native RUNNING Paper 1.21.11 community/minecraft 1.15.29 App, exact six provider-neutral owner fields bound to one COLLAGE source-only request/service UUID/realm context, and zero mutation during observation; key provision and guest CA trust bootstrap are separate disposable setup mutations. Pin COLLAGE main fbda94f3fea0a216ddd9c35b38a09d325bed3640, validated by CI #38210962222 (25 source/policy tests), adapter consumer PR #397 merged SHA 6017846980559e5d8a6e76ed4e7ca15a1acb0179, and exact Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e; keep prior Paper server/protocol 774 plus external world retention and zero-residue complete oracle. New specialized P2 receipt required after source and preflight gates; do not claim Two Rivers/vanilla 26.3, CloudNet managed adoption, cryptographic realm identity, operational actor authority, or physical TrueNAS HIL. On failure preserve bounded causal receipt and do NOT automatically rerun unchanged. One shared serialized heavyweight public GHA slot only; physical APPLY HOLD. Correction before heavyweight execution: original request #38231134539 failed cheap contract 2/158 due to old product allowlists, and did not boot a guest. Smallest owning fix PR #398 merged experiment head eb1b0ac601d9c9dd108862a1310b000f744572f5; merge-triggered public workflow #38231334737 contract SUCCESS and heavy guest SKIPPED. This is first actual P2 heavyweight request after corrected source gate; no unchanged blind retry." + "purpose": "Exactly one qualified corrective real disposable COLLAGE P2 read-only TrueNAS 25.04.1/Paper observation after causal failed guest run 38231423577; there is no accepted readback. Original P2-002 guest completed and cleaned owned App/ZFS with zero residue and healthy middleware; product ORACLE_FAILURE was APPS_READ_ROLE_NOT_AVAILABLE at helper checking privilege.roles as dict, which is incorrect under vendor version 25.04.1 role-descriptor-array API. New correction Agent Dispatch PR #400 merged branch commit 28720b6fb2be90f21fb97f36f49cd0d3a568d0a5; source contract tests reject malformed, duplicate and absent roles before mutations. This new request uniquely verifies parsed array, provisioned disposable least-privileged APPS_READ account/API key and verified TLS Go App readback from accepted COLLAGE source fbda94f3fea0a216ddd9c35b38a09d325bed3640. Pin exact paper TrueNAS 25.04.1/1.21.11 protocol 774, Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, native App six ownership fields, world retention and zero cleanup. Preserve causal failure evidence and do not silently retry an unchanged failure. Public-free-tier serialized heavyweight slot only, never private GHA minutes. No physical TrueNAS mutations, no production credentials, no CloudNet adoption/Two Rivers/vanilla claims." } From 0678a7375c9abefee3b622d19a542d500fbd2bf5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:25:40 +0200 Subject: [PATCH 654/719] diag: distinguish exact TrueNAS observer privilege, primary group and effective-role failures (#401) * diag(rdte): fail closed on exact created privilege, primary group and observer effective-role mismatch * test(rdte): distinguish missing, inherited-extra and group-linkage roles before API key creation * ci(rdte): qualify effective-role boundary and refusal before disposable API-key issuance --- .../workflows/qualify-collage-p2-source.yml | 5 +- .../truenas_middleware_collage_readback_p2.py | 24 +++++- tests/test_collage_p2_effective_roles.py | 74 +++++++++++++++++++ 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/test_collage_p2_effective_roles.py diff --git a/.github/workflows/qualify-collage-p2-source.yml b/.github/workflows/qualify-collage-p2-source.yml index 9d7c83e02..041a389f2 100644 --- a/.github/workflows/qualify-collage-p2-source.yml +++ b/.github/workflows/qualify-collage-p2-source.yml @@ -11,6 +11,7 @@ on: - 'scripts/gha_kvm_truenas_rdte.sh' - 'tests/test_collage_p2_native.py' - 'tests/test_collage_p2_roles.py' + - 'tests/test_collage_p2_effective_roles.py' - 'tests/test_truenas_t6_minecraft_control.py' - '.github/workflows/gha-kvm-system-rdte.yml' - '.github/workflows/qualify-collage-p2-source.yml' @@ -36,10 +37,12 @@ jobs: scripts/truenas_middleware_minecraft_t6_probe.py \ scripts/assert_collage_p2_receipt.py \ tests/test_collage_p2_native.py \ - tests/test_collage_p2_roles.py + tests/test_collage_p2_roles.py \ + tests/test_collage_p2_effective_roles.py python3 -m unittest -v \ tests.test_collage_p2_native \ tests.test_collage_p2_roles \ + tests.test_collage_p2_effective_roles \ tests.test_truenas_t6_minecraft_control - name: Declare no live-system result run: | diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 950173655..55103ab17 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -85,10 +85,28 @@ def provision_guest_readonly(call) -> str: }]) require(isinstance(privilege, dict) and privilege.get("name") == ROLE_NAME, "APPS_READ_ASSIGNMENT_UNVERIFIED") + require(privilege.get("roles") == ["APPS_READ"], + "CREATED_PRIVILEGE_ROLES_NOT_EXACT") + # Distinguish role/primary-group propagation problems from extra authority, + # without emitting public role names, user metadata or sensitive API fields. + primary = call("group.query", [[["id", "=", group["id"]]], {"get": True}]) + require(isinstance(primary, dict) and primary.get("id") == group["id"], + "OBSERVER_PRIMARY_GROUP_NOT_FOUND") + require(primary.get("roles") == ["APPS_READ"], + "OBSERVER_PRIMARY_GROUP_ROLE_NOT_EXACT") user_roles = call("user.query", [[["username", "=", NAME]], {"get": True}]) - require(isinstance(user_roles, dict) - and user_roles.get("roles") == ["APPS_READ"], - "APPS_READ_EFFECTIVE_ROLE_NOT_EXACT") + require(isinstance(user_roles, dict) and user_roles.get("username") == NAME, + "OBSERVER_USER_READBACK_NOT_FOUND") + user_primary = user_roles.get("group") + require(isinstance(user_primary, dict) + and user_primary.get("id") == group["id"], + "OBSERVER_USER_PRIMARY_GROUP_DRIFT") + effective = user_roles.get("roles") + require(isinstance(effective, list) + and all(isinstance(role, str) for role in effective), + "OBSERVER_USER_ROLES_SHAPE_DRIFT") + require("APPS_READ" in effective, "OBSERVER_USER_REQUIRED_ROLE_ABSENT") + require(effective == ["APPS_READ"], "OBSERVER_USER_EXTRA_ROLES_REFUSED") expiry = (dt.datetime.now(dt.timezone.utc) + dt.timedelta(hours=1)).isoformat() key = call("api_key.create", [{ "name": "disposable-collage-p2", "username": NAME, diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py new file mode 100644 index 000000000..f8e1dd9c1 --- /dev/null +++ b/tests/test_collage_p2_effective_roles.py @@ -0,0 +1,74 @@ +"""Pure request/response policy checks; never claims a running TrueNAS guest.""" +import unittest + +from scripts.truenas_middleware_collage_readback_p2 import ( + ObservationError, provision_guest_readonly, +) + +ROLE = "APPS_READ" +USER = "collage_p2_observer" +PRIVILEGE = "collage-p2-apps-read" +GROUP_ID = 1013 + + +def authority_responses(*, primary_roles=None, user_roles=None, + user_group_id=GROUP_ID, privilege_roles=None): + primary_roles = [ROLE] if primary_roles is None else primary_roles + user_roles = [ROLE] if user_roles is None else user_roles + privilege_roles = [ROLE] if privilege_roles is None else privilege_roles + responses = [ + ("user.query", []), + ("privilege.query", []), + ("privilege.roles", [{"name": ROLE, "includes": []}]), + ("user.create", {"username": USER, "group": {"id": GROUP_ID}}), + ("privilege.create", {"name": PRIVILEGE, "roles": privilege_roles}), + ("group.query", {"id": GROUP_ID, "roles": primary_roles}), + ("user.query", {"username": USER, "group": {"id": user_group_id}, + "roles": user_roles}), + ("api_key.create", {"key": "disposable-test-only-key-value-not-real"}), + ] + return responses + + +class RoleLinkageTests(unittest.TestCase): + def call_with(self, responses): + called = [] + + def call(method, params): + self.assertLess(len(called), len(responses), "unexpected API call") + want, result = responses[len(called)] + self.assertEqual(method, want, f"wrong operation at position {len(called)}") + called.append(method) + return result + return call, called + + def test_source_contract_allows_only_exact_role_linkage(self): + fn, methods = self.call_with(authority_responses()) + self.assertEqual(provision_guest_readonly(fn), + "disposable-test-only-key-value-not-real") + self.assertEqual(methods[-1], "api_key.create") + + def test_unauthorized_roles_never_receive_api_key(self): + cases = [ + ({"user_roles": []}, "OBSERVER_USER_REQUIRED_ROLE_ABSENT"), + ({"user_roles": ["APPS_READ", "FULL_ADMIN"]}, + "OBSERVER_USER_EXTRA_ROLES_REFUSED"), + ({"user_roles": "APPS_READ"}, "OBSERVER_USER_ROLES_SHAPE_DRIFT"), + ({"user_group_id": GROUP_ID + 1}, + "OBSERVER_USER_PRIMARY_GROUP_DRIFT"), + ({"primary_roles": []}, "OBSERVER_PRIMARY_GROUP_ROLE_NOT_EXACT"), + ({"primary_roles": ["FULL_ADMIN"]}, + "OBSERVER_PRIMARY_GROUP_ROLE_NOT_EXACT"), + ({"privilege_roles": ["APPS_READ", "FULL_ADMIN"]}, + "CREATED_PRIVILEGE_ROLES_NOT_EXACT"), + ] + for opts, reason in cases: + with self.subTest(opts=opts): + fn, methods = self.call_with(authority_responses(**opts)) + with self.assertRaisesRegex(ObservationError, reason): + provision_guest_readonly(fn) + self.assertNotIn("api_key.create", methods) + + +if __name__ == "__main__": + unittest.main() From 615a42141ad6b5824a5952a9bf9acbfc6ac263fa Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:28:01 +0200 Subject: [PATCH 655/719] rdte(collage): admit unique P2-004 guest to disambiguate effective-role group/user authority --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index d1ad291fa..2c3b8413b 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-collage-native-paper-p2-003-array-roles-observer", + "request_id": "tn-25-04-1-t6-collage-native-paper-p2-004-effective-role-chain-diagnostic", "version": "25.04.1", "rung": "t6", "product": "collage-native-paper", "authority_issue": 411, "consumer_authority_issue": 614, - "purpose": "Exactly one qualified corrective real disposable COLLAGE P2 read-only TrueNAS 25.04.1/Paper observation after causal failed guest run 38231423577; there is no accepted readback. Original P2-002 guest completed and cleaned owned App/ZFS with zero residue and healthy middleware; product ORACLE_FAILURE was APPS_READ_ROLE_NOT_AVAILABLE at helper checking privilege.roles as dict, which is incorrect under vendor version 25.04.1 role-descriptor-array API. New correction Agent Dispatch PR #400 merged branch commit 28720b6fb2be90f21fb97f36f49cd0d3a568d0a5; source contract tests reject malformed, duplicate and absent roles before mutations. This new request uniquely verifies parsed array, provisioned disposable least-privileged APPS_READ account/API key and verified TLS Go App readback from accepted COLLAGE source fbda94f3fea0a216ddd9c35b38a09d325bed3640. Pin exact paper TrueNAS 25.04.1/1.21.11 protocol 774, Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e, native App six ownership fields, world retention and zero cleanup. Preserve causal failure evidence and do not silently retry an unchanged failure. Public-free-tier serialized heavyweight slot only, never private GHA minutes. No physical TrueNAS mutations, no production credentials, no CloudNet adoption/Two Rivers/vanilla claims." + "purpose": "Single new causal diagnostic experiment after P2-003 run 38266734446 guest failure APPS_READ_EFFECTIVE_ROLE_NOT_EXACT (artifact 11755604268, zero residue and platform healthy). It is not a replay of a passed experiment. Source PR #401 merged 0678a7375c9abefee3b622d19a542d500fbd2bf5; source/static CI passed and source only refined exact role evidence: newly created privilege role assignment, primary group role, user primary group, missing or extra effective roles, no plaintext user/group details, no keys in log. Keep exact 25.04.1 KVM + Paper 1.21.11 digest Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e and COLLAGE fbda94f3fea0a216ddd9c35b38a09d325bed3640, six owner markers. Require exact APPS_READ only, no API key if group or user mismatch, no FULL_ADMIN workaround, no TLS bypass, no false native observation PASS. Distinguish guest setup mutations from zero-mutation observer. Public free-tier serialized KVM only, no private hosted GHA minutes, no local/physical TrueNAS. On failure retain causal receipt and stop; do not automatically retry unchanged." } From 51104038997c6ac489ce7bd31d038e5f994ab9b8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:09:18 +0200 Subject: [PATCH 656/719] Guard COLLAGE TrueNAS KVM against upstream Minecraft catalog version retirement (#402) * fix(rdte): detect independently rotating official minecraft catalog before costly KVM * test(rdte): qualify immutable catalog preflight without contacting a guest * ci(rdte): fail early on retired Minecraft catalog pin before renting disposable guest * ci(rdte): source qualify retired catalog negative gate without invoking remote network --- .github/workflows/gha-kvm-system-rdte.yml | 7 ++ .../workflows/qualify-collage-p2-source.yml | 7 +- scripts/check_collage_catalog_preflight.py | 83 +++++++++++++++++++ tests/test_collage_catalog_preflight.py | 33 ++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 scripts/check_collage_catalog_preflight.py create mode 100644 tests/test_collage_catalog_preflight.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 1bfcb29b3..f861a2d9e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -382,6 +382,13 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + - name: Refuse floating upstream Minecraft catalog before heavyweight KVM + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + run: | + set -euo pipefail + python3 -m unittest -v tests.test_collage_catalog_preflight + python3 scripts/check_collage_catalog_preflight.py --expected 1.15.29 + - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail diff --git a/.github/workflows/qualify-collage-p2-source.yml b/.github/workflows/qualify-collage-p2-source.yml index 041a389f2..ffa9d35b2 100644 --- a/.github/workflows/qualify-collage-p2-source.yml +++ b/.github/workflows/qualify-collage-p2-source.yml @@ -12,6 +12,8 @@ on: - 'tests/test_collage_p2_native.py' - 'tests/test_collage_p2_roles.py' - 'tests/test_collage_p2_effective_roles.py' + - 'tests/test_collage_catalog_preflight.py' + - 'scripts/check_collage_catalog_preflight.py' - 'tests/test_truenas_t6_minecraft_control.py' - '.github/workflows/gha-kvm-system-rdte.yml' - '.github/workflows/qualify-collage-p2-source.yml' @@ -38,11 +40,14 @@ jobs: scripts/assert_collage_p2_receipt.py \ tests/test_collage_p2_native.py \ tests/test_collage_p2_roles.py \ - tests/test_collage_p2_effective_roles.py + tests/test_collage_p2_effective_roles.py \ + tests/test_collage_catalog_preflight.py \ + scripts/check_collage_catalog_preflight.py python3 -m unittest -v \ tests.test_collage_p2_native \ tests.test_collage_p2_roles \ tests.test_collage_p2_effective_roles \ + tests.test_collage_catalog_preflight \ tests.test_truenas_t6_minecraft_control - name: Declare no live-system result run: | diff --git a/scripts/check_collage_catalog_preflight.py b/scripts/check_collage_catalog_preflight.py new file mode 100644 index 000000000..6af3fb9dd --- /dev/null +++ b/scripts/check_collage_catalog_preflight.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Cheap read-only pre-KVM guard for rotating official TrueNAS Minecraft catalog. + +Do not confuse this remote branch preflight with in-guest native catalog proof. +The official TrueNAS Apps catalog is independently updated and may retire old +versions despite a pinned Foundry source render. No credentials or mutation. +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +import urllib.error +import urllib.request + +URI = "https://api.github.com/repos/truenas/apps/contents/trains/community/minecraft?ref=main" +VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$") + + +def version_inventory(rows: object) -> list[str]: + if type(rows) is not list or len(rows) > 1000: + raise ValueError("CATALOG_LIST_INVALID") + versions = [] + for row in rows: + if type(row) is not dict: + raise ValueError("CATALOG_LIST_INVALID") + name = row.get("name") + kind = row.get("type") + if not isinstance(name, str) or kind not in {"dir", "file"}: + raise ValueError("CATALOG_LIST_INVALID") + if kind == "dir" and VERSION.fullmatch(name): + versions.append(name) + if not versions or len(set(versions)) != len(versions): + raise ValueError("CATALOG_LIST_AMBIGUOUS") + return sorted(versions) + + +def evaluate(rows: object, expected: str) -> dict: + if not isinstance(expected, str) or not VERSION.fullmatch(expected): + raise ValueError("INVALID_PINNED_VERSION") + available = version_inventory(rows) + return { + "schema": "collage.official-catalog-preflight/1", + "classification": "PRE_KVM_CATALOG_VERSION_AVAILABLE" + if expected in available else "PRE_KVM_CATALOG_DRIFT", + "expected_version": expected, + "visible_versions": available, + "guest_catalog_observed": False, + "live_minecraft_qualified": False, + "world_mutated": False, + } + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--expected", required=True) + a = p.parse_args() + req = urllib.request.Request(URI, headers={ + "User-Agent": "SupraCraft-COLLAGE-immutable-catalog-preflight", + "Accept": "application/vnd.github+json", + }) + try: + with urllib.request.urlopen(req, timeout=20) as stream: + payload = stream.read(512 * 1024 + 1) + if len(payload) > 512 * 1024: + raise ValueError("CATALOG_LIST_OVERSIZED") + row = evaluate(json.loads(payload), a.expected) + status = 0 if row["classification"] == "PRE_KVM_CATALOG_VERSION_AVAILABLE" else 2 + except (OSError, ValueError, urllib.error.HTTPError): + row = { + "schema": "collage.official-catalog-preflight/1", + "classification": "PRE_KVM_CATALOG_INCONCLUSIVE", + "reason": "OFFICIAL_READONLY_CATALOG_UNAVAILABLE_OR_UNEXPECTED", + "guest_catalog_observed": False, "world_mutated": False, + } + status = 3 + print(json.dumps(row, sort_keys=True)) + return status + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_collage_catalog_preflight.py b/tests/test_collage_catalog_preflight.py new file mode 100644 index 000000000..aca0d066a --- /dev/null +++ b/tests/test_collage_catalog_preflight.py @@ -0,0 +1,33 @@ +"""Offline source tests: preflight must reject missing pins, not launch a VM.""" +import unittest +from scripts.check_collage_catalog_preflight import evaluate, version_inventory + +class CatalogPreflightTests(unittest.TestCase): + def test_available_exact_pin(self): + rows=[{"name":"1.15.29","type":"dir"},{"name":"item.yaml","type":"file"}] + v=evaluate(rows,"1.15.29") + self.assertEqual(v["classification"],"PRE_KVM_CATALOG_VERSION_AVAILABLE") + self.assertFalse(v["guest_catalog_observed"]) + self.assertFalse(v["world_mutated"]) + + def test_upstream_retires_exact_pin(self): + rows=[{"name":"1.15.30","type":"dir"},{"name":"item.yaml","type":"file"}] + v=evaluate(rows,"1.15.29") + self.assertEqual(v["classification"],"PRE_KVM_CATALOG_DRIFT") + self.assertEqual(v["visible_versions"],["1.15.30"]) + self.assertFalse(v["live_minecraft_qualified"]) + + def test_malformed_catalog_fails_closed(self): + for value in ({}, None, [], [{"name":"1.15.29"}], + [{"name":"1.15.29","type":"dir"}]*2): + with self.subTest(value=value),self.assertRaises(ValueError): + version_inventory(value) + + def test_no_float_latest_pin(self): + rows=[{"name":"1.15.30","type":"dir"}] + for version in ("latest","1.15","1.15.29\n",None): + with self.subTest(version=version),self.assertRaises(ValueError): + evaluate(rows,version) + +if __name__=="__main__": + unittest.main() From ddbaab33aec4450ab8df9213518e63985092e1fd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:25:37 +0200 Subject: [PATCH 657/719] fix: execute rotating-catalog preflight in TrueNAS job, never Proxmox (#403) * fix(rdte): place catalog version drift preflight inside actual TrueNAS KVM job * test(rdte): assert catalog gate belongs to TrueNAS, not adjacent Proxmox job --- .github/workflows/gha-kvm-system-rdte.yml | 14 +++++++------- tests/test_collage_catalog_preflight.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index f861a2d9e..afd0b3d3d 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -382,13 +382,6 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false - - name: Refuse floating upstream Minecraft catalog before heavyweight KVM - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' - run: | - set -euo pipefail - python3 -m unittest -v tests.test_collage_catalog_preflight - python3 scripts/check_collage_catalog_preflight.py --expected 1.15.29 - - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -578,6 +571,13 @@ jobs: with: name: garm-provider-truenas-container-pre-b4-fixture path: ${{ runner.temp }}/gha-kvm-system-rdte/container-pre-b4-fixture + - name: Refuse floating upstream Minecraft catalog before heavyweight KVM + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + run: | + set -euo pipefail + python3 -m unittest -v tests.test_collage_catalog_preflight + python3 scripts/check_collage_catalog_preflight.py --expected 1.15.29 + - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail diff --git a/tests/test_collage_catalog_preflight.py b/tests/test_collage_catalog_preflight.py index aca0d066a..20fbf081d 100644 --- a/tests/test_collage_catalog_preflight.py +++ b/tests/test_collage_catalog_preflight.py @@ -1,5 +1,6 @@ """Offline source tests: preflight must reject missing pins, not launch a VM.""" import unittest +import pathlib from scripts.check_collage_catalog_preflight import evaluate, version_inventory class CatalogPreflightTests(unittest.TestCase): @@ -23,6 +24,17 @@ def test_malformed_catalog_fails_closed(self): with self.subTest(value=value),self.assertRaises(ValueError): version_inventory(value) + def test_causal_preflight_is_in_truenas_job_not_proxmox(self): + root=pathlib.Path(__file__).resolve().parents[1] + yml=(root/".github/workflows/gha-kvm-system-rdte.yml").read_text() + prox=yml.split(" proxmox-9-2:",1)[1].split(" truenas-26-beta3:",1)[0] + guest=yml.split(" truenas-26-beta3:",1)[1] + guard="python3 scripts/check_collage_catalog_preflight.py --expected 1.15.29" + self.assertNotIn(guard,prox) + self.assertIn(guard,guest) + self.assertLess(guest.index(guard), + guest.index(" - name: Ensure bounded QEMU/KVM prerequisites")) + def test_no_float_latest_pin(self): rows=[{"name":"1.15.30","type":"dir"}] for version in ("latest","1.15","1.15.29\n",None): From 476a2e5684df1ea3596564afb193b5aa097bf4f6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:38:21 +0200 Subject: [PATCH 658/719] RDTE: isolate COLLAGE P2 native Minecraft catalog 1.15.30 and immutable Foundry producer (#404) * feat(rdte): explicit P2-only 1.15.30 native App source/image profile with legacy default preserved * feat(rdte): select new native catalog version for isolated COLLAGE observation * feat(rdte): source-route COLLAGE only to separate pinned Minecraft 1.15.30 Foundry producer * test(rdte): refuse COLLAGE guest PASS unless new catalog/image/Foudry tuple independently observed * test(rdte): strengthen COLLLAGE P2 v1.15.30 receipt and refuse inherited 1.15.29 PASS * test(rdte): guard new version in exact TrueNAS product job * test(rdte): preserve legacy Paper 1.15.29 and qualify opt-in new digest/source seam * test(rdte): assert exact new producer admission and cheap 1.15.30 preflight * ci(rdte): qualify separate pinned 1.15.30 profile and historical isolation before guest * test(rdte): import exact native guest probe through existing scripts module path --- .github/workflows/gha-kvm-system-rdte.yml | 31 +++++++++--- .../workflows/qualify-collage-p2-source.yml | 3 ++ scripts/assert_collage_p2_receipt.py | 11 +++++ scripts/gha_kvm_truenas_rdte.sh | 1 + .../truenas_middleware_minecraft_t6_probe.py | 40 ++++++++++++---- tests/test_collage_catalog_preflight.py | 2 +- tests/test_collage_p2_native.py | 29 +++++++++++ tests/test_collage_p2_new_native_version.py | 48 +++++++++++++++++++ 8 files changed, 148 insertions(+), 17 deletions(-) create mode 100644 tests/test_collage_p2_new_native_version.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index afd0b3d3d..190ca4245 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -277,7 +277,7 @@ jobs: foundry-minecraft-paper-t6-control: needs: [changes, contract] - if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-paper-t6-control.yml@2cc7e1fe3772f8c532e933b3d7832e6cf985db4e with: foundry_ref: 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e @@ -285,6 +285,16 @@ jobs: permissions: contents: read + foundry-collage-paper-11530-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-paper-11530-t6-control.yml@403321031b6d4e66aa64a8299fd7621eee290b0d + with: + foundry_ref: 403321031b6d4e66aa64a8299fd7621eee290b0d + target_version: "25.04.1" + permissions: + contents: read + provider-g2-fixtures: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' @@ -429,7 +439,7 @@ jobs: group: agent-dispatch-heavyweight-rdte queue: max cancel-in-progress: false - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, foundry-minecraft-paper-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix, provider-container-pre-b4-fixture] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, foundry-minecraft-paper-t6-control, foundry-collage-paper-11530-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix, provider-container-pre-b4-fixture] if: >- always() && needs.contract.result == 'success' && @@ -443,7 +453,8 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'garm-controller-f0f5' && needs['foundry-garm-f0-f5-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'official-catalog' && needs['foundry-official-catalog-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'foliorelay' && needs['foundry-foliorelay-t6-control'].result == 'success') || - ((needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') && needs['foundry-minecraft-paper-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' && needs['foundry-minecraft-paper-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'collage-native-paper' && needs['foundry-collage-paper-11530-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') || @@ -493,11 +504,17 @@ jobs: name: foliorelay-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Download exact Minecraft Paper Foundry control - if: needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-paper' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: minecraft-paper-truenas-t6-control path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact COLLAGE 1.15.30 native Paper Foundry control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: minecraft-paper-11530-truenas-t6-candidate + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Checkout exact Minecraft product status oracle if: needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 @@ -576,7 +593,7 @@ jobs: run: | set -euo pipefail python3 -m unittest -v tests.test_collage_catalog_preflight - python3 scripts/check_collage_catalog_preflight.py --expected 1.15.29 + python3 scripts/check_collage_catalog_preflight.py --expected 1.15.30 - name: Ensure bounded QEMU/KVM prerequisites run: | @@ -639,8 +656,10 @@ jobs: foundry_commit="60d56cc1752bc825cb232d72b397ff17fe6d47c4" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "foliorelay" ]]; then foundry_commit="81deb97185760975fd8d3162df42056c77b3c0fd" - elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" || "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then foundry_commit="2cc7e1fe3772f8c532e933b3d7832e6cf985db4e" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then + foundry_commit="403321031b6d4e66aa64a8299fd7621eee290b0d" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi diff --git a/.github/workflows/qualify-collage-p2-source.yml b/.github/workflows/qualify-collage-p2-source.yml index ffa9d35b2..788e6f78e 100644 --- a/.github/workflows/qualify-collage-p2-source.yml +++ b/.github/workflows/qualify-collage-p2-source.yml @@ -10,6 +10,7 @@ on: - 'scripts/assert_collage_p2_receipt.py' - 'scripts/gha_kvm_truenas_rdte.sh' - 'tests/test_collage_p2_native.py' + - 'tests/test_collage_p2_new_native_version.py' - 'tests/test_collage_p2_roles.py' - 'tests/test_collage_p2_effective_roles.py' - 'tests/test_collage_catalog_preflight.py' @@ -39,12 +40,14 @@ jobs: scripts/truenas_middleware_minecraft_t6_probe.py \ scripts/assert_collage_p2_receipt.py \ tests/test_collage_p2_native.py \ + tests/test_collage_p2_new_native_version.py \ tests/test_collage_p2_roles.py \ tests/test_collage_p2_effective_roles.py \ tests/test_collage_catalog_preflight.py \ scripts/check_collage_catalog_preflight.py python3 -m unittest -v \ tests.test_collage_p2_native \ + tests.test_collage_p2_new_native_version \ tests.test_collage_p2_roles \ tests.test_collage_p2_effective_roles \ tests.test_collage_catalog_preflight \ diff --git a/scripts/assert_collage_p2_receipt.py b/scripts/assert_collage_p2_receipt.py index ad8940be2..54bf1040b 100644 --- a/scripts/assert_collage_p2_receipt.py +++ b/scripts/assert_collage_p2_receipt.py @@ -36,6 +36,17 @@ def need(cond, reason): and p.get("world_retained_on_delete") is True and p.get("world_rebound_after_reinstall") is True, "native Paper lifecycle did not qualify") + catalog = p.get("catalog_discovery") or {} + need(p.get("foundry_ref") == "403321031b6d4e66aa64a8299fd7621eee290b0d" + and catalog.get("requested_version") == "1.15.30" + and catalog.get("upstream_apps_commit") == + "559b9fe85212c800bc6770f152a65211df137a44" + and p.get("image_digest_qualification") == "SOURCE_AND_GUEST_REF_MATCH", + "new immutable native catalog/source qualification missing") + qualified_image = "ghcr.io/itzg/minecraft-server:2026.10.0-java21@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644" + digest_only = "ghcr.io/itzg/minecraft-server@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644" + need(p.get("observed_image_reference") in {qualified_image, digest_only}, + "new native OCI image digest absent") stages = p.get("readiness") or {} for label in ("first", "restart", "redeploy", "reinstall"): stage = stages.get(label) or {} diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 8b5c87726..08ee2df24 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -1163,6 +1163,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-p --collage-source-dir "$REPO_ROOT/.deps/collage-source" --collage-https-port "$HTTPS_PORT" --collage-source-ref "fbda94f3fea0a216ddd9c35b38a09d325bed3640" + --native-catalog-profile "1.15.30" ) fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 9472af4c8..447f1ef56 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -24,6 +24,16 @@ EXPECTED_PROFILE = "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" EXPECTED_APPS_COMMIT = "bb96ef6fd8c2708765012a4a3ca3739370a4f92c" EXPECTED_IMAGE_FRAGMENT = "ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa" +CATALOG_PROFILES = { + "1.15.29": { + "apps_commit": EXPECTED_APPS_COMMIT, + "image": EXPECTED_IMAGE_FRAGMENT, + }, + "1.15.30": { + "apps_commit": "559b9fe85212c800bc6770f152a65211df137a44", + "image": "ghcr.io/itzg/minecraft-server:2026.10.0-java21@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644", + }, +} EXPECTED_MIDDLEWARE = "74ab5a2d373be4097dece257d00e1086376333ba" APP = "rdte-t6-minecraft-paper" DATASET = "rdtepool/minecraft-paper-t6-world" @@ -45,16 +55,16 @@ class MinecraftT6Error(RuntimeError): pass -def qualified_native_image(app: dict) -> str | None: +def qualified_native_image(app: dict, image_fragment: str = EXPECTED_IMAGE_FRAGMENT) -> str | None: """Accept only the digest-pinned OCI reference, with optional tag normalization.""" work = app.get("active_workloads") or {} candidates = list(work.get("images") or []) for item in work.get("container_details") or []: if isinstance(item, dict) and isinstance(item.get("image"), str): candidates.append(item["image"]) - repository_tag, digest = EXPECTED_IMAGE_FRAGMENT.split("@sha256:", 1) + repository_tag, digest = image_fragment.split("@sha256:", 1) repository = repository_tag.rsplit(":", 1)[0] - valid = {EXPECTED_IMAGE_FRAGMENT, repository + "@sha256:" + digest} + valid = {image_fragment, repository + "@sha256:" + digest} return next((v for v in candidates if isinstance(v, str) and v in valid), None) @@ -85,7 +95,10 @@ def canonical_sha256(doc: dict) -> str: return hashlib.sha256(json.dumps(doc, sort_keys=True, separators=(",", ":")).encode()).hexdigest() -def load_control(path: pathlib.Path, expected_commit: str) -> dict: +def load_control(path: pathlib.Path, expected_commit: str, catalog_version: str = "1.15.29") -> dict: + if catalog_version not in CATALOG_PROFILES: + raise MinecraftT6Error("unadmitted native catalog version profile") + profile = CATALOG_PROFILES[catalog_version] control = json.loads((path / "control.json").read_text(encoding="utf-8")) if not isinstance(control, dict) or control.get("schema") != SCHEMA: raise MinecraftT6Error("Minecraft Foundry control schema drift") @@ -104,16 +117,16 @@ def load_control(path: pathlib.Path, expected_commit: str) -> dict: target.get("middleware_commit") != EXPECTED_MIDDLEWARE): raise MinecraftT6Error("exact TrueNAS target/profile drift") source = control.get("source") or {} - if (source.get("apps_commit") != EXPECTED_APPS_COMMIT or + if (source.get("apps_commit") != profile["apps_commit"] or source.get("app") != "community/minecraft" or - source.get("image_reference") != EXPECTED_IMAGE_FRAGMENT or + source.get("image_reference") != profile["image"] or source.get("library_version") != "2.3.4" or not re.fullmatch("[0-9a-f]{64}", str(source.get("source_compose_sha256", ""))) or not re.fullmatch("[0-9a-f]{64}", str(source.get("values_raw_sha256", "")))): raise MinecraftT6Error("pinned native Minecraft catalog source drift") runtime = control.get("runtime") or {} if (runtime.get("app_name") != APP or runtime.get("catalog_app") != "minecraft" or - runtime.get("train") != "community" or runtime.get("catalog_version_candidate") != "1.15.29" or + runtime.get("train") != "community" or runtime.get("catalog_version_candidate") != catalog_version or runtime.get("paper_version") != "1.21.11" or runtime.get("required_owned_world_dataset") != DATASET or runtime.get("status_oracle") != "minecraft-java-status-oracle/v1"): @@ -293,6 +306,8 @@ def main() -> int: p.add_argument("--collage-source-dir", type=pathlib.Path) p.add_argument("--collage-https-port", type=int) p.add_argument("--collage-source-ref") + p.add_argument("--native-catalog-profile", choices=sorted(CATALOG_PROFILES), + default="1.15.29") p.add_argument("--tls", action="store_true") p.add_argument("--timeout", type=float, default=8) p.add_argument("--job-timeout", type=float, default=600) @@ -310,7 +325,11 @@ def main() -> int: dataset_created = False app_created = False try: - control = load_control(a.control_dir, a.foundry_commit) + if (a.native_catalog_profile == "1.15.30" and + a.collage_source_dir is None): + raise MinecraftT6Error("1.15.30 catalog profile is COLLAGE P2 opt-in only") + control = load_control(a.control_dir, a.foundry_commit, + catalog_version=a.native_catalog_profile) if a.expected_system_version != "TrueNAS-" + EXPECTED_TARGET: raise MinecraftT6Error("requested exact system version drift") desired = runtime_values(control) @@ -357,7 +376,8 @@ def wait_app(): while time.monotonic() < deadline: app = getapp() if isinstance(app, dict) and app.get("state") == "RUNNING": - matching_image = qualified_native_image(app) + matching_image = qualified_native_image(app, + image_fragment=CATALOG_PROFILES[a.native_catalog_profile]["image"]) if matching_image is None: raise MinecraftT6Error("native Minecraft App runtime image differs from digest-pinned source") payload["observed_image_reference"] = matching_image @@ -490,7 +510,7 @@ def delete_app(label): "healthy": (catalog or {}).get("healthy"), "visible_versions": sorted(str(v) for v in versions)[:32] if isinstance(versions, dict) else [], "requested_version": expected_catalog, - "upstream_apps_commit": EXPECTED_APPS_COMMIT, + "upstream_apps_commit": CATALOG_PROFILES[a.native_catalog_profile]["apps_commit"], } if not isinstance(versions, dict) or expected_catalog not in versions: raise MinecraftT6Error("exact native Minecraft catalog version unavailable") diff --git a/tests/test_collage_catalog_preflight.py b/tests/test_collage_catalog_preflight.py index 20fbf081d..1065dd8a8 100644 --- a/tests/test_collage_catalog_preflight.py +++ b/tests/test_collage_catalog_preflight.py @@ -29,7 +29,7 @@ def test_causal_preflight_is_in_truenas_job_not_proxmox(self): yml=(root/".github/workflows/gha-kvm-system-rdte.yml").read_text() prox=yml.split(" proxmox-9-2:",1)[1].split(" truenas-26-beta3:",1)[0] guest=yml.split(" truenas-26-beta3:",1)[1] - guard="python3 scripts/check_collage_catalog_preflight.py --expected 1.15.29" + guard="python3 scripts/check_collage_catalog_preflight.py --expected 1.15.30" self.assertNotIn(guard,prox) self.assertIn(guard,guest) self.assertLess(guest.index(guard), diff --git a/tests/test_collage_p2_native.py b/tests/test_collage_p2_native.py index 208ca7d66..25eafe2a6 100644 --- a/tests/test_collage_p2_native.py +++ b/tests/test_collage_p2_native.py @@ -34,6 +34,13 @@ def test_paper_success_is_not_collage_readback_success(self): "schema": "truenas-minecraft-paper-t6/v1", "classification": "SUPPORTED", "oracleSatisfied": True, "catalog_version_verified": True, + "foundry_ref": "403321031b6d4e66aa64a8299fd7621eee290b0d", + "catalog_discovery": { + "requested_version": "1.15.30", + "upstream_apps_commit": "559b9fe85212c800bc6770f152a65211df137a44", + }, + "image_digest_qualification": "SOURCE_AND_GUEST_REF_MATCH", + "observed_image_reference": "ghcr.io/itzg/minecraft-server:2026.10.0-java21@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644", "world_retained_on_delete": True, "world_rebound_after_reinstall": True, "cleanup": {"zero_residue": True, "app_absent": True, @@ -71,6 +78,28 @@ def test_paper_success_is_not_collage_readback_success(self): with self.assertRaisesRegex(ValueError, "observation missing"): check(fake) + def test_old_catalog_and_image_cannot_fake_new_collage_p2(self): + native = { + "contract": "gha-kvm-system-lab/v1", + "target": {"version": "25.04.1", "rung": "T6", "product": "truenas"}, + "classification": "SUPPORTED", "oracleSatisfied": True, + "foundry_materialization": { + "schema": "truenas-minecraft-paper-t6/v1", + "classification": "SUPPORTED", "oracleSatisfied": True, + "catalog_version_verified": True, + "foundry_ref": "2cc7e1fe3772f8c532e933b3d7832e6cf985db4e", + "catalog_discovery": {"requested_version": "1.15.29", + "upstream_apps_commit": + "bb96ef6fd8c2708765012a4a3ca3739370a4f92c"}, + "image_digest_qualification": "SOURCE_AND_GUEST_REF_MATCH", + "observed_image_reference": "ghcr.io/itzg/minecraft-server:2026.9.2-java21@sha256:edf50d431ddc6f73929368e82f4334a405796874f7b0ae11f1edcbb267d670aa", + "world_retained_on_delete": True, + "world_rebound_after_reinstall": True, + }, + } + with self.assertRaisesRegex(ValueError, "new immutable native catalog"): + check(native) + def test_existing_paper_fixture_is_opt_in_unchanged(self): paper = (ROOT / "scripts/truenas_middleware_minecraft_t6_probe.py").read_text() shell = (ROOT / "scripts/gha_kvm_truenas_rdte.sh").read_text() diff --git a/tests/test_collage_p2_new_native_version.py b/tests/test_collage_p2_new_native_version.py new file mode 100644 index 000000000..f4cafbbb9 --- /dev/null +++ b/tests/test_collage_p2_new_native_version.py @@ -0,0 +1,48 @@ +"""Cheap negative and isolation tests for new opt-in COLLAGE catalog product.""" +import pathlib +import sys +import unittest + +ROOT=pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/"scripts")) +from scripts.truenas_middleware_minecraft_t6_probe import ( + CATALOG_PROFILES, EXPECTED_APPS_COMMIT, EXPECTED_IMAGE_FRAGMENT, + qualified_native_image, +) + +NEW_COMMIT="559b9fe85212c800bc6770f152a65211df137a44" +NEW_IMAGE=("ghcr.io/itzg/minecraft-server:2026.10.0-java21@sha256:" + "d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644") +OLD_COMMIT="bb96ef6fd8c2708765012a4a3ca3739370a4f92c" + +class CatalogProfilesTests(unittest.TestCase): + def test_existing_legacy_default_does_not_migrate(self): + self.assertEqual(EXPECTED_APPS_COMMIT, OLD_COMMIT) + self.assertEqual(CATALOG_PROFILES["1.15.29"]["apps_commit"], OLD_COMMIT) + self.assertEqual(CATALOG_PROFILES["1.15.29"]["image"], EXPECTED_IMAGE_FRAGMENT) + self.assertNotEqual(CATALOG_PROFILES["1.15.29"]["image"], NEW_IMAGE) + + def test_new_exact_image_match_and_old_image_refusal(self): + self.assertEqual(CATALOG_PROFILES["1.15.30"], + {"apps_commit": NEW_COMMIT, "image": NEW_IMAGE}) + old=EXPECTED_IMAGE_FRAGMENT + self.assertEqual(qualified_native_image({"active_workloads": { + "images": [NEW_IMAGE]}}, image_fragment=NEW_IMAGE), NEW_IMAGE) + self.assertIsNone(qualified_native_image({"active_workloads": { + "images": [old]}}, image_fragment=NEW_IMAGE)) + + def test_runner_binds_real_new_producer_separately(self): + workflow=(ROOT/".github/workflows/gha-kvm-system-rdte.yml").read_text() + shell=(ROOT/"scripts/gha_kvm_truenas_rdte.sh").read_text() + self.assertIn("foundry-collage-paper-11530-t6-control:", workflow) + self.assertIn("export-minecraft-paper-11530-t6-control.yml@", workflow) + self.assertIn("403321031b6d4e66aa64a8299fd7621eee290b0d", workflow) + self.assertIn("minecraft-paper-11530-truenas-t6-candidate", workflow) + self.assertIn("--native-catalog-profile", shell) + self.assertIn('"1.15.30"', shell) + self.assertIn("python3 scripts/check_collage_catalog_preflight.py --expected 1.15.30", workflow) + self.assertIn("needs['foundry-collage-paper-11530-t6-control'].result == 'success'", workflow) + + +if __name__=="__main__": + unittest.main() From 815a9f33c2bd4550852f4e56618a54501c7ab94d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:39:00 +0200 Subject: [PATCH 659/719] rdte(collage): launch one new 1.15.30 source-locked native TrueNAS observer P2-005 --- config/truenas-rdte-run-request.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 2c3b8413b..5b7be4794 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-collage-native-paper-p2-004-effective-role-chain-diagnostic", + "request_id": "tn-25-04-1-t6-collage-native-paper-p2-005-11530-source-locked-readback", "version": "25.04.1", "rung": "t6", "product": "collage-native-paper", "authority_issue": 411, - "consumer_authority_issue": 614, - "purpose": "Single new causal diagnostic experiment after P2-003 run 38266734446 guest failure APPS_READ_EFFECTIVE_ROLE_NOT_EXACT (artifact 11755604268, zero residue and platform healthy). It is not a replay of a passed experiment. Source PR #401 merged 0678a7375c9abefee3b622d19a542d500fbd2bf5; source/static CI passed and source only refined exact role evidence: newly created privilege role assignment, primary group role, user primary group, missing or extra effective roles, no plaintext user/group details, no keys in log. Keep exact 25.04.1 KVM + Paper 1.21.11 digest Foundry 2cc7e1fe3772f8c532e933b3d7832e6cf985db4e and COLLAGE fbda94f3fea0a216ddd9c35b38a09d325bed3640, six owner markers. Require exact APPS_READ only, no API key if group or user mismatch, no FULL_ADMIN workaround, no TLS bypass, no false native observation PASS. Distinguish guest setup mutations from zero-mutation observer. Public free-tier serialized KVM only, no private hosted GHA minutes, no local/physical TrueNAS. On failure retain causal receipt and stop; do not automatically retry unchanged." + "consumer_authority_issue": 625, + "purpose": "One genuinely new public free-tier nested disposable TrueNAS 25.04.1 COLLAGE P2 live test with new EXACT native App community/minecraft 1.15.30 source and OCI image; NOT a replay of failed P2-004. Old catalog 1.15.29 retired, P2-004 run 38270449044 refused before native App creation, evidence artifact 11757373390. Upstream source 559b9fe85212c800bc6770f152a65211df137a44, OCI 2026.10.0-java21@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644, exact Foundry source/control producer 403321031b6d4e66aa64a8299fd7621eee290b0d from qualified PRs #129-132. Agent Dispatch native consumer PR #404 merge 476a2e5684df1ea3596564afb193b5aa097bf4f6 passed dedicated COLLAGE source, Minecraft consumer, static. Preserve legacy Paper 1.15.29 accepted native T6 independently at old source/guest #38200675099. For this new product require real guest app 1.15.30, digest match, Java Paper 1.21.11 status protocol 774, independent external world persistence/rebind, App lifecycle/cleanup, six provider-neutral ownership fields, and THEN scoped APPS_READ guest-only TLS-verified read-only Go observer with effective group/user role checks. Never broaden permissions or emit credentials. Source/image preflight before KVM, isolate producer/consumer, one serialized heavyweight public runner, no private hosted GH minutes, no physical TrueNAS APPLY or Two Rivers/vanilla 26.3 acceptance transfer. On new failure preserve receipt/cause and do not replay unchanged." } From afac37390210023b5b450a22f236c9a9c70afcc6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:16:57 +0200 Subject: [PATCH 660/719] rdte: fail-closed vanilla26.3 native T6 pre-admission (#407) * feat(rdte): exact current-generation Minecraft vanilla26.3 source-control guest admission guard * config(rdte): reserve distinct non-executing vanilla26.3 guest admission request identity * test(rdte): refuse legacy Paper, provenance drift, wrong realm, guest claims and unsafe authority * ci(rdte): public-free exact current-gen native guest source admission without KVM --- .../qualify-vanilla263-native-admission.yml | 39 ++++ ...s-vanilla263-native-admission-request.json | 10 ++ .../qualify_vanilla263_native_admission.py | 169 ++++++++++++++++++ tests/test_vanilla263_native_admission.py | 119 ++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 .github/workflows/qualify-vanilla263-native-admission.yml create mode 100644 config/truenas-vanilla263-native-admission-request.json create mode 100644 scripts/qualify_vanilla263_native_admission.py create mode 100644 tests/test_vanilla263_native_admission.py diff --git a/.github/workflows/qualify-vanilla263-native-admission.yml b/.github/workflows/qualify-vanilla263-native-admission.yml new file mode 100644 index 000000000..c074696c8 --- /dev/null +++ b/.github/workflows/qualify-vanilla263-native-admission.yml @@ -0,0 +1,39 @@ +name: Source-only vanilla 26.3 current native TrueNAS guest admission guard +on: + pull_request: + branches: [experiment/gha-kvm-system-rdte] + paths: + - 'scripts/qualify_vanilla263_native_admission.py' + - 'config/truenas-vanilla263-native-admission-request.json' + - 'tests/test_vanilla263_native_admission.py' + - '.github/workflows/qualify-vanilla263-native-admission.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + fail-closed-source-admission: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 7 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Source and disjoint authority checks without guest or credentials + run: | + set -euo pipefail + python3 -m py_compile scripts/qualify_vanilla263_native_admission.py + python3 -m unittest discover -s tests -p 'test_vanilla263_native_admission.py' -v + python3 - <<'PY' + import json + from pathlib import Path + req=json.loads(Path("config/truenas-vanilla263-native-admission-request.json").read_text()) + old=json.loads(Path("config/truenas-rdte-run-request.json").read_text()) + assert req["request_id"]!=old["request_id"] + assert req["product"]=="minecraft-vanilla-263" and old["product"]!="minecraft-vanilla-263" + assert req["consumer_authority_issue"]==627 + assert req["version"]=="25.04.2.6" + PY + - name: Exact nonclaims + run: | + echo 'SOURCE-ONLY qualification of new TrueNAS25.04.2.6 Minecraft vanilla26.3 control admission, no GHA request edge, no KVM, no app creation, no online player or physical APPLY. Real Foundry producer control must be downloaded and validated separately before dispatch.' >> "$GITHUB_STEP_SUMMARY" diff --git a/config/truenas-vanilla263-native-admission-request.json b/config/truenas-vanilla263-native-admission-request.json new file mode 100644 index 000000000..9e3704c84 --- /dev/null +++ b/config/truenas-vanilla263-native-admission-request.json @@ -0,0 +1,10 @@ +{ + "schema": "gha-kvm-truenas-run-request/v1", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4b-001", + "version": "25.04.2.6", + "rung": "t6", + "product": "minecraft-vanilla-263", + "authority_issue": 423, + "consumer_authority_issue": 627, + "purpose": "Source-only eligibility for a new isolated TrueNAS 25.04.2.6 vanilla26.3 Java25 native T6 product, with distinct exact Foundry control, pinned catalog image and independently owned world. This document is NOT wired to request-edge GHA, not a live guest admission, and never physical TrueNAS APPLY." +} diff --git a/scripts/qualify_vanilla263_native_admission.py b/scripts/qualify_vanilla263_native_admission.py new file mode 100644 index 000000000..44c2f028b --- /dev/null +++ b/scripts/qualify_vanilla263_native_admission.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Fail-closed read-only admission edge for distinct vanilla 26.3 native guest. + +Only evaluates immutable Foundry control and exact public RDTE target registry. +It neither dispatches a workflow nor contacts or mutates TrueNAS. +""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import re + +SCHEMA="semper-supra.minecraft-vanilla-263-truenas-t6-control/1" +REQUEST="gha-kvm-truenas-run-request/v1" +VERSION="25.04.2.6" +IMAGE=("ghcr.io/itzg/minecraft-server:2026.10.0-java25@sha256:" + "5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da") +APPS="559b9fe85212c800bc6770f152a65211df137a44" +BLOB="631467044411c96a0fe6d361688b99a794f07c80" +MIDDLEWARE="244b717370fe35fb9eefc3096acfbc25f12b57b6" +ISSUE=627 +OWNER={"SUPRACRAFT_COLLAGE_MANAGED":"true", + "SUPRACRAFT_COLLAGE_SCHEMA":"1", + "SUPRACRAFT_COLLAGE_MANAGER_ID":"manager-rdte", + "SUPRACRAFT_COLLAGE_FLEET_ID":"rdte", + "SUPRACRAFT_COLLAGE_SERVICE_ID":"11111111-1111-4111-8111-111111111111", + "SUPRACRAFT_COLLAGE_WORLD_ID":"lantern-rdte"} + + +class Refused(RuntimeError): + pass + + +def require(ok:bool, code:str): + if not ok: raise Refused(code) + + +def sha(doc): + return hashlib.sha256(json.dumps(doc,sort_keys=True,separators=(",",":")).encode()).hexdigest() + + +def check(request:dict, control:dict, registry:dict)->dict: + require(type(request) is dict and type(control) is dict and type(registry) is dict, + "INVALID_DOCUMENT") + require(set(request)=={"schema","request_id","version","rung","product", + "authority_issue","consumer_authority_issue","purpose"}, + "REQUEST_SHAPE_DRIFT") + require(request.get("schema")==REQUEST and request.get("version")==VERSION + and request.get("rung")=="t6" and + request.get("product")=="minecraft-vanilla-263" and + request.get("authority_issue")==423 and + request.get("consumer_authority_issue")==ISSUE and + isinstance(request.get("purpose"),str) and len(request["purpose"])>=20, + "WRONG_PRODUCT_TARGET_OR_AUTHORITY") + ident=request.get("request_id") + require(type(ident) is str and re.fullmatch("[a-z0-9][a-z0-9-]{9,99}",ident) + and ident.startswith("tn-25-04-2-6-t6-vanilla-263-") + and not any(x in ident for x in ("p2-005","paper","replay")), + "REUSED_OR_AMBIGUOUS_REQUEST_ID") + require(control.get("schema")==SCHEMA and + type(control.get("foundry_ref")) is str and + re.fullmatch("[a-f0-9]{40}",control["foundry_ref"]), + "FOUNDRY_CONTROL_AUTHORITY_UNVERIFIED") + c=dict(control) + stated=c.pop("control_sha256",None) + require(type(stated) is str and re.fullmatch("[a-f0-9]{64}",stated) and + stated==sha(c),"FOUNDRY_CONTROL_CANONICAL_SHA_MISMATCH") + t=control.get("target") or {} + s=control.get("source") or {} + runtime=control.get("runtime") or {} + evidence=control.get("evidence") or {} + require(t.get("truenas_version")==VERSION and + t.get("expected_system_version")=="TrueNAS-"+VERSION and + t.get("middleware_commit")==MIDDLEWARE and + t.get("profile_git_blob_sha")==BLOB, + "TRUENAS_TARGET_PROFILE_DRIFT") + require(s.get("apps_commit")==APPS and + s.get("catalog_app")=="community/minecraft" and + s.get("catalog_version")=="1.15.30" and + s.get("image_reference")==IMAGE and + type(s.get("source_compose_sha256")) is str and + re.fullmatch("[a-f0-9]{64}",s["source_compose_sha256"]), + "UPSTREAM_CATALOG_IMAGE_OR_SOURCE_DRIFT") + require(runtime.get("minecraft_version")=="26.3" and + runtime.get("minecraft_protocol")==777 and + runtime.get("java_version")==25 and + runtime.get("image_selector")=="j25_image" and + runtime.get("catalog_version_candidate")=="1.15.30" and + runtime.get("app_name")=="rdte-t6-minecraft-vanilla-263" and + runtime.get("required_independent_world_dataset")== + "rdtepool/minecraft-vanilla-263-t6-world", + "NATIVE_RUNTIME_OR_WORLD_IDENTITY_DRIFT") + v=runtime.get("values_template") or {} + game=v.get("minecraft") or {} + extra=game.get("additional_envs") + require(type(extra) is list and len(extra)==len(OWNER) and + all(type(x) is dict and set(x)=={"name","value"} for x in extra) and + {x["name"]:x["value"] for x in extra}==OWNER, + "OWNER_SERVICE_OR_REALM_DRIFT") + require(game.get("version")=="26.3" and game.get("type")=="VANILLA" and + game.get("image_selector")=="j25_image" and + game.get("online_mode") is True and game.get("enable_rcon") is False, + "NATIVE_APP_VALUES_DRIFT") + mounts=v.get("storage",{}).get("additional_storage",[]) + require(type(mounts) is list and len(mounts)==1 and + mounts[0].get("mount_path")=="/data/world" and + mounts[0].get("type")=="host_path" and + mounts[0].get("read_only") is False and + mounts[0].get("host_path_config",{}).get("path")== + "/opt/tests/collage-vanilla-263-world", + "EXTERNAL_WORLD_DATASET_DRIFT") + require(v.get("network",{}).get("host_network") is False, + "UNSAFE_NETWORK_BOUNDARY") + require(evidence.get("source_render_pass") is True and + all(evidence.get(x) is False for x in ( + "live_catalog_version_verified","native_guest_created", + "gameplay_qualified_by_native_guest", + "external_world_retention_qualified_by_native_guest", + "collage_managed_deployment_qualified", + "physical_truenas_qualified")) and + control.get("operational_selection") is False and + control.get("physical_truenas_apply")=="HOLD", + "UNSUPPORTED_GUEST_OR_PHYSICAL_CLAIM") + target=next((x for x in registry.get("targets",[]) if type(x) is dict and + x.get("version")==VERSION),None) + require(type(target) is dict and + target.get("system_version")=="TrueNAS-"+VERSION and + target.get("middleware_commit")==MIDDLEWARE and + target.get("foundry_profile_blob")==BLOB and + target.get("authority_issue")==423 and + registry.get("policy",{}).get("heavyweight_slot")=="shared-one-at-a-time" and + registry.get("policy",{}).get("vendor_digest_sidecar_required") is True, + "PUBLIC_RUNNER_TARGET_REGISTRY_DRIFT") + return {"schema":"supracraft.minecraft-vanilla263-native-t6-admission/1", + "classification":"SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED", + "request_id":ident,"consumer_authority_issue":ISSUE, + "target_version":VERSION,"foundry_ref":control["foundry_ref"], + "control_sha256":stated,"oci_image":IMAGE, + "native_app_created":False,"guest_run_requested":False, + "provider_observed":False,"realm_admission_qualified":False, + "physical_truenas_apply":"HOLD", + "next_gate":"EXACT_NATIVE_GUEST_CONSUMER_SOURCE_AND_NEW_REQUEST_EDGE"} + + +def main(): + p=argparse.ArgumentParser() + for arg in ("request","control","registry","out"): + p.add_argument("--"+arg,required=True,type=Path) + a=p.parse_args() + try: + ret=check(json.loads(a.request.read_text()), + json.loads(a.control.read_text()), + json.loads(a.registry.read_text())) + code=0 + except (ValueError,OSError,Refused,TypeError) as exc: + ret={"schema":"supracraft.minecraft-vanilla263-native-t6-admission/1", + "classification":"REFUSED","reason":str(exc)[:150], + "guest_run_requested":False,"physical_truenas_apply":"HOLD"} + code=2 + a.out.parent.mkdir(parents=True,exist_ok=True) + a.out.write_text(json.dumps(ret,sort_keys=True,indent=2)+"\n") + print(json.dumps({"classification":ret["classification"], + "reason":ret.get("reason")},sort_keys=True)) + return code + + +if __name__=="__main__": + raise SystemExit(main()) diff --git a/tests/test_vanilla263_native_admission.py b/tests/test_vanilla263_native_admission.py new file mode 100644 index 000000000..ced494552 --- /dev/null +++ b/tests/test_vanilla263_native_admission.py @@ -0,0 +1,119 @@ +"""Fail-closed source admission tests; never start a guest or ask for APPLY.""" +import copy, json, pathlib, sys, unittest +ROOT=pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/"scripts")) +from qualify_vanilla263_native_admission import ( + BLOB, IMAGE, MIDDLEWARE, OWNER, check, sha, Refused +) + +def tuple_(): + request=json.loads((ROOT/"config/truenas-vanilla263-native-admission-request.json").read_text()) + registry=json.loads((ROOT/"config/truenas-rdte-targets.json").read_text()) + extra=[{"name":k,"value":v} for k,v in OWNER.items()] + control={ + "schema":"semper-supra.minecraft-vanilla-263-truenas-t6-control/1", + "foundry_ref":"a"*40, + "target":{"truenas_version":"25.04.2.6", + "expected_system_version":"TrueNAS-25.04.2.6", + "middleware_commit":MIDDLEWARE, + "profile_git_blob_sha":BLOB}, + "source":{"apps_commit":"559b9fe85212c800bc6770f152a65211df137a44", + "catalog_app":"community/minecraft", + "catalog_version":"1.15.30","image_reference":IMAGE, + "source_compose_sha256":"b"*64}, + "runtime":{"minecraft_version":"26.3","minecraft_protocol":777, + "java_version":25,"image_selector":"j25_image", + "catalog_version_candidate":"1.15.30", + "app_name":"rdte-t6-minecraft-vanilla-263", + "required_independent_world_dataset":"rdtepool/minecraft-vanilla-263-t6-world", + "values_template":{"minecraft":{ + "version":"26.3","type":"VANILLA","image_selector":"j25_image", + "online_mode":True,"enable_rcon":False, + "additional_envs":extra}, + "storage":{"additional_storage":[{ + "mount_path":"/data/world","type":"host_path","read_only":False, + "host_path_config":{"path":"/opt/tests/collage-vanilla-263-world"}}]}, + "network":{"host_network":False}}}, + "evidence":{"source_render_pass":True, + "live_catalog_version_verified":False, + "native_guest_created":False, + "gameplay_qualified_by_native_guest":False, + "external_world_retention_qualified_by_native_guest":False, + "collage_managed_deployment_qualified":False, + "physical_truenas_qualified":False}, + "operational_selection":False,"physical_truenas_apply":"HOLD", + } + control["control_sha256"]=sha(control) + return request,control,registry + + +def amend(t,i,path,value): + t=list(copy.deepcopy(t)) + target=t[i] + for part in path[:-1]: + target=target[part] + target[path[-1]]=value + if i==1: + t[1]["control_sha256"]=sha({k:v for k,v in t[1].items() if k!="control_sha256"}) + return tuple(t) + + +class GuestAdmissionTests(unittest.TestCase): + def test_accept_exact_synthetic_control_but_do_not_dispatch(self): + r=check(*tuple_()) + self.assertEqual(r["classification"],"SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED") + self.assertEqual(r["consumer_authority_issue"],627) + self.assertFalse(r["guest_run_requested"]) + self.assertFalse(r["native_app_created"]) + self.assertEqual(r["physical_truenas_apply"],"HOLD") + def test_reject_old_p2_request_and_wrong_target(self): + for key,value in (("product","collage-native-paper"), + ("version","25.04.1"), + ("consumer_authority_issue",625), + ("authority_issue",411), + ("request_id","tn-25-04-2-6-t6-vanilla-263-p2-005-replay")): + with self.subTest(key=key),self.assertRaises(Refused): + check(*amend(tuple_(),0,[key],value)) + def test_reject_control_digest_tampering(self): + t=list(tuple_());t[1]["control_sha256"]="0"*64 + with self.assertRaisesRegex(Refused,"CANONICAL"): + check(*t) + def test_reject_backend_version_image_target_identity(self): + for path,value in ( + (["runtime","minecraft_version"],"1.21.11"), + (["runtime","minecraft_protocol"],774), + (["runtime","java_version"],21), + (["source","image_reference"],"wrong-image"), + (["target","profile_git_blob_sha"],"c"*40), + (["runtime","values_template","minecraft","version"],26.3), + (["runtime","values_template","minecraft","type"],"PAPER"), + (["runtime","values_template","network","host_network"],True), + ): + with self.subTest(field=path),self.assertRaises(Refused): + check(*amend(tuple_(),1,path,value)) + def test_reject_forged_guest_qualification_and_physical_apply(self): + for path,value in ( + (["evidence","native_guest_created"],True), + (["evidence","physical_truenas_qualified"],True), + (["physical_truenas_apply"],"APPLY"), + (["operational_selection"],True), + ): + with self.subTest(field=path),self.assertRaises(Refused): + check(*amend(tuple_(),1,path,value)) + def test_reject_wrong_world_or_owner_identity(self): + for path,value in ( + (["runtime","values_template","storage","additional_storage"],[]), + (["runtime","required_independent_world_dataset"],"foreign-world"), + (["runtime","values_template","minecraft","additional_envs"],[]), + ): + with self.subTest(field=path),self.assertRaises(Refused): + check(*amend(tuple_(),1,path,value)) + def test_reject_wrong_or_missing_target_registry(self): + t=list(tuple_()) + t[2]["targets"]=[] + with self.assertRaisesRegex(Refused,"REGISTRY"): + check(*t) + + +if __name__=="__main__": + unittest.main() From ab6f7bf10756fa317b5d21b6c7db9de62dd472c4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:24:25 +0200 Subject: [PATCH 661/719] ci(rdte): verify merged Foundry vanilla26.3 control in independent guest admission (#408) --- ...ify-vanilla263-native-control-consumer.yml | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .github/workflows/qualify-vanilla263-native-control-consumer.yml diff --git a/.github/workflows/qualify-vanilla263-native-control-consumer.yml b/.github/workflows/qualify-vanilla263-native-control-consumer.yml new file mode 100644 index 000000000..b4ac8dcca --- /dev/null +++ b/.github/workflows/qualify-vanilla263-native-control-consumer.yml @@ -0,0 +1,98 @@ +name: Vanilla 26.3 native guest control producer-consumer admission (public read-only) +on: + pull_request: + branches: [experiment/gha-kvm-system-rdte] + paths: + - '.github/workflows/qualify-vanilla263-native-control-consumer.yml' + - 'scripts/qualify_vanilla263_native_admission.py' + - 'config/truenas-vanilla263-native-admission-request.json' + - 'tests/test_vanilla263_native_admission.py' + workflow_dispatch: +permissions: + contents: read +concurrency: + group: vanilla263-native-control-consumer-${{ github.ref }} + cancel-in-progress: false +jobs: + exact-source-guest-admission: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout exact Agent Dispatch consumer + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Checkout accepted exact merged Foundry producer, never floating main + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SemperSupra/truenas-app-foundry + ref: 29ec6cba7df51e316b0329926aa85807805dd07b + path: .deps/vanilla263-foundry + persist-credentials: false + - name: Source admission and exact producer-head negative gates + run: | + set -euo pipefail + test "$(git -C .deps/vanilla263-foundry rev-parse HEAD)" = 29ec6cba7df51e316b0329926aa85807805dd07b + python3 -m unittest discover -s tests -p 'test_vanilla263_native_admission.py' -v + python3 -m unittest discover -s .deps/vanilla263-foundry/tests \ + -p 'test_export_minecraft_vanilla_263_t6_control.py' -v + python3 -m unittest discover -s .deps/vanilla263-foundry/tests \ + -p 'test_vanilla_263_native_source.py' -v + docker compose version + - name: Real upstream native source render from exact merged commit + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/vanilla263" + python3 .deps/vanilla263-foundry/tools/validate_minecraft_world_control.py \ + --product vanilla-263 --catalog-profile 1.15.30 \ + --target-version 25.04.2.6 \ + > "$RUNNER_TEMP/vanilla263/merged-source.json" + - name: Produce new signed-by-digest merged-commit control + run: | + set -euo pipefail + python3 .deps/vanilla263-foundry/tools/export_minecraft_vanilla_263_t6_control.py \ + --foundry-ref 29ec6cba7df51e316b0329926aa85807805dd07b \ + --target-version 25.04.2.6 \ + --source-evidence "$RUNNER_TEMP/vanilla263/merged-source.json" \ + --out "$RUNNER_TEMP/vanilla263/control.json" + - name: Independently enforce Agent Dispatch exact tuple admission, no KVM + run: | + set -euo pipefail + python3 scripts/qualify_vanilla263_native_admission.py \ + --request config/truenas-vanilla263-native-admission-request.json \ + --control "$RUNNER_TEMP/vanilla263/control.json" \ + --registry config/truenas-rdte-targets.json \ + --out "$RUNNER_TEMP/vanilla263/admission.json" + python3 - "$RUNNER_TEMP/vanilla263/control.json" "$RUNNER_TEMP/vanilla263/admission.json" <<'PY' + import hashlib,json,sys + c=json.load(open(sys.argv[1])) + a=json.load(open(sys.argv[2])) + raw=dict(c) + claimed=raw.pop("control_sha256") + assert claimed == hashlib.sha256(json.dumps(raw,sort_keys=True,separators=(",",":")).encode()).hexdigest() + assert c["foundry_ref"] == "29ec6cba7df51e316b0329926aa85807805dd07b" + assert a["classification"] == "SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED" + assert a["control_sha256"] == claimed + assert a["foundry_ref"] == c["foundry_ref"] + assert a["request_id"] == "tn-25-04-2-6-t6-vanilla-263-op4b-001" + assert a["target_version"] == "25.04.2.6" + assert a["guest_run_requested"] is False + assert a["native_app_created"] is False + assert a["provider_observed"] is False + assert a["physical_truenas_apply"] == "HOLD" + PY + - name: Retain signed merged-source control and guest admission receipt only + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: vanilla263-native-current-source-consumer-admission + path: | + ${{ runner.temp }}/vanilla263/control.json + ${{ runner.temp }}/vanilla263/admission.json + if-no-files-found: warn + retention-days: 14 + - name: Exact limits + if: always() + run: | + echo 'Actual merged Foundry producer source rerendered; new SHA-bound control verified by independent Agent Dispatch guest admission from distinct request. This is source-only, no KVM, no native App, no live provider observation, no authenticated user, no physical TrueNAS APPLY.' >> "$GITHUB_STEP_SUMMARY" From 5f6e0c69139e05cebf1fdf944f55a1c5bd62a6da Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:12:52 +0200 Subject: [PATCH 662/719] fix(collage): admit documented #625 P2 source-locked authority successor in request contract --- tests/test_truenas_rdte_target.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index be6ed5498..598d1663f 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -82,7 +82,10 @@ def test_run_request_is_exact_registered_target(self): if request.get("product") == "collage-native-paper": self.assertEqual(request["rung"], "t6") self.assertEqual(request["version"], "25.04.1") - self.assertEqual(request["consumer_authority_issue"], 614) + # #614 owns the P2 read-only join; #625 is its independently + # source-locked Minecraft 1.15.30 qualification successor. + # This is a constrained allowlist, not arbitrary issue authority. + self.assertIn(request["consumer_authority_issue"], {614, 625}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": From 5e76748bd0e3930f1aef8e51f118d1123cda6251 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:16:04 +0200 Subject: [PATCH 663/719] run(collage): request unique P2-006 disposable guest after #625 contract fix --- config/truenas-rdte-run-request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 5b7be4794..947613dee 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-collage-native-paper-p2-005-11530-source-locked-readback", + "request_id": "tn-25-04-1-t6-collage-native-paper-p2-006-11530-authority-contract-correction", "version": "25.04.1", "rung": "t6", "product": "collage-native-paper", "authority_issue": 411, "consumer_authority_issue": 625, - "purpose": "One genuinely new public free-tier nested disposable TrueNAS 25.04.1 COLLAGE P2 live test with new EXACT native App community/minecraft 1.15.30 source and OCI image; NOT a replay of failed P2-004. Old catalog 1.15.29 retired, P2-004 run 38270449044 refused before native App creation, evidence artifact 11757373390. Upstream source 559b9fe85212c800bc6770f152a65211df137a44, OCI 2026.10.0-java21@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644, exact Foundry source/control producer 403321031b6d4e66aa64a8299fd7621eee290b0d from qualified PRs #129-132. Agent Dispatch native consumer PR #404 merge 476a2e5684df1ea3596564afb193b5aa097bf4f6 passed dedicated COLLAGE source, Minecraft consumer, static. Preserve legacy Paper 1.15.29 accepted native T6 independently at old source/guest #38200675099. For this new product require real guest app 1.15.30, digest match, Java Paper 1.21.11 status protocol 774, independent external world persistence/rebind, App lifecycle/cleanup, six provider-neutral ownership fields, and THEN scoped APPS_READ guest-only TLS-verified read-only Go observer with effective group/user role checks. Never broaden permissions or emit credentials. Source/image preflight before KVM, isolate producer/consumer, one serialized heavyweight public runner, no private hosted GH minutes, no physical TrueNAS APPLY or Two Rivers/vanilla 26.3 acceptance transfer. On new failure preserve receipt/cause and do not replay unchanged." + "purpose": "Single NEW bounded public/free disposable TrueNAS 25.04.1 COLLAGE native Minecraft 1.15.30 P2 read-only observer test after genuine static contract failure run #38351220220. That attempted request failed pre-guest: test_truenas_rdte_target.py hard-coded original consumer authority 614 while current source-locked 1.15.30 successor authority agent-dispatch-private#625 was independently documented. New narrow static PR #414 corrected only assertion to permit explicit {614,625}, was tested in public GHA static #38436881076 SUCCESS, and merged experimental branch at exact c55c32c2bc43b2a9a54aa8793bcc3702f8357c22. Never infer runtime PASS from contract PASS. Preserve full prior request source locks: exact native community/minecraft 1.15.30 upstream truenas/apps@559b9fe85212c800bc6770f152a65211df137a44, OCI 2026.10.0-java21@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644, Foundry producer@403321031b6d4e66aa64a8299fd7621eee290b0d, exact Paper Java 1.21.11 protocol 774 and external world sentinel, native lifecycle/reinstall and zero residue, then scoped APPS_READ TLS-verified provider-neutral owner readback. No auth broadening, no secret leakage, no physical TrueNAS, no private Actions minutes, no Paper-to-vanilla 26.3/Two Rivers inheritance. One shared heavyweight GHA slot; on failure save exact root error and DLE baton, never blind-retry." } From 1d0fff8fa965df644109d66fecdcbfc23076f639 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:59:00 +0200 Subject: [PATCH 664/719] Admit immutable vanilla 26.3 native TrueNAS source control without guest promotion (#417) * OP-4B: exact signed vanilla263 Foundry native control consumer, no guest authority * OP-4B: adversarial exact Foundry vanilla263 source consumer and explicit guest HOLD * OP-4B: cheap independent exact merged Foundry vanilla263 producer-consumer source qualification * OP-4B: test forged control with actual production verifier before fixture assertions --- .../vanilla263-t6-source-admission.yml | 79 ++++++++ scripts/admit_vanilla263_native_t6_control.py | 188 ++++++++++++++++++ tests/test_vanilla263_t6_source_admission.py | 150 ++++++++++++++ 3 files changed, 417 insertions(+) create mode 100644 .github/workflows/vanilla263-t6-source-admission.yml create mode 100644 scripts/admit_vanilla263_native_t6_control.py create mode 100644 tests/test_vanilla263_t6_source_admission.py diff --git a/.github/workflows/vanilla263-t6-source-admission.yml b/.github/workflows/vanilla263-t6-source-admission.yml new file mode 100644 index 000000000..3b368ea5b --- /dev/null +++ b/.github/workflows/vanilla263-t6-source-admission.yml @@ -0,0 +1,79 @@ +name: Vanilla 26.3 TrueNAS 25.04.2.6 signed-source guest-admission gate +on: + pull_request: + paths: + - 'scripts/admit_vanilla263_native_t6_control.py' + - 'tests/test_vanilla263_t6_source_admission.py' + - '.github/workflows/vanilla263-t6-source-admission.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + exact-source: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - name: Checkout exact Agent Dispatch candidate without mutating request edge + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Checkout immutable accepted Foundry producer + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SemperSupra/truenas-app-foundry + ref: 29ec6cba7df51e316b0329926aa85807805dd07b + path: .deps/vanilla-foundry + persist-credentials: false + - name: Validate source and test fixtures before render + run: | + set -euo pipefail + test "$(git -C .deps/vanilla-foundry rev-parse HEAD)" = 29ec6cba7df51e316b0329926aa85807805dd07b + python3 -m pip install --disable-pip-version-check 'PyYAML==6.0.2' + python3 -m py_compile scripts/admit_vanilla263_native_t6_control.py tests/test_vanilla263_t6_source_admission.py + - name: Independently source render exact native community catalog + run: | + set -euo pipefail + python3 .deps/vanilla-foundry/tools/validate_minecraft_world_control.py \ + --product vanilla-263 --catalog-profile 1.15.30 \ + --target-version 25.04.2.6 > "$RUNNER_TEMP/vanilla-source.json" + - name: Produce signed source control at accepted merged Foundry ref + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/vanilla-control" + python3 .deps/vanilla-foundry/tools/export_minecraft_vanilla_263_t6_control.py \ + --foundry-ref 29ec6cba7df51e316b0329926aa85807805dd07b \ + --target-version 25.04.2.6 \ + --source-evidence "$RUNNER_TEMP/vanilla-source.json" \ + --out "$RUNNER_TEMP/vanilla-control/control.json" + echo "VANILLA263_SOURCE_CONTROL=$RUNNER_TEMP/vanilla-control/control.json" >> "$GITHUB_ENV" + - name: Independent Agent Dispatch consumer and exhaustive cheap red-blue + run: | + set -euo pipefail + python3 -m unittest -v tests/test_vanilla263_t6_source_admission.py + python3 scripts/admit_vanilla263_native_t6_control.py \ + --control "$VANILLA263_SOURCE_CONTROL" \ + --out "$RUNNER_TEMP/vanilla-control/consumer-receipt.json" + python3 - "$RUNNER_TEMP/vanilla-control/consumer-receipt.json" <<'PY' + import json,sys + r=json.load(open(sys.argv[1])) + assert r["schema"]=="semper-supra.minecraft-vanilla263-t6-consumer-admission/1" + assert r["classification"]=="SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED" + assert r["control_sha256"]=="c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615" + assert r["minecraft_version"]=="26.3" and r["minecraft_protocol"]==777 + assert r["java_version"]==25 and r["truenas_version"]=="25.04.2.6" + assert r["native_guest_created"] is False and r["native_guest_qualified"] is False + assert r["physical_truenas_apply"]=="HOLD" + PY + - name: Publish bounded safe control admission receipt only + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: vanilla263-native-control-consumer-receipt + path: ${{ runner.temp }}/vanilla-control/consumer-receipt.json + if-no-files-found: warn + retention-days: 14 + - name: Honest guest gate + if: always() + run: | + echo 'This job never dispatches a KVM guest, creates a TrueNAS App, tests Java in a guest or authorizes physical TrueNAS APPLY. Selector and guest observer require separate implementation before a new request edge.' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/admit_vanilla263_native_t6_control.py b/scripts/admit_vanilla263_native_t6_control.py new file mode 100644 index 000000000..84f396103 --- /dev/null +++ b/scripts/admit_vanilla263_native_t6_control.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""OP-4B independent immutable Foundry control admission for vanilla 26.3. + +This deliberately does NOT create a TrueNAS App or generalize old Paper T6. +Native guest selector must remain unavailable until an independent guest probe +and exact guest receipt gate exist. No physical TrueNAS APPLY is authorized. +""" +from __future__ import annotations +import argparse +import hashlib +import json +from pathlib import Path +import re +import sys + +SCHEMA = "semper-supra.minecraft-vanilla-263-truenas-t6-control/1" +PRODUCER = "29ec6cba7df51e316b0329926aa85807805dd07b" +CONTROL_SHA = "c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615" +APPS_REF = "559b9fe85212c800bc6770f152a65211df137a44" +OCI = ("ghcr.io/itzg/minecraft-server:2026.10.0-java25@sha256:" + "5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da") +TARGET = "25.04.2.6" +MIDDLEWARE = "244b717370fe35fb9eefc3096acfbc25f12b57b6" +PROFILE_BLOB = "631467044411c96a0fe6d361688b99a794f07c80" +APP = "rdte-t6-minecraft-vanilla-263" +WORLD = "rdtepool/minecraft-vanilla-263-t6-world" +OWNER = { + "SUPRACRAFT_COLLAGE_MANAGED": "true", + "SUPRACRAFT_COLLAGE_SCHEMA": "1", + "SUPRACRAFT_COLLAGE_MANAGER_ID": "manager-rdte", + "SUPRACRAFT_COLLAGE_FLEET_ID": "rdte", + "SUPRACRAFT_COLLAGE_SERVICE_ID": "11111111-1111-4111-8111-111111111111", + "SUPRACRAFT_COLLAGE_WORLD_ID": "lantern-rdte", +} +RECEIPT = "semper-supra.minecraft-vanilla263-t6-consumer-admission/1" + + +class Refused(ValueError): + pass + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise Refused(reason) + + +def canonical_hash(doc: dict) -> str: + return hashlib.sha256(json.dumps(doc, sort_keys=True, + separators=(",", ":")).encode()).hexdigest() + + +def verify(control: dict, *, expected_producer: str = PRODUCER, + expected_digest: str = CONTROL_SHA) -> dict: + """Verify entire canonical signer/digest and critical materialization invariants.""" + require(type(control) is dict and control.get("schema") == SCHEMA, + "VANILLA_CONTROL_SCHEMA_DRIFT") + signed = dict(control) + signature = signed.pop("control_sha256", None) + require(re.fullmatch(r"[0-9a-f]{40}", expected_producer) is not None, + "FOUNDRY_IMMUTABLE_REF_REQUIRED") + require(re.fullmatch(r"[0-9a-f]{64}", expected_digest) is not None, + "CANONICAL_DIGEST_REQUIRED") + require(control.get("foundry_ref") == expected_producer == PRODUCER, + "FOUNDRY_PRODUCER_REF_DRIFT") + require(signature == canonical_hash(signed) == expected_digest == CONTROL_SHA, + "CANONICAL_CONTROL_SHA_DRIFT") + target = control.get("target") or {} + require(type(target) is dict and + target.get("truenas_version") == TARGET and + target.get("expected_system_version") == "TrueNAS-" + TARGET and + target.get("middleware_commit") == MIDDLEWARE and + target.get("profile_git_blob_sha") == PROFILE_BLOB, + "NATIVE_GUEST_TARGET_DRIFT") + source = control.get("source") or {} + require(type(source) is dict and + source.get("apps_commit") == APPS_REF and + source.get("catalog_app") == "community/minecraft" and + source.get("catalog_version") == "1.15.30" and + source.get("library_version") == "2.3.4" and + source.get("image_reference") == OCI and + isinstance(source.get("source_compose_sha256"), str) and + re.fullmatch("[0-9a-f]{64}", source["source_compose_sha256"]) and + isinstance(source.get("values_raw_sha256"), str) and + re.fullmatch("[0-9a-f]{64}", source["values_raw_sha256"]), + "NATIVE_CATALOG_SOURCE_DRIFT") + runtime = control.get("runtime") or {} + require(type(runtime) is dict and + runtime.get("app_name") == APP and + runtime.get("catalog_app") == "minecraft" and + runtime.get("train") == "community" and + runtime.get("catalog_version_candidate") == "1.15.30" and + runtime.get("minecraft_version") == "26.3" and + runtime.get("minecraft_protocol") == 777 and + runtime.get("java_version") == 25 and + runtime.get("image_selector") == "j25_image" and + runtime.get("required_independent_world_dataset") == WORLD and + runtime.get("status_oracle") == "minecraft-java-status-oracle/v1", + "NATIVE_VANILLA_PRODUCT_DRIFT") + values = runtime.get("values_template") or {} + require(type(values) is dict, "NATIVE_VALUES_SCHEMA_DRIFT") + minecraft = values.get("minecraft") or {} + require(type(minecraft) is dict and + minecraft.get("type") == "VANILLA" and + minecraft.get("version") == "26.3" and + minecraft.get("image_selector") == "j25_image" and + minecraft.get("eula") is True and + minecraft.get("online_mode") is True and + minecraft.get("enable_rcon") is False and + minecraft.get("rcon_password") == "", + "NATIVE_VANILLA_RUNTIME_INPUT_DRIFT") + envs = minecraft.get("additional_envs") + require(type(envs) is list and len(envs) == len(OWNER) and + all(type(v) is dict and set(v) == {"name", "value"} for v in envs) and + {v["name"]: v["value"] for v in envs} == OWNER, + "NATIVE_OWNERSHIP_DRIFT") + storage = values.get("storage") or {} + mounts = storage.get("additional_storage") + require(type(storage) is dict and + (storage.get("data") or {}).get("type") == "ix_volume" and + type(mounts) is list and len(mounts) == 1 and + type(mounts[0]) is dict and + mounts[0].get("mount_path") == "/data/world" and + mounts[0].get("type") == "host_path" and + mounts[0].get("read_only") is False and + (mounts[0].get("host_path_config") or {}).get("path") == + "/opt/tests/collage-vanilla-263-world", + "NATIVE_WORLD_OWNER_BOUNDARY_DRIFT") + network = values.get("network") or {} + require(type(network) is dict and network.get("host_network") is False and + (network.get("server_port") or {}).get("port_number") == 25565 and + (network.get("server_port") or {}).get("bind_mode") == "published", + "NATIVE_PORT_POLICY_DRIFT") + evidence = control.get("evidence") or {} + require(type(evidence) is dict and evidence.get("source_render_pass") is True and + all(evidence.get(field) is False for field in ( + "live_catalog_version_verified", "native_guest_created", + "gameplay_qualified_by_native_guest", + "external_world_retention_qualified_by_native_guest", + "collage_managed_deployment_qualified", + "physical_truenas_qualified")) and + control.get("operational_selection") is False and + control.get("physical_truenas_apply") == "HOLD", + "SOURCE_ONLY_GUEST_OR_PHYSICAL_PROMOTION_DRIFT") + return { + "schema": RECEIPT, + "classification": "SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED", + "product": "minecraft-vanilla-263", + "foundry_ref": expected_producer, + "control_sha256": expected_digest, + "truenas_version": TARGET, + "catalog_version": "1.15.30", + "minecraft_version": "26.3", + "minecraft_protocol": 777, + "java_version": 25, + "image_sha256": OCI.split("@sha256:", 1)[1], + "world_id": OWNER["SUPRACRAFT_COLLAGE_WORLD_ID"], + "native_guest_created": False, + "native_guest_qualified": False, + "physical_truenas_apply": "HOLD", + } + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--control", type=Path, required=True) + p.add_argument("--out", type=Path, required=True) + a = p.parse_args() + try: + require(a.control.is_file() and a.control.stat().st_size <= 128 * 1024, + "CONTROL_MISSING_OR_OVERSIZED") + result = verify(json.loads(a.control.read_text(encoding="utf-8"))) + ok = True + except (OSError, ValueError, TypeError) as ex: + raw = str(ex) + reason = raw if re.fullmatch("[A-Z0-9_]{3,90}", raw) else "SOURCE_CONTROL_INVALID" + result = {"schema": RECEIPT, "classification": "REFUSED", + "failure_code": reason, "native_guest_qualified": False, + "physical_truenas_apply": "HOLD"} + ok = False + a.out.parent.mkdir(parents=True, exist_ok=True) + a.out.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n") + print(json.dumps({"classification": result["classification"], + "failure_code": result.get("failure_code")}, sort_keys=True)) + return 0 if ok else 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_vanilla263_t6_source_admission.py b/tests/test_vanilla263_t6_source_admission.py new file mode 100644 index 000000000..b363b8be4 --- /dev/null +++ b/tests/test_vanilla263_t6_source_admission.py @@ -0,0 +1,150 @@ +"""Native vanilla26.3 admission red/blue tests with exact Foundry producer fixtures.""" +import copy +import importlib.util +import json +import os +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT=Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/"scripts")) +import admit_vanilla263_native_t6_control as consumer + +FOUNDRY=ROOT/".deps"/"vanilla-foundry" +EXPORT=FOUNDRY/"tools"/"export_minecraft_vanilla_263_t6_control.py" + + +def producer(): + if not EXPORT.is_file(): + raise AssertionError("EXACT_FOUNDRY_PRODUCER_NOT_CHECKED_OUT") + spec=importlib.util.spec_from_file_location("foundry_vanilla263_exact", EXPORT) + module=importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def source_document(): + f=producer() + import yaml + pin=json.loads(f.PIN.read_text()) + values=yaml.safe_load(f.VALUES.read_text()) + profile=json.loads(f.PROFILE.read_text()) + evidence={ + "result":"PASS","truenas_target":"25.04.2.6", + "runtime_qualification":False, + "upstream":{"ref":f.SOURCE,"app":"community/minecraft"}, + "server":{"catalog_version":"1.15.30","type":"VANILLA", + "minecraft_version":"26.3","image_selector":"j25_image", + "image_ref":f.IMAGE,"ownership_metadata":f.IDENTITY, + "world_mount":{"source":"/opt/tests/collage-vanilla-263-world"}}, + "compose_sha256":"a"*64, + } + return f.produce(consumer.PRODUCER,evidence,pin,values,profile,f.BLOB) + + +def readmit_local_fixture(doc): + # Fixture canonical hash differs from real rendered upstream compose hash. + # Relax only test EXPECTED digest while keeping every semantic gate exact. + source=copy.deepcopy(doc) + digest=source.pop("control_sha256") + assert digest==consumer.canonical_hash(source) + with patch.object(consumer,"CONTROL_SHA",digest): + return consumer.verify(doc,expected_digest=digest) + + +class TestNativeVanilla263SourceAdmission(unittest.TestCase): + def test_signed_source_is_not_a_guest(self): + row=readmit_local_fixture(source_document()) + self.assertEqual(row["classification"], + "SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED") + self.assertEqual(row["minecraft_protocol"],777) + self.assertFalse(row["native_guest_qualified"]) + self.assertEqual(row["physical_truenas_apply"],"HOLD") + + def test_real_rendered_control_from_exact_producer(self): + loc=os.environ.get("VANILLA263_SOURCE_CONTROL") + if not loc: + self.fail("VANILLA263_SOURCE_CONTROL_MISSING") + c=json.loads(Path(loc).read_text()) + got=consumer.verify(c) + self.assertEqual(got["control_sha256"],consumer.CONTROL_SHA) + self.assertEqual(got["foundry_ref"],consumer.PRODUCER) + + def test_signed_digest_forgery_fails_closed(self): + c=source_document() + original_digest=c["control_sha256"] + c["source"]["image_reference"]="ghcr.io/example:latest" + # Exercise the production signature check, not the fixture helper's + # self-consistency assertion. Semantic checks come only after signing. + with patch.object(consumer,"CONTROL_SHA",original_digest): + with self.assertRaisesRegex(consumer.Refused,"CANONICAL_CONTROL_SHA_DRIFT"): + consumer.verify(c,expected_digest=original_digest) + with self.assertRaisesRegex(consumer.Refused,"CANONICAL_CONTROL_SHA_DRIFT"): + consumer.verify(c) + + def test_precise_fail_closed_target_runtime_owner_world(self): + mutations=[ + (("target","truenas_version"),"25.04.1","NATIVE_GUEST_TARGET_DRIFT"), + (("target","middleware_commit"),"f"*40,"NATIVE_GUEST_TARGET_DRIFT"), + (("source","apps_commit"),"f"*40,"NATIVE_CATALOG_SOURCE_DRIFT"), + (("source","catalog_version"),"1.15.29","NATIVE_CATALOG_SOURCE_DRIFT"), + (("source","image_reference"),"ghcr.io/example:latest","NATIVE_CATALOG_SOURCE_DRIFT"), + (("runtime","minecraft_version"),"1.21.11","NATIVE_VANILLA_PRODUCT_DRIFT"), + (("runtime","minecraft_protocol"),774,"NATIVE_VANILLA_PRODUCT_DRIFT"), + (("runtime","java_version"),21,"NATIVE_VANILLA_PRODUCT_DRIFT"), + (("runtime","values_template","minecraft","type"),"PAPER","NATIVE_VANILLA_RUNTIME_INPUT_DRIFT"), + (("runtime","values_template","minecraft","online_mode"),False,"NATIVE_VANILLA_RUNTIME_INPUT_DRIFT"), + (("runtime","values_template","minecraft","enable_rcon"),True,"NATIVE_VANILLA_RUNTIME_INPUT_DRIFT"), + (("runtime","values_template","storage","data","type"),"host_path","NATIVE_WORLD_OWNER_BOUNDARY_DRIFT"), + (("runtime","values_template","storage","additional_storage"),[],"NATIVE_WORLD_OWNER_BOUNDARY_DRIFT"), + (("runtime","values_template","network","host_network"),True,"NATIVE_PORT_POLICY_DRIFT"), + (("evidence","native_guest_created"),True,"SOURCE_ONLY_GUEST_OR_PHYSICAL_PROMOTION_DRIFT"), + (("physical_truenas_apply",),"READY","SOURCE_ONLY_GUEST_OR_PHYSICAL_PROMOTION_DRIFT"), + ] + for path,value,refusal in mutations: + with self.subTest(path=path): + doc=source_document() + node=doc + for key in path[:-1]: + node=node[key] + node[path[-1]]=value + sign=dict(doc) + sign.pop("control_sha256") + doc["control_sha256"]=consumer.canonical_hash(sign) + with self.assertRaisesRegex(consumer.Refused,refusal): + readmit_local_fixture(doc) + + def test_owner_env_duplicate_and_foreign_mount_refused(self): + for change in ("duplicate_env","foreign_world","readonly_world"): + with self.subTest(change=change): + doc=source_document() + values=doc["runtime"]["values_template"] + if change=="duplicate_env": + values["minecraft"]["additional_envs"].append( + dict(values["minecraft"]["additional_envs"][0])) + elif change=="foreign_world": + values["storage"]["additional_storage"][0]["host_path_config"]["path"]="/tmp/foreign" + else: + values["storage"]["additional_storage"][0]["read_only"]=True + sign=dict(doc) + sign.pop("control_sha256") + doc["control_sha256"]=consumer.canonical_hash(sign) + with self.assertRaises(consumer.Refused): + readmit_local_fixture(doc) + + def test_source_only_control_does_not_dispatch_heavy_guest(self): + current=(ROOT/"config/truenas-rdte-run-request.json").read_text() + self.assertNotIn('"product": "minecraft-vanilla-263"',current) + proposed=json.loads((ROOT/"config/truenas-vanilla263-native-admission-request.json").read_text()) + self.assertEqual(proposed["version"],"25.04.2.6") + self.assertEqual(proposed["product"],"minecraft-vanilla-263") + self.assertEqual(proposed["consumer_authority_issue"],627) + self.assertIn("NOT wired",proposed["purpose"]) + workflow=(ROOT/".github/workflows/gha-kvm-system-rdte.yml").read_text() + self.assertNotIn("minecraft-vanilla-263",workflow) + +if __name__=="__main__": + unittest.main() From a51c2fdc9d819ce9fcf9480657afe74eb6d945c8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:00:26 +0200 Subject: [PATCH 665/719] Qualify distinct vanilla 26.3 native T6 guest oracle and preserve Paper regressions (#418) --- .../vanilla263-native-guest-static.yml | 39 ++++ .../assert_vanilla263_native_t6_receipt.py | 135 ++++++++++++++ .../truenas_middleware_minecraft_t6_probe.py | 92 ++++++++-- tests/test_truenas_t6_vanilla263_guest.py | 171 ++++++++++++++++++ 4 files changed, 424 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/vanilla263-native-guest-static.yml create mode 100644 scripts/assert_vanilla263_native_t6_receipt.py create mode 100644 tests/test_truenas_t6_vanilla263_guest.py diff --git a/.github/workflows/vanilla263-native-guest-static.yml b/.github/workflows/vanilla263-native-guest-static.yml new file mode 100644 index 000000000..d9d29f9e1 --- /dev/null +++ b/.github/workflows/vanilla263-native-guest-static.yml @@ -0,0 +1,39 @@ +name: Vanilla 26.3 native guest observer and exact receipt cheap gates +on: + pull_request: + paths: + - 'scripts/truenas_middleware_minecraft_t6_probe.py' + - 'scripts/assert_vanilla263_native_t6_receipt.py' + - 'tests/test_truenas_t6_vanilla263_guest.py' + - '.github/workflows/vanilla263-native-guest-static.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + adversarial-source: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 6 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Compile exact native observer and independent acceptance oracle + run: | + set -euo pipefail + python3 -m py_compile \ + scripts/truenas_middleware_minecraft_t6_probe.py \ + scripts/admit_vanilla263_native_t6_control.py \ + scripts/assert_vanilla263_native_t6_receipt.py + python3 -m py_compile tests/test_truenas_t6_vanilla263_guest.py + - name: Native vanilla real guest boundary source and red-blue negatives + run: | + set -euo pipefail + python3 -m unittest -v tests/test_truenas_t6_vanilla263_guest.py + - name: Preserve legacy Paper 25.04.1 T6 regression + run: | + set -euo pipefail + python3 -m unittest -v tests/test_truenas_t6_minecraft_control.py + python3 -m unittest -v tests/test_minecraft_t6_receipt_gate.py + - name: No guest or physical promotion + run: echo 'Cheap exact vanilla26.3 native observer contract only. No KVM guest, no native App and physical TrueNAS APPLY HOLD.' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py new file mode 100644 index 000000000..edde660dc --- /dev/null +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Independent EXACT TrueNAS 25.04.2.6 / vanilla26.3 native guest receipt oracle. + +Cannot accept Paper T6 evidence, source-only control, an OCI-only container, +offline Java protocol from another host, or a provider's mere RUNNING status. +""" +from __future__ import annotations +import argparse +import json +from pathlib import Path +import sys + +from admit_vanilla263_native_t6_control import ( + APP, CONTROL_SHA, MIDDLEWARE, OCI, PRODUCER, TARGET, WORLD, +) + +SCHEMA = "truenas-minecraft-vanilla263-t6/v1" +STATUS_SCHEMA = "minecraft-java-status-oracle/v1" +IMAGE_SHA = OCI.split("@sha256:", 1)[1] + + +def check(root: dict) -> None: + def need(condition: bool, reason: str) -> None: + if not condition: + raise ValueError(reason) + need(type(root) is dict and root.get("contract") == "gha-kvm-system-lab/v1", + "guest root receipt schema") + target = root.get("target") or {} + need(type(target) is dict and + target.get("product") == "truenas" and + target.get("version") == TARGET and + target.get("rung") == "T6", "wrong exact guest identity") + need(root.get("classification") == "SUPPORTED" and + root.get("oracleSatisfied") is True and + root.get("phase") == "foundry-materialization", + "guest/root unsupported") + source = root.get("source") or {} + need(type(source) is dict and + source.get("system_version_expected") == "TrueNAS-" + TARGET and + source.get("middleware_commit") == MIDDLEWARE and + source.get("foundry_profile_blob") == "631467044411c96a0fe6d361688b99a794f07c80", + "guest exact source/profile mismatch") + shape = (root.get("requested_shape") or {}).get("app") or {} + need(type(shape) is dict and + shape.get("name") == APP and + shape.get("catalog_app") == "minecraft" and + shape.get("catalog_version_candidate") == "1.15.30" and + shape.get("minecraft_version") == "26.3" and + shape.get("minecraft_protocol") == 777, + "wrong native workload shape") + p = root.get("foundry_materialization") or {} + need(type(p) is dict and + p.get("schema") == SCHEMA and + p.get("classification") == "SUPPORTED" and + p.get("oracleSatisfied") is True and + p.get("product") == "vanilla-26.3" and + p.get("target") == TARGET, + "wrong product guest oracle") + need(p.get("foundry_ref") == PRODUCER and + p.get("control_sha256") == CONTROL_SHA, + "guest product signer/control mismatch") + need(p.get("catalog_version_verified") is True, + "exact native catalog not verified by guest") + observed = p.get("observed_image_reference") + need(isinstance(observed, str) and + (observed == OCI or + observed == OCI.split(":2026.10.0-java25")[0] + "@sha256:" + IMAGE_SHA) and + p.get("image_digest_qualification") == "SOURCE_AND_GUEST_REF_MATCH", + "native running OCI image digest not verified") + need(p.get("world_retained_on_delete") is True and + p.get("world_rebound_after_reinstall") is True, + "independent world not retained") + readiness = p.get("readiness") or {} + need(type(readiness) is dict, "guest readiness stages absent") + for phase in ("first", "restart", "redeploy", "reinstall"): + stage = readiness.get(phase) or {} + protocol = stage.get("protocol") or {} + level = stage.get("level") or {} + need(type(protocol) is dict and + protocol.get("schema") == STATUS_SCHEMA and + protocol.get("classification") == "SUPPORTED" and + protocol.get("oracleSatisfied") is True and + protocol.get("java_version") == "26.3" and + protocol.get("server_implementation") == "vanilla" and + protocol.get("status_name_contract") == "vanilla_exact" and + protocol.get("server_protocol") == 777 and + protocol.get("status_packet_valid") is True and + protocol.get("players_counter_valid") is True, + "independent vanilla protocol not accepted at " + phase) + need(type(level) is dict and + type(level.get("size")) is int and level["size"] > 0, + "independent owned world level.dat not observed at " + phase) + cleanup = p.get("cleanup") or {} + need(type(cleanup) is dict and + all(cleanup.get(k) is True for k in ( + "attempted", "zero_residue", "app_absent", + "owned_world_dataset_absent")), + "native App/ZFS world cleanup left residue") + need(p.get("secret_values_captured") is False, + "guest receipt admits secrets") + need((root.get("foundry_materialization") or {}).get("target") == TARGET, + "guest receipt cross-target mismatch") + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--receipt", required=True, type=Path) + a = p.parse_args() + try: + check(json.loads(a.receipt.read_text(encoding="utf-8"))) + except (OSError, ValueError, TypeError) as ex: + # Error messages generated by check() are constant, never propagate + # arbitrary provider receipt fields, credentials or real addresses. + msg = str(ex) if str(ex) in { + "guest root receipt schema", "wrong exact guest identity", + "guest/root unsupported", "guest exact source/profile mismatch", + "wrong native workload shape", "wrong product guest oracle", + "guest product signer/control mismatch", + "exact native catalog not verified by guest", + "native running OCI image digest not verified", + "independent world not retained", "guest readiness stages absent", + "native App/ZFS world cleanup left residue", + "guest receipt admits secrets", "guest receipt cross-target mismatch", + } or str(ex).startswith(( + "independent vanilla protocol not accepted at ", + "independent owned world level.dat not observed at ", + )) else "guest receipt unreadable/invalid" + print("Vanilla263 native T6 acceptance REFUSED: " + msg, file=sys.stderr) + return 2 + print("Vanilla263 native T6 SUPPORTED: exact guest/catalog/Java25/protocol777/world/cleanup") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 447f1ef56..a0b4c831b 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -39,6 +39,12 @@ DATASET = "rdtepool/minecraft-paper-t6-world" WORLD = "/mnt/rdtepool/minecraft-paper-t6-world" PORT = 25580 +MC_TYPE = "PAPER" +MC_VERSION = "1.21.11" +MC_PROTOCOL = 774 +MC_IMPLEMENTATION = "paper" +MC_STATUS_NAME_CONTRACT = "paper_brand_exact" +VANILLA263 = False WORLD_LEVEL = WORLD + "/level.dat" SENTINEL_PATH = WORLD + "/.supracraft-paper-t6-sentinel" REQUIRED_OWNER = { @@ -51,6 +57,42 @@ } +def configure_exact_vanilla263() -> None: + """Explicit per-process T6 product profile, not a Paper acceptance alias. + + The old Paper behavior remains the default and all targets are exact. + This process handles exactly one guest/product. The new source consumer + independently checks the signed merged Foundry control before mutation. + """ + global SCHEMA, EXPECTED_TARGET, EXPECTED_PROFILE, EXPECTED_APPS_COMMIT + global EXPECTED_IMAGE_FRAGMENT, EXPECTED_MIDDLEWARE, APP, DATASET + global WORLD, PORT, WORLD_LEVEL, SENTINEL_PATH, REQUIRED_OWNER + global MC_TYPE, MC_VERSION, MC_PROTOCOL, MC_IMPLEMENTATION + global MC_STATUS_NAME_CONTRACT, VANILLA263, CATALOG_PROFILES + from admit_vanilla263_native_t6_control import ( + APP as NATIVE_APP, APPS_REF, OCI, OWNER, TARGET, + MIDDLEWARE, PROFILE_BLOB, WORLD as NATIVE_DATASET, + ) + SCHEMA = "semper-supra.minecraft-vanilla-263-truenas-t6-control/1" + EXPECTED_TARGET = TARGET + EXPECTED_PROFILE = PROFILE_BLOB + EXPECTED_APPS_COMMIT = APPS_REF + EXPECTED_IMAGE_FRAGMENT = OCI + EXPECTED_MIDDLEWARE = MIDDLEWARE + APP = NATIVE_APP + DATASET = NATIVE_DATASET + WORLD = "/mnt/" + NATIVE_DATASET + PORT = 25580 + WORLD_LEVEL = WORLD + "/level.dat" + SENTINEL_PATH = WORLD + "/.supracraft-vanilla263-t6-sentinel" + REQUIRED_OWNER = OWNER + MC_TYPE, MC_VERSION, MC_PROTOCOL = "VANILLA", "26.3", 777 + MC_IMPLEMENTATION, MC_STATUS_NAME_CONTRACT = "vanilla", "vanilla_exact" + VANILLA263 = True + CATALOG_PROFILES = dict(CATALOG_PROFILES) + CATALOG_PROFILES["1.15.30"] = {"apps_commit": APPS_REF, "image": OCI} + + class MinecraftT6Error(RuntimeError): pass @@ -96,6 +138,16 @@ def canonical_sha256(doc: dict) -> str: def load_control(path: pathlib.Path, expected_commit: str, catalog_version: str = "1.15.29") -> dict: + if VANILLA263: + from admit_vanilla263_native_t6_control import verify + if catalog_version != "1.15.30": + raise MinecraftT6Error("vanilla263 exact native catalog version refused") + try: + control = json.loads((path / "control.json").read_text(encoding="utf-8")) + verify(control, expected_producer=expected_commit) + return control + except (OSError, ValueError, TypeError) as exc: + raise MinecraftT6Error("exact vanilla263 signed source control refused") from None if catalog_version not in CATALOG_PROFILES: raise MinecraftT6Error("unadmitted native catalog version profile") profile = CATALOG_PROFILES[catalog_version] @@ -185,12 +237,18 @@ def runtime_values(control: dict) -> dict: def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, - expected_version: str = "1.21.11") -> dict: + expected_version: str = "1.21.11", *, + implementation: str = "paper", + expected_protocol: int = 774) -> dict: + if implementation not in {"paper", "vanilla"} or ( + implementation == "paper" and expected_protocol != 774) or ( + implementation == "vanilla" and expected_protocol != 777): + raise MinecraftT6Error("exact product protocol/implementation profile refused") if not script.is_file() or script.name != "minecraft_java_status_oracle.py": raise MinecraftT6Error("exact Minecraft product oracle file absent") cmd = [sys.executable, str(script), "--port", str(port), "--expected-version", expected_version, - "--implementation", "paper", "--expected-protocol", "774", + "--implementation", implementation, "--expected-protocol", str(expected_protocol), "--timeout", "5", "--out", str(out)] result = subprocess.run(cmd, capture_output=True, text=True, timeout=12) if result.returncode != 0 or not out.is_file(): @@ -200,9 +258,10 @@ def safe_status_oracle(script: pathlib.Path, port: int, out: pathlib.Path, verdict.get("classification") != "SUPPORTED" or verdict.get("oracleSatisfied") is not True or verdict.get("java_version") != expected_version or - verdict.get("server_implementation") != "paper" or - verdict.get("status_name_contract") != "paper_brand_exact" or - verdict.get("server_protocol") != 774 or + verdict.get("server_implementation") != implementation or + verdict.get("status_name_contract") != ( + "paper_brand_exact" if implementation == "paper" else "vanilla_exact") or + verdict.get("server_protocol") != expected_protocol or verdict.get("status_packet_valid") is not True): raise MinecraftT6Error("Minecraft Java status receipt invalid") return verdict @@ -280,8 +339,8 @@ def owner_readback_vector(config: dict, wanted: dict) -> dict[str, bool]: env = minecraft.get("additional_envs") or [] if isinstance(minecraft, dict) else [] found_owner = {v.get("name"): v.get("value") for v in env if isinstance(v, dict)} if isinstance(env, list) else {} return { - "minecraft_type": minecraft.get("type") == "PAPER" if isinstance(minecraft, dict) else False, - "minecraft_version": minecraft.get("version") == "1.21.11" if isinstance(minecraft, dict) else False, + "minecraft_type": minecraft.get("type") == MC_TYPE if isinstance(minecraft, dict) else False, + "minecraft_version": minecraft.get("version") == MC_VERSION if isinstance(minecraft, dict) else False, "owner_environment": found_owner == REQUIRED_OWNER, "additional_storage": (config.get("storage") or {}).get("additional_storage") == wanted["storage"]["additional_storage"] if isinstance(config, dict) and isinstance(config.get("storage") or {}, dict) else False, "server_port": native_server_port_matches((config.get("network") or {}).get("server_port"), wanted["network"]["server_port"]) if isinstance(config, dict) and isinstance(config.get("network") or {}, dict) else False, @@ -308,16 +367,20 @@ def main() -> int: p.add_argument("--collage-source-ref") p.add_argument("--native-catalog-profile", choices=sorted(CATALOG_PROFILES), default="1.15.29") + p.add_argument("--product-profile", choices=("legacy-paper", "vanilla263"), + default="legacy-paper") p.add_argument("--tls", action="store_true") p.add_argument("--timeout", type=float, default=8) p.add_argument("--job-timeout", type=float, default=600) p.add_argument("--state-timeout", type=float, default=480) a = p.parse_args() + if a.product_profile == "vanilla263": + configure_exact_vanilla263() started = time.monotonic() payload = { - "schema": "truenas-minecraft-paper-t6/v1", + "schema": "truenas-minecraft-vanilla263-t6/v1" if VANILLA263 else "truenas-minecraft-paper-t6/v1", "classification": "ORACLE_FAILURE", "oracleSatisfied": False, - "product": "paper-upstream-1.21.11", "target": EXPECTED_TARGET, + "product": "vanilla-26.3" if VANILLA263 else "paper-upstream-1.21.11", "target": EXPECTED_TARGET, "cleanup": {"attempted": False, "zero_residue": False}, "secret_values_captured": False, } @@ -325,7 +388,7 @@ def main() -> int: dataset_created = False app_created = False try: - if (a.native_catalog_profile == "1.15.30" and + if (not VANILLA263 and a.native_catalog_profile == "1.15.30" and a.collage_source_dir is None): raise MinecraftT6Error("1.15.30 catalog profile is COLLAGE P2 opt-in only") control = load_control(a.control_dir, a.foundry_commit, @@ -333,7 +396,7 @@ def main() -> int: if a.expected_system_version != "TrueNAS-" + EXPECTED_TARGET: raise MinecraftT6Error("requested exact system version drift") desired = runtime_values(control) - sentinel_bytes = ("SUPRACRAFT-PAPER-T6:" + control["control_sha256"] + "\n").encode("ascii") + sentinel_bytes = (("SUPRACRAFT-VANILLA263-T6:" if VANILLA263 else "SUPRACRAFT-PAPER-T6:") + control["control_sha256"] + "\n").encode("ascii") payload["foundry_ref"] = a.foundry_commit payload["control_sha256"] = control["control_sha256"] if not a.status_oracle.is_file(): @@ -430,7 +493,10 @@ def wait_status(label): attempts += 1 receipt = a.out.with_name(a.out.stem + "-status-" + label + ".json") try: - verdict = safe_status_oracle(a.status_oracle, a.status_port, receipt) + verdict = safe_status_oracle(a.status_oracle, a.status_port, receipt, + expected_version=MC_VERSION, + implementation=MC_IMPLEMENTATION, + expected_protocol=MC_PROTOCOL) status_ever_satisfied = True status_cause = "status_supported" level_stat_attempted = True @@ -602,7 +668,7 @@ def delete_app(label): "owned_world_dataset_absent": True} payload["classification"] = "SUPPORTED" payload["oracleSatisfied"] = True - payload["detail"] = "native Paper Minecraft status/world lifecycle verified in disposable TrueNAS; not vanilla 26.3 or physical site" + payload["detail"] = ("native vanilla 26.3 status/world lifecycle verified in disposable TrueNAS; not physical site or managed COLLAGE" if VANILLA263 else "native Paper Minecraft status/world lifecycle verified in disposable TrueNAS; not vanilla 26.3 or physical site") except Exception as exc: payload["failure_type"] = type(exc).__name__ payload["detail"] = str(exc) if isinstance(exc, MinecraftT6Error) else "runtime exception; inspect bounded guest diagnostics" diff --git a/tests/test_truenas_t6_vanilla263_guest.py b/tests/test_truenas_t6_vanilla263_guest.py new file mode 100644 index 000000000..6281764c3 --- /dev/null +++ b/tests/test_truenas_t6_vanilla263_guest.py @@ -0,0 +1,171 @@ +"""Adversarial vanilla263 T6 guest and legacy-paper isolation source tests.""" +from __future__ import annotations +import copy +import json +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +ROOT=Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/"scripts")) +import truenas_middleware_minecraft_t6_probe as mc +from assert_vanilla263_native_t6_receipt import check as check_guest +import admit_vanilla263_native_t6_control as native + + +def guest_fixture(): + status={ + "schema":"minecraft-java-status-oracle/v1", + "classification":"SUPPORTED","oracleSatisfied":True, + "java_version":"26.3","server_implementation":"vanilla", + "status_name_contract":"vanilla_exact","server_protocol":777, + "status_packet_valid":True,"players_counter_valid":True, + } + return { + "contract":"gha-kvm-system-lab/v1", + "target":{"product":"truenas","version":"25.04.2.6","rung":"T6"}, + "classification":"SUPPORTED","oracleSatisfied":True, + "phase":"foundry-materialization", + "source":{"system_version_expected":"TrueNAS-25.04.2.6", + "middleware_commit":native.MIDDLEWARE, + "foundry_profile_blob":native.PROFILE_BLOB}, + "requested_shape":{"app":{ + "name":native.APP,"catalog_app":"minecraft", + "catalog_version_candidate":"1.15.30", + "minecraft_version":"26.3","minecraft_protocol":777}}, + "foundry_materialization":{ + "schema":"truenas-minecraft-vanilla263-t6/v1", + "classification":"SUPPORTED","oracleSatisfied":True, + "product":"vanilla-26.3","target":"25.04.2.6", + "foundry_ref":native.PRODUCER, + "control_sha256":native.CONTROL_SHA, + "catalog_version_verified":True, + "observed_image_reference":native.OCI, + "image_digest_qualification":"SOURCE_AND_GUEST_REF_MATCH", + "world_retained_on_delete":True, + "world_rebound_after_reinstall":True, + "readiness":{stage:{"protocol":dict(status), + "level":{"size":2048}} + for stage in ("first","restart","redeploy","reinstall")}, + "cleanup":{"attempted":True,"zero_residue":True, + "app_absent":True,"owned_world_dataset_absent":True}, + "secret_values_captured":False, + }, + } + + +class TestNativeVanilla263Guest(unittest.TestCase): + def test_full_independent_guest_oracle_fixture(self): + check_guest(guest_fixture()) + + def test_never_promote_paper_legacy_or_source_only(self): + for change in ("legacy_version","legacy_paper","source_only","wrong_phase"): + r=guest_fixture() + if change=="legacy_version": + r["target"]["version"]="25.04.1" + if change=="legacy_paper": + r["foundry_materialization"]["schema"]="truenas-minecraft-paper-t6/v1" + if change=="source_only": + r["classification"]="SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED" + if change=="wrong_phase": + r["phase"]="source-admission" + with self.subTest(change=change),self.assertRaises(ValueError): + check_guest(r) + + def test_every_phase_real_vanilla_status_and_world_required(self): + baseline=guest_fixture() + for phase in ("first","restart","redeploy","reinstall"): + for field,val in (("java_version","1.21.11"), + ("server_implementation","paper"), + ("server_protocol",774), + ("status_name_contract","paper_brand_exact"), + ("status_packet_valid",False), + ("players_counter_valid",False)): + r=copy.deepcopy(baseline) + r["foundry_materialization"]["readiness"][phase]["protocol"][field]=val + with self.subTest(phase=phase,field=field),self.assertRaises(ValueError): + check_guest(r) + for size in (0,-1,None): + r=copy.deepcopy(baseline) + r["foundry_materialization"]["readiness"][phase]["level"]["size"]=size + with self.assertRaises(ValueError): + check_guest(r) + + def test_identity_integrity_and_cleanup_negative(self): + for path,val in [ + (("source","middleware_commit"),"f"*40), + (("requested_shape","app","name"),"foreign"), + (("foundry_materialization","control_sha256"),"a"*64), + (("foundry_materialization","observed_image_reference"),"ghcr.io/itzg/minecraft-server:latest"), + (("foundry_materialization","catalog_version_verified"),False), + (("foundry_materialization","world_retained_on_delete"),False), + (("foundry_materialization","world_rebound_after_reinstall"),False), + (("foundry_materialization","cleanup","zero_residue"),False), + (("foundry_materialization","cleanup","app_absent"),False), + (("foundry_materialization","cleanup","owned_world_dataset_absent"),False), + (("foundry_materialization","secret_values_captured"),True), + ]: + r=guest_fixture() + node=r + for key in path[:-1]: node=node[key] + node[path[-1]]=val + with self.subTest(path=path),self.assertRaises(ValueError): + check_guest(r) + + def test_00_legacy_paper_profile_remains_exact_default(self): + self.assertFalse(mc.VANILLA263) + self.assertEqual(mc.EXPECTED_TARGET,"25.04.1") + self.assertEqual(mc.MC_VERSION,"1.21.11") + self.assertEqual(mc.MC_PROTOCOL,774) + self.assertEqual(mc.APP,"rdte-t6-minecraft-paper") + + def test_explicit_vanilla_profile_is_distinct(self): + mc.configure_exact_vanilla263() + self.assertTrue(mc.VANILLA263) + self.assertEqual(mc.EXPECTED_TARGET,"25.04.2.6") + self.assertEqual(mc.EXPECTED_PROFILE,native.PROFILE_BLOB) + self.assertEqual(mc.APP,native.APP) + self.assertEqual(mc.DATASET,native.WORLD) + self.assertEqual(mc.EXPECTED_IMAGE_FRAGMENT,native.OCI) + self.assertEqual(mc.MC_TYPE,"VANILLA") + self.assertEqual(mc.MC_VERSION,"26.3") + self.assertEqual(mc.MC_PROTOCOL,777) + self.assertEqual(mc.CATALOG_PROFILES["1.15.30"]["image"],native.OCI) + + def test_vanilla_status_requires_exact_version_protocol_implementation(self): + mc.configure_exact_vanilla263() + with tempfile.TemporaryDirectory() as d: + script=Path(d)/"minecraft_java_status_oracle.py" + script.write_text("probe stub, subprocess mocked") + out=Path(d)/"status.json" + valid=guest_fixture()["foundry_materialization"]["readiness"]["first"]["protocol"] + def good(cmd,**kwargs): + self.assertEqual(cmd[cmd.index("--implementation")+1],"vanilla") + self.assertEqual(cmd[cmd.index("--expected-protocol")+1],"777") + self.assertEqual(cmd[cmd.index("--expected-version")+1],"26.3") + out.write_text(json.dumps(valid)) + return type("Proc",(),{"returncode":0})() + with patch.object(mc.subprocess,"run",side_effect=good): + self.assertEqual(mc.safe_status_oracle( + script,25580,out,expected_version="26.3", + implementation="vanilla",expected_protocol=777)["server_protocol"],777) + for field,val in (("server_protocol",774), + ("server_implementation","paper"), + ("java_version","1.21.11")): + forged=dict(valid,**{field:val}) + def bad(cmd,**kwargs): + out.write_text(json.dumps(forged)) + return type("Proc",(),{"returncode":0})() + with patch.object(mc.subprocess,"run",side_effect=bad): + with self.subTest(field=field),self.assertRaises(mc.MinecraftT6Error): + mc.safe_status_oracle(script,25580,out, + expected_version="26.3",implementation="vanilla", + expected_protocol=777) + with self.assertRaises(mc.MinecraftT6Error): + mc.safe_status_oracle(script,25580,out, + expected_version="26.3",implementation="paper",expected_protocol=777) + +if __name__=="__main__": + unittest.main() From b355079232bcf595823863829130f588b77be408 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:05:43 +0200 Subject: [PATCH 666/719] Wire exact vanilla 26.3 TrueNAS T6 signed producer, guest route and receipt (#419) * OP-4B: wire distinct native vanilla26.3 producer, SHA admission, oracle and guest receipt; no request edge * OP-4B: route exact native vanilla263 guest into existing serialized T6 with independent control/status and cleanup * OP-4B: fail-closed exact native guest selector/source/oracle/request-edge routing tests * OP-4B: remove inert assertion in request-edge gate regression * OP-4B: run native guest router red-blue and shell syntax in cheap CI prior KVM * OP-4B: preserve exact G5 exception regression while admitting separately gated vanilla263 T6 * OP-4B: retain official catalog exact T6 exception test with native vanilla admission --- .github/workflows/gha-kvm-system-rdte.yml | 52 +++++++++++++- .../vanilla263-native-guest-static.yml | 5 ++ scripts/gha_kvm_truenas_rdte.sh | 28 ++++++-- tests/test_gha_kvm_system_rdte.py | 2 +- ...est_truenas_t6_official_catalog_control.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 69 +++++++++++++++++++ 6 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 tests/test_vanilla263_t6_guest_routing.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 190ca4245..424b961fd 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -57,6 +57,7 @@ on: - foliorelay - minecraft-paper - collage-native-paper + - minecraft-vanilla-263 proxmox_compute_fixture: description: "Optional native PVE REST compute fixture" required: true @@ -187,7 +188,7 @@ jobs: fi truenas_version="$(jq -er '.version | select(type == "string" and length > 0)' "$RUN_REQUEST_PATH")" truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" - truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay" or . == "minecraft-paper" or . == "collage-native-paper")' "$RUN_REQUEST_PATH")" + truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay" or . == "minecraft-paper" or . == "collage-native-paper" or . == "minecraft-vanilla-263")' "$RUN_REQUEST_PATH")" truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ @@ -285,6 +286,16 @@ jobs: permissions: contents: read + foundry-minecraft-vanilla263-t6-control: + needs: [changes, contract] + if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_version == '25.04.2.6' + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-vanilla-263-t6-control.yml@29ec6cba7df51e316b0329926aa85807805dd07b + with: + foundry_ref: 29ec6cba7df51e316b0329926aa85807805dd07b + target_version: "25.04.2.6" + permissions: + contents: read + foundry-collage-paper-11530-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' @@ -439,7 +450,7 @@ jobs: group: agent-dispatch-heavyweight-rdte queue: max cancel-in-progress: false - needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, foundry-minecraft-paper-t6-control, foundry-collage-paper-11530-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix, provider-container-pre-b4-fixture] + needs: [changes, contract, proxmox-9-2, foundry-t6-control, foundry-wow-t6-control, foundry-garm-t6-control, foundry-garm-f0-f5-control, foundry-official-catalog-t6-control, foundry-foliorelay-t6-control, foundry-minecraft-paper-t6-control, foundry-collage-paper-11530-t6-control, foundry-minecraft-vanilla263-t6-control, provider-g2-fixtures, provider-g3-fixture, provider-g4-fixture, provider-g5-runner-fixture, provider-g5-version-matrix, provider-container-pre-b4-fixture] if: >- always() && needs.contract.result == 'success' && @@ -455,6 +466,7 @@ jobs: (needs.changes.outputs.truenas_t6_product == 'foliorelay' && needs['foundry-foliorelay-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' && needs['foundry-minecraft-paper-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'collage-native-paper' && needs['foundry-collage-paper-11530-t6-control'].result == 'success') || + (needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_version == '25.04.2.6' && needs['foundry-minecraft-vanilla263-t6-control'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' && needs['provider-g2-fixtures'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g3' && needs['provider-g3-fixture'].result == 'success') || (needs.changes.outputs.truenas_t6_product == 'garm-provider-g4' && needs['provider-g4-fixture'].result == 'success') || @@ -515,6 +527,20 @@ jobs: with: name: minecraft-paper-11530-truenas-t6-candidate path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Download exact vanilla26.3 25.04.2.6 merged-source native control + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: minecraft-vanilla263-java25-truenas-250426-t6-source-candidate + path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control + - name: Independently verify immutable native vanilla control pre-KVM + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' + run: | + set -euo pipefail + python3 scripts/admit_vanilla263_native_t6_control.py \ + --control "$RUNNER_TEMP/gha-kvm-system-rdte/foundry-control/control.json" \ + --out "$RUNNER_TEMP/vanilla263-pre-kvm-control.json" + - name: Checkout exact Minecraft product status oracle if: needs.changes.outputs.truenas_rung == 't6' && (needs.changes.outputs.truenas_t6_product == 'minecraft-paper' || needs.changes.outputs.truenas_t6_product == 'collage-native-paper') uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 @@ -528,6 +554,21 @@ jobs: run: | test "$(git -C .deps/minecraft-product rev-parse HEAD)" = 1b2227847d56007cdf8b668c00845326827e5fd5 python3 .deps/minecraft-product/tools/test_minecraft_java_status_oracle.py + - name: Checkout independently qualified vanilla26.3 protocol777 status oracle + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SupraCraft/mc-server-images + ref: 4df9fbbfea0ae0e824719db367bd551349a86378 + path: .deps/minecraft-product + persist-credentials: false + - name: Verify exact independent vanilla status oracle source + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' + run: | + set -euo pipefail + test "$(git -C .deps/minecraft-product rev-parse HEAD)" = 4df9fbbfea0ae0e824719db367bd551349a86378 + python3 .deps/minecraft-product/tools/test_minecraft_java_status_oracle.py + - name: Checkout exact accepted COLLAGE read-only source if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 @@ -589,7 +630,7 @@ jobs: name: garm-provider-truenas-container-pre-b4-fixture path: ${{ runner.temp }}/gha-kvm-system-rdte/container-pre-b4-fixture - name: Refuse floating upstream Minecraft catalog before heavyweight KVM - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' || needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' run: | set -euo pipefail python3 -m unittest -v tests.test_collage_catalog_preflight @@ -660,6 +701,8 @@ jobs: foundry_commit="2cc7e1fe3772f8c532e933b3d7832e6cf985db4e" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then foundry_commit="403321031b6d4e66aa64a8299fd7621eee290b0d" + elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-vanilla-263" ]]; then + foundry_commit="29ec6cba7df51e316b0329926aa85807805dd07b" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi @@ -680,6 +723,9 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" || "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then python3 scripts/assert_minecraft_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" fi + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-vanilla-263" ]]; then + python3 scripts/assert_vanilla263_native_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + fi if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then python3 scripts/assert_collage_p2_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" fi diff --git a/.github/workflows/vanilla263-native-guest-static.yml b/.github/workflows/vanilla263-native-guest-static.yml index d9d29f9e1..135f1a5b4 100644 --- a/.github/workflows/vanilla263-native-guest-static.yml +++ b/.github/workflows/vanilla263-native-guest-static.yml @@ -6,6 +6,9 @@ on: - 'scripts/assert_vanilla263_native_t6_receipt.py' - 'tests/test_truenas_t6_vanilla263_guest.py' - '.github/workflows/vanilla263-native-guest-static.yml' + - 'tests/test_vanilla263_t6_guest_routing.py' + - 'scripts/gha_kvm_truenas_rdte.sh' + - '.github/workflows/gha-kvm-system-rdte.yml' workflow_dispatch: permissions: contents: read @@ -30,6 +33,8 @@ jobs: run: | set -euo pipefail python3 -m unittest -v tests/test_truenas_t6_vanilla263_guest.py + bash -n scripts/gha_kvm_truenas_rdte.sh + python3 -m unittest -v tests/test_vanilla263_t6_guest_routing.py - name: Preserve legacy Paper 25.04.1 T6 regression run: | set -euo pipefail diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 08ee2df24..715500935 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -18,7 +18,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper|collage-native-paper] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper|collage-native-paper|minecraft-vanilla-263] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" } OUT="" @@ -76,14 +76,17 @@ if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then exit 2 fi if [[ "$RUNG" == "t6" ]]; then - if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then + if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" && "$T6_PRODUCT" != "minecraft-vanilla-263" ]]; then if [[ "$T6_PRODUCT" == "collage-native-paper" ]]; then [[ "$VERSION" == "25.04.1" ]] || { echo "COLLAGE P2 admitted only on isolated TrueNAS 25.04.1" >&2; exit 2; } else [[ "$VERSION" == "26.0.0-BETA.3" ]] || { echo "product-specific T6 controls remain admitted only for exact TrueNAS 26.0.0-BETA.3" >&2; exit 2; } fi fi - [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "garm-provider-container-pre-b4" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" || "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + [[ "$T6_PRODUCT" == "litellm" || "$T6_PRODUCT" == "wow-sidecar" || "$T6_PRODUCT" == "garm" || "$T6_PRODUCT" == "garm-controller-f0f5" || "$T6_PRODUCT" == "garm-provider-g2" || "$T6_PRODUCT" == "garm-provider-g3" || "$T6_PRODUCT" == "garm-provider-g4" || "$T6_PRODUCT" == "garm-provider-g5" || "$T6_PRODUCT" == "garm-provider-container-pre-b4" || "$T6_PRODUCT" == "official-catalog" || "$T6_PRODUCT" == "foliorelay" || "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" || "$T6_PRODUCT" == "minecraft-vanilla-263" ]] || { echo "unsupported T6 product: $T6_PRODUCT" >&2; exit 2; } + if [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then + [[ "$VERSION" == "25.04.2.6" ]] || { echo "vanilla26.3 native product requires exact TrueNAS25.04.2.6" >&2; exit 2; } + fi if [[ "$T6_PRODUCT" == "garm-provider-g2" ]]; then [[ -n "$G2_FIXTURE_DIR" && -d "$G2_FIXTURE_DIR" ]] || { echo "garm-provider-g2 requires --g2-fixture-dir" >&2; exit 2; } [[ "$G2_FIXTURE_PRODUCER" =~ ^[0-9a-f]{40}$ ]] || { echo "garm-provider-g2 requires exact --g2-fixture-producer SHA" >&2; exit 2; } @@ -233,6 +236,8 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" else {"name": "rdte-t6-foliorelay", "images": foliorelay_images} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" + else {"name": "rdte-t6-minecraft-vanilla-263", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.30", "minecraft_version": "26.3", "minecraft_protocol": 777} + if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "minecraft-vanilla-263" else {"name": "rdte-t6-minecraft-paper", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.29", "paper_version": "1.21.11"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") in {"minecraft-paper", "collage-native-paper"} else {"name": "rdte-t6-litellm", "image": "ghcr.io/sempersupra/litellm-appliance@sha256:225c899db85865929f6099d3e1fe27097cafaed5af823fa397e75e1eb6ec51ac"} @@ -383,7 +388,7 @@ if [[ "$RUNG" == "t2" || "$RUNG" == "t3" || "$RUNG" == "t4" || "$RUNG" == "t5" | elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_wow_sidecar_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing TrueNAS T6 WOW Sidecar control client" - elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ]]; then + elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" || "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then [[ -f "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" ]] || fail_evidence HARNESS_FAILURE preflight "missing Minecraft T6 product probe" [[ -f "$REPO_ROOT/.deps/minecraft-product/tools/minecraft_java_status_oracle.py" ]] || @@ -662,7 +667,7 @@ FOLIORELAY_OBSERVER_HOST_PORT="" FOLIORELAY_HOSTFWD="" MINECRAFT_HOST_PORT="" MINECRAFT_HOSTFWD="" -if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ) ]]; then +if [[ "$RUNG" == "t6" && ( "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" || "$T6_PRODUCT" == "minecraft-vanilla-263" ) ]]; then MINECRAFT_HOST_PORT="$(python3 - <<'PY' import socket s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close() @@ -1175,6 +1180,17 @@ elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-p --foundry-commit "$FOUNDRY_COMMIT" --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ "${COLLAGE_P2_ARGS[@]}" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true +elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then + [[ "$VERSION" == "25.04.2.6" ]] || fail_evidence HARNESS_FAILURE foundry-materialization "native vanilla guest exact version drift" + python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ + --product-profile vanilla263 --native-catalog-profile 1.15.30 \ + --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ + --status-port "$MINECRAFT_HOST_PORT" \ + --status-oracle "$REPO_ROOT/.deps/minecraft-product/tools/minecraft_java_status_oracle.py" \ + "${MIDDLEWARE_TLS_ARG[@]}" \ + --password-file "$PASSWORD_FILE" --control-dir "$FOUNDRY_CONTROL_DIR" \ + --foundry-commit "$FOUNDRY_COMMIT" --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ + --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ @@ -1228,6 +1244,8 @@ elif [[ "$T6_PRODUCT" == "garm" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported GARM controller control passed exact appliance and secret-normalized config read-back, persistent state, external HTTPS, restart persistence, and zero-residue cleanup; GitHub/JIT registration intentionally not exercised" elif [[ "$T6_PRODUCT" == "wow-sidecar" ]]; then write_receipt SUPPORTED true foundry-materialization "exact Foundry-exported WOW Sidecar control passed immutable image/config read-back, permissions/seed metadata, public-fixture worker runtime, restart persistence, and zero-residue cleanup" +elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then + write_receipt SUPPORTED true foundry-materialization "exact TrueNAS25.04.2.6 native vanilla26.3 Java25/protocol777, independent owned world across native App lifecycle, status and zero residue; source signer/control bound, no physical APPLY" elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-paper" ]]; then write_receipt SUPPORTED true foundry-materialization "exact native TrueNAS Minecraft Paper server passed protocol status + world level.dat persistence across App stop/start, redeploy, retain-world delete/reinstall and owned cleanup; no vanilla 26.3 or physical TrueNAS claim" elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then diff --git a/tests/test_gha_kvm_system_rdte.py b/tests/test_gha_kvm_system_rdte.py index 2e7200cee..25db35980 100644 --- a/tests/test_gha_kvm_system_rdte.py +++ b/tests/test_gha_kvm_system_rdte.py @@ -338,7 +338,7 @@ def test_g5_routing_is_source_exact_cross_version_and_public_safe(self): self.assertIn("truenas_middleware_garm_provider_g5_probe.py", text) self.assertIn('G5_MATRIX_PRODUCER', text) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" && "$T6_PRODUCT" != "minecraft-vanilla-263" ]]; then', text, ) g5_probe = (ROOT / "scripts" / "truenas_middleware_garm_provider_g5_probe.py").read_text( diff --git a/tests/test_truenas_t6_official_catalog_control.py b/tests/test_truenas_t6_official_catalog_control.py index 6ad15c2ab..6b40b9583 100644 --- a/tests/test_truenas_t6_official_catalog_control.py +++ b/tests/test_truenas_t6_official_catalog_control.py @@ -182,7 +182,7 @@ def test_workflow_and_harness_bind_exact_official_catalog_producer(self): self.assertIn('"official-catalog"', harness) self.assertIn("truenas_middleware_official_catalog_t6_probe.py", harness) self.assertIn( - 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" ]]; then', + 'if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" && "$T6_PRODUCT" != "minecraft-vanilla-263" ]]; then', harness, ) diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py new file mode 100644 index 000000000..034bc235f --- /dev/null +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -0,0 +1,69 @@ +"""Static red/blue: distinct native guest selector; no request-edge replay.""" +from __future__ import annotations +import json +from pathlib import Path +import re +import unittest + +ROOT=Path(__file__).resolve().parents[1] +WORKFLOW=(ROOT/".github/workflows/gha-kvm-system-rdte.yml").read_text() +HARNESS=(ROOT/"scripts/gha_kvm_truenas_rdte.sh").read_text() + + +class TestVanilla263GuestRouting(unittest.TestCase): + def test_native_product_is_explicit_not_paper_alias(self): + self.assertIn("- minecraft-vanilla-263",WORKFLOW) + self.assertIn('or . == "minecraft-vanilla-263"',WORKFLOW) + self.assertIn('needs.changes.outputs.truenas_t6_product == \'minecraft-vanilla-263\'',WORKFLOW) + self.assertIn('needs.changes.outputs.truenas_version == \'25.04.2.6\'',WORKFLOW) + self.assertIn('[[ "$VERSION" == "25.04.2.6" ]]',HARNESS) + self.assertIn('|| "$T6_PRODUCT" == "minecraft-vanilla-263"',HARNESS) + + def test_producer_signer_and_fresh_exact_artifact_seam(self): + pin="29ec6cba7df51e316b0329926aa85807805dd07b" + self.assertIn("foundry-minecraft-vanilla263-t6-control:",WORKFLOW) + self.assertIn( + "export-minecraft-vanilla-263-t6-control.yml@"+pin,WORKFLOW) + self.assertIn("foundry_ref: "+pin,WORKFLOW) + self.assertIn( + "name: minecraft-vanilla263-java25-truenas-250426-t6-source-candidate",WORKFLOW) + self.assertIn("scripts/admit_vanilla263_native_t6_control.py",WORKFLOW) + self.assertIn('foundry_commit="'+pin+'"',WORKFLOW) + self.assertIn( + "needs['foundry-minecraft-vanilla263-t6-control'].result == 'success'", + WORKFLOW) + + def test_exact_oracle_not_legacy_paper_status(self): + self.assertIn("Checkout independently qualified vanilla26.3 protocol777 status oracle",WORKFLOW) + self.assertIn("4df9fbbfea0ae0e824719db367bd551349a86378",WORKFLOW) + self.assertIn("python3 scripts/assert_vanilla263_native_t6_receipt.py",WORKFLOW) + self.assertIn("--product-profile vanilla263 --native-catalog-profile 1.15.30",HARNESS) + self.assertIn('"minecraft_protocol": 777',HARNESS) + self.assertIn('"minecraft_version": "26.3"',HARNESS) + self.assertIn('"rdte-t6-minecraft-vanilla-263"',HARNESS) + self.assertIn("--status-port \"$MINECRAFT_HOST_PORT\"",HARNESS) + self.assertIn("MINECRAFT_HOSTFWD",HARNESS) + + def test_existing_request_edge_untouched_during_source_qualification(self): + current=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) + proposal=json.loads((ROOT/"config/truenas-vanilla263-native-admission-request.json").read_text()) + self.assertNotEqual(current.get("product"),"minecraft-vanilla-263") + self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") + self.assertEqual(proposal["consumer_authority_issue"],627) + self.assertIn("NOT wired",proposal["purpose"]) + # Triggering branch merely edits source/probe/workflow. Heavy runs + # are ONLY driven by a separate unique request-file transition. + self.assertIn("config/truenas-rdte-run-request.json)",WORKFLOW) + self.assertIn('"$current_request_id" == "$previous_request_id"',WORKFLOW) + + def test_no_fallback_guest_or_physical_promotion(self): + self.assertNotIn('product == "minecraft-vanilla-263" && needs[\'foundry-minecraft-paper',WORKFLOW) + self.assertIn("agent-dispatch-heavyweight-rdte",WORKFLOW) + self.assertIn("queue: max",WORKFLOW) + self.assertIn("cancel-in-progress: false",WORKFLOW) + # Exact accepted source consumer must refuse a wrong Foundry SHA + from scripts.admit_vanilla263_native_t6_control import CONTROL_SHA + self.assertEqual(CONTROL_SHA,"c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615") + +if __name__=="__main__": + unittest.main() From 9b156b07b212a398b0c54e7939700e6a9ef15a54 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:08:32 +0200 Subject: [PATCH 667/719] Admit one disposable TrueNAS 25.04.2.6 vanilla 26.3 native guest request (#421) * OP-4B: recognize only exact current vanilla263 25.04.2.6 request and private #627 authority * OP-4B: bind routing tests to one new authorized, nonreplayable native T6 request * OP-4B: source-locked new 25.04.2.6 vanilla263 one-guest request edge after static gates * OP-4B: update WOW isolation regression to recognize exact separately authorized vanilla263 --- config/truenas-rdte-run-request.json | 12 ++++++------ tests/test_truenas_rdte_target.py | 9 ++++++++- tests/test_truenas_t6_wow_sidecar_control.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 11 ++++++++--- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 947613dee..de0ec422c 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-1-t6-collage-native-paper-p2-006-11530-authority-contract-correction", - "version": "25.04.1", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4b-002-native-guest", + "version": "25.04.2.6", "rung": "t6", - "product": "collage-native-paper", - "authority_issue": 411, - "consumer_authority_issue": 625, - "purpose": "Single NEW bounded public/free disposable TrueNAS 25.04.1 COLLAGE native Minecraft 1.15.30 P2 read-only observer test after genuine static contract failure run #38351220220. That attempted request failed pre-guest: test_truenas_rdte_target.py hard-coded original consumer authority 614 while current source-locked 1.15.30 successor authority agent-dispatch-private#625 was independently documented. New narrow static PR #414 corrected only assertion to permit explicit {614,625}, was tested in public GHA static #38436881076 SUCCESS, and merged experimental branch at exact c55c32c2bc43b2a9a54aa8793bcc3702f8357c22. Never infer runtime PASS from contract PASS. Preserve full prior request source locks: exact native community/minecraft 1.15.30 upstream truenas/apps@559b9fe85212c800bc6770f152a65211df137a44, OCI 2026.10.0-java21@sha256:d4057e7019e9b19f25e039d4a204cc502e1a112d1a2305966a534f3d7079e644, Foundry producer@403321031b6d4e66aa64a8299fd7621eee290b0d, exact Paper Java 1.21.11 protocol 774 and external world sentinel, native lifecycle/reinstall and zero residue, then scoped APPS_READ TLS-verified provider-neutral owner readback. No auth broadening, no secret leakage, no physical TrueNAS, no private Actions minutes, no Paper-to-vanilla 26.3/Two Rivers inheritance. One shared heavyweight GHA slot; on failure save exact root error and DLE baton, never blind-retry." + "product": "minecraft-vanilla-263", + "authority_issue": 423, + "consumer_authority_issue": 627, + "purpose": "One uniquely identified public/free-tier disposable TrueNAS 25.04.2.6 KVM T6 native minecraft-vanilla-263 Java25/protocol777 guest. Preserve exact accepted merged Foundry producer 29ec6cba7df51e316b0329926aa85807805dd07b and canonical source-control digest c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615, pinned upstream 1.15.30 Java25 OCI SHA5a2ab..., independent rdtepool/minecraft-vanilla-263-t6-world, product-specific provider readback and status/oracle across create/restart/redeploy/reinstall/delete with zero residue. New request edge after independent signed-source PR #417, native guest adapter PR #418, selector PR #419 and cheap gates PASS. Do not inherit Paper 1.21.11/25.04.1 evidence. Never mutate physical TrueNAS, never consume private GHA minutes, never replay unchanged request_id. On failure retain sanitized evidence and hold subsequent heavyweight jobs until diagnosis." } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 598d1663f..eda667195 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -78,7 +78,7 @@ def test_run_request_is_exact_registered_target(self): target=self.targets[request["version"]] self.assertEqual(request["authority_issue"], target["authority_issue"]) if request["rung"] == "t6": - self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","garm-provider-container-pre-b4","official-catalog","foliorelay","minecraft-paper","collage-native-paper"}) + self.assertIn(request.get("product", "litellm"), {"litellm","wow-sidecar","garm","garm-controller-f0f5","garm-provider-g2","garm-provider-g3","garm-provider-g4","garm-provider-g5","garm-provider-container-pre-b4","official-catalog","foliorelay","minecraft-paper","collage-native-paper","minecraft-vanilla-263"}) if request.get("product") == "collage-native-paper": self.assertEqual(request["rung"], "t6") self.assertEqual(request["version"], "25.04.1") @@ -86,6 +86,13 @@ def test_run_request_is_exact_registered_target(self): # source-locked Minecraft 1.15.30 qualification successor. # This is a constrained allowlist, not arbitrary issue authority. self.assertIn(request["consumer_authority_issue"], {614, 625}) + if request.get("product") == "minecraft-vanilla-263": + self.assertEqual(request["rung"], "t6") + self.assertEqual(request["version"], "25.04.2.6") + self.assertEqual(request["authority_issue"], 423) + self.assertEqual(request["consumer_authority_issue"], 627) + self.assertEqual(request["request_id"], + "tn-25-04-2-6-t6-vanilla-263-op4b-002-native-guest") if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_truenas_t6_wow_sidecar_control.py b/tests/test_truenas_t6_wow_sidecar_control.py index 0ceee8844..784a1ea55 100644 --- a/tests/test_truenas_t6_wow_sidecar_control.py +++ b/tests/test_truenas_t6_wow_sidecar_control.py @@ -64,7 +64,7 @@ def test_workflow_routes_exact_wow_export_without_reopening_litellm_default(self self.assertIn('T6_PRODUCT="litellm"', harness) if request["rung"] == "t6": product = request.get("product", "litellm") - self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "garm-provider-container-pre-b4", "official-catalog", "foliorelay", "minecraft-paper", "collage-native-paper"}) + self.assertIn(product, {"litellm", "wow-sidecar", "garm", "garm-controller-f0f5", "garm-provider-g2", "garm-provider-g3", "garm-provider-g4", "garm-provider-g5", "garm-provider-container-pre-b4", "official-catalog", "foliorelay", "minecraft-paper", "collage-native-paper", "minecraft-vanilla-263"}) if product == "wow-sidecar": self.assertEqual("26.0.0-BETA.3", request["version"]) self.assertEqual(396, request["authority_issue"]) diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 034bc235f..16a73da11 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -47,12 +47,17 @@ def test_exact_oracle_not_legacy_paper_status(self): def test_existing_request_edge_untouched_during_source_qualification(self): current=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) proposal=json.loads((ROOT/"config/truenas-vanilla263-native-admission-request.json").read_text()) - self.assertNotEqual(current.get("product"),"minecraft-vanilla-263") + self.assertEqual(current.get("product"),"minecraft-vanilla-263") + self.assertEqual(current["version"],"25.04.2.6") + self.assertEqual(current["rung"],"t6") + self.assertEqual(current["authority_issue"],423) + self.assertEqual(current["consumer_authority_issue"],627) + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-002-native-guest") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) - # Triggering branch merely edits source/probe/workflow. Heavy runs - # are ONLY driven by a separate unique request-file transition. + # Exactly one bounded heavyweight request edge is admitted in this PR. + # A replay of this request_id without a new distinct edge is forbidden. self.assertIn("config/truenas-rdte-run-request.json)",WORKFLOW) self.assertIn('"$current_request_id" == "$previous_request_id"',WORKFLOW) From 18c7c9d61b6f31edba64e341ab3f0d3aef645265 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:13:23 +0200 Subject: [PATCH 668/719] Correct official TrueNAS Apps catalog master preflight before native Minecraft KVM (#422) * Correct official TrueNAS Apps live catalog preflight to actual master branch, no pin downgrade * Pin explicit verified official master catalog URL and preserve exact version negative fixtures * Add real public official Minecraft 1.15.30 catalog preflight CI before costly KVM --- .../official-minecraft-catalog-preflight.yml | 42 +++++++++++++++++++ scripts/check_collage_catalog_preflight.py | 6 ++- tests/test_collage_catalog_preflight.py | 11 ++++- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/official-minecraft-catalog-preflight.yml diff --git a/.github/workflows/official-minecraft-catalog-preflight.yml b/.github/workflows/official-minecraft-catalog-preflight.yml new file mode 100644 index 000000000..628f46249 --- /dev/null +++ b/.github/workflows/official-minecraft-catalog-preflight.yml @@ -0,0 +1,42 @@ +name: Read-only official Minecraft native catalog preflight +on: + pull_request: + paths: + - 'scripts/check_collage_catalog_preflight.py' + - 'tests/test_collage_catalog_preflight.py' + - '.github/workflows/official-minecraft-catalog-preflight.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + actual-official-catalog: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Cheap catalog guard and 404 regression tests + run: | + set -euo pipefail + python3 -m py_compile scripts/check_collage_catalog_preflight.py + python3 -m unittest -v tests.test_collage_catalog_preflight + - name: Verify upstream live official master with exact immutable 1.15.30 admission + run: | + set -euo pipefail + python3 scripts/check_collage_catalog_preflight.py --expected 1.15.30 \ + > "$RUNNER_TEMP/official-catalog-result.json" + python3 - "$RUNNER_TEMP/official-catalog-result.json" <<'PY' + import json,sys + v=json.load(open(sys.argv[1])) + assert v["classification"] == "PRE_KVM_CATALOG_VERSION_AVAILABLE" + assert v["expected_version"] == "1.15.30" + assert "1.15.30" in v["visible_versions"] + assert v["guest_catalog_observed"] is False + assert v["live_minecraft_qualified"] is False + assert v["world_mutated"] is False + PY + - name: Report exact read-only external preflight boundary + if: always() + run: echo 'Only official source catalog read; no KVM, native TrueNAS App, game-world mutation or physical APPLY.' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/check_collage_catalog_preflight.py b/scripts/check_collage_catalog_preflight.py index 6af3fb9dd..feb351398 100644 --- a/scripts/check_collage_catalog_preflight.py +++ b/scripts/check_collage_catalog_preflight.py @@ -3,7 +3,9 @@ Do not confuse this remote branch preflight with in-guest native catalog proof. The official TrueNAS Apps catalog is independently updated and may retire old -versions despite a pinned Foundry source render. No credentials or mutation. +versions despite a pinned Foundry source render. Query the vendor's published +master branch (not the nonexistent main branch); caller still requires exact +pinned catalog version in the response. No credentials or mutation. """ from __future__ import annotations @@ -14,7 +16,7 @@ import urllib.error import urllib.request -URI = "https://api.github.com/repos/truenas/apps/contents/trains/community/minecraft?ref=main" +URI = "https://api.github.com/repos/truenas/apps/contents/trains/community/minecraft?ref=master" VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$") diff --git a/tests/test_collage_catalog_preflight.py b/tests/test_collage_catalog_preflight.py index 1065dd8a8..f42073c7d 100644 --- a/tests/test_collage_catalog_preflight.py +++ b/tests/test_collage_catalog_preflight.py @@ -1,9 +1,18 @@ """Offline source tests: preflight must reject missing pins, not launch a VM.""" import unittest import pathlib -from scripts.check_collage_catalog_preflight import evaluate, version_inventory +from scripts.check_collage_catalog_preflight import evaluate, version_inventory, URI class CatalogPreflightTests(unittest.TestCase): + def test_public_vendor_catalog_uses_real_master_not_nonexistent_main(self): + # 2026-10-14 causal failure of guest run 38488438084: + # official truenas/apps default branch = master; ?ref=main HTTP 404. + self.assertEqual( + URI, + "https://api.github.com/repos/truenas/apps/contents/trains/community/minecraft?ref=master", + ) + self.assertNotIn("?ref=main", URI) + def test_available_exact_pin(self): rows=[{"name":"1.15.29","type":"dir"},{"name":"item.yaml","type":"file"}] v=evaluate(rows,"1.15.29") From 7ed95e6741df9757669f6cc18c275177bc2e3a64 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:17:34 +0200 Subject: [PATCH 669/719] Admit one fresh vanilla26.3 TrueNAS native guest after official master catalog correction (#424) * OP-4B: source-only test binds absent actuator, not globally forbidden guest requests * OP-4B: issue fresh 003 exact guest request after independently verified master catalog, config/truenas-rdte-run-request.json * OP-4B: issue fresh 003 exact guest request after independently verified master catalog, tests/test_truenas_rdte_target.py * OP-4B: issue fresh 003 exact guest request after independently verified master catalog, tests/test_vanilla263_t6_guest_routing.py --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 2 +- tests/test_vanilla263_t6_source_admission.py | 14 ++++++++++---- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index de0ec422c..ebb8c5515 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,10 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4b-002-native-guest", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4b-003-master-catalog-qualified", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "One uniquely identified public/free-tier disposable TrueNAS 25.04.2.6 KVM T6 native minecraft-vanilla-263 Java25/protocol777 guest. Preserve exact accepted merged Foundry producer 29ec6cba7df51e316b0329926aa85807805dd07b and canonical source-control digest c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615, pinned upstream 1.15.30 Java25 OCI SHA5a2ab..., independent rdtepool/minecraft-vanilla-263-t6-world, product-specific provider readback and status/oracle across create/restart/redeploy/reinstall/delete with zero residue. New request edge after independent signed-source PR #417, native guest adapter PR #418, selector PR #419 and cheap gates PASS. Do not inherit Paper 1.21.11/25.04.1 evidence. Never mutate physical TrueNAS, never consume private GHA minutes, never replay unchanged request_id. On failure retain sanitized evidence and hold subsequent heavyweight jobs until diagnosis." + "purpose": "One uniquely identified public/free-tier disposable TrueNAS 25.04.2.6 KVM T6 native minecraft-vanilla-263 Java25/protocol777 guest. Preserve exact accepted merged Foundry producer 29ec6cba7df51e316b0329926aa85807805dd07b and canonical source-control digest c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615, pinned upstream 1.15.30 Java25 OCI SHA5a2ab..., independent rdtepool/minecraft-vanilla-263-t6-world, product-specific provider readback and status/oracle across create/restart/redeploy/reinstall/delete with zero residue. Fresh one-time request after native signed-source PR #417, guest observer PR #418, selector PR #419, original request PR #421, and official catalog preflight 404 correction PR #422 with public real master-branch preflight SUCCESS (38488860714). Prior OP4B-002 request failed PRE-KVM at hard-coded ref=main; this is not a guest retry of existing request ID. Do not inherit Paper 1.21.11/25.04.1 evidence. Never mutate physical TrueNAS, never consume private GHA minutes, never replay unchanged request_id. On failure retain sanitized evidence and hold subsequent heavyweight jobs until diagnosis." } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index eda667195..764074d13 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4b-002-native-guest") + "tn-25-04-2-6-t6-vanilla-263-op4b-003-master-catalog-qualified") if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 16a73da11..8de08cf2d 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-002-native-guest") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-003-master-catalog-qualified") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) diff --git a/tests/test_vanilla263_t6_source_admission.py b/tests/test_vanilla263_t6_source_admission.py index b363b8be4..6475043a3 100644 --- a/tests/test_vanilla263_t6_source_admission.py +++ b/tests/test_vanilla263_t6_source_admission.py @@ -136,15 +136,21 @@ def test_owner_env_duplicate_and_foreign_mount_refused(self): readmit_local_fixture(doc) def test_source_only_control_does_not_dispatch_heavy_guest(self): - current=(ROOT/"config/truenas-rdte-run-request.json").read_text() - self.assertNotIn('"product": "minecraft-vanilla-263"',current) + # Source qualification has no actuator, even once an independently + # admitted request-edge legitimately selects vanilla263 for a guest. proposed=json.loads((ROOT/"config/truenas-vanilla263-native-admission-request.json").read_text()) self.assertEqual(proposed["version"],"25.04.2.6") self.assertEqual(proposed["product"],"minecraft-vanilla-263") self.assertEqual(proposed["consumer_authority_issue"],627) self.assertIn("NOT wired",proposed["purpose"]) - workflow=(ROOT/".github/workflows/gha-kvm-system-rdte.yml").read_text() - self.assertNotIn("minecraft-vanilla-263",workflow) + workflow=(ROOT/".github/workflows/vanilla263-t6-source-admission.yml").read_text() + self.assertIn("scripts/admit_vanilla263_native_t6_control.py",workflow) + self.assertIn("SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED",workflow) + self.assertNotIn("gha_kvm_truenas_rdte.sh",workflow) + self.assertNotIn("--t6-product",workflow) + self.assertNotIn("config/truenas-rdte-run-request.json",workflow) + self.assertNotIn("native_guest_qualified\"] is True",workflow) + if __name__=="__main__": unittest.main() From cca995304b096266ddb488256d6a06051be408bf Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:45:19 +0200 Subject: [PATCH 670/719] Correct scoped COLLAGE observer grants to POSIX GID, not database group ID (#427) * Correct guest APPS_READ privilege local_groups to POSIX GID and verify normalized readback; never broaden scope * Adversarially distinguish TrueNAS group datastore id from POSIX gid for scoped APPS_READ grant * Fix direct group.query filter structure assertion: datastore ID not POSIX gid --- .../truenas_middleware_collage_readback_p2.py | 13 +++++++++++-- tests/test_collage_p2_effective_roles.py | 19 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 55103ab17..9edb49684 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -77,16 +77,25 @@ def provision_guest_readonly(call) -> str: require(isinstance(user, dict) and user.get("username") == NAME, "READONLY_ACCOUNT_CREATION_REFUSED") group = user.get("group") - require(isinstance(group, dict) and type(group.get("id")) is int, + require(isinstance(group, dict) and type(group.get("id")) is int + and type(group.get("gid")) is int, "READONLY_ACCOUNT_GROUP_UNAVAILABLE") + # Exact TrueNAS 25.04.2.6 middleware privilege.create.local_groups + # requires POSIX GIDs, NOT middleware account.group datastore IDs. + # group.query(id=...) and user.query(group.id) still use datastore IDs. privilege = call("privilege.create", [{ - "name": ROLE_NAME, "local_groups": [group["id"]], + "name": ROLE_NAME, "local_groups": [group["gid"]], "roles": ["APPS_READ"], "web_shell": False, }]) require(isinstance(privilege, dict) and privilege.get("name") == ROLE_NAME, "APPS_READ_ASSIGNMENT_UNVERIFIED") require(privilege.get("roles") == ["APPS_READ"], "CREATED_PRIVILEGE_ROLES_NOT_EXACT") + members = privilege.get("local_groups") + require(type(members) is list and len(members) == 1 + and type(members[0]) is dict + and members[0].get("gid") == group["gid"], + "CREATED_PRIVILEGE_GID_LINK_NOT_EXACT") # Distinguish role/primary-group propagation problems from extra authority, # without emitting public role names, user metadata or sensitive API fields. primary = call("group.query", [[["id", "=", group["id"]]], {"get": True}]) diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index f8e1dd9c1..32458e02b 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -9,10 +9,12 @@ USER = "collage_p2_observer" PRIVILEGE = "collage-p2-apps-read" GROUP_ID = 1013 +GROUP_GID = 32013 # POSIX GID, deliberately not middleware group datastore id def authority_responses(*, primary_roles=None, user_roles=None, - user_group_id=GROUP_ID, privilege_roles=None): + user_group_id=GROUP_ID, privilege_roles=None, + group_gid=GROUP_GID, privilege_group_gid=GROUP_GID): primary_roles = [ROLE] if primary_roles is None else primary_roles user_roles = [ROLE] if user_roles is None else user_roles privilege_roles = [ROLE] if privilege_roles is None else privilege_roles @@ -20,8 +22,9 @@ def authority_responses(*, primary_roles=None, user_roles=None, ("user.query", []), ("privilege.query", []), ("privilege.roles", [{"name": ROLE, "includes": []}]), - ("user.create", {"username": USER, "group": {"id": GROUP_ID}}), - ("privilege.create", {"name": PRIVILEGE, "roles": privilege_roles}), + ("user.create", {"username": USER, "group": {"id": GROUP_ID, "gid": group_gid}}), + ("privilege.create", {"name": PRIVILEGE, "roles": privilege_roles, + "local_groups": [{"gid": privilege_group_gid}]}), ("group.query", {"id": GROUP_ID, "roles": primary_roles}), ("user.query", {"username": USER, "group": {"id": user_group_id}, "roles": user_roles}), @@ -38,6 +41,14 @@ def call(method, params): self.assertLess(len(called), len(responses), "unexpected API call") want, result = responses[len(called)] self.assertEqual(method, want, f"wrong operation at position {len(called)}") + if method == "privilege.create": + # Caller must send UNIX GID, not middleware group datastore id. + self.assertEqual(params[0]["local_groups"], [GROUP_GID]) + self.assertNotEqual(params[0]["local_groups"], [GROUP_ID]) + self.assertEqual(params[0]["roles"], ["APPS_READ"]) + if method == "group.query": + self.assertEqual(params[0][0][0], "id") + self.assertEqual(params[0][0][2], GROUP_ID) called.append(method) return result return call, called @@ -61,6 +72,8 @@ def test_unauthorized_roles_never_receive_api_key(self): "OBSERVER_PRIMARY_GROUP_ROLE_NOT_EXACT"), ({"privilege_roles": ["APPS_READ", "FULL_ADMIN"]}, "CREATED_PRIVILEGE_ROLES_NOT_EXACT"), + ({"privilege_group_gid": GROUP_GID + 1}, + "CREATED_PRIVILEGE_GID_LINK_NOT_EXACT"), ] for opts, reason in cases: with self.subTest(opts=opts): From c869a5a4d0dfe0d46f0c724167d7097660552f5d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 08:57:36 +0200 Subject: [PATCH 671/719] Add opt-in scoped COLLAGE TLS App readback to native vanilla263 guest (#428) * OP-4C: parameterize existing strictly read-only COLLAGE P2 observer for exact vanilla263 App owner * OP-4C: pass exact vanilla263 profile to already bounded guest-only COLLAGE Go read-only observer * OP-4C: strict native guest read-only request tuple; legacy Paper untouched and unplayable fixture explicit * OP-4C: opt-in only native vanilla guest read-only COLLAGE observer with exact source SHA and strict scope * OP-4C: separate fail-closed native guest Go APPS_READ/TLS readback acceptance flag * OP-4C: read-only TLS/APPS_READ/source-identity receipt positives and forged role/guest negatives * OP-4C: optional request-authorized native vanilla APPS_READ TLS Go probe, single existing guest job with independent receipt * OP-4C: negative static guard that native COLLAGE TLS readback is opt-in and never guest-default * Correct source-only readback flag test to actual workflow if scope line --- .github/workflows/gha-kvm-system-rdte.yml | 43 +++++++++++++- .../assert_vanilla263_native_t6_receipt.py | 30 +++++++++- scripts/gha_kvm_truenas_rdte.sh | 19 ++++++- .../truenas_middleware_collage_readback_p2.py | 57 ++++++++++++++----- .../truenas_middleware_minecraft_t6_probe.py | 1 + tests/test_collage_p2_roles.py | 27 +++++++++ tests/test_truenas_t6_vanilla263_guest.py | 46 +++++++++++++++ tests/test_vanilla263_t6_guest_routing.py | 24 ++++++++ 8 files changed, 229 insertions(+), 18 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 424b961fd..a32c70bbc 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -92,6 +92,7 @@ jobs: truenas_version: ${{ steps.scope.outputs.truenas_version }} truenas_rung: ${{ steps.scope.outputs.truenas_rung }} truenas_t6_product: ${{ steps.scope.outputs.truenas_t6_product }} + truenas_collage_readback: ${{ steps.scope.outputs.truenas_collage_readback }} truenas_compute_fixture: ${{ steps.scope.outputs.truenas_compute_fixture }} proxmox_compute_fixture: ${{ steps.scope.outputs.proxmox_compute_fixture }} steps: @@ -120,6 +121,7 @@ jobs: truenas_version="${DISPATCH_TRUENAS_VERSION:-26.0.0-BETA.3}" truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" truenas_t6_product="${DISPATCH_TRUENAS_T6_PRODUCT:-litellm}" + truenas_collage_readback=false truenas_compute_fixture="${DISPATCH_TRUENAS_COMPUTE_FIXTURE:-none}" proxmox_compute_fixture="${DISPATCH_PROXMOX_COMPUTE_FIXTURE:-none}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then @@ -190,6 +192,12 @@ jobs: truenas_rung="$(jq -er '.rung | select(. == "t0" or . == "t1" or . == "t2" or . == "t3" or . == "t4" or . == "t5" or . == "t6")' "$RUN_REQUEST_PATH")" truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay" or . == "minecraft-paper" or . == "collage-native-paper" or . == "minecraft-vanilla-263")' "$RUN_REQUEST_PATH")" truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" + truenas_collage_readback="$(jq -er '(.collage_readback // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + if [[ "$truenas_collage_readback" == "true" ]]; then + [[ "$truenas_t6_product" == "minecraft-vanilla-263" && "$truenas_version" == "25.04.2.6" && "$truenas_rung" == "t6" ]] || { + echo "read-only COLLAGE observer requires exact native vanilla263/25.04.2.6/T6" >&2; exit 2; + } + fi python3 scripts/truenas_rdte_target.py \ --registry config/truenas-rdte-targets.json \ --version "$truenas_version" >/dev/null @@ -205,6 +213,7 @@ jobs: echo "truenas_version=$truenas_version" >>"$GITHUB_OUTPUT" echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" echo "truenas_t6_product=$truenas_t6_product" >>"$GITHUB_OUTPUT" + echo "truenas_collage_readback=$truenas_collage_readback" >>"$GITHUB_OUTPUT" echo "truenas_compute_fixture=$truenas_compute_fixture" >>"$GITHUB_OUTPUT" echo "proxmox_compute_fixture=$proxmox_compute_fixture" >>"$GITHUB_OUTPUT" printf 'target scope: proxmox=%s proxmox_compute_fixture=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s truenas_compute_fixture=%s\n' "$proxmox" "$proxmox_compute_fixture" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" @@ -593,6 +602,31 @@ jobs: (cd .deps/collage-source/tools/truenas-readonly-probe && go mod download && go mod verify && go test ./... && go build -o /dev/null .) + - name: Checkout pinned COLLAGE 25.04.2.6 vanilla read-only observer + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_collage_readback == 'true' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SupraCraft/collage + ref: 17dfacc4351053fa06f8bdbaed7d931c97c1707e + path: .deps/collage-source + persist-credentials: false + - name: Prepare pinned scoped native vanilla COLLAGE Go read-only observer + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_collage_readback == 'true' + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version-file: .deps/collage-source/tools/truenas-readonly-probe/go.mod + cache: false + - name: Verify actual COLLAGE read-only Go source and negative semantics before KVM + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_collage_readback == 'true' + run: | + set -euo pipefail + test "$(git -C .deps/collage-source rev-parse HEAD)" = 17dfacc4351053fa06f8bdbaed7d931c97c1707e + (cd .deps/collage-source && + python3 -m unittest -v tests.test_collage_native_readback tests.test_consume_vanilla_datapack_source) + (cd .deps/collage-source/tools/truenas-readonly-probe && + go mod download && go mod verify && + go test ./... && go build -o /dev/null .) + - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c @@ -716,6 +750,9 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-paper" ]]; then args+=(--session-manifest config/truenas-session-minecraft-paper-25-04-1.json) fi + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-vanilla-263" && "${{ needs.changes.outputs.truenas_collage_readback }}" == "true" ]]; then + args+=(--vanilla-collage-readback) + fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" # A green job is not a Minecraft product pass without full guest evidence. @@ -724,7 +761,11 @@ jobs: python3 scripts/assert_minecraft_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" fi if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-vanilla-263" ]]; then - python3 scripts/assert_vanilla263_native_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" + additional=() + if [[ "${{ needs.changes.outputs.truenas_collage_readback }}" == "true" ]]; then + additional+=(--require-collage-readback) + fi + python3 scripts/assert_vanilla263_native_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" "${additional[@]}" fi if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then python3 scripts/assert_collage_p2_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py index edde660dc..1af4166b0 100644 --- a/scripts/assert_vanilla263_native_t6_receipt.py +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -19,7 +19,7 @@ IMAGE_SHA = OCI.split("@sha256:", 1)[1] -def check(root: dict) -> None: +def check(root: dict, *, require_collage: bool = False) -> None: def need(condition: bool, reason: str) -> None: if not condition: raise ValueError(reason) @@ -100,14 +100,40 @@ def need(condition: bool, reason: str) -> None: "guest receipt admits secrets") need((root.get("foundry_materialization") or {}).get("target") == TARGET, "guest receipt cross-target mismatch") + if require_collage: + observer = p.get("collage_readback") or {} + need(type(observer) is dict and + observer.get("schema") == "semper-supra.collage-native-readback-p2/1" and + observer.get("classification") == "PROVIDER_READBACK_MATCH" and + observer.get("source_profile") == "vanilla263" and + observer.get("source_sha") == "17dfacc4351053fa06f8bdbaed7d931c97c1707e" and + observer.get("native_app") == APP and + observer.get("truenas_version") == TARGET and + observer.get("world_id") == "lantern-rdte" and + observer.get("service_id") == "11111111-1111-4111-8111-111111111111" and + observer.get("ownership_fields_matched") == 6 and + observer.get("transport") == "isolated_guest_tls_verified" and + observer.get("credential_scope") == "APPS_READ" and + observer.get("credential_mutation_phase") == + "disposable_guest_setup_before_observation" and + observer.get("observation_mutation_count") == 0 and + observer.get("world_artifact_format_verified") is False and + observer.get("world_activated") is False and + observer.get("cloudnet_adoption_executed") is False and + observer.get("physical_true_nas_mutated") is False and + isinstance(observer.get("tls_cert_sha256"), str) and + len(observer["tls_cert_sha256"]) == 64, + "independent scoped native vanilla readback missing or unqualified") def main() -> int: p = argparse.ArgumentParser() p.add_argument("--receipt", required=True, type=Path) + p.add_argument("--require-collage-readback", action="store_true") a = p.parse_args() try: - check(json.loads(a.receipt.read_text(encoding="utf-8"))) + check(json.loads(a.receipt.read_text(encoding="utf-8")), + require_collage=a.require_collage_readback) except (OSError, ValueError, TypeError) as ex: # Error messages generated by check() are constant, never propagate # arbitrary provider receipt fields, credentials or real addresses. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 715500935..02eea9e55 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -18,7 +18,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper|collage-native-paper|minecraft-vanilla-263] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper|collage-native-paper|minecraft-vanilla-263] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE] [--vanilla-collage-readback]" } OUT="" @@ -39,6 +39,7 @@ G5_MATRIX_PRODUCER="" CONTAINER_FIXTURE_DIR="" CONTAINER_FIXTURE_PRODUCER="" SESSION_MANIFEST="" +VANILLA_COLLAGE_READBACK=false while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -60,6 +61,7 @@ while [[ $# -gt 0 ]]; do --container-fixture-dir) CONTAINER_FIXTURE_DIR="$2"; shift 2 ;; --container-fixture-producer) CONTAINER_FIXTURE_PRODUCER="$2"; shift 2 ;; --session-manifest) SESSION_MANIFEST="$2"; shift 2 ;; + --vanilla-collage-readback) VANILLA_COLLAGE_READBACK=true; shift ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -75,6 +77,10 @@ if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then echo "compute fixture $COMPUTE_FIXTURE requires rung t3" >&2 exit 2 fi +if [[ "$VANILLA_COLLAGE_READBACK" == "true" ]]; then + [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || + { echo "vanilla scoped readback only admitted on exact 25.04.2.6/T6/vanilla263" >&2; exit 2; } +fi if [[ "$RUNG" == "t6" ]]; then if [[ "$T6_PRODUCT" != "official-catalog" && "$T6_PRODUCT" != "garm-provider-g5" && "$T6_PRODUCT" != "foliorelay" && "$T6_PRODUCT" != "minecraft-paper" && "$T6_PRODUCT" != "garm-controller-f0f5" && "$T6_PRODUCT" != "minecraft-vanilla-263" ]]; then if [[ "$T6_PRODUCT" == "collage-native-paper" ]]; then @@ -1182,6 +1188,16 @@ elif [[ "$T6_PRODUCT" == "minecraft-paper" || "$T6_PRODUCT" == "collage-native-p --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then [[ "$VERSION" == "25.04.2.6" ]] || fail_evidence HARNESS_FAILURE foundry-materialization "native vanilla guest exact version drift" + NATIVE_READBACK_ARGS=() + if [[ "$VANILLA_COLLAGE_READBACK" == "true" ]]; then + [[ -d "$REPO_ROOT/.deps/collage-source" ]] || + fail_evidence HARNESS_FAILURE foundry-materialization "exact current COLLAGE read-only checkout absent" + NATIVE_READBACK_ARGS=( + --collage-source-dir "$REPO_ROOT/.deps/collage-source" + --collage-https-port "$HTTPS_PORT" + --collage-source-ref "17dfacc4351053fa06f8bdbaed7d931c97c1707e" + ) + fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ --product-profile vanilla263 --native-catalog-profile 1.15.30 \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ @@ -1190,6 +1206,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then "${MIDDLEWARE_TLS_ARG[@]}" \ --password-file "$PASSWORD_FILE" --control-dir "$FOUNDRY_CONTROL_DIR" \ --foundry-commit "$FOUNDRY_COMMIT" --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ + "${NATIVE_READBACK_ARGS[@]}" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 9edb49684..c1fc7927b 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -26,6 +26,9 @@ ROLE_NAME = "collage-p2-apps-read" OWNER = "rdte-t6-minecraft-paper" WORLD_ID = "world-rdte-001" +VANILLA263_OWNER = "rdte-t6-minecraft-vanilla-263" +VANILLA263_WORLD = "lantern-rdte" +VANILLA263_SOURCE_SHA = "17dfacc4351053fa06f8bdbaed7d931c97c1707e" DEFAULT_SOURCE_SHA = "fbda94f3fea0a216ddd9c35b38a09d325bed3640" @@ -162,9 +165,38 @@ def discover_tls_anchor(port: int, out: pathlib.Path) -> tuple[str, str]: return hostname, hashlib.sha256(der).hexdigest() +def exact_source_readback_request(plan: dict, *, profile: str) -> tuple[dict, str, str]: + """Build only a read-only source intent; never establish artifact format. + + Use the already admitted COLLAGE source-only fixture for the provider + ownership probe. This fixture is NOT a playable datapack and is never + unpacked, applied or counted as real-world artifact deployment. + """ + require(profile in {"paper", "vanilla263"}, "COLLAGE_READBACK_PROFILE_UNSUPPORTED") + request = json.loads(json.dumps(plan)) + native = profile == "vanilla263" + owner = VANILLA263_OWNER if native else OWNER + world = VANILLA263_WORLD if native else WORLD_ID + request["request_id"] = "collage-native-vanilla263-readonly" if native else "collage-p2-native-paper" + request["target"].update({ + "deployment_id": owner, "service_id": SERVICE_UUID, + "world_id": world, "minecraft_version": "26.3" if native else "1.21.11", + "backend": "vanilla" if native else "paper", "purpose": "generic", + }) + request["artifact"].update({ + "world_id": world, "minecraft_version": "26.3" if native else "1.21.11", + }) + if native: + # The source artifact remains an intentionally unplayable fixture. + # This is permissible only for read-only provider observation, where + # artifact_format_verified must remain false; not actual deployment. + request["artifact"]["kind"] = "datapack" + return request, owner, world + + def run_real_readback(call, *, collage: pathlib.Path, https_port: int, state_dir: pathlib.Path, expected_source_sha: str - = DEFAULT_SOURCE_SHA) -> dict: + = DEFAULT_SOURCE_SHA, profile: str = "paper") -> dict: """Return ONLY allowlisted, non-secret independent readback evidence.""" require(collage.is_dir(), "PINNED_COLLAGE_CHECKOUT_MISSING") git = subprocess.run(["git", "-C", str(collage), "rev-parse", "HEAD"], @@ -175,16 +207,11 @@ def run_real_readback(call, *, collage: pathlib.Path, https_port: int, fixture = collage / "examples" / "contract-only-world.fixture" require(source_plan.is_file() and fixture.is_file(), "SOURCE_ONLY_FIXTURE_UNAVAILABLE") - request = json.loads(source_plan.read_text(encoding="utf-8")) - request["request_id"] = "collage-p2-native-paper" - request["target"].update({ - "deployment_id": OWNER, "service_id": SERVICE_UUID, - "world_id": WORLD_ID, "minecraft_version": "1.21.11", - "backend": "paper", "purpose": "generic", - }) - request["artifact"].update({ - "world_id": WORLD_ID, "minecraft_version": "1.21.11", - }) + request, expected_owner, expected_world = exact_source_readback_request( + json.loads(source_plan.read_text(encoding="utf-8")), profile=profile) + if profile == "vanilla263": + require(expected_source_sha == VANILLA263_SOURCE_SHA, + "VANILLA263_COLLAGE_SOURCE_REF_DRIFT") source = state_dir / "collage-p2-request.json" source.write_text(json.dumps(request, indent=2, sort_keys=True) + "\n") key = provision_guest_readonly(call) @@ -214,21 +241,23 @@ def run_real_readback(call, *, collage: pathlib.Path, https_port: int, and verdict.get("mutation_authorized") is False and verdict.get("ownership_fields_matched") == 6 and verdict.get("service_id") == SERVICE_UUID - and verdict.get("native_app") == OWNER, + and verdict.get("native_app") == expected_owner + and verdict.get("world_id") == expected_world, "NATIVE_READONLY_GUEST_JOIN_FAILED") return { "schema": SCHEMA, "classification": "PROVIDER_READBACK_MATCH", "source_sha": expected_source_sha, "source_plan_sha256": verdict["source_plan_sha256"], - "native_app": OWNER, + "native_app": expected_owner, "truenas_version": verdict["truenas_version"], "service_id": SERVICE_UUID, - "world_id": WORLD_ID, + "world_id": expected_world, "ownership_fields_matched": 6, "tls_cert_sha256": cert_sha, "transport": "isolated_guest_tls_verified", "credential_scope": "APPS_READ", + "source_profile": profile, "credential_mutation_phase": "disposable_guest_setup_before_observation", "observation_mutation_count": 0, "world_artifact_format_verified": False, diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index a0b4c831b..33d5feb1c 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -623,6 +623,7 @@ def delete_app(label): https_port=a.collage_https_port, state_dir=a.out.parent, expected_source_sha=a.collage_source_ref, + profile="vanilla263" if VANILLA263 else "paper", ) except ObservationError as err: # The helper emits only a fixed reason code, no raw auth, diff --git a/tests/test_collage_p2_roles.py b/tests/test_collage_p2_roles.py index d2dd6aaac..30c84a5b7 100644 --- a/tests/test_collage_p2_roles.py +++ b/tests/test_collage_p2_roles.py @@ -3,10 +3,37 @@ from scripts.truenas_middleware_collage_readback_p2 import ( ObservationError, available_role_names, provision_guest_readonly, + exact_source_readback_request, VANILLA263_OWNER, VANILLA263_WORLD, ) class RolesTests(unittest.TestCase): + def test_native_vanilla263_source_readback_mode_exact_and_nonplayable(self): + source = { + "request_id": "original-source", + "target": {"provider": "truenas"}, + "artifact": {"kind": "world-save", "sha256": "a" * 64}, + } + native,app,world = exact_source_readback_request(source, profile="vanilla263") + self.assertEqual(app,VANILLA263_OWNER) + self.assertEqual(world,VANILLA263_WORLD) + self.assertEqual(native["target"]["deployment_id"], "rdte-t6-minecraft-vanilla-263") + self.assertEqual(native["target"]["backend"], "vanilla") + self.assertEqual(native["target"]["minecraft_version"], "26.3") + self.assertEqual(native["target"]["world_id"], "lantern-rdte") + self.assertEqual(native["target"]["service_id"], "11111111-1111-4111-8111-111111111111") + self.assertEqual(native["artifact"]["kind"], "datapack") + self.assertEqual(native["artifact"]["minecraft_version"], "26.3") + self.assertEqual(native["artifact"]["world_id"], "lantern-rdte") + self.assertEqual(source["target"], {"provider": "truenas"}) + self.assertEqual(source["artifact"]["kind"], "world-save") + paper,oldapp,oldworld = exact_source_readback_request(source, profile="paper") + self.assertEqual(oldapp,"rdte-t6-minecraft-paper") + self.assertEqual(oldworld,"world-rdte-001") + self.assertEqual(paper["artifact"]["kind"],"world-save") + with self.assertRaisesRegex(ObservationError,"COLLAGE_READBACK_PROFILE_UNSUPPORTED"): + exact_source_readback_request(source,profile="operations") + def test_role_descriptors_are_an_array(self): rows = [ {"name": "APPS_READ", "includes": [], "builtin": False, "stig": None}, diff --git a/tests/test_truenas_t6_vanilla263_guest.py b/tests/test_truenas_t6_vanilla263_guest.py index 6281764c3..f42d37109 100644 --- a/tests/test_truenas_t6_vanilla263_guest.py +++ b/tests/test_truenas_t6_vanilla263_guest.py @@ -60,6 +60,52 @@ class TestNativeVanilla263Guest(unittest.TestCase): def test_full_independent_guest_oracle_fixture(self): check_guest(guest_fixture()) + def test_optional_native_collage_readback_requires_independent_tls_and_apps_read(self): + root=guest_fixture() + with self.assertRaisesRegex(ValueError,"independent scoped native vanilla readback"): + check_guest(root,require_collage=True) + proof={ + "schema":"semper-supra.collage-native-readback-p2/1", + "classification":"PROVIDER_READBACK_MATCH", + "source_profile":"vanilla263", + "source_sha":"17dfacc4351053fa06f8bdbaed7d931c97c1707e", + "native_app":native.APP, "truenas_version":"25.04.2.6", + "world_id":"lantern-rdte", + "service_id":"11111111-1111-4111-8111-111111111111", + "ownership_fields_matched":6, + "transport":"isolated_guest_tls_verified", + "tls_cert_sha256":"a"*64, + "credential_scope":"APPS_READ", + "credential_mutation_phase":"disposable_guest_setup_before_observation", + "observation_mutation_count":0, + "world_artifact_format_verified":False, + "world_activated":False, + "cloudnet_adoption_executed":False, + "physical_true_nas_mutated":False, + } + root["foundry_materialization"]["collage_readback"]=proof + check_guest(root,require_collage=True) + for field,bad in ( + ("classification","SOURCE_VALIDATED"), + ("source_profile","paper"),("source_sha","f"*40), + ("native_app","rdte-t6-minecraft-paper"), + ("truenas_version","25.04.1"), + ("world_id","world-rdte-001"), + ("ownership_fields_matched",5), + ("transport","unverified-tls"), + ("credential_scope","FULL_ADMIN"), + ("observation_mutation_count",1), + ("world_activated",True), + ("tls_cert_sha256","invalid"), + ): + with self.subTest(field=field): + changed=copy.deepcopy(root) + changed["foundry_materialization"]["collage_readback"][field]=bad + with self.assertRaises(ValueError): + check_guest(changed,require_collage=True) + # Legacy native guest acceptance remains unchanged by absent readback. + check_guest(guest_fixture(),require_collage=False) + def test_never_promote_paper_legacy_or_source_only(self): for change in ("legacy_version","legacy_paper","source_only","wrong_phase"): r=guest_fixture() diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 8de08cf2d..36286bb2d 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -61,6 +61,30 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertIn("config/truenas-rdte-run-request.json)",WORKFLOW) self.assertIn('"$current_request_id" == "$previous_request_id"',WORKFLOW) + def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): + self.assertIn('truenas_collage_readback=false',WORKFLOW) + self.assertIn('(.collage_readback // false)',WORKFLOW) + self.assertIn('truenas_collage_readback=$truenas_collage_readback',WORKFLOW) + self.assertIn('--vanilla-collage-readback',WORKFLOW) + self.assertIn('needs.changes.outputs.truenas_collage_readback',WORKFLOW) + self.assertIn('--require-collage-readback',WORKFLOW) + self.assertIn('ref: 17dfacc4351053fa06f8bdbaed7d931c97c1707e',WORKFLOW) + self.assertIn('[[ "$truenas_collage_readback" == "true" ]]',WORKFLOW) + self.assertIn('"$T6_PRODUCT" == "minecraft-vanilla-263"',HARNESS) + self.assertIn('VANILLA_COLLAGE_READBACK=false',HARNESS) + self.assertIn('--collage-source-ref "17dfacc4351053fa06f8bdbaed7d931c97c1707e"',HARNESS) + self.assertIn('NATIVE_READBACK_ARGS=()',HARNESS) + self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) + request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) + self.assertNotIn("collage_readback",request) + self.assertNotIn("--vanilla-collage-readback", + str(request)) + # Synthetic source tests cannot count as a live TLS/Go observation. + probe=(ROOT/"scripts/truenas_middleware_collage_readback_p2.py").read_text() + self.assertIn('VANILLA263_SOURCE_SHA',probe) + self.assertIn('profile="vanilla263" if VANILLA263 else "paper"', + (ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text()) + def test_no_fallback_guest_or_physical_promotion(self): self.assertNotIn('product == "minecraft-vanilla-263" && needs[\'foundry-minecraft-paper',WORKFLOW) self.assertIn("agent-dispatch-heavyweight-rdte",WORKFLOW) From be8772f51809782557c5246c7de43b9986bba47e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:00:35 +0200 Subject: [PATCH 672/719] Admit one disposable vanilla 26.3 scoped TLS read-only COLLAGE guest request (#430) * OP-4C: one unique exact native vanilla T6 public guest request with scoped COLLAGE TLS readback * OP-4C: source gate exact one-time 004 scoped readback request and authority negatives * OP-4C: source gate exact one-time 004 scoped readback request and authority negatives --- config/truenas-rdte-run-request.json | 5 +++-- tests/test_truenas_rdte_target.py | 6 +++++- tests/test_vanilla263_t6_guest_routing.py | 7 +++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index ebb8c5515..2d9268377 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,10 +1,11 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4b-003-master-catalog-qualified", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "One uniquely identified public/free-tier disposable TrueNAS 25.04.2.6 KVM T6 native minecraft-vanilla-263 Java25/protocol777 guest. Preserve exact accepted merged Foundry producer 29ec6cba7df51e316b0329926aa85807805dd07b and canonical source-control digest c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615, pinned upstream 1.15.30 Java25 OCI SHA5a2ab..., independent rdtepool/minecraft-vanilla-263-t6-world, product-specific provider readback and status/oracle across create/restart/redeploy/reinstall/delete with zero residue. Fresh one-time request after native signed-source PR #417, guest observer PR #418, selector PR #419, original request PR #421, and official catalog preflight 404 correction PR #422 with public real master-branch preflight SUCCESS (38488860714). Prior OP4B-002 request failed PRE-KVM at hard-coded ref=main; this is not a guest retry of existing request ID. Do not inherit Paper 1.21.11/25.04.1 evidence. Never mutate physical TrueNAS, never consume private GHA minutes, never replay unchanged request_id. On failure retain sanitized evidence and hold subsequent heavyweight jobs until diagnosis." + "purpose": "OP-4C unique one-time public/free-tier disposable TrueNAS25.04.2.6 T6 native vanilla26.3/Java25/protocol777 guest plus optional COLLAGE live Go TLS-verified read-only APPS_READ observer. Current native source, OCI, Foundry SHA/canonical digest, App/world/service identity unchanged. Bounded guest-only provision of one ephemeral APPS_READ key AFTER exact GID/primary/effective-role checks; do not use FULL_ADMIN, bypass TLS, leak secrets, grant caller mutation, or transfer guest credentials to operations. Reuse merged COLLAGE read-only source 17dfacc4351053fa06f8bdbaed7d931c97c1707e and accepted native App lifecycle. Observer may only attest live provider readback with zero observation mutations; checked-in source-only artifact fixture not a world/datapack deployment. Native guest invariant and independent scoped TLS/ownership receipt both mandatory; on any failure retain bounded causal receipt, no request replay. Physical TrueNAS APPLY HOLD; no private hosted minutes. Native-only OP4B-003 already passed run 38489327594; this is a distinct scoped guest observation test.", + "collage_readback": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 764074d13..11dcc4180 100755 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,11 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4b-003-master-catalog-qualified") + "tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback") + self.assertIs(request.get("collage_readback"), True) + self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", + "authority_issue", "consumer_authority_issue", + "purpose", "collage_readback"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 36286bb2d..95afc336b 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-003-master-catalog-qualified") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -76,7 +76,10 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('NATIVE_READBACK_ARGS=()',HARNESS) self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) - self.assertNotIn("collage_readback",request) + self.assertIs(request.get("collage_readback"),True) + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback") + self.assertEqual(request["product"],"minecraft-vanilla-263") + self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", str(request)) # Synthetic source tests cannot count as a live TLS/Go observation. From 2118bed802e827229241dcdc56d348d50db68bcd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:09:15 +0200 Subject: [PATCH 673/719] Pin exact corrected COLLAGE native Go Raw TrueNAS version source (#432) --- .github/workflows/gha-kvm-system-rdte.yml | 4 ++-- scripts/assert_vanilla263_native_t6_receipt.py | 2 +- scripts/gha_kvm_truenas_rdte.sh | 2 +- scripts/truenas_middleware_collage_readback_p2.py | 2 +- tests/test_truenas_t6_vanilla263_guest.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index a32c70bbc..07a1c956b 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -607,7 +607,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: repository: SupraCraft/collage - ref: 17dfacc4351053fa06f8bdbaed7d931c97c1707e + ref: 2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8 path: .deps/collage-source persist-credentials: false - name: Prepare pinned scoped native vanilla COLLAGE Go read-only observer @@ -620,7 +620,7 @@ jobs: if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_collage_readback == 'true' run: | set -euo pipefail - test "$(git -C .deps/collage-source rev-parse HEAD)" = 17dfacc4351053fa06f8bdbaed7d931c97c1707e + test "$(git -C .deps/collage-source rev-parse HEAD)" = 2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8 (cd .deps/collage-source && python3 -m unittest -v tests.test_collage_native_readback tests.test_consume_vanilla_datapack_source) (cd .deps/collage-source/tools/truenas-readonly-probe && diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py index 1af4166b0..4ac90a0f1 100644 --- a/scripts/assert_vanilla263_native_t6_receipt.py +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -106,7 +106,7 @@ def need(condition: bool, reason: str) -> None: observer.get("schema") == "semper-supra.collage-native-readback-p2/1" and observer.get("classification") == "PROVIDER_READBACK_MATCH" and observer.get("source_profile") == "vanilla263" and - observer.get("source_sha") == "17dfacc4351053fa06f8bdbaed7d931c97c1707e" and + observer.get("source_sha") == "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8" and observer.get("native_app") == APP and observer.get("truenas_version") == TARGET and observer.get("world_id") == "lantern-rdte" and diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 02eea9e55..be88c584a 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -1195,7 +1195,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then NATIVE_READBACK_ARGS=( --collage-source-dir "$REPO_ROOT/.deps/collage-source" --collage-https-port "$HTTPS_PORT" - --collage-source-ref "17dfacc4351053fa06f8bdbaed7d931c97c1707e" + --collage-source-ref "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8" ) fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index c1fc7927b..26bbd9b2a 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -28,7 +28,7 @@ WORLD_ID = "world-rdte-001" VANILLA263_OWNER = "rdte-t6-minecraft-vanilla-263" VANILLA263_WORLD = "lantern-rdte" -VANILLA263_SOURCE_SHA = "17dfacc4351053fa06f8bdbaed7d931c97c1707e" +VANILLA263_SOURCE_SHA = "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8" DEFAULT_SOURCE_SHA = "fbda94f3fea0a216ddd9c35b38a09d325bed3640" diff --git a/tests/test_truenas_t6_vanilla263_guest.py b/tests/test_truenas_t6_vanilla263_guest.py index f42d37109..43830ff61 100644 --- a/tests/test_truenas_t6_vanilla263_guest.py +++ b/tests/test_truenas_t6_vanilla263_guest.py @@ -68,7 +68,7 @@ def test_optional_native_collage_readback_requires_independent_tls_and_apps_read "schema":"semper-supra.collage-native-readback-p2/1", "classification":"PROVIDER_READBACK_MATCH", "source_profile":"vanilla263", - "source_sha":"17dfacc4351053fa06f8bdbaed7d931c97c1707e", + "source_sha":"2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8", "native_app":native.APP, "truenas_version":"25.04.2.6", "world_id":"lantern-rdte", "service_id":"11111111-1111-4111-8111-111111111111", diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 95afc336b..6de6888e4 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -68,11 +68,11 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('--vanilla-collage-readback',WORKFLOW) self.assertIn('needs.changes.outputs.truenas_collage_readback',WORKFLOW) self.assertIn('--require-collage-readback',WORKFLOW) - self.assertIn('ref: 17dfacc4351053fa06f8bdbaed7d931c97c1707e',WORKFLOW) + self.assertIn('ref: 2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8',WORKFLOW) self.assertIn('[[ "$truenas_collage_readback" == "true" ]]',WORKFLOW) self.assertIn('"$T6_PRODUCT" == "minecraft-vanilla-263"',HARNESS) self.assertIn('VANILLA_COLLAGE_READBACK=false',HARNESS) - self.assertIn('--collage-source-ref "17dfacc4351053fa06f8bdbaed7d931c97c1707e"',HARNESS) + self.assertIn('--collage-source-ref "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8"',HARNESS) self.assertIn('NATIVE_READBACK_ARGS=()',HARNESS) self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) From 31f6d2c9cd81ed90df2892b98bc65dfaf4c06e94 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:39:03 +0200 Subject: [PATCH 674/719] Read disposable observer POSIX GID via middleware group.query (#433) * Read native guest primary group GID from explicit TrueNAS group.query before creating scoped APPS_READ privilege * Red/blue: explicit TrueNAS group.query resolves real POSIX GID, refuses null, foreign or nonlocal group before API key --- .../truenas_middleware_collage_readback_p2.py | 19 ++++++++++++++----- tests/test_collage_p2_effective_roles.py | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 26bbd9b2a..f2ef9e8a1 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -80,14 +80,23 @@ def provision_guest_readonly(call) -> str: require(isinstance(user, dict) and user.get("username") == NAME, "READONLY_ACCOUNT_CREATION_REFUSED") group = user.get("group") - require(isinstance(group, dict) and type(group.get("id")) is int - and type(group.get("gid")) is int, - "READONLY_ACCOUNT_GROUP_UNAVAILABLE") + # TrueNAS user.create returns a populated group reference (datastore ID) + # but does not guarantee that it includes the Unix GID. Fetch the exact + # group by its immutable internal ID before creating a privilege. Never + # fall back to treating the database ID as POSIX GID. + require(isinstance(group, dict) and type(group.get("id")) is int, + "READONLY_ACCOUNT_PRIMARY_GROUP_ID_UNAVAILABLE") + gid_record = call("group.query", [[["id", "=", group["id"]]], {"get": True}]) + require(type(gid_record) is dict and gid_record.get("id") == group["id"] + and type(gid_record.get("gid")) is int and gid_record["gid"] >= 0 + and gid_record.get("local") is True, + "READONLY_ACCOUNT_PRIMARY_GROUP_GID_UNAVAILABLE") + group_gid = gid_record["gid"] # Exact TrueNAS 25.04.2.6 middleware privilege.create.local_groups # requires POSIX GIDs, NOT middleware account.group datastore IDs. # group.query(id=...) and user.query(group.id) still use datastore IDs. privilege = call("privilege.create", [{ - "name": ROLE_NAME, "local_groups": [group["gid"]], + "name": ROLE_NAME, "local_groups": [group_gid], "roles": ["APPS_READ"], "web_shell": False, }]) require(isinstance(privilege, dict) and privilege.get("name") == ROLE_NAME, @@ -97,7 +106,7 @@ def provision_guest_readonly(call) -> str: members = privilege.get("local_groups") require(type(members) is list and len(members) == 1 and type(members[0]) is dict - and members[0].get("gid") == group["gid"], + and members[0].get("gid") == group_gid, "CREATED_PRIVILEGE_GID_LINK_NOT_EXACT") # Distinguish role/primary-group propagation problems from extra authority, # without emitting public role names, user metadata or sensitive API fields. diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index 32458e02b..fae003382 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -14,7 +14,8 @@ def authority_responses(*, primary_roles=None, user_roles=None, user_group_id=GROUP_ID, privilege_roles=None, - group_gid=GROUP_GID, privilege_group_gid=GROUP_GID): + group_gid=GROUP_GID, privilege_group_gid=GROUP_GID, + group_local=True, group_found=True): primary_roles = [ROLE] if primary_roles is None else primary_roles user_roles = [ROLE] if user_roles is None else user_roles privilege_roles = [ROLE] if privilege_roles is None else privilege_roles @@ -22,7 +23,9 @@ def authority_responses(*, primary_roles=None, user_roles=None, ("user.query", []), ("privilege.query", []), ("privilege.roles", [{"name": ROLE, "includes": []}]), - ("user.create", {"username": USER, "group": {"id": GROUP_ID, "gid": group_gid}}), + ("user.create", {"username": USER, "group": {"id": GROUP_ID}}), + ("group.query", {"id": GROUP_ID, "gid": group_gid, "local": group_local} + if group_found else None), ("privilege.create", {"name": PRIVILEGE, "roles": privilege_roles, "local_groups": [{"gid": privilege_group_gid}]}), ("group.query", {"id": GROUP_ID, "roles": primary_roles}), @@ -58,6 +61,8 @@ def test_source_contract_allows_only_exact_role_linkage(self): self.assertEqual(provision_guest_readonly(fn), "disposable-test-only-key-value-not-real") self.assertEqual(methods[-1], "api_key.create") + self.assertEqual(methods.count("group.query"),2) + self.assertLess(methods.index("group.query"),methods.index("privilege.create")) def test_unauthorized_roles_never_receive_api_key(self): cases = [ @@ -75,6 +80,11 @@ def test_unauthorized_roles_never_receive_api_key(self): ({"privilege_group_gid": GROUP_GID + 1}, "CREATED_PRIVILEGE_GID_LINK_NOT_EXACT"), ] + cases += [ + ({"group_gid": None}, "READONLY_ACCOUNT_PRIMARY_GROUP_GID_UNAVAILABLE"), + ({"group_local": False}, "READONLY_ACCOUNT_PRIMARY_GROUP_GID_UNAVAILABLE"), + ({"group_found": False}, "READONLY_ACCOUNT_PRIMARY_GROUP_GID_UNAVAILABLE"), + ] for opts, reason in cases: with self.subTest(opts=opts): fn, methods = self.call_with(authority_responses(**opts)) From 3a43f873b61fd80deca5ac72e0e6a03687823099 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:42:51 +0200 Subject: [PATCH 675/719] Admit scoped TrueNAS vanilla263 COLLAGE readback rep 005 (#434) --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) mode change 100755 => 100644 tests/test_truenas_rdte_target.py diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 2d9268377..6a90d600c 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,11 +1,11 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP-4C unique one-time public/free-tier disposable TrueNAS25.04.2.6 T6 native vanilla26.3/Java25/protocol777 guest plus optional COLLAGE live Go TLS-verified read-only APPS_READ observer. Current native source, OCI, Foundry SHA/canonical digest, App/world/service identity unchanged. Bounded guest-only provision of one ephemeral APPS_READ key AFTER exact GID/primary/effective-role checks; do not use FULL_ADMIN, bypass TLS, leak secrets, grant caller mutation, or transfer guest credentials to operations. Reuse merged COLLAGE read-only source 17dfacc4351053fa06f8bdbaed7d931c97c1707e and accepted native App lifecycle. Observer may only attest live provider readback with zero observation mutations; checked-in source-only artifact fixture not a world/datapack deployment. Native guest invariant and independent scoped TLS/ownership receipt both mandatory; on any failure retain bounded causal receipt, no request replay. Physical TrueNAS APPLY HOLD; no private hosted minutes. Native-only OP4B-003 already passed run 38489327594; this is a distinct scoped guest observation test.", + "purpose": "One NEW serialized free/public TrueNAS25.04.2.6 vanilla26.3 Java25 T6 disposable guest + actual scoped TLS verified Go read-only COLLAGE ownership query. Prior unique 004 failed guest-local credential user.create.primary-group GID expectation (NOT native App failure), zero residue; test POSIX group.query exact ID/GID fix merged #433, original #427 and Go raw TrueNAS version #432. Exact Foundry control/image/guest source unchanged; keep APPS_READ only, never FULL_ADMIN. Native App lifecycle+Go readback independently must PASS including zero observation mutations. This request does not deploy a Lantern/Two Rivers world artifact. Cleanup in ephemeral guest; physical APPLY HOLD.", "collage_readback": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py old mode 100755 new mode 100644 index 11dcc4180..633cc0875 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback") + "tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified") self.assertIs(request.get("collage_readback"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", "authority_issue", "consumer_authority_issue", diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 6de6888e4..93213a4e4 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-004-scoped-tls-readback") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", From be04f1cef72de2659725aad757f48ffff203606e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:04:03 +0200 Subject: [PATCH 676/719] Bound every scoped TrueNAS APPS_READ RPC failure by safe phase code (#435) * OP4C: fail-closed nonsecret exact RPC ordinal diagnostics to identify real nested guest APPS_READ provisioning failure * Source red/blue: every scoped guest RBAC RPC failure gets exact sanitized phase, no credential or raw response leak --- .../truenas_middleware_collage_readback_p2.py | 36 ++++++++++++++++++- tests/test_collage_p2_effective_roles.py | 34 +++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index f2ef9e8a1..48116cb8d 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -203,6 +203,40 @@ def exact_source_readback_request(plan: dict, *, profile: str) -> tuple[dict, st return request, owner, world +# Fixed, non-secret RPC names and call order. No untrusted exception message +# or credential ever crosses into public job evidence. The prior real 005 guest +# failed with generic RuntimeError, masking WHICH guest RBAC API rejected. +RBAC_CALL_SEQUENCE = ( + ("user.query", "precheck_user"), + ("privilege.query", "precheck_privilege"), + ("privilege.roles", "discover_roles"), + ("user.create", "create_user"), + ("group.query", "resolve_group_gid"), + ("privilege.create", "grant_apps_read"), + ("group.query", "check_group_roles"), + ("user.query", "check_user_roles"), + ("api_key.create", "issue_scoped_key"), +) + + +def bounded_rbac_call(call): + position = 0 + + def invoke(method, params): + nonlocal position + require(position < len(RBAC_CALL_SEQUENCE) and + method == RBAC_CALL_SEQUENCE[position][0], + "GUEST_RBAC_API_SEQUENCE_DRIFT") + operation = RBAC_CALL_SEQUENCE[position][1] + position += 1 + try: + return call(method, params) + except Exception: + raise ObservationError("GUEST_RBAC_" + operation.upper() + + "_API_FAILED") from None + return invoke + + def run_real_readback(call, *, collage: pathlib.Path, https_port: int, state_dir: pathlib.Path, expected_source_sha: str = DEFAULT_SOURCE_SHA, profile: str = "paper") -> dict: @@ -223,7 +257,7 @@ def run_real_readback(call, *, collage: pathlib.Path, https_port: int, "VANILLA263_COLLAGE_SOURCE_REF_DRIFT") source = state_dir / "collage-p2-request.json" source.write_text(json.dumps(request, indent=2, sort_keys=True) + "\n") - key = provision_guest_readonly(call) + key = provision_guest_readonly(bounded_rbac_call(call)) trust = state_dir / "collage-p2-guest-cert.pem" hostname, cert_sha = discover_tls_anchor(https_port, trust) env = os.environ.copy() diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index fae003382..90c1ae165 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -2,7 +2,7 @@ import unittest from scripts.truenas_middleware_collage_readback_p2 import ( - ObservationError, provision_guest_readonly, + ObservationError, provision_guest_readonly, bounded_rbac_call, RBAC_CALL_SEQUENCE, ) ROLE = "APPS_READ" @@ -64,6 +64,38 @@ def test_source_contract_allows_only_exact_role_linkage(self): self.assertEqual(methods.count("group.query"),2) self.assertLess(methods.index("group.query"),methods.index("privilege.create")) + def test_all_nine_real_guest_rbac_rpc_failure_phases_are_bounded_and_private(self): + sequence=authority_responses() + self.assertEqual([item[0] for item in sequence], + [item[0] for item in RBAC_CALL_SEQUENCE]) + for failing in range(len(RBAC_CALL_SEQUENCE)): + seen=[] + def remote(method, params): + number=len(seen) + seen.append(method) + self.assertEqual(method,sequence[number][0]) + if number==failing: + raise RuntimeError("API-SECRET-EXAMPLE MUST NEVER APPEAR") + return sequence[number][1] + with self.subTest(failing=failing): + label=RBAC_CALL_SEQUENCE[failing][1].upper() + with self.assertRaises(ObservationError) as caught: + provision_guest_readonly(bounded_rbac_call(remote)) + self.assertEqual(str(caught.exception), + "GUEST_RBAC_"+label+"_API_FAILED") + self.assertNotIn("API-SECRET",str(caught.exception)) + self.assertEqual(len(seen),failing+1) + if failing<8: + self.assertNotIn("api_key.create",seen) + fn,_=self.call_with(authority_responses()) + self.assertTrue(provision_guest_readonly(bounded_rbac_call(fn)).startswith( + "disposable-test-only-key-value")) + + def test_bounded_rbac_method_sequence_prevents_out_of_contract_rpc(self): + cb=bounded_rbac_call(lambda method,params: []) + with self.assertRaisesRegex(ObservationError,"GUEST_RBAC_API_SEQUENCE_DRIFT"): + cb("app.delete",[]) + def test_unauthorized_roles_never_receive_api_key(self): cases = [ ({"user_roles": []}, "OBSERVER_USER_REQUIRED_ROLE_ABSENT"), From 4a5e8d83066789369dc7bf2057b078f001dac593 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:39:06 +0200 Subject: [PATCH 677/719] Stage real pinned Lantern 26.3 ZIP into disposable native TrueNAS App with byte and lifecycle oracles (#436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * OP-4D source: exact real Lantern ZIP check, quiesced-only native guest ZFS byte stage and independent readback * OP4D: exact source fixture, nonmutation before quiesce, guest-owned byte roundtrip and negative stage tests * OP4D: explicitly gated real immutable ZIP staging on stopped vanilla App, independent ZFS readback on restart/reinstall * OP4D independent receipt: native pinned ZIP stage + restart/reinstall file hash, never infer in-game effect * OP4D shell: stage real ZIP only after explicit native+COLLAGE opt-in and verified immutable staged file inputs * OP4D optional native KVM real Lantern producer → COLLAGE native source plan → guest ZFS byte stage and independent receipt * OP4D: receipt negatives for genuine native ZFS Lantern ZIP, retention and nonpromotion to gameplay * OP4D: inexpensive native Lantern ZIP stage negatives and exact receipt CI before disposable guest * OP4D static fail-closed guest slot: verified real ZIP stage explicitly off by default, no accepted game-effects claim * OP4D: perform actual reversible native artifact stage and world retention first; preserve independent scoped observer as separately required gate * Correct nested scoped observer method indentation after source stage ordering refactor * Preserve legacy exact Paper scoped observer opt-in static admission while deferring only native Lantern variant * OP4D: cheap GHA real immutable ZIP manifest and archive validation before heavyweight guest --- .github/workflows/gha-kvm-system-rdte.yml | 59 ++++++++ .../vanilla263-native-guest-static.yml | 43 ++++++ .../assert_vanilla263_native_t6_receipt.py | 28 +++- scripts/gha_kvm_truenas_rdte.sh | 20 ++- ...qualify_vanilla263_native_lantern_stage.py | 139 ++++++++++++++++++ .../truenas_middleware_minecraft_t6_probe.py | 55 ++++++- tests/test_truenas_t6_vanilla263_guest.py | 38 +++++ tests/test_vanilla263_native_lantern_stage.py | 132 +++++++++++++++++ tests/test_vanilla263_t6_guest_routing.py | 21 +++ 9 files changed, 531 insertions(+), 4 deletions(-) create mode 100644 scripts/qualify_vanilla263_native_lantern_stage.py create mode 100644 tests/test_vanilla263_native_lantern_stage.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 07a1c956b..8f20a1838 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -93,6 +93,7 @@ jobs: truenas_rung: ${{ steps.scope.outputs.truenas_rung }} truenas_t6_product: ${{ steps.scope.outputs.truenas_t6_product }} truenas_collage_readback: ${{ steps.scope.outputs.truenas_collage_readback }} + truenas_lantern_stage: ${{ steps.scope.outputs.truenas_lantern_stage }} truenas_compute_fixture: ${{ steps.scope.outputs.truenas_compute_fixture }} proxmox_compute_fixture: ${{ steps.scope.outputs.proxmox_compute_fixture }} steps: @@ -122,6 +123,7 @@ jobs: truenas_rung="${DISPATCH_TRUENAS_RUNG:-t6}" truenas_t6_product="${DISPATCH_TRUENAS_T6_PRODUCT:-litellm}" truenas_collage_readback=false + truenas_lantern_stage=false truenas_compute_fixture="${DISPATCH_TRUENAS_COMPUTE_FIXTURE:-none}" proxmox_compute_fixture="${DISPATCH_PROXMOX_COMPUTE_FIXTURE:-none}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then @@ -193,6 +195,13 @@ jobs: truenas_t6_product="$(jq -er '(.product // "litellm") | select(. == "litellm" or . == "wow-sidecar" or . == "garm" or . == "garm-controller-f0f5" or . == "garm-provider-g2" or . == "garm-provider-g3" or . == "garm-provider-g4" or . == "garm-provider-g5" or . == "garm-provider-container-pre-b4" or . == "official-catalog" or . == "foliorelay" or . == "minecraft-paper" or . == "collage-native-paper" or . == "minecraft-vanilla-263")' "$RUN_REQUEST_PATH")" truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" truenas_collage_readback="$(jq -er '(.collage_readback // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + truenas_lantern_stage="$(jq -er '(.lantern_stage // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + if [[ "$truenas_lantern_stage" == "true" ]]; then + [[ "$truenas_collage_readback" == "true" && "$truenas_t6_product" == "minecraft-vanilla-263" && + "$truenas_version" == "25.04.2.6" && "$truenas_rung" == "t6" ]] || { + echo "native Lantern stage requires exact one disposable vanilla263 T6 + read-only COLLAGE" >&2; exit 2; + } + fi if [[ "$truenas_collage_readback" == "true" ]]; then [[ "$truenas_t6_product" == "minecraft-vanilla-263" && "$truenas_version" == "25.04.2.6" && "$truenas_rung" == "t6" ]] || { echo "read-only COLLAGE observer requires exact native vanilla263/25.04.2.6/T6" >&2; exit 2; @@ -214,6 +223,7 @@ jobs: echo "truenas_rung=$truenas_rung" >>"$GITHUB_OUTPUT" echo "truenas_t6_product=$truenas_t6_product" >>"$GITHUB_OUTPUT" echo "truenas_collage_readback=$truenas_collage_readback" >>"$GITHUB_OUTPUT" + echo "truenas_lantern_stage=$truenas_lantern_stage" >>"$GITHUB_OUTPUT" echo "truenas_compute_fixture=$truenas_compute_fixture" >>"$GITHUB_OUTPUT" echo "proxmox_compute_fixture=$proxmox_compute_fixture" >>"$GITHUB_OUTPUT" printf 'target scope: proxmox=%s proxmox_compute_fixture=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s truenas_compute_fixture=%s\n' "$proxmox" "$proxmox_compute_fixture" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" @@ -627,6 +637,48 @@ jobs: go mod download && go mod verify && go test ./... && go build -o /dev/null .) + - name: Checkout immutable real vanilla 26.3 Lantern datapack producer + if: needs.changes.outputs.truenas_lantern_stage == 'true' + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SupraCraft/mc-server-images + ref: 7f888b5829f4e5576d5ce512d86327348c43f01e + path: .deps/lantern-producer + persist-credentials: false + - name: Verify exact real Lantern ZIP and native-target COLLAGE source plan before guest mutation + if: needs.changes.outputs.truenas_lantern_stage == 'true' + run: | + set -euo pipefail + test "$(git -C .deps/lantern-producer rev-parse HEAD)" = 7f888b5829f4e5576d5ce512d86327348c43f01e + mkdir -p "$RUNNER_TEMP/lantern-native" + python3 .deps/lantern-producer/tools/export_core_restoration_datapack_26_3.py \ + --out "$RUNNER_TEMP/lantern-native/lantern-restoration-26.3.zip" \ + --manifest "$RUNNER_TEMP/lantern-native/manifest.json" \ + --world-id lantern-rdte --release-id lantern-core-restore-26-3 \ + > /dev/null + python3 - "$RUNNER_TEMP/lantern-native" <<'PY' + from pathlib import Path + import sys + from scripts.qualify_vanilla263_native_lantern_stage import verify_exact_candidate,SHA + root=Path(sys.argv[1]) + content,proof=verify_exact_candidate(root/"lantern-restoration-26.3.zip",root/"manifest.json") + assert proof["sha256"]==SHA and proof["size"]==len(content) + PY + python3 .deps/collage-source/tools/consume_vanilla_datapack_source.py \ + --manifest "$RUNNER_TEMP/lantern-native/manifest.json" \ + --artifact "$RUNNER_TEMP/lantern-native/lantern-restoration-26.3.zip" \ + --artifact-root "$RUNNER_TEMP/lantern-native" \ + --native-target-profile vanilla263-250426 \ + > "$RUNNER_TEMP/lantern-native/collage-source-intent.json" + python3 - "$RUNNER_TEMP/lantern-native/collage-source-intent.json" <<'PY' + import json,sys + d=json.load(open(sys.argv[1])) + assert d["classification"]=="SOURCE_ARTIFACT_AND_INTENT_PARITY_VALIDATED" + assert d["expected_native_app"]=="rdte-t6-minecraft-vanilla-263" + assert set(d["audience_receipts"])=={"human","automation","agent"} + assert d["native_guest_with_this_artifact_qualified"] is False + assert d["mutation_authorized"] is False + PY - name: Download exact GARM provider G2 fixtures if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'garm-provider-g2' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c @@ -753,6 +805,10 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-vanilla-263" && "${{ needs.changes.outputs.truenas_collage_readback }}" == "true" ]]; then args+=(--vanilla-collage-readback) fi + if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_lantern_stage }}" == "true" ]]; then + args+=(--vanilla-lantern-zip "$RUNNER_TEMP/lantern-native/lantern-restoration-26.3.zip" + --vanilla-lantern-manifest "$RUNNER_TEMP/lantern-native/manifest.json") + fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" # A green job is not a Minecraft product pass without full guest evidence. @@ -765,6 +821,9 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_collage_readback }}" == "true" ]]; then additional+=(--require-collage-readback) fi + if [[ "${{ needs.changes.outputs.truenas_lantern_stage }}" == "true" ]]; then + additional+=(--require-lantern-staged) + fi python3 scripts/assert_vanilla263_native_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" "${additional[@]}" fi if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then diff --git a/.github/workflows/vanilla263-native-guest-static.yml b/.github/workflows/vanilla263-native-guest-static.yml index 135f1a5b4..3a3a73434 100644 --- a/.github/workflows/vanilla263-native-guest-static.yml +++ b/.github/workflows/vanilla263-native-guest-static.yml @@ -3,6 +3,8 @@ on: pull_request: paths: - 'scripts/truenas_middleware_minecraft_t6_probe.py' + - 'scripts/qualify_vanilla263_native_lantern_stage.py' + - 'tests/test_vanilla263_native_lantern_stage.py' - 'scripts/assert_vanilla263_native_t6_receipt.py' - 'tests/test_truenas_t6_vanilla263_guest.py' - '.github/workflows/vanilla263-native-guest-static.yml' @@ -13,6 +15,45 @@ on: permissions: contents: read jobs: + + real-artifact-source: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 7 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Checkout immutable already-qualified real Lantern producer + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + repository: SupraCraft/mc-server-images + ref: 7f888b5829f4e5576d5ce512d86327348c43f01e + path: .deps/lantern-producer + persist-credentials: false + - name: Rebuild and independently accept actual ZIP bytes before paying for guest + run: | + set -euo pipefail + test "$(git -C .deps/lantern-producer rev-parse HEAD)" = 7f888b5829f4e5576d5ce512d86327348c43f01e + mkdir -p "$RUNNER_TEMP/lantern-artifact" + python3 .deps/lantern-producer/tools/export_core_restoration_datapack_26_3.py \ + --out "$RUNNER_TEMP/lantern-artifact/lantern-restoration-26.3.zip" \ + --manifest "$RUNNER_TEMP/lantern-artifact/manifest.json" \ + --world-id lantern-rdte \ + --release-id lantern-core-restore-26-3 + python3 - "$RUNNER_TEMP/lantern-artifact" <<'PY' + from pathlib import Path + import sys + from scripts.qualify_vanilla263_native_lantern_stage import SHA, verify_exact_candidate + root=Path(sys.argv[1]) + content,evidence=verify_exact_candidate( + root/"lantern-restoration-26.3.zip", root/"manifest.json") + assert evidence["sha256"]==SHA and evidence["size"]==len(content) + assert evidence["world"]=="lantern-rdte" + assert evidence["target"]=="disposable_native_truenas_25.04.2.6" + PY + - name: Guest boundary + run: echo 'Actual immutable ZIP and manifest validated on hosted runner. No KVM, TrueNAS, stage, player gameplay, or physical APPLY.' >> "$GITHUB_STEP_SUMMARY" adversarial-source: if: github.event.repository.private == false runs-on: ubuntu-24.04 @@ -26,12 +67,14 @@ jobs: set -euo pipefail python3 -m py_compile \ scripts/truenas_middleware_minecraft_t6_probe.py \ + scripts/qualify_vanilla263_native_lantern_stage.py \ scripts/admit_vanilla263_native_t6_control.py \ scripts/assert_vanilla263_native_t6_receipt.py python3 -m py_compile tests/test_truenas_t6_vanilla263_guest.py - name: Native vanilla real guest boundary source and red-blue negatives run: | set -euo pipefail + python3 -m unittest -v tests/test_vanilla263_native_lantern_stage.py python3 -m unittest -v tests/test_truenas_t6_vanilla263_guest.py bash -n scripts/gha_kvm_truenas_rdte.sh python3 -m unittest -v tests/test_vanilla263_t6_guest_routing.py diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py index 4ac90a0f1..f3efb5c57 100644 --- a/scripts/assert_vanilla263_native_t6_receipt.py +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -19,7 +19,8 @@ IMAGE_SHA = OCI.split("@sha256:", 1)[1] -def check(root: dict, *, require_collage: bool = False) -> None: +def check(root: dict, *, require_collage: bool = False, + require_lantern_staged: bool = False) -> None: def need(condition: bool, reason: str) -> None: if not condition: raise ValueError(reason) @@ -100,6 +101,27 @@ def need(condition: bool, reason: str) -> None: "guest receipt admits secrets") need((root.get("foundry_materialization") or {}).get("target") == TARGET, "guest receipt cross-target mismatch") + if require_lantern_staged: + stage = p.get("lantern_artifact_stage") or {} + need(type(stage) is dict and + stage.get("schema") == "supracraft.native-vanilla263-lantern-artifact-stage/1" and + stage.get("classification") == "REAL_NATIVE_GUEST_ARTIFACT_BYTES_STAGED" and + stage.get("artifact_sha256") == + "da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8" and + stage.get("download_readback_sha256") == + "da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8" and + stage.get("world_id") == "lantern-rdte" and + stage.get("native_app") == APP and + stage.get("native_truenas_version") == TARGET and + stage.get("zfs_world_owned") is True and + stage.get("app_quiesced_before_write") is True and + stage.get("persisted_on_restart") is True and + stage.get("persisted_on_reinstall") is True and + stage.get("artifact_gameplay_effect_verified") is False and + stage.get("collage_managed_deployment") is False and + stage.get("principal_authorized_to_apply") is False and + stage.get("physical_truenas_apply") == "HOLD", + "actual native Lantern ZIP byte stage/retention was not qualified") if require_collage: observer = p.get("collage_readback") or {} need(type(observer) is dict and @@ -130,10 +152,12 @@ def main() -> int: p = argparse.ArgumentParser() p.add_argument("--receipt", required=True, type=Path) p.add_argument("--require-collage-readback", action="store_true") + p.add_argument("--require-lantern-staged", action="store_true") a = p.parse_args() try: check(json.loads(a.receipt.read_text(encoding="utf-8")), - require_collage=a.require_collage_readback) + require_collage=a.require_collage_readback, + require_lantern_staged=a.require_lantern_staged) except (OSError, ValueError, TypeError) as ex: # Error messages generated by check() are constant, never propagate # arbitrary provider receipt fields, credentials or real addresses. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index be88c584a..69552ff34 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -18,7 +18,7 @@ MIN_HOST_MEM_KIB=$((11 * 1024 * 1024)) MIN_HOST_FREE_KIB=$((28 * 1024 * 1024)) usage() { - echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper|collage-native-paper|minecraft-vanilla-263] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE] [--vanilla-collage-readback]" + echo "Usage: gha_kvm_truenas_rdte.sh --out RECEIPT [--state-dir DIR] [--target-version VERSION] [--rung t0|t1|t2|t3|t4|t5|t6] [--compute-fixture none|container-c0|vm-v0|vm-v1] [--t6-product litellm|wow-sidecar|garm|garm-controller-f0f5|garm-provider-g2|garm-provider-g3|garm-provider-g4|garm-provider-g5|garm-provider-container-pre-b4|official-catalog|foliorelay|minecraft-paper|collage-native-paper|minecraft-vanilla-263] [--foundry-control-dir DIR] [--foundry-commit SHA] [--g2-fixture-dir DIR] [--g2-fixture-producer SHA] [--g3-fixture-dir DIR] [--g3-fixture-producer SHA] [--g4-fixture-dir DIR] [--g4-fixture-producer SHA] [--g5-matrix-dir DIR] [--g5-matrix-producer SHA] [--container-fixture-dir DIR] [--container-fixture-producer SHA] [--session-manifest FILE] [--vanilla-collage-readback] [--vanilla-lantern-zip FILE --vanilla-lantern-manifest FILE]" } OUT="" @@ -40,6 +40,8 @@ CONTAINER_FIXTURE_DIR="" CONTAINER_FIXTURE_PRODUCER="" SESSION_MANIFEST="" VANILLA_COLLAGE_READBACK=false +VANILLA_LANTERN_ZIP="" +VANILLA_LANTERN_MANIFEST="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -62,6 +64,8 @@ while [[ $# -gt 0 ]]; do --container-fixture-producer) CONTAINER_FIXTURE_PRODUCER="$2"; shift 2 ;; --session-manifest) SESSION_MANIFEST="$2"; shift 2 ;; --vanilla-collage-readback) VANILLA_COLLAGE_READBACK=true; shift ;; + --vanilla-lantern-zip) VANILLA_LANTERN_ZIP="$2"; shift 2 ;; + --vanilla-lantern-manifest) VANILLA_LANTERN_MANIFEST="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -77,6 +81,12 @@ if [[ "$COMPUTE_FIXTURE" != "none" && "$RUNG" != "t3" ]]; then echo "compute fixture $COMPUTE_FIXTURE requires rung t3" >&2 exit 2 fi +if [[ -n "$VANILLA_LANTERN_ZIP" || -n "$VANILLA_LANTERN_MANIFEST" ]]; then + [[ -f "$VANILLA_LANTERN_ZIP" && -f "$VANILLA_LANTERN_MANIFEST" && + "$VANILLA_COLLAGE_READBACK" == "true" && + "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || + { echo "native Lantern ZIP stage requires exact disposable vanilla263 and scoped COLLAGE readback" >&2; exit 2; } +fi if [[ "$VANILLA_COLLAGE_READBACK" == "true" ]]; then [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || { echo "vanilla scoped readback only admitted on exact 25.04.2.6/T6/vanilla263" >&2; exit 2; } @@ -1198,6 +1208,13 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then --collage-source-ref "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8" ) fi + NATIVE_LANTERN_ARGS=() + if [[ -n "$VANILLA_LANTERN_ZIP" ]]; then + NATIVE_LANTERN_ARGS=( + --lantern-zip "$VANILLA_LANTERN_ZIP" + --lantern-manifest "$VANILLA_LANTERN_MANIFEST" + ) + fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ --product-profile vanilla263 --native-catalog-profile 1.15.30 \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ @@ -1207,6 +1224,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then --password-file "$PASSWORD_FILE" --control-dir "$FOUNDRY_CONTROL_DIR" \ --foundry-commit "$FOUNDRY_COMMIT" --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ "${NATIVE_READBACK_ARGS[@]}" \ + "${NATIVE_LANTERN_ARGS[@]}" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ diff --git a/scripts/qualify_vanilla263_native_lantern_stage.py b/scripts/qualify_vanilla263_native_lantern_stage.py new file mode 100644 index 000000000..811a4f7b9 --- /dev/null +++ b/scripts/qualify_vanilla263_native_lantern_stage.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Bounded actual Lantern ZIP stage into ONE disposable TrueNAS vanilla26.3 ZFS world. + +No untrusted archive extraction, no general file manager or world controller, +no player semantics and no physical TrueNAS authority. The native T6 owner +probe supplies authenticated guest-only middleware callbacks. +""" +from __future__ import annotations + +import hashlib +import json +from pathlib import Path, PurePosixPath +import zipfile + +SCHEMA = "supracraft.native-vanilla263-lantern-artifact-stage/1" +PRODUCER = "SupraCraft/mc-server-images@7f888b5829f4e5576d5ce512d86327348c43f01e" +WORLD = "/mnt/rdtepool/minecraft-vanilla-263-t6-world" +NAME = "lantern-restoration-26.3.zip" +SHA = "da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8" +TARGET = WORLD + "/datapacks/" + NAME +MEMBERS = { + "pack.mcmeta", + "data/minecraft/tags/function/tick.json", + "data/supracraft_core_restore/function/tick.mcfunction", +} +MAX_ZIP = 1024 * 1024 + +class Refused(RuntimeError): + pass + +def require(test: bool, code: str) -> None: + if not test: + raise Refused(code) + +def verify_exact_candidate(blob_path: Path, manifest_path: Path) -> tuple[bytes,dict]: + require(blob_path.is_file() and not blob_path.is_symlink() + and manifest_path.is_file() and not manifest_path.is_symlink(), + "PINNED_REAL_ARTIFACT_MISSING") + require(0 < blob_path.stat().st_size < MAX_ZIP + and 0 < manifest_path.stat().st_size < 65536, + "PINNED_REAL_ARTIFACT_SIZE_OUT_OF_BOUNDS") + m = json.loads(manifest_path.read_text(encoding="utf-8")) + require(type(m) is dict and + m.get("schema") == "supracraft-vanilla-datapack-candidate/1" and + m.get("classification") == "SOURCE_ARTIFACT_EXPORTED" and + m.get("producer") == PRODUCER and + m.get("kind") == "datapack" and m.get("backend") == "vanilla" and + m.get("minecraft_version") == "26.3" and + m.get("world_id") == "lantern-rdte" and + m.get("release_id") == "lantern-core-restore-26-3" and + m.get("pack_format") == [121, 0] and + m.get("relative_path") == NAME and + m.get("world_save_included") is False and + m.get("collage_deployed") is False, + "PINNED_REAL_ARTIFACT_CONTRACT_DRIFT") + content = blob_path.read_bytes() + require(hashlib.sha256(content).hexdigest() == SHA and + m.get("sha256") == SHA and + m.get("archive_size") == len(content), + "PINNED_REAL_ARTIFACT_SHA_DRIFT") + require(type(m.get("members")) is list and len(m["members"]) == 3, + "PINNED_REAL_ARTIFACT_MEMBERS_INVALID") + rows = m["members"] + require(all(type(r) is dict and set(r) == {"path","sha256","bytes"} and + type(r.get("path")) is str and r["path"] in MEMBERS and + isinstance(r.get("sha256"),str) and + type(r.get("bytes")) is int for r in rows) and + {r["path"] for r in rows} == MEMBERS, + "PINNED_REAL_ARTIFACT_MEMBER_DECLARATION_DRIFT") + expected = {r["path"]: r for r in rows} + with zipfile.ZipFile(blob_path) as z: + info = z.infolist() + require(len(info) == len(MEMBERS) and + {f.filename for f in info} == MEMBERS, + "PINNED_REAL_ZIP_MEMBER_SET_DRIFT") + for row in info: + require(not row.is_dir() and not (row.flag_bits & 1) and + (row.external_attr >> 16) & 0o170000 == 0o100000 and + row.file_size <= 128*1024 and + not row.filename.startswith("/") and + ".." not in PurePosixPath(row.filename).parts, + "PINNED_REAL_ZIP_MEMBER_UNSAFE") + member = z.read(row) + want = expected[row.filename] + require(len(member) == want["bytes"] and + hashlib.sha256(member).hexdigest() == want["sha256"], + "PINNED_REAL_ZIP_MEMBER_SHA_DRIFT") + metadata = json.loads(z.read("pack.mcmeta")) + require(metadata.get("pack",{}).get("min_format") == [121,0] and + metadata.get("pack",{}).get("max_format") == [121,0], + "PINNED_REAL_ZIP_PACK_FORMAT_DRIFT") + return content, {"schema":SCHEMA, "producer":PRODUCER, "sha256":SHA, + "size":len(content), "world":"lantern-rdte", + "release":"lantern-core-restore-26-3", + "target": "disposable_native_truenas_25.04.2.6"} + +def stage(call, wait_job, multipart_upload, http_bytes, *, + host: str, port: int, tls: bool, username: str, password: str, + timeout: float, blob: bytes, evidence: dict) -> dict: + """Stage solely while exact native App is STOPPED. No filesystem shell.""" + require(type(blob) is bytes and hashlib.sha256(blob).hexdigest() == SHA and + evidence.get("sha256") == SHA, "UNVERIFIED_ARTIFACT_BYTES") + rows = call("app.query", [[["id", "=", "rdte-t6-minecraft-vanilla-263"]]]) + require(type(rows) is list and len(rows) == 1 and + rows[0].get("state") == "STOPPED", + "NATIVE_APP_MUST_BE_QUIESCED_BEFORE_STAGE") + parent = call("filesystem.stat", [WORLD + "/datapacks"]) + require(type(parent) is dict and parent.get("type") == "DIRECTORY", + "OWNED_WORLD_DATAPACK_DIRECTORY_ABSENT") + try: + current = call("filesystem.stat", [TARGET]) + except Exception: + current = None + require(current is None, "REFUSE_TO_OVERWRITE_EXISTING_NATIVE_WORLD_ARTIFACT") + upload_job = multipart_upload(host,port,tls,username,password,TARGET,blob,0o644,timeout) + wait_job(upload_job,"Lantern datapack exact native ZFS world file stage") + observed = readback(call,wait_job,http_bytes,host=host,port=port,tls=tls,timeout=timeout) + require(observed == SHA, "NATIVE_WORLD_ARTIFACT_UPLOAD_READBACK_SHA_DRIFT") + return {"schema":SCHEMA,"classification":"REAL_NATIVE_GUEST_ARTIFACT_BYTES_STAGED", + "artifact_sha256":SHA,"artifact_size":len(blob), + "world_id":"lantern-rdte","release_id":"lantern-core-restore-26-3", + "native_truenas_version":"25.04.2.6", + "native_app":"rdte-t6-minecraft-vanilla-263", + "zfs_world_owned":True,"app_quiesced_before_write":True, + "download_readback_sha256":observed, + "artifact_gameplay_effect_verified":False, + "collage_managed_deployment":False,"principal_authorized_to_apply":False, + "physical_truenas_apply":"HOLD"} + +def readback(call,wait_job,http_bytes, *,host:str,port:int,tls:bool,timeout:float)->str: + row = call("core.download",["filesystem.get",[TARGET],"lantern-stage-sha-readback.zip",True]) + require(type(row) is list and len(row) == 2 and type(row[0]) is int and + isinstance(row[1],str) and row[1].startswith("/_download/") and + ".." not in row[1], "NATIVE_WORLD_ARTIFACT_DOWNLOAD_REF_INVALID") + wait_job(row[0],"native Lantern artifact byte readback") + actual = http_bytes(host,port,tls,row[1],timeout=timeout) + require(type(actual) is bytes and len(actual) <= MAX_ZIP, + "NATIVE_WORLD_ARTIFACT_READBACK_OVERSIZED") + return hashlib.sha256(actual).hexdigest() diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 33d5feb1c..66cdf99d0 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -365,6 +365,8 @@ def main() -> int: p.add_argument("--collage-source-dir", type=pathlib.Path) p.add_argument("--collage-https-port", type=int) p.add_argument("--collage-source-ref") + p.add_argument("--lantern-zip", type=pathlib.Path) + p.add_argument("--lantern-manifest", type=pathlib.Path) p.add_argument("--native-catalog-profile", choices=sorted(CATALOG_PROFILES), default="1.15.29") p.add_argument("--product-profile", choices=("legacy-paper", "vanilla263"), @@ -391,6 +393,19 @@ def main() -> int: if (not VANILLA263 and a.native_catalog_profile == "1.15.30" and a.collage_source_dir is None): raise MinecraftT6Error("1.15.30 catalog profile is COLLAGE P2 opt-in only") + if (a.lantern_zip is None) != (a.lantern_manifest is None): + raise MinecraftT6Error("native Lantern source ZIP and manifest required as pair") + if a.lantern_zip is not None and (not VANILLA263 or + a.collage_source_dir is None): + raise MinecraftT6Error("native Lantern staging needs exact vanilla263 and read-only COLLAGE source") + lantern_blob = None + lantern_evidence = None + if a.lantern_zip is not None: + from qualify_vanilla263_native_lantern_stage import ( + verify_exact_candidate, + ) + lantern_blob, lantern_evidence = verify_exact_candidate( + a.lantern_zip, a.lantern_manifest) control = load_control(a.control_dir, a.foundry_commit, catalog_version=a.native_catalog_profile) if a.expected_system_version != "TrueNAS-" + EXPECTED_TARGET: @@ -609,7 +624,7 @@ def delete_app(label): wait_app() wait_status("first") read_sentinel() - if a.collage_source_dir is not None: + def observe_collage_after_native_readiness(): if (a.collage_https_port is None or a.collage_source_ref is None or a.collage_https_port <= 0): raise MinecraftT6Error("COLLAGE P2 readback admission incomplete") @@ -631,12 +646,42 @@ def delete_app(label): raise MinecraftT6Error("COLLAGE P2: " + str(err)) from None read_sentinel() + # The original native-only/Paper P2 observer order is unchanged. + # When actual native artifact stage is admitted, run the provider + # read-only observer AFTER world stage/restart/reinstall so a failed + # RBAC observer cannot mask independent byte-level integration results. + if a.collage_source_dir is not None: + if lantern_blob is None: + observe_collage_after_native_readiness() + first_level = payload["readiness"]["first"]["level"]["size"] wait_job(call("app.stop", [APP]), "Minecraft stop") + if lantern_blob is not None: + from qualify_vanilla263_native_lantern_stage import stage + try: + payload["lantern_artifact_stage"] = stage( + call, wait_job, multipart_upload, middleware_http_bytes, + host=a.host, port=a.port, tls=a.tls, + username="truenas_admin", password=password, + timeout=a.timeout, blob=lantern_blob, evidence=lantern_evidence) + except Exception as exc: + # Report only the bounded code. Never emit credentials, + # middleware errors or raw artifacts into public GHA logs. + from qualify_vanilla263_native_lantern_stage import Refused + if isinstance(exc, Refused): + raise MinecraftT6Error("native Lantern stage: " + str(exc)) from None + raise MinecraftT6Error("native Lantern stage: GUEST_STAGE_UNAVAILABLE") from None wait_job(call("app.start", [APP]), "Minecraft start") wait_app() wait_status("restart") + if lantern_blob is not None: + from qualify_vanilla263_native_lantern_stage import readback, SHA + require_stage = readback(call,wait_job,middleware_http_bytes, + host=a.host,port=a.port,tls=a.tls,timeout=a.timeout) + if require_stage != SHA: + raise MinecraftT6Error("native Lantern staged file lost during restart") + payload["lantern_artifact_stage"]["persisted_on_restart"] = True wait_job(call("app.redeploy", [APP]), "Minecraft redeploy") wait_app() wait_status("redeploy") @@ -651,6 +696,14 @@ def delete_app(label): wait_job(call("app.create", [create_payload]), "Minecraft reinstall") wait_app() wait_status("reinstall") + if lantern_blob is not None: + from qualify_vanilla263_native_lantern_stage import readback, SHA + actual = readback(call,wait_job,middleware_http_bytes, + host=a.host,port=a.port,tls=a.tls,timeout=a.timeout) + if actual != SHA: + raise MinecraftT6Error("native Lantern staged file lost across reinstall") + payload["lantern_artifact_stage"]["persisted_on_reinstall"] = True + observe_collage_after_native_readiness() read_sentinel() if first_level <= 0: raise MinecraftT6Error("world level.dat size invalid") diff --git a/tests/test_truenas_t6_vanilla263_guest.py b/tests/test_truenas_t6_vanilla263_guest.py index 43830ff61..d3730c801 100644 --- a/tests/test_truenas_t6_vanilla263_guest.py +++ b/tests/test_truenas_t6_vanilla263_guest.py @@ -106,6 +106,44 @@ def test_optional_native_collage_readback_requires_independent_tls_and_apps_read # Legacy native guest acceptance remains unchanged by absent readback. check_guest(guest_fixture(),require_collage=False) + def test_optional_real_native_lantern_file_stage_does_not_inherit_gameplay(self): + root=guest_fixture() + with self.assertRaisesRegex(ValueError,"actual native Lantern ZIP byte stage"): + check_guest(root,require_lantern_staged=True) + hashid="da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8" + stage={ + "schema":"supracraft.native-vanilla263-lantern-artifact-stage/1", + "classification":"REAL_NATIVE_GUEST_ARTIFACT_BYTES_STAGED", + "artifact_sha256":hashid,"download_readback_sha256":hashid, + "world_id":"lantern-rdte", "native_app":native.APP, + "native_truenas_version":"25.04.2.6", + "zfs_world_owned":True,"app_quiesced_before_write":True, + "persisted_on_restart":True,"persisted_on_reinstall":True, + "artifact_gameplay_effect_verified":False, + "collage_managed_deployment":False, + "principal_authorized_to_apply":False, + "physical_truenas_apply":"HOLD", + } + root["foundry_materialization"]["lantern_artifact_stage"]=stage + check_guest(root,require_lantern_staged=True) + for field,bad in ( + ("classification","SOURCE_VALIDATED"), + ("artifact_sha256","f"*64), + ("download_readback_sha256","f"*64), + ("native_app","foreign"), + ("app_quiesced_before_write",False), + ("persisted_on_restart",False), + ("persisted_on_reinstall",False), + ("artifact_gameplay_effect_verified",True), + ("collage_managed_deployment",True), + ("physical_truenas_apply","APPLY"), + ): + wrong=copy.deepcopy(root) + wrong["foundry_materialization"]["lantern_artifact_stage"][field]=bad + with self.subTest(field=field),self.assertRaises(ValueError): + check_guest(wrong,require_lantern_staged=True) + check_guest(guest_fixture(),require_lantern_staged=False) + def test_never_promote_paper_legacy_or_source_only(self): for change in ("legacy_version","legacy_paper","source_only","wrong_phase"): r=guest_fixture() diff --git a/tests/test_vanilla263_native_lantern_stage.py b/tests/test_vanilla263_native_lantern_stage.py new file mode 100644 index 000000000..0e27cfd23 --- /dev/null +++ b/tests/test_vanilla263_native_lantern_stage.py @@ -0,0 +1,132 @@ +"""OP4D static, synthetic and negative source tests; NOT a guest-acceptance claim.""" +import hashlib +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +import zipfile + +from scripts import qualify_vanilla263_native_lantern_stage as s + + +class StageContract(unittest.TestCase): + def make(self): + self.td=tempfile.TemporaryDirectory() + self.addCleanup(self.td.cleanup) + root=Path(self.td.name) + path=root/s.NAME + payloads={ + "pack.mcmeta": json.dumps({"pack":{"min_format":[121,0], + "max_format":[121,0]}}).encode(), + "data/minecraft/tags/function/tick.json": b'{"values":[]}\n', + "data/supracraft_core_restore/function/tick.mcfunction": b"say synthetic\n", + } + with zipfile.ZipFile(path,"w") as z: + for name,data in sorted(payloads.items()): + row=zipfile.ZipInfo(name,(1980,1,1,0,0,0)) + row.create_system=3 + row.external_attr=0o100644<<16 + z.writestr(row,data) + blob=path.read_bytes() + sha=hashlib.sha256(blob).hexdigest() + m={ + "schema":"supracraft-vanilla-datapack-candidate/1", + "classification":"SOURCE_ARTIFACT_EXPORTED", + "producer":s.PRODUCER,"kind":"datapack","backend":"vanilla", + "minecraft_version":"26.3","world_id":"lantern-rdte", + "release_id":"lantern-core-restore-26-3","pack_format":[121,0], + "relative_path":s.NAME,"world_save_included":False, + "collage_deployed":False,"sha256":sha,"archive_size":len(blob), + "members":[{"path":name,"sha256":hashlib.sha256(data).hexdigest(), + "bytes":len(data)} for name,data in sorted(payloads.items())], + } + manifest=root/"manifest.json" + manifest.write_text(json.dumps(m)) + return path,manifest,blob,sha + + def test_real_manifest_shape_synthetic_zip_never_confused_with_immutable_bytes(self): + zip_file,manifest,blob,sha=self.make() + with self.assertRaisesRegex(s.Refused,"PINNED_REAL_ARTIFACT_SHA_DRIFT"): + s.verify_exact_candidate(zip_file,manifest) + with patch.object(s,"SHA",sha): + content,receipt=s.verify_exact_candidate(zip_file,manifest) + self.assertEqual(content,blob) + self.assertEqual(receipt["sha256"],sha) + for field,bad in (("minecraft_version","1.21.11"), + ("world_id","two-rivers-operations"), + ("collage_deployed",True), + ("producer","foreign/provenance@"+"0"*40), + ("pack_format",[80,0])): + m=json.loads(manifest.read_text()) + m[field]=bad + manifest.write_text(json.dumps(m)) + with patch.object(s,"SHA",sha),self.subTest(field=field): + with self.assertRaises(s.Refused): + s.verify_exact_candidate(zip_file,manifest) + m[field]=json.loads(json.dumps({ + "minecraft_version":"26.3","world_id":"lantern-rdte", + "collage_deployed":False,"producer":s.PRODUCER, + "pack_format":[121,0]}))[field] + manifest.write_text(json.dumps(m)) + + def test_disposable_app_must_be_stopped_before_native_file_upload(self): + _,_,blob,sha=self.make() + called=[] + def fake(method,args): + called.append(method) + if method=="app.query": + return [{"state":"RUNNING"}] + raise AssertionError("mutating step called before quiesce") + with patch.object(s,"SHA",sha): + with self.assertRaisesRegex(s.Refused,"NATIVE_APP_MUST_BE_QUIESCED"): + s.stage(fake,None,None,None,host="127.0.0.1",port=80,tls=False, + username="rdte",password="test-not-secret",timeout=8, + blob=blob,evidence={"sha256":sha}) + self.assertEqual(called,["app.query"]) + + def test_native_own_world_writes_and_two_separate_digest_reads(self): + _,_,blob,sha=self.make() + called=[] + def fake(method,args): + called.append(method) + if method=="app.query":return [{"state":"STOPPED"}] + if method=="filesystem.stat": + if args[0].endswith("/datapacks"): + return {"type":"DIRECTORY"} + raise FileNotFoundError() + if method=="core.download": + self.assertEqual(args[1][0],s.TARGET) + return [77,"/_download/exact-owned-artifact"] + raise AssertionError(method) + def upload(host,port,tls,user,password,target,content,mode,timeout): + self.assertEqual(target,s.TARGET) + self.assertEqual(content,blob) + self.assertEqual(mode,0o644) + called.append("filesystem.put") + return 76 + def jobs(job,label): + self.assertIn(job,(76,77)) + called.append("job_"+str(job)) + def http(host,port,tls,path,timeout): + called.append("download_bytes") + return blob + with patch.object(s,"SHA",sha): + r=s.stage(fake,jobs,upload,http,host="127.0.0.1",port=80, + tls=False,username="rdte",password="test-not-secret", + timeout=8,blob=blob,evidence={"sha256":sha}) + self.assertEqual(r["classification"],"REAL_NATIVE_GUEST_ARTIFACT_BYTES_STAGED") + self.assertFalse(r["artifact_gameplay_effect_verified"]) + self.assertFalse(r["collage_managed_deployment"]) + self.assertEqual(r["physical_truenas_apply"],"HOLD") + self.assertLess(called.index("app.query"),called.index("filesystem.put")) + self.assertEqual(called.count("download_bytes"),1) + + def test_unsafe_readback_contract_refused(self): + with self.assertRaisesRegex(s.Refused,"NATIVE_WORLD_ARTIFACT_DOWNLOAD_REF_INVALID"): + s.readback(lambda method,args:[4,"https://external-host/forbidden"],None,None, + host="127.0.0.1",port=80,tls=False,timeout=8) + + +if __name__=="__main__": + unittest.main() diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 93213a4e4..491ab4848 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -88,6 +88,27 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('profile="vanilla263" if VANILLA263 else "paper"', (ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text()) + def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self): + self.assertIn('truenas_lantern_stage=false', WORKFLOW) + self.assertIn('(.lantern_stage // false)', WORKFLOW) + self.assertIn('truenas_lantern_stage=$truenas_lantern_stage', WORKFLOW) + self.assertIn('--vanilla-lantern-zip',WORKFLOW) + self.assertIn('--vanilla-lantern-manifest',WORKFLOW) + self.assertIn('--require-lantern-staged',WORKFLOW) + self.assertIn('ref: 7f888b5829f4e5576d5ce512d86327348c43f01e',WORKFLOW) + self.assertIn('verify_exact_candidate',WORKFLOW) + self.assertIn('native-target-profile vanilla263-250426',WORKFLOW) + self.assertIn('VANILLA_LANTERN_ZIP=""',HARNESS) + self.assertIn('"$VANILLA_COLLAGE_READBACK" == "true"',HARNESS) + self.assertIn('NATIVE_LANTERN_ARGS=()',HARNESS) + current=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) + self.assertNotIn("lantern_stage",current) + source=(ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text() + self.assertIn('wait_job(call("app.stop", [APP])',source) + self.assertIn('stage(',source) + self.assertIn('persisted_on_reinstall',source) + self.assertIn('persisted_on_restart',source) + def test_no_fallback_guest_or_physical_promotion(self): self.assertNotIn('product == "minecraft-vanilla-263" && needs[\'foundry-minecraft-paper',WORKFLOW) self.assertIn("agent-dispatch-heavyweight-rdte",WORKFLOW) From 8109991f3b943f7f101ee7caf25bd352bbc8ead1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:41:49 +0200 Subject: [PATCH 678/719] Admit single disposable native TrueNAS real Lantern ZIP and scoped readback rep 006 (#439) --- config/truenas-rdte-run-request.json | 7 ++++--- tests/test_truenas_rdte_target.py | 5 +++-- tests/test_vanilla263_t6_guest_routing.py | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 6a90d600c..45770d8f4 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,11 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "One NEW serialized free/public TrueNAS25.04.2.6 vanilla26.3 Java25 T6 disposable guest + actual scoped TLS verified Go read-only COLLAGE ownership query. Prior unique 004 failed guest-local credential user.create.primary-group GID expectation (NOT native App failure), zero residue; test POSIX group.query exact ID/GID fix merged #433, original #427 and Go raw TrueNAS version #432. Exact Foundry control/image/guest source unchanged; keep APPS_READ only, never FULL_ADMIN. Native App lifecycle+Go readback independently must PASS including zero observation mutations. This request does not deploy a Lantern/Two Rivers world artifact. Cleanup in ephemeral guest; physical APPLY HOLD.", - "collage_readback": true + "purpose": "One uniquely authorized serialized public/free disposable native TrueNAS 25.04.2.6 / catalog Minecraft 1.15.30 Java25 vanilla26.3 protocol777 OP4D-006: preserve signed Foundry and OCI pin; inspect exact real Lantern 26.3 ZIP SHA da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8, three COLLAGE human/automation/agent equal-intent source plans, stage only under quiesced real native App into owned world /datapacks and independently hash readback after restart and reinstall. Run scoped APPS_READ Go TLS provider observer with full six owner fields and zero observation mutations only after byte-level stage, and require guest native lifecycle/cleanup receipts. Native offline gameplay semantic qualification remains separate: canonical Foundry has online_mode=true and RCON=false. Original OP4C-005 reported observer RBAC RuntimeError with zero residue; bounded nonsecret RPC ordinal diagnostic #435 is now merged. No replay of 005, no increased observer privilege, no physical TrueNAS APPLY, no private GHA minutes.", + "collage_readback": true, + "lantern_stage": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 633cc0875..9a4868915 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,11 +92,12 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified") + "tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage") self.assertIs(request.get("collage_readback"), True) + self.assertIs(request.get("lantern_stage"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", "authority_issue", "consumer_authority_issue", - "purpose", "collage_readback"}) + "purpose", "collage_readback", "lantern_stage"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 491ab4848..b3a653048 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4c-005-group-gid-verified") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -102,7 +102,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) self.assertIn('"$VANILLA_COLLAGE_READBACK" == "true"',HARNESS) self.assertIn('NATIVE_LANTERN_ARGS=()',HARNESS) current=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) - self.assertNotIn("lantern_stage",current) + self.assertIs(current.get("lantern_stage"),True) source=(ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text() self.assertIn('wait_job(call("app.stop", [APP])',source) self.assertIn('stage(',source) From 685cd7787201afe82724777c1ce53b1088e9ab3f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:52:25 +0200 Subject: [PATCH 679/719] Source-qualify loopback-only synthetic vanilla26.3 player variant without changing operations (#440) * Source-only immutable Foundry-derived strict disposable offline bot gameplay overlay, no RCON or physical APPLY * Red/blue loopback-only Mineflayer RDTE auth overlay invariants and immutable production control boundary * Public cheap source gate: loopback-only synthetic player profile separate from canonical TrueNAS operation --- ...alify-native-loopback-gameplay-overlay.yml | 29 +++++ ...tive_vanilla263_loopback_player_overlay.py | 95 ++++++++++++++ ...tive_vanilla263_loopback_player_overlay.py | 122 ++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 .github/workflows/qualify-native-loopback-gameplay-overlay.yml create mode 100644 scripts/qualify_native_vanilla263_loopback_player_overlay.py create mode 100644 tests/test_native_vanilla263_loopback_player_overlay.py diff --git a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml new file mode 100644 index 000000000..47629aca2 --- /dev/null +++ b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml @@ -0,0 +1,29 @@ +name: Native vanilla263 synthetic player loopback source qualification +on: + pull_request: + paths: + - 'scripts/qualify_native_vanilla263_loopback_player_overlay.py' + - 'tests/test_native_vanilla263_loopback_player_overlay.py' + - '.github/workflows/qualify-native-loopback-gameplay-overlay.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + explicit-offline-rdte-contract: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 4 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Compile and adversarially test the source-only loopback variant + run: | + set -euo pipefail + python3 -m py_compile \ + scripts/qualify_native_vanilla263_loopback_player_overlay.py \ + tests/test_native_vanilla263_loopback_player_overlay.py + python3 -m unittest -v tests.test_native_vanilla263_loopback_player_overlay + bash -n scripts/gha_kvm_truenas_rdte.sh + - name: Preserve operational auth and physical TrueNAS hold + run: echo 'Source-only variant; immutable operational online_mode=true, RCON disabled; no QEMU guest, player, world mutation, or physical TrueNAS APPLY.' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/qualify_native_vanilla263_loopback_player_overlay.py b/scripts/qualify_native_vanilla263_loopback_player_overlay.py new file mode 100644 index 000000000..c33ae4319 --- /dev/null +++ b/scripts/qualify_native_vanilla263_loopback_player_overlay.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Source-only derived offline player profile for one disposable native TrueNAS guest. + +The signed Foundry control is immutable. This is a separately named, +allowlisted RDTE overlay for live synthetic Mineflayer action tests, not a +general rendering service and never an operational configuration. +""" +from __future__ import annotations +import copy +import hashlib +import json +from typing import Any + +from admit_vanilla263_native_t6_control import ( + APP, CONTROL_SHA, OWNER, Refused, TARGET, verify, +) + +PROFILE = "minecraft-vanilla263-disposable-loopback-player/1" +BOT_NAME = "SupraCraftProbe" +WORLD = "/mnt/rdtepool/minecraft-vanilla-263-t6-world" +HOST_FORWARD = "tcp:127.0.0.1:25580-:25580" + +def require(cond: bool, code: str) -> None: + if not cond: + raise Refused(code) + +def derive(control: dict, admission: dict) -> dict[str, Any]: + """Prepare a synthetic-world profile only; no RPC, filesystem, or guest.""" + verify(control) + require(type(admission) is dict and + admission.get("schema") == PROFILE and + admission.get("rdte_disposable") is True and + admission.get("experiment") == "offline-mineflayer-player-causal" and + admission.get("physical_truenas_apply") == "HOLD" and + admission.get("operational_selection") is False and + admission.get("external_ingress") is False and + admission.get("qemu_network") == "user" and + admission.get("host_forward") == HOST_FORWARD and + admission.get("guest_target") == "TrueNAS-" + TARGET and + admission.get("authorized_bot") == BOT_NAME and + set(admission) == { + "schema", "rdte_disposable", "experiment", + "physical_truenas_apply", "operational_selection", + "external_ingress", "qemu_network", "host_forward", + "guest_target", "authorized_bot", + }, "NOT_DISPOSABLE_LOOPBACK_PLAYER_ADMISSION") + values = copy.deepcopy(control["runtime"]["values_template"]) + mc = values["minecraft"] + require(mc["online_mode"] is True and + mc["enable_rcon"] is False and mc["rcon_password"] == "" and + mc["ops"] == [] and mc["whitelist"] == [], + "EXACT_BASELINE_AUTH_POLICY_DRIFT") + require(values["network"]["host_network"] is False and + values["network"]["server_port"]["bind_mode"] == "published" and + values["network"]["server_port"]["port_number"] == 25565, + "EXACT_BASELINE_NETWORK_POLICY_DRIFT") + require(values["storage"]["additional_storage"][0]["mount_path"] == "/data/world" and + {x["name"]: x["value"] for x in mc["additional_envs"]} == OWNER, + "EXACT_BASELINE_WORLD_OWNER_DRIFT") + # Only changes to gameplay authentication are the locally isolated + # synthetic client identity and granting that bot in-game commands. + # No RCON or remote management listener introduced, and all six + # COLLAGE owner labels remain exact. + mc["online_mode"] = False + mc["ops"] = [BOT_NAME] + # Identical site bindings as accepted guest native app probe. + values["storage"]["additional_storage"][0]["host_path_config"]["path"] = WORLD + values["network"]["server_port"]["port_number"] = 25580 + mc.pop("seed", None) if mc.get("seed") is None else None + require(mc["enable_rcon"] is False and mc["rcon_password"] == "" and + mc["whitelist"] == [] and mc["ops"] == [BOT_NAME] and + mc["online_mode"] is False and + {x["name"]: x["value"] for x in mc["additional_envs"]} == OWNER, + "DISPOSABLE_PLAYER_OVERLAY_ESCAPED_ALLOWLIST") + digest=hashlib.sha256(json.dumps(values,sort_keys=True, + separators=(",",":")).encode()).hexdigest() + return { + "schema": PROFILE, + "classification": "SOURCE_ONLY_DISPOSABLE_PLAYER_VARIANT", + "immutable_base_control_sha256": CONTROL_SHA, + "app": APP, + "truenas_version": TARGET, + "minecraft_version": "26.3", + "protocol": 777, + "network": "QEMU_USER_LOOPBACK_ONLY_SOURCE_REQUIREMENT", + "bot": BOT_NAME, + "derived_runtime_values_sha256": digest, + "values": values, + "online_account_auth_qualified": False, + "in_guest_bot_connected": False, + "datapack_effect_in_native_guest_qualified": False, + "collage_managed_deployment": False, + "physical_truenas_apply": "HOLD", + "operational_selection": False, + } diff --git a/tests/test_native_vanilla263_loopback_player_overlay.py b/tests/test_native_vanilla263_loopback_player_overlay.py new file mode 100644 index 000000000..8cfec20e4 --- /dev/null +++ b/tests/test_native_vanilla263_loopback_player_overlay.py @@ -0,0 +1,122 @@ +"""Exact-source semantics of a disposable offline synthetic player variant. + +Static tests intentionally do not claim actual guest isolation or gameplay. +The canonical Foundry signature is independently checked by existing tests; +these tests isolate the additional source-only constrained derivation. +""" +import copy +from pathlib import Path +import sys +import unittest +from unittest.mock import patch + +ROOT=Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/"scripts")) +import qualify_native_vanilla263_loopback_player_overlay as overlay + +def baseline(): + return { + "runtime":{"values_template":{ + "minecraft":{ + "online_mode":True,"enable_rcon":False,"rcon_password":"", + "ops":[],"whitelist":[],"seed":None, + "additional_envs":[{"name":k,"value":v} + for k,v in overlay.OWNER.items()], + }, + "network":{"host_network":False, + "server_port":{"bind_mode":"published","port_number":25565}}, + "storage":{"additional_storage":[{"mount_path":"/data/world", + "host_path_config":{"path":"/opt/tests/collage-vanilla-263-world"}}]}, + }}, + } + +def admission(): + return { + "schema":overlay.PROFILE, + "rdte_disposable":True, + "experiment":"offline-mineflayer-player-causal", + "physical_truenas_apply":"HOLD", + "operational_selection":False, + "external_ingress":False, + "qemu_network":"user", + "host_forward":overlay.HOST_FORWARD, + "guest_target":"TrueNAS-"+overlay.TARGET, + "authorized_bot":"SupraCraftProbe", + } + +def derive(control=None, request=None): + ctl=control if control is not None else baseline() + req=request if request is not None else admission() + with patch.object(overlay,"verify",return_value={"classification":"SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED"}) as check: + result=overlay.derive(ctl,req) + check.assert_called_once_with(ctl) + return result + +class OverlaySourceTests(unittest.TestCase): + def test_synthetic_player_overlay_preserves_operational_control_and_is_source_only(self): + before=baseline() + frozen=copy.deepcopy(before) + result=derive(before,admission()) + self.assertEqual(before,frozen) + self.assertEqual(result["classification"],"SOURCE_ONLY_DISPOSABLE_PLAYER_VARIANT") + self.assertEqual(result["immutable_base_control_sha256"],overlay.CONTROL_SHA) + minecraft=result["values"]["minecraft"] + self.assertIs(minecraft["online_mode"],False) + self.assertEqual(minecraft["ops"],["SupraCraftProbe"]) + self.assertFalse(minecraft["enable_rcon"]) + self.assertEqual(minecraft["rcon_password"],"") + self.assertFalse(minecraft["whitelist"]) + self.assertNotIn("seed",minecraft) + self.assertEqual(result["values"]["network"]["server_port"]["port_number"],25580) + self.assertEqual(result["values"]["storage"]["additional_storage"][0]["host_path_config"]["path"],overlay.WORLD) + self.assertEqual({x["name"]:x["value"] for x in minecraft["additional_envs"]},overlay.OWNER) + for field in ("online_account_auth_qualified","in_guest_bot_connected", + "datapack_effect_in_native_guest_qualified", + "collage_managed_deployment","operational_selection"): + self.assertIs(result[field],False) + self.assertEqual(result["physical_truenas_apply"],"HOLD") + self.assertEqual(len(result["derived_runtime_values_sha256"]),64) + + def test_all_unadmitted_exposure_and_target_variants_refused(self): + for key,bad in ( + ("rdte_disposable",False), + ("external_ingress",True), + ("qemu_network","tap"), + ("host_forward","tcp:0.0.0.0:25580-:25580"), + ("host_forward","tcp:127.0.0.1:25580-:25565"), + ("guest_target","TrueNAS-25.04.1"), + ("authorized_bot","human-owner"), + ("physical_truenas_apply","APPLY"), + ("operational_selection",True), + ("experiment","live-minecraft"), + ): + q=admission() + q[key]=bad + with self.subTest(key=key),self.assertRaises(overlay.Refused): + derive(baseline(),q) + q=admission() + q["allow_admin"]=True + with self.assertRaises(overlay.Refused): + derive(baseline(),q) + + def test_canonical_baseline_cannot_arrive_already_degraded(self): + for section,key,bad in ( + ("minecraft","online_mode",False), + ("minecraft","enable_rcon",True), + ("minecraft","ops",["OtherUser"]), + ("minecraft","whitelist",["Anybody"]), + ("network","host_network",True), + ): + c=baseline() + c["runtime"]["values_template"][section][key]=bad + with self.subTest(section=section,key=key),self.assertRaises(overlay.Refused): + derive(c,admission()) + + def test_real_canonical_signature_gate_called_not_bypassed(self): + original=baseline() + with patch.object(overlay,"verify",side_effect=overlay.Refused("CANONICAL_CONTROL_SHA_DRIFT")): + with self.assertRaisesRegex(overlay.Refused,"CANONICAL_CONTROL_SHA_DRIFT"): + overlay.derive(original,admission()) + +if __name__=="__main__": + unittest.main() From 4b8f2df1440f80b33b7ac07b75b3da7843208d31 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:58:50 +0200 Subject: [PATCH 680/719] Bound TrueNAS scoped API-key DDP failures to safe numeric code only (#441) * Qualify bounded safe numeric TrueNAS api_key.create DDP failure class without exposing raw API errors or broadening role * Red-blue DDP API-key error numeric category classifier emits only exact safe status; suppress raw credentials --- .../truenas_middleware_collage_readback_p2.py | 33 +++++++++++++++++-- tests/test_collage_p2_effective_roles.py | 27 +++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 48116cb8d..7edbda86e 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -219,6 +219,33 @@ def exact_source_readback_request(plan: dict, *, profile: str) -> tuple[dict, st ) +def classify_scoped_key_ddp_failure(exc: Exception) -> str: + """Return only a tiny allowlisted numeric DDP errno, never raw API error. + + ddp_call formats RuntimeError as 'api_key.create: {JSON error object}'. + No remotely supplied reason, message, params, principal or secret values + can leave this guest. Absent/unrecognized codes remain unspecific. + """ + if type(exc) is not RuntimeError or not exc.args or type(exc.args[0]) is not str: + return "" + data = exc.args[0] + prefix = "api_key.create: " + if len(data) > 8192 or not data.startswith(prefix): + return "" + try: + obj = json.loads(data[len(prefix):]) + except ValueError: + return "" + if type(obj) is not dict: + return "" + code = obj.get("error") + if type(code) is int and code in {1, 2, 5, 11, 13, 17, 22, 38, 400, 401, 403, 404, 409, 422, 500, 503}: + return "_E" + str(code) + if type(code) is str and code in {"EINVAL","EPERM","EACCES","VALIDATION_ERRORS","METHOD_NOT_FOUND"}: + return "_" + code + return "" + + def bounded_rbac_call(call): position = 0 @@ -231,9 +258,11 @@ def invoke(method, params): position += 1 try: return call(method, params) - except Exception: + except Exception as exc: + suffix = (classify_scoped_key_ddp_failure(exc) + if operation == "issue_scoped_key" else "") raise ObservationError("GUEST_RBAC_" + operation.upper() + - "_API_FAILED") from None + "_API_FAILED" + suffix) from None return invoke diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index 90c1ae165..6be5e18b0 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -3,6 +3,7 @@ from scripts.truenas_middleware_collage_readback_p2 import ( ObservationError, provision_guest_readonly, bounded_rbac_call, RBAC_CALL_SEQUENCE, + classify_scoped_key_ddp_failure, ) ROLE = "APPS_READ" @@ -56,6 +57,32 @@ def call(method, params): return result return call, called + def test_api_key_create_ddp_rejection_only_emits_allowlisted_error_number(self): + for code in (13,22,400,403,422,500): + err=RuntimeError('api_key.create: {"error": '+str(code)+ + ', "reason": "API-SECRET-NEVER-LOG-THIS"}') + self.assertEqual(classify_scoped_key_ddp_failure(err),"_E"+str(code)) + def remote(method, params): + if method=="api_key.create": + raise err + return next(x[1] for x in authority_responses() if x[0]==method) + # The actual guest call order is exercised separately below. + fn,seq=self.call_with(authority_responses()) + def failing(method,params): + if method=="api_key.create": raise err + return fn(method,params) + with self.assertRaises(ObservationError) as caught: + provision_guest_readonly(bounded_rbac_call(failing)) + self.assertEqual(str(caught.exception), + "GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED_E"+str(code)) + self.assertNotIn("API-SECRET",str(caught.exception)) + for msg in ('api_key.create: {"error": 987654,"reason":"SECRET"}', + 'api_key.create: {"error":"CUSTOM_PRIVATE"}', + 'api_key.create: bad-json', + 'other.method: {"error": 403}', + "api_key.create: "+("secret"*2000)): + self.assertEqual(classify_scoped_key_ddp_failure(RuntimeError(msg)),"") + def test_source_contract_allows_only_exact_role_linkage(self): fn, methods = self.call_with(authority_responses()) self.assertEqual(provision_guest_readonly(fn), From 6c19571be9381ed8834e769df9eb757d0e27fcad Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:01:36 +0200 Subject: [PATCH 681/719] Admit one disposable native Lantern and scoped key errno rep 007 (#442) --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 45770d8f4..b55a335ce 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "One uniquely authorized serialized public/free disposable native TrueNAS 25.04.2.6 / catalog Minecraft 1.15.30 Java25 vanilla26.3 protocol777 OP4D-006: preserve signed Foundry and OCI pin; inspect exact real Lantern 26.3 ZIP SHA da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8, three COLLAGE human/automation/agent equal-intent source plans, stage only under quiesced real native App into owned world /datapacks and independently hash readback after restart and reinstall. Run scoped APPS_READ Go TLS provider observer with full six owner fields and zero observation mutations only after byte-level stage, and require guest native lifecycle/cleanup receipts. Native offline gameplay semantic qualification remains separate: canonical Foundry has online_mode=true and RCON=false. Original OP4C-005 reported observer RBAC RuntimeError with zero residue; bounded nonsecret RPC ordinal diagnostic #435 is now merged. No replay of 005, no increased observer privilege, no physical TrueNAS APPLY, no private GHA minutes.", + "purpose": "One distinct public/free serialized 25.04.2.6 T6 native vanilla26.3/Java25 scoped COLLAGE readback diagnostic with actual pinned Lantern ZIP stage/restart/reinstall. 006 native ZIP staging and persistence PASSED independent byte proof and clean rollback; overall 006 FAILED only GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED at final api_key.create, following all preceding account/group/role gates. Newly accepted #441 permits only allowlisted numeric DDP error code suffix, never raw remote message/secret. This request 007 is a diagnostic rep for safe key issuance and exact APPS_READ/TLS ownership; do not claim world-player causal proof. No role expansion, API bypass or unspecified retry. Restore exact owned App/world zero residue and preserve failed native receipt. Physical APPLY HOLD, no private GHA minutes.", "collage_readback": true, "lantern_stage": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 9a4868915..8cbcea636 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage") + "tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index b3a653048..8f560caab 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-006-real-lantern-stage") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", From 6ae028ad3b17d00d58f84403a354671b770acc55 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:09:53 +0200 Subject: [PATCH 682/719] fix(op4f): bound scoped guest DDP key creation and sanitize failure causes * fix(op4f): bound scoped key DDP creation and never reuse failed request IDs * fix(op4f): classify key-creation transport and DDP failure shape without exposing error content * test(op4f): adversarial DDP error, timeout and secret non-disclosure regressions * refactor(op4f): expose sequential DDP correlation for independent timeout tests * test(op4f): verify scoped timeout, socket restoration and no correlation ID replay * ci(op4f): exercise scoped-key red-blue tests in public no-KVM guest source gate --- .../vanilla263-native-guest-static.yml | 4 ++ .../truenas_middleware_collage_readback_p2.py | 34 ++++++++---- .../truenas_middleware_minecraft_t6_probe.py | 42 ++++++++++++--- tests/test_collage_p2_effective_roles.py | 53 ++++++++++++++++--- tests/test_truenas_t6_vanilla263_guest.py | 45 ++++++++++++++++ 5 files changed, 155 insertions(+), 23 deletions(-) diff --git a/.github/workflows/vanilla263-native-guest-static.yml b/.github/workflows/vanilla263-native-guest-static.yml index 3a3a73434..5f9a8f7f2 100644 --- a/.github/workflows/vanilla263-native-guest-static.yml +++ b/.github/workflows/vanilla263-native-guest-static.yml @@ -3,6 +3,8 @@ on: pull_request: paths: - 'scripts/truenas_middleware_minecraft_t6_probe.py' + - 'scripts/truenas_middleware_collage_readback_p2.py' + - 'tests/test_collage_p2_effective_roles.py' - 'scripts/qualify_vanilla263_native_lantern_stage.py' - 'tests/test_vanilla263_native_lantern_stage.py' - 'scripts/assert_vanilla263_native_t6_receipt.py' @@ -67,6 +69,7 @@ jobs: set -euo pipefail python3 -m py_compile \ scripts/truenas_middleware_minecraft_t6_probe.py \ + scripts/truenas_middleware_collage_readback_p2.py \ scripts/qualify_vanilla263_native_lantern_stage.py \ scripts/admit_vanilla263_native_t6_control.py \ scripts/assert_vanilla263_native_t6_receipt.py @@ -76,6 +79,7 @@ jobs: set -euo pipefail python3 -m unittest -v tests/test_vanilla263_native_lantern_stage.py python3 -m unittest -v tests/test_truenas_t6_vanilla263_guest.py + python3 -m unittest -v tests/test_collage_p2_effective_roles.py bash -n scripts/gha_kvm_truenas_rdte.sh python3 -m unittest -v tests/test_vanilla263_t6_guest_routing.py - name: Preserve legacy Paper 25.04.1 T6 regression diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 7edbda86e..4595ae3b2 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -220,30 +220,44 @@ def exact_source_readback_request(plan: dict, *, profile: str) -> tuple[dict, st def classify_scoped_key_ddp_failure(exc: Exception) -> str: - """Return only a tiny allowlisted numeric DDP errno, never raw API error. + """Only fixed failure categories or allowlisted DDP codes leave the guest. - ddp_call formats RuntimeError as 'api_key.create: {JSON error object}'. - No remotely supplied reason, message, params, principal or secret values - can leave this guest. Absent/unrecognized codes remain unspecific. + In addition to provider rejection, this RPC can time out while TrueNAS + performs password hash/PAM generation. Timeouts are indeterminate: the + key might already exist, so the caller must never retry issuance blindly. + Error messages, principal metadata and secret values remain private. """ + if isinstance(exc, TimeoutError): + return "_TRANSPORT_TIMEOUT_INDETERMINATE" + if isinstance(exc, (ConnectionError, EOFError)): + return "_TRANSPORT_DISCONNECTED" + if isinstance(exc, OSError): + return "_TRANSPORT_IO_FAILURE" if type(exc) is not RuntimeError or not exc.args or type(exc.args[0]) is not str: - return "" + return "_LOCAL_UNEXPECTED_FAILURE" data = exc.args[0] prefix = "api_key.create: " - if len(data) > 8192 or not data.startswith(prefix): - return "" + if len(data) > 8192: + return "_DDP_OVERSIZE_ERROR" + if not data.startswith(prefix): + return "_DDP_UNSTRUCTURED_ERROR" try: obj = json.loads(data[len(prefix):]) except ValueError: - return "" + return "_DDP_MALFORMED_ERROR" if type(obj) is not dict: - return "" + return "_DDP_NONOBJECT_ERROR" code = obj.get("error") if type(code) is int and code in {1, 2, 5, 11, 13, 17, 22, 38, 400, 401, 403, 404, 409, 422, 500, 503}: return "_E" + str(code) if type(code) is str and code in {"EINVAL","EPERM","EACCES","VALIDATION_ERRORS","METHOD_NOT_FOUND"}: return "_" + code - return "" + if type(code) is dict: + return "_DDP_NESTED_ERROR" + if type(code) is list: + return "_DDP_VALIDATION_LIST" + return "_DDP_UNRECOGNIZED_CODE" + def bounded_rbac_call(call): diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 66cdf99d0..8b59a7e68 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -19,6 +19,41 @@ from truenas_middleware_ddp_probe import WebSocket, ddp_call, wait_for from truenas_middleware_foliorelay_t6_probe import middleware_http_bytes, multipart_upload +# Only the disposable scoped credential-issuance RPC gets a longer DDP +# response deadline. The TrueNAS nested CPU/PAM workload may exceed the +# ordinary 8-second socket deadline. A timeout is NOT proof the RPC failed or +# that the key was not created: never retry this non-idempotent operation. +SCOPED_KEY_CREATE_TIMEOUT_SECONDS = 60.0 + + +def scoped_ddp_call(ws, request_id, method, params, *, transport=ddp_call): + """One bounded scoped exception to ordinary DDP transport timeouts. + + No role changes, TLS downgrades, credential retries or raw error logging. + Existing request IDs must be unique before this function is called. + """ + if method != "api_key.create": + return transport(ws, request_id, method, params) + prior = ws.sock.gettimeout() + try: + ws.sock.settimeout(max(prior or 0.0, SCOPED_KEY_CREATE_TIMEOUT_SECONDS)) + return transport(ws, request_id, method, params) + finally: + ws.sock.settimeout(prior) + + +def sequenced_guest_ddp_call(ws, *, transport=scoped_ddp_call): + """Unique DDP correlation IDs, including after indeterminate timeouts.""" + next_id = 0 + + def call(method, params): + nonlocal next_id + next_id += 1 + return transport(ws, str(next_id), method, params) + + return call + + SCHEMA = "semper-supra.minecraft-paper-truenas-t6-control/1" EXPECTED_TARGET = "25.04.1" EXPECTED_PROFILE = "800aca038ded29e6a91bfc4c1b1fe2d2199182e8" @@ -421,12 +456,7 @@ def main() -> int: ws.send_json({"msg": "connect", "version": "1", "support": ["1"]}) if wait_for(ws, lambda m: m.get("msg") in {"connected", "failed"}).get("msg") != "connected": raise MinecraftT6Error("DDP connection failed") - ident = 1 - def call(method, params): - nonlocal ident - result = ddp_call(ws, str(ident), method, params) - ident += 1 - return result + call = sequenced_guest_ddp_call(ws) def one(method, filters): rows = call(method, [filters]) if not isinstance(rows, list) or len(rows) > 1: diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index 6be5e18b0..da9a45599 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -76,12 +76,50 @@ def failing(method,params): self.assertEqual(str(caught.exception), "GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED_E"+str(code)) self.assertNotIn("API-SECRET",str(caught.exception)) - for msg in ('api_key.create: {"error": 987654,"reason":"SECRET"}', - 'api_key.create: {"error":"CUSTOM_PRIVATE"}', - 'api_key.create: bad-json', - 'other.method: {"error": 403}', - "api_key.create: "+("secret"*2000)): - self.assertEqual(classify_scoped_key_ddp_failure(RuntimeError(msg)),"") + malformed = ( + ('api_key.create: {"error": 987654,"reason":"SECRET"}', + "_DDP_UNRECOGNIZED_CODE"), + ('api_key.create: {"error":"CUSTOM_PRIVATE"}', + "_DDP_UNRECOGNIZED_CODE"), + ('api_key.create: {"error": {"error":403},"reason":"SECRET"}', + "_DDP_NESTED_ERROR"), + ('api_key.create: {"error":[{"field":"PRIVATE"}]}', + "_DDP_VALIDATION_LIST"), + ('api_key.create: bad-json', "_DDP_MALFORMED_ERROR"), + ('other.method: {"error": 403}', "_DDP_UNSTRUCTURED_ERROR"), + ("api_key.create: "+("secret"*2000), "_DDP_OVERSIZE_ERROR"), + ) + for msg, category in malformed: + with self.subTest(category=category): + self.assertEqual(classify_scoped_key_ddp_failure(RuntimeError(msg)),category) + for exc, category in ( + (TimeoutError("PRIVATE-KEY-NEVER-LOG"), "_TRANSPORT_TIMEOUT_INDETERMINATE"), + (ConnectionError("PRIVATE-USER-NEVER-LOG"), "_TRANSPORT_DISCONNECTED"), + (OSError("PRIVATE-ENDPOINT-NEVER-LOG"), "_TRANSPORT_IO_FAILURE"), + (ValueError("PRIVATE-ROLE-NEVER-LOG"), "_LOCAL_UNEXPECTED_FAILURE"), + ): + self.assertEqual(classify_scoped_key_ddp_failure(exc),category) + self.assertNotIn("PRIVATE",classify_scoped_key_ddp_failure(exc)) + # Failures propagate through the actual nine-operation RBAC wrapper, + # but the caller sees neither raw DDP text nor a spurious success. + for exc,category in ( + (TimeoutError("DO-NOT-EMIT"),"_TRANSPORT_TIMEOUT_INDETERMINATE"), + (RuntimeError('api_key.create: {"error":[{"secret":"DO-NOT-EMIT"}]}'), + "_DDP_VALIDATION_LIST"), + ): + responses=authority_responses() + called=[] + def failure(method,params): + ordinal=len(called) + called.append(method) + if method=="api_key.create": + raise exc + return responses[ordinal][1] + with self.assertRaises(ObservationError) as caught: + provision_guest_readonly(bounded_rbac_call(failure)) + self.assertEqual(str(caught.exception), + "GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED"+category) + self.assertNotIn("DO-NOT-EMIT",str(caught.exception)) def test_source_contract_allows_only_exact_role_linkage(self): fn, methods = self.call_with(authority_responses()) @@ -108,8 +146,9 @@ def remote(method, params): label=RBAC_CALL_SEQUENCE[failing][1].upper() with self.assertRaises(ObservationError) as caught: provision_guest_readonly(bounded_rbac_call(remote)) + suffix = "_DDP_UNSTRUCTURED_ERROR" if failing == 8 else "" self.assertEqual(str(caught.exception), - "GUEST_RBAC_"+label+"_API_FAILED") + "GUEST_RBAC_"+label+"_API_FAILED"+suffix) self.assertNotIn("API-SECRET",str(caught.exception)) self.assertEqual(len(seen),failing+1) if failing<8: diff --git a/tests/test_truenas_t6_vanilla263_guest.py b/tests/test_truenas_t6_vanilla263_guest.py index d3730c801..5e5d3aef8 100644 --- a/tests/test_truenas_t6_vanilla263_guest.py +++ b/tests/test_truenas_t6_vanilla263_guest.py @@ -57,6 +57,51 @@ def guest_fixture(): class TestNativeVanilla263Guest(unittest.TestCase): + def test_api_key_create_has_bounded_longer_deadline_and_restores_socket(self): + class Socket: + def __init__(self): self.value=8.0 + def gettimeout(self): return self.value + def settimeout(self, value): self.value=value + class WS: + def __init__(self): self.sock=Socket() + ws=WS() + seen=[] + def transport(sock, ident, method, params): + seen.append((ident,method,sock.sock.gettimeout())) + if method=="api_key.create": + raise TimeoutError("synthetic secret MUST NOT surface") + return "ok" + call=mc.sequenced_guest_ddp_call( + ws, transport=lambda w, i, m, p: + mc.scoped_ddp_call(w, i, m, p, transport=transport)) + self.assertEqual(call("app.query", []),"ok") + with self.assertRaises(TimeoutError): + call("api_key.create", [{"name":"synthetic-only"}]) + self.assertEqual(ws.sock.gettimeout(),8.0) + self.assertEqual(call("app.query", []),"ok") + self.assertEqual([item[0] for item in seen],["1","2","3"]) + self.assertEqual([item[2] for item in seen], + [8.0, mc.SCOPED_KEY_CREATE_TIMEOUT_SECONDS, 8.0]) + self.assertEqual(mc.SCOPED_KEY_CREATE_TIMEOUT_SECONDS,60.0) + + def test_scoped_key_success_also_restores_prior_socket_deadline(self): + class Socket: + value=11.0 + def gettimeout(self): return self.value + def settimeout(self,value): self.value=value + class WS: + sock=Socket() + ws=WS() + observed=[] + def transport(w, ident, method, params): + observed.append(w.sock.gettimeout()) + return {"key":"synthetic-test-only"} + result=mc.scoped_ddp_call(ws,"7","api_key.create",[], + transport=transport) + self.assertEqual(result,{"key":"synthetic-test-only"}) + self.assertEqual(observed,[60.0]) + self.assertEqual(ws.sock.gettimeout(),11.0) + def test_full_independent_guest_oracle_fixture(self): check_guest(guest_fixture()) From 1e2b8368f6a06c80524c94af42bd1912c28606b6 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:15:50 +0200 Subject: [PATCH 683/719] admit(op4f): unique 008 native scoped Go observer diagnostic * admit(op4f): unique 008 diagnostic native key issuance and exact Go TLS observation * test(op4f): bind admission regression to new single-use 008 request ID --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index b55a335ce..467cd81ef 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "One distinct public/free serialized 25.04.2.6 T6 native vanilla26.3/Java25 scoped COLLAGE readback diagnostic with actual pinned Lantern ZIP stage/restart/reinstall. 006 native ZIP staging and persistence PASSED independent byte proof and clean rollback; overall 006 FAILED only GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED at final api_key.create, following all preceding account/group/role gates. Newly accepted #441 permits only allowlisted numeric DDP error code suffix, never raw remote message/secret. This request 007 is a diagnostic rep for safe key issuance and exact APPS_READ/TLS ownership; do not claim world-player causal proof. No role expansion, API bypass or unspecified retry. Restore exact owned App/world zero residue and preserve failed native receipt. Physical APPLY HOLD, no private GHA minutes.", + "purpose": "OP4F one-time public/free serialized native TrueNAS 25.04.2.6 T6 vanilla 26.3/Java25 with previously accepted exact Lantern ZIP staging and independent cleanup; challenge last guest OP4D-007 api_key.create failure by separately bounded 60-second DDP issuance deadline, monotonic non-replayed RPC IDs and codes-only transport/DDP failure categories, without any role expansion or raw error publication. Require actual APPS_READ-scoped TLS-verified Go COLLAGE native owner read-only receipt and zero observation mutations for overall acceptance. Guest may still REFUSE: preserve causal category and independent stage/cleanup proof. No automatic retries or replay 007/008, no full-admin COLLAGE probe, operational online authentication/RCON defaults untouched, no in-guest player/gameplay or managed activation claim, physical TrueNAS APPLY HOLD.", "collage_readback": true, "lantern_stage": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 8cbcea636..936c05686 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer") + "tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", From 837971e98ff5b3d92a513d2ff3f21db8bdf2e5c1 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:26:24 +0200 Subject: [PATCH 684/719] qual(op4f): bind offline player source overlay to runner-owned QEMU loopback * feat(op4f): accept dynamic runner-owned localhost QEMU guest player forward * fix(op4f): use literal IPv4 dot matching in player forward admission * test(op4f): falsify foreign ports and host identities in disposable player admission * fix(op4f): reject noncanonical leading-zero host-port encoding --- ...tive_vanilla263_loopback_player_overlay.py | 22 +++++++++++++++- ...tive_vanilla263_loopback_player_overlay.py | 25 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/scripts/qualify_native_vanilla263_loopback_player_overlay.py b/scripts/qualify_native_vanilla263_loopback_player_overlay.py index c33ae4319..60170d01e 100644 --- a/scripts/qualify_native_vanilla263_loopback_player_overlay.py +++ b/scripts/qualify_native_vanilla263_loopback_player_overlay.py @@ -9,6 +9,7 @@ import copy import hashlib import json +import re from typing import Any from admit_vanilla263_native_t6_control import ( @@ -20,6 +21,23 @@ WORLD = "/mnt/rdtepool/minecraft-vanilla-263-t6-world" HOST_FORWARD = "tcp:127.0.0.1:25580-:25580" +def runner_owned_loopback_forward(value: object) -> int: + """Accept QEMU's allocated ephemeral host port, never broad guest ingress. + + The existing native T6 harness independently allocates an unused host port, + forwarded to the *guest* :25580 on QEMU user networking. This parser + checks only source shape; the live harness must still verify the actual + localhost listener belongs to the newly spawned disposable guest. + """ + require(type(value) is str and len(value) < 64, + "PLAYER_FORWARD_NOT_ISOLATED_LOOPBACK") + match = re.fullmatch(r"tcp:127[.]0[.]0[.]1:([1-9][0-9]{3,4})-:25580", value) + require(match is not None, "PLAYER_FORWARD_NOT_ISOLATED_LOOPBACK") + port = int(match.group(1)) + require(1024 < port < 65536 and port not in {25565, 443, 80}, + "PLAYER_FORWARD_NOT_ISOLATED_LOOPBACK") + return port + def require(cond: bool, code: str) -> None: if not cond: raise Refused(code) @@ -35,7 +53,7 @@ def derive(control: dict, admission: dict) -> dict[str, Any]: admission.get("operational_selection") is False and admission.get("external_ingress") is False and admission.get("qemu_network") == "user" and - admission.get("host_forward") == HOST_FORWARD and + runner_owned_loopback_forward(admission.get("host_forward")) > 0 and admission.get("guest_target") == "TrueNAS-" + TARGET and admission.get("authorized_bot") == BOT_NAME and set(admission) == { @@ -83,6 +101,8 @@ def derive(control: dict, admission: dict) -> dict[str, Any]: "minecraft_version": "26.3", "protocol": 777, "network": "QEMU_USER_LOOPBACK_ONLY_SOURCE_REQUIREMENT", + "host_forward": admission["host_forward"], + "host_port": runner_owned_loopback_forward(admission["host_forward"]), "bot": BOT_NAME, "derived_runtime_values_sha256": digest, "values": values, diff --git a/tests/test_native_vanilla263_loopback_player_overlay.py b/tests/test_native_vanilla263_loopback_player_overlay.py index 8cfec20e4..f53c46e7d 100644 --- a/tests/test_native_vanilla263_loopback_player_overlay.py +++ b/tests/test_native_vanilla263_loopback_player_overlay.py @@ -77,6 +77,31 @@ def test_synthetic_player_overlay_preserves_operational_control_and_is_source_on self.assertEqual(result["physical_truenas_apply"],"HOLD") self.assertEqual(len(result["derived_runtime_values_sha256"]),64) + def test_runner_owned_ephemeral_forward_matches_native_harness(self): + req=admission() + req["host_forward"]="tcp:127.0.0.1:45451-:25580" + result=derive(baseline(),req) + self.assertEqual(result["host_forward"],req["host_forward"]) + self.assertEqual(result["host_port"],45451) + self.assertEqual(result["network"],"QEMU_USER_LOOPBACK_ONLY_SOURCE_REQUIREMENT") + self.assertFalse(result["in_guest_bot_connected"]) + # A source parser does not prove QEMU listener ownership or a guest player. + for forbidden in ( + "tcp:0.0.0.0:45451-:25580", + "tcp:192.168.1.1:45451-:25580", + "tcp:[::1]:45451-:25580", + "tcp:127.0.0.1:45451-:25565", + "tcp:127.0.0.1:22-:25580", + "tcp:127.0.0.1:01025-:25580", + "tcp:127.0.0.1:65536-:25580", + "tcp:127.0.0.1:45451-:25580,hostfwd=other", + "tcp:127.0.0.1:45451-:25580\\nINJECT", + ): + bad=admission() + bad["host_forward"]=forbidden + with self.subTest(forbidden=forbidden),self.assertRaises(overlay.Refused): + derive(baseline(),bad) + def test_all_unadmitted_exposure_and_target_variants_refused(self): for key,bad in ( ("rdte_disposable",False), From d5b240fbed60dc838654df2b3a0d57c8c281321f Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:39:38 +0200 Subject: [PATCH 685/719] fix(op4f): safely classify structured oversized TrueNAS key-issuance errors * fix(op4f): classify bounded large structured DDP key failures without publishing traceback * test(op4f): red-blue 25KB+ structured error, trace secrecy and 262KB bounded parser * test(op4f): align nested allowlisted errno positive with new extractor * test(op4f): bind source-routing fixture to current consumed 008 without replay --- .../truenas_middleware_collage_readback_p2.py | 19 ++++++++++++++++--- tests/test_collage_p2_effective_roles.py | 12 ++++++++++-- tests/test_vanilla263_t6_guest_routing.py | 4 ++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 4595ae3b2..49c18f4aa 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -237,13 +237,18 @@ def classify_scoped_key_ddp_failure(exc: Exception) -> str: return "_LOCAL_UNEXPECTED_FAILURE" data = exc.args[0] prefix = "api_key.create: " - if len(data) > 8192: - return "_DDP_OVERSIZE_ERROR" if not data.startswith(prefix): return "_DDP_UNSTRUCTURED_ERROR" + # Native 008 returned an oversized *structured* error after 119 s + # overall successful world staging. Keep the parser size-bounded, but + # inspect an error object of realistic traceback size in memory so the + # fixed allowlist can classify the DDP numeric code. Never publish the + # remote reason/trace/details, even when malicious or unusually long. + if len(data) > 262144: + return "_DDP_OVERSIZE_ERROR" try: obj = json.loads(data[len(prefix):]) - except ValueError: + except (ValueError, RecursionError): return "_DDP_MALFORMED_ERROR" if type(obj) is not dict: return "_DDP_NONOBJECT_ERROR" @@ -253,6 +258,14 @@ def classify_scoped_key_ddp_failure(exc: Exception) -> str: if type(code) is str and code in {"EINVAL","EPERM","EACCES","VALIDATION_ERRORS","METHOD_NOT_FOUND"}: return "_" + code if type(code) is dict: + # Middleware may nest its code/error/errno fields beneath error. + # At most one level, with a hardcoded numeric-only allowlist; never + # derive a public string from untrusted remote metadata. + nested = code.get("errno") + if type(nested) is not int: + nested = code.get("error") + if type(nested) is int and nested in {1, 2, 5, 11, 13, 17, 22, 38, 400, 401, 403, 404, 409, 422, 500, 503}: + return "_E" + str(nested) return "_DDP_NESTED_ERROR" if type(code) is list: return "_DDP_VALIDATION_LIST" diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index da9a45599..c089ac0cc 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -82,12 +82,20 @@ def failing(method,params): ('api_key.create: {"error":"CUSTOM_PRIVATE"}', "_DDP_UNRECOGNIZED_CODE"), ('api_key.create: {"error": {"error":403},"reason":"SECRET"}', - "_DDP_NESTED_ERROR"), + "_E403"), ('api_key.create: {"error":[{"field":"PRIVATE"}]}', "_DDP_VALIDATION_LIST"), ('api_key.create: bad-json', "_DDP_MALFORMED_ERROR"), ('other.method: {"error": 403}', "_DDP_UNSTRUCTURED_ERROR"), - ("api_key.create: "+("secret"*2000), "_DDP_OVERSIZE_ERROR"), + ("api_key.create: "+("secret"*2000), "_DDP_MALFORMED_ERROR"), + ('api_key.create: {"error": 403, "reason": "' + ("SENSITIVE"*3000) + '"}', + "_E403"), + ('api_key.create: {"error": {"errno": 422, "reason": "PRIVATE"}}', + "_E422"), + ('api_key.create: {"error": {"errno": 99999, "reason": "PRIVATE"}}', + "_DDP_NESTED_ERROR"), + ('api_key.create: {"error": 500, "reason": "' + ("PRIVATE"*40000) + '"}', + "_DDP_OVERSIZE_ERROR"), ) for msg, category in malformed: with self.subTest(category=category): diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 8f560caab..2126716d5 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4d-007-key-errno-observer") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", From b8f05a7dc07263e3919d70e93a62fbfdb177a195 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:43:28 +0200 Subject: [PATCH 686/719] fix(op4f): attempt bounded scoped observer before repeated native ZIP work * fix(op4f): gate costly native ZIP lifecycle on early exact read-only Go credential observation * test(op4f): ensure bounded guest fails scoped readback before repeated native world lifecycle * test(op4f): use exact staged callback position without escape-dependent parsing --- .../truenas_middleware_minecraft_t6_probe.py | 16 ++++++++------- tests/test_truenas_t6_vanilla263_guest.py | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 8b59a7e68..c2c838638 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -676,13 +676,14 @@ def observe_collage_after_native_readiness(): raise MinecraftT6Error("COLLAGE P2: " + str(err)) from None read_sentinel() - # The original native-only/Paper P2 observer order is unchanged. - # When actual native artifact stage is admitted, run the provider - # read-only observer AFTER world stage/restart/reinstall so a failed - # RBAC observer cannot mask independent byte-level integration results. + # The exact Lantern ZIP stage/persistence was independently accepted + # in 006/007/008 despite terminal scoped-key failures. Do not repay + # three App restarts before attempting the still-unqualified G1/G2 + # read-only Go observer. Observe the verified first native App here; + # on success, retain this same App/world for real ZIP stage/reinstall. + # Legacy native-only and Paper P2 default paths remain unchanged. if a.collage_source_dir is not None: - if lantern_blob is None: - observe_collage_after_native_readiness() + observe_collage_after_native_readiness() first_level = payload["readiness"]["first"]["level"]["size"] @@ -733,7 +734,8 @@ def observe_collage_after_native_readiness(): if actual != SHA: raise MinecraftT6Error("native Lantern staged file lost across reinstall") payload["lantern_artifact_stage"]["persisted_on_reinstall"] = True - observe_collage_after_native_readiness() + # The same exact App owner is rechecked by wait_app() above; + # pre-stage TLS Go observation remains a separate G2 receipt. read_sentinel() if first_level <= 0: raise MinecraftT6Error("world level.dat size invalid") diff --git a/tests/test_truenas_t6_vanilla263_guest.py b/tests/test_truenas_t6_vanilla263_guest.py index 5e5d3aef8..704aa1ec0 100644 --- a/tests/test_truenas_t6_vanilla263_guest.py +++ b/tests/test_truenas_t6_vanilla263_guest.py @@ -57,6 +57,26 @@ def guest_fixture(): class TestNativeVanilla263Guest(unittest.TestCase): + def test_scoped_tls_go_readback_precedes_redundant_zip_lifecycle(self): + # Actual 006/007/008 native ZIP staging was independently qualified. + # Fail early on still-unqualified scoped key instead of repaying + # stop/restart/redeploy/reinstall just to encounter the same blocker. + import inspect + body=inspect.getsource(mc.main) + guard=body.index('if a.collage_source_dir is not None:') + callback=body.index(' observe_collage_after_native_readiness()',guard) + first=body.index('wait_status("first")') + stage=body.index('payload["lantern_artifact_stage"] = stage(') + final=body.index('payload["lantern_artifact_stage"]["persisted_on_reinstall"] = True') + self.assertLess(first,guard) + self.assertLess(guard,stage) + self.assertLess(guard,callback) + self.assertLess(callback,stage) + self.assertLess(stage,final) + self.assertEqual(body.count(' observe_collage_after_native_readiness()'),1) + self.assertIn('if lantern_blob is not None:',body) + self.assertIn('if a.collage_source_dir is not None:',body) + def test_api_key_create_has_bounded_longer_deadline_and_restores_socket(self): class Socket: def __init__(self): self.value=8.0 From 15892a3f4e7253fde03263c188b2b1a829a9b4dd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:40:51 +0200 Subject: [PATCH 687/719] admit(op4f): single 009 native scoped DDP diagnosis after preflight * admit(op4f): unique 009 early exact key-code and Go observer before stage * test(op4f): assert exact current one-time 009 request edge without replay * test(op4f): assert exact current one-time 009 request edge without replay --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 467cd81ef..c00926d21 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4F one-time public/free serialized native TrueNAS 25.04.2.6 T6 vanilla 26.3/Java25 with previously accepted exact Lantern ZIP staging and independent cleanup; challenge last guest OP4D-007 api_key.create failure by separately bounded 60-second DDP issuance deadline, monotonic non-replayed RPC IDs and codes-only transport/DDP failure categories, without any role expansion or raw error publication. Require actual APPS_READ-scoped TLS-verified Go COLLAGE native owner read-only receipt and zero observation mutations for overall acceptance. Guest may still REFUSE: preserve causal category and independent stage/cleanup proof. No automatic retries or replay 007/008, no full-admin COLLAGE probe, operational online authentication/RCON defaults untouched, no in-guest player/gameplay or managed activation claim, physical TrueNAS APPLY HOLD.", + "purpose": "OP4F unique one-time 009 public/free serialized disposable native TrueNAS 25.04.2.6/vanilla 26.3/Java25 source+TLS Go observer. First verify App RUNNING/owner/world readback; attempt APPS_READ-only scoped api_key.create before expensive ZIP staging/restart/reinstall, read only allowlisted numeric/shape class from <=262144-character structured DDP errors and retain zero raw trace/user/key. On observer PASS proceed to exact 739-byte Lantern ZIP staging and owned world lifecycle; on credential refusal emit sanitized category, cleanup owned App/ZFS and stop. 008 is consumed FAIL; no 008/009 retry, no FULL_ADMIN/disabled TLS, no stock online auth claim, no private GHA minutes, no physical TrueNAS APPLY.", "collage_readback": true, "lantern_stage": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 936c05686..37ecba1b0 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer") + "tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 2126716d5..a9137bb3f 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-008-bounded-key-tls-observer") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", From 0ea980a5bade76b0b32a50ce97df5a813022ff84 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:18:16 +0200 Subject: [PATCH 688/719] fix(op4f): privacy-safe DDP validation field and postfailure count diagnostic * fix(op4f): classify scoped TrueNAS validation fields and count owned key after failure * test(op4f): adversarial validation-field redaction and one-shot postfailure read-only count --- .../truenas_middleware_collage_readback_p2.py | 56 ++++++++++- tests/test_collage_p2_effective_roles.py | 95 ++++++++++++++++++- 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index 49c18f4aa..e9bcf0672 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -219,6 +219,54 @@ def exact_source_readback_request(plan: dict, *, profile: str) -> tuple[dict, st ) + +# Fixed allowlist only: TrueNAS 25.04 serializes ValidationErrors as +# errno 11 with type=VALIDATION and extra [attribute, message, errno] tuples. +# Attribute strings not explicitly listed here may never appear in receipts. +_VALIDATION_FIELDS = { + "api_key_create": "GENERAL", + "api_key_create.name": "NAME", + "api_key_create.username": "USERNAME", + "api_key_create.expires_at": "EXPIRES_AT", +} + + +def scoped_validation_field_suffix(obj: dict) -> str: + """Classify only a recognized validation field, never its message/value.""" + if obj.get("type") != "VALIDATION": + return "" + extra = obj.get("extra") + if type(extra) is not list or not (1 <= len(extra) <= 12): + return "_VALIDATION_FIELD_UNKNOWN" + categories = set() + for item in extra: + if type(item) is not list or len(item) != 3: + return "_VALIDATION_FIELD_UNKNOWN" + field = item[0] + if type(field) is not str or field not in _VALIDATION_FIELDS: + return "_VALIDATION_FIELD_UNKNOWN" + categories.add(_VALIDATION_FIELDS[field]) + return ("_VALIDATION_" + next(iter(categories))) if len(categories) == 1 else "_VALIDATION_MULTI" + + +def scoped_key_record_count_suffix(call) -> str: + """Read-only exact owned key count once; no listing, retries or disclosure.""" + try: + count = call("api_key.query", [ + [["username", "=", NAME], ["name", "=", "disposable-collage-p2"]], + {"count": True}, + ]) + except Exception: + return "_KEY_COUNT_UNAVAILABLE" + if type(count) is not int or count < 0: + return "_KEY_COUNT_INVALID" + if count == 0: + return "_KEY_COUNT0" + if count == 1: + return "_KEY_COUNT1" + return "_KEY_COUNT_MULTIPLE" + + def classify_scoped_key_ddp_failure(exc: Exception) -> str: """Only fixed failure categories or allowlisted DDP codes leave the guest. @@ -254,7 +302,7 @@ def classify_scoped_key_ddp_failure(exc: Exception) -> str: return "_DDP_NONOBJECT_ERROR" code = obj.get("error") if type(code) is int and code in {1, 2, 5, 11, 13, 17, 22, 38, 400, 401, 403, 404, 409, 422, 500, 503}: - return "_E" + str(code) + return "_E" + str(code) + scoped_validation_field_suffix(obj) if type(code) is str and code in {"EINVAL","EPERM","EACCES","VALIDATION_ERRORS","METHOD_NOT_FOUND"}: return "_" + code if type(code) is dict: @@ -288,6 +336,12 @@ def invoke(method, params): except Exception as exc: suffix = (classify_scoped_key_ddp_failure(exc) if operation == "issue_scoped_key" else "") + if operation == "issue_scoped_key" and ( + suffix.startswith("_E11") or + suffix == "_TRANSPORT_TIMEOUT_INDETERMINATE"): + # A failed issuance may be indeterminate. One exact, read-only + # count only; never retry api_key.create or fetch key records. + suffix += scoped_key_record_count_suffix(call) raise ObservationError("GUEST_RBAC_" + operation.upper() + "_API_FAILED" + suffix) from None return invoke diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index c089ac0cc..8a71fb11a 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -1,9 +1,10 @@ """Pure request/response policy checks; never claims a running TrueNAS guest.""" import unittest +import json from scripts.truenas_middleware_collage_readback_p2 import ( ObservationError, provision_guest_readonly, bounded_rbac_call, RBAC_CALL_SEQUENCE, - classify_scoped_key_ddp_failure, + classify_scoped_key_ddp_failure, scoped_key_record_count_suffix, ) ROLE = "APPS_READ" @@ -126,9 +127,99 @@ def failure(method,params): with self.assertRaises(ObservationError) as caught: provision_guest_readonly(bounded_rbac_call(failure)) self.assertEqual(str(caught.exception), - "GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED"+category) + "GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED"+category+ + ("_KEY_COUNT_UNAVAILABLE" if category == + "_TRANSPORT_TIMEOUT_INDETERMINATE" else "")) self.assertNotIn("DO-NOT-EMIT",str(caught.exception)) + def test_validation_error_field_allowlist_and_no_provider_data_leakage(self): + # Frozen TrueNAS 25.04 websocket_app turns ValidationErrors into + # errno EAGAIN (11) with extra [field, message, errno] tuples. + cases = ( + ("api_key_create", "_E11_VALIDATION_GENERAL"), + ("api_key_create.name", "_E11_VALIDATION_NAME"), + ("api_key_create.username", "_E11_VALIDATION_USERNAME"), + ("api_key_create.expires_at", "_E11_VALIDATION_EXPIRES_AT"), + ("api_key_create.username.extra", "_E11_VALIDATION_FIELD_UNKNOWN"), + ("api_key_create.password", "_E11_VALIDATION_FIELD_UNKNOWN"), + ("evil.api_key_create.username", "_E11_VALIDATION_FIELD_UNKNOWN"), + ) + for field, expected in cases: + payload = {"error": 11, "type": "VALIDATION", + "extra": [[field, "SENSITIVE-KEY-DO-NOT-EMIT", 22]], + "reason": "SENSITIVE-USER-DO-NOT-EMIT", + "trace": {"formatted": "SENSITIVE-TRACE-DO-NOT-EMIT"}} + value = classify_scoped_key_ddp_failure( + RuntimeError("api_key.create: " + json.dumps(payload))) + with self.subTest(field=field): + self.assertEqual(value, expected) + self.assertNotIn("SENSITIVE", value) + self.assertNotIn(field, value) + extra_cases = ( + ([], "_E11_VALIDATION_FIELD_UNKNOWN"), + (["api_key_create.username"], "_E11_VALIDATION_FIELD_UNKNOWN"), + ([["api_key_create.username", "secret"]], "_E11_VALIDATION_FIELD_UNKNOWN"), + ([[True, "secret", 22]], "_E11_VALIDATION_FIELD_UNKNOWN"), + ([[["api_key_create.username"], "secret", 22]], "_E11_VALIDATION_FIELD_UNKNOWN"), + ([[ "api_key_create.username", "secret", 22]]*13, + "_E11_VALIDATION_FIELD_UNKNOWN"), + ([["api_key_create.username", "secret", 22], + ["api_key_create.name", "secret", 22]], "_E11_VALIDATION_MULTI"), + ) + for extra, expected in extra_cases: + payload = {"error": 11, "type": "VALIDATION", "extra": extra, + "trace": "DO-NOT-EMIT"*1000} + self.assertEqual(classify_scoped_key_ddp_failure( + RuntimeError("api_key.create: " + json.dumps(payload))), expected) + for bad_type in (None, False, "VALIDATION_WRONG", [], {}): + payload = {"error": 11, "type": bad_type, + "extra": [["api_key_create.name", "secret", 22]]} + self.assertEqual(classify_scoped_key_ddp_failure( + RuntimeError("api_key.create: " + json.dumps(payload))), "_E11") + + def test_failed_nonidempotent_key_creation_performs_only_one_exact_readonly_count(self): + for count, expected in ((0, "_KEY_COUNT0"), (1, "_KEY_COUNT1"), + (2, "_KEY_COUNT_MULTIPLE"), (False, "_KEY_COUNT_INVALID"), + (-1, "_KEY_COUNT_INVALID"), ([], "_KEY_COUNT_INVALID")): + calls = [] + def read_only(method, params): + calls.append((method, params)) + self.assertEqual(method, "api_key.query") + self.assertEqual(params, [ + [["username", "=", USER], ["name", "=", "disposable-collage-p2"]], + {"count": True}, + ]) + return count + with self.subTest(count=count): + self.assertEqual(scoped_key_record_count_suffix(read_only), expected) + self.assertEqual(len(calls), 1) + def fail_query(method, params): + raise RuntimeError("SECRET-NEVER-PUBLISH") + self.assertEqual(scoped_key_record_count_suffix(fail_query), + "_KEY_COUNT_UNAVAILABLE") + + responses = authority_responses() + called = [] + def failed_key(method, params): + called.append(method) + if method == "api_key.create": + raise RuntimeError("api_key.create: " + json.dumps({ + "error": 11, "type": "VALIDATION", + "extra": [["api_key_create.username", "SECRET", 22]], + "trace": "SECRET-NEVER-PUBLISH"})) + if method == "api_key.query": + self.assertEqual(params[1], {"count": True}) + return 0 + return responses[len(called)-1][1] + with self.assertRaises(ObservationError) as raised: + provision_guest_readonly(bounded_rbac_call(failed_key)) + self.assertEqual(str(raised.exception), + "GUEST_RBAC_ISSUE_SCOPED_KEY_API_FAILED" + "_E11_VALIDATION_USERNAME_KEY_COUNT0") + self.assertEqual(called[-2:], ["api_key.create", "api_key.query"]) + self.assertEqual(called.count("api_key.create"), 1) + self.assertNotIn("SECRET", str(raised.exception)) + def test_source_contract_allows_only_exact_role_linkage(self): fn, methods = self.call_with(authority_responses()) self.assertEqual(provision_guest_readonly(fn), From a639201d2b4a88b7c25dd7a11910e328c54a5340 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:21:36 +0200 Subject: [PATCH 689/719] admit(op4f): single 010 validation-field count evidence request * admit(op4f): bind unique 010 validation-field guest edge config/truenas-rdte-run-request.json * admit(op4f): bind unique 010 validation-field guest edge tests/test_truenas_rdte_target.py * admit(op4f): bind unique 010 validation-field guest edge tests/test_vanilla263_t6_guest_routing.py --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index c00926d21..baafd91f9 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4F unique one-time 009 public/free serialized disposable native TrueNAS 25.04.2.6/vanilla 26.3/Java25 source+TLS Go observer. First verify App RUNNING/owner/world readback; attempt APPS_READ-only scoped api_key.create before expensive ZIP staging/restart/reinstall, read only allowlisted numeric/shape class from <=262144-character structured DDP errors and retain zero raw trace/user/key. On observer PASS proceed to exact 739-byte Lantern ZIP staging and owned world lifecycle; on credential refusal emit sanitized category, cleanup owned App/ZFS and stop. 008 is consumed FAIL; no 008/009 retry, no FULL_ADMIN/disabled TLS, no stock online auth claim, no private GHA minutes, no physical TrueNAS APPLY.", + "purpose": "OP4F-010 one-use serialized public/free disposable TrueNAS25.04.2.6 native vanilla26.3/Java25/777 scoped COLLAGE G2 evidence rep after real 009 E11 ValidationErrors refusal. Exact accepted signed Foundry and 739-byte Lantern ZIP, APPS_READ-only short-lived observer, strict TLS. Diagnose E11 by fixed allowlisted validation-field category and one exact postfailure read-only api_key.query count, no error text or returned rows; no retry of api_key.create. Run independent native owner/first readiness, scoped TLS Go observer BEFORE costly world ZIP stage; if G2 fails, classify, clean own App/ZFS and stop. If G2 passes, qualify unchanged real ZIP native stage/persistence. No operational identity or in-guest gameplay claim, no role escalation, no TLS disable, no private GHA minutes, no physical TrueNAS APPLY. 009 and earlier request IDs consumed, do not replay 010.", "collage_readback": true, "lantern_stage": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 37ecba1b0..42e475af1 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer") + "tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index a9137bb3f..8800b8fc4 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-009-early-key-code-observer") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", From 4e0372e85b030cd39d2789a082997503144d4f4a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:59:07 +0200 Subject: [PATCH 690/719] fix(op4f): one-hour TrueNAS EJSON datetime for scoped observer * fix(op4f): encode scoped one-hour expiry using frozen TrueNAS EJSON datetime * test(op4f): frozen EJSON one-hour typed expiry, timezone and malformed negatives --- .../truenas_middleware_collage_readback_p2.py | 26 ++++++++++++- tests/test_collage_p2_effective_roles.py | 38 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index e9bcf0672..ce3763c2f 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -60,6 +60,26 @@ def available_role_names(roles: object) -> set[str]: return names + +def scoped_key_expiry_ejson(now: dt.datetime | None = None) -> dict[str, int]: + """One-hour UTC expiry in pinned TrueNAS legacy WebSocket EJSON format. + + TrueNAS api_client release/25.04.2.6/ejson.py serializes datetime as + {"$date": UTC milliseconds}. The pinned middleware BaseModel has + strict=True: a plain datetime.isoformat() string is rejected as type + datetime by the 25.04.2.6 api_key.create input validator. Do not bypass + expiration or change role/transport to avoid this type mismatch. + """ + if now is None: + now = dt.datetime.now(dt.timezone.utc) + require(type(now) is dt.datetime and now.tzinfo is not None and + now.utcoffset() is not None, + "SCOPED_KEY_EXPIRY_REQUIRES_AWARE_DATETIME") + future = now.astimezone(dt.timezone.utc) + dt.timedelta(hours=1) + # Exactly the UTC-second canonicalization in the frozen EJSON encoder. + return {"$date": int(future.timestamp()) * 1000} + + def provision_guest_readonly(call) -> str: """Called on *already isolated* disposable TrueNAS 25.04.1 via admin DDP. @@ -128,7 +148,11 @@ def provision_guest_readonly(call) -> str: "OBSERVER_USER_ROLES_SHAPE_DRIFT") require("APPS_READ" in effective, "OBSERVER_USER_REQUIRED_ROLE_ABSENT") require(effective == ["APPS_READ"], "OBSERVER_USER_EXTRA_ROLES_REFUSED") - expiry = (dt.datetime.now(dt.timezone.utc) + dt.timedelta(hours=1)).isoformat() + # The 25.04.2.6 BaseModel is strict: a bare ISO string is NOT a + # datetime in Python-mode validation. Legacy DDP uses extended JSON + # {"$date": epoch_ms}; the pinned TrueNAS decoder turns it into a + # timezone-aware datetime before API validation. Keep the one-hour TTL. + expiry = scoped_key_expiry_ejson() key = call("api_key.create", [{ "name": "disposable-collage-p2", "username": NAME, "expires_at": expiry, diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index 8a71fb11a..ae901b287 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -1,10 +1,12 @@ """Pure request/response policy checks; never claims a running TrueNAS guest.""" import unittest import json +import datetime as dt from scripts.truenas_middleware_collage_readback_p2 import ( ObservationError, provision_guest_readonly, bounded_rbac_call, RBAC_CALL_SEQUENCE, classify_scoped_key_ddp_failure, scoped_key_record_count_suffix, + scoped_key_expiry_ejson, ) ROLE = "APPS_READ" @@ -46,6 +48,16 @@ def call(method, params): self.assertLess(len(called), len(responses), "unexpected API call") want, result = responses[len(called)] self.assertEqual(method, want, f"wrong operation at position {len(called)}") + if method == "api_key.create": + # Assert the exact 25.04.2.6 legacy DDP EJSON datetime shape, + # NOT the previously E11-refused ISO string or nonexpiring key. + expires = params[0]["expires_at"] + self.assertEqual(set(expires), {"$date"}) + self.assertIs(type(expires["$date"]), int) + seconds_ahead = expires["$date"] / 1000 - dt.datetime.now( + dt.timezone.utc).timestamp() + self.assertLess(seconds_ahead, 3605) + self.assertGreater(seconds_ahead, 3550) if method == "privilege.create": # Caller must send UNIX GID, not middleware group datastore id. self.assertEqual(params[0]["local_groups"], [GROUP_GID]) @@ -132,6 +144,32 @@ def failure(method,params): "_TRANSPORT_TIMEOUT_INDETERMINATE" else "")) self.assertNotIn("DO-NOT-EMIT",str(caught.exception)) + def test_scoped_expiry_is_timezone_aware_pinned_ejson_datetime_not_string(self): + # Frozen trueNAS_api_client release/25.04.2.6 ejson.JSONEncoder + # emits UTC milliseconds; object_hook restores timezone-aware datetime. + original = dt.datetime(2026, 10, 14, 15, 0, 0, 456000, + tzinfo=dt.timezone.utc) + value = scoped_key_expiry_ejson(original) + self.assertEqual(value, {"$date": 1791993600000}) + restored = dt.datetime.fromtimestamp(value["$date"] / 1000, + tz=dt.timezone.utc) + self.assertIs(type(restored), dt.datetime) + self.assertEqual(restored, dt.datetime(2026, 10, 14, 16, 0, + tzinfo=dt.timezone.utc)) + self.assertNotIsInstance(value, str) + different_zone = original.astimezone(dt.timezone(dt.timedelta(hours=7))) + self.assertEqual(scoped_key_expiry_ejson(different_zone), value) + with self.assertRaisesRegex(ObservationError, "AWARE_DATETIME"): + scoped_key_expiry_ejson(dt.datetime(2026, 10, 14, 15)) + for invalid in (None, "2026-10-14T16:00:00Z", 1791990000, {}): + # None uses the live clock by documented default; malformed + # non-null inputs must fail closed. + if invalid is None: + assert set(scoped_key_expiry_ejson()) == {"$date"} + else: + with self.assertRaises(ObservationError): + scoped_key_expiry_ejson(invalid) + def test_validation_error_field_allowlist_and_no_provider_data_leakage(self): # Frozen TrueNAS 25.04 websocket_app turns ValidationErrors into # errno EAGAIN (11) with extra [field, message, errno] tuples. From 08410f377acf9e30f34e5298ea8bd411196ad573 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:02:44 +0200 Subject: [PATCH 691/719] admit(op4f): one serialized 011 EJSON scoped native G2 rep * admit(op4f): unique one-use 011 EJSON-native guest edge config/truenas-rdte-run-request.json * admit(op4f): unique one-use 011 EJSON-native guest edge tests/test_truenas_rdte_target.py * admit(op4f): unique one-use 011 EJSON-native guest edge tests/test_vanilla263_t6_guest_routing.py --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index baafd91f9..e37cfb5cb 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4F-010 one-use serialized public/free disposable TrueNAS25.04.2.6 native vanilla26.3/Java25/777 scoped COLLAGE G2 evidence rep after real 009 E11 ValidationErrors refusal. Exact accepted signed Foundry and 739-byte Lantern ZIP, APPS_READ-only short-lived observer, strict TLS. Diagnose E11 by fixed allowlisted validation-field category and one exact postfailure read-only api_key.query count, no error text or returned rows; no retry of api_key.create. Run independent native owner/first readiness, scoped TLS Go observer BEFORE costly world ZIP stage; if G2 fails, classify, clean own App/ZFS and stop. If G2 passes, qualify unchanged real ZIP native stage/persistence. No operational identity or in-guest gameplay claim, no role escalation, no TLS disable, no private GHA minutes, no physical TrueNAS APPLY. 009 and earlier request IDs consumed, do not replay 010.", + "purpose": "OP4F-011 single serialized public/free disposable TrueNAS25.04.2.6 native vanilla26.3/Java25/protocol777 scoped COLLAGE G2 EJSON expiry qualification. Following consumed 010 real E11_VALIDATION_EXPIRES_AT_KEY_COUNT0 and signed strict Pydantic/native 25.04 legacy EJSON source repair, issue one scoped short-lived APPS_READ guest-only observer key using 1h UTC {'$date': epoch_ms}, no retry after ambiguity and only allowlisted field/count failure categories. Verify first native App readiness and TLS-pinned Go read-only six-marker owner observation with observation_mutation_count=0 BEFORE real 739-byte Lantern ZIP stage/stop-restart/reinstall. If observation fails, preserve bounded receipt, independently clean owned App/ZFS and STOP; if passes, complete existing exact artifact stage/readback/retain/cleanup without managed COLLAGE APPLY claims. Current 011 is one-use; no replay 006-010, no role escalation, TLS bypass, secrets or raw DDP trace, private GHA minutes, native player auth claim or physical TrueNAS APPLY.", "collage_readback": true, "lantern_stage": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 42e475af1..385f105e7 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count") + "tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 8800b8fc4..b27fd9df7 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-010-validation-field-count") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", From 8670fc91685d5bd95a6d6313fe0123cf40a18581 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:01:40 +0200 Subject: [PATCH 692/719] feat(op4g): source-qualify exact owner native world rollback mechanics * feat(op4g): fixed-owner disposable native ZFS rollback transaction helper * test(op4g): bounded real-native rollback seam red/blue source oracles * ci(op4g): source-admit bounded native rollback and replay/foreign-owner negatives * fix(op4g): support package unit tests and native top-level guest import --- .../vanilla263-native-guest-static.yml | 4 + ...ualify_vanilla263_native_world_rollback.py | 125 +++++++++++++ .../test_vanilla263_native_world_rollback.py | 166 ++++++++++++++++++ 3 files changed, 295 insertions(+) create mode 100644 scripts/qualify_vanilla263_native_world_rollback.py create mode 100644 tests/test_vanilla263_native_world_rollback.py diff --git a/.github/workflows/vanilla263-native-guest-static.yml b/.github/workflows/vanilla263-native-guest-static.yml index 5f9a8f7f2..1044d4c08 100644 --- a/.github/workflows/vanilla263-native-guest-static.yml +++ b/.github/workflows/vanilla263-native-guest-static.yml @@ -6,6 +6,8 @@ on: - 'scripts/truenas_middleware_collage_readback_p2.py' - 'tests/test_collage_p2_effective_roles.py' - 'scripts/qualify_vanilla263_native_lantern_stage.py' + - 'scripts/qualify_vanilla263_native_world_rollback.py' + - 'tests/test_vanilla263_native_world_rollback.py' - 'tests/test_vanilla263_native_lantern_stage.py' - 'scripts/assert_vanilla263_native_t6_receipt.py' - 'tests/test_truenas_t6_vanilla263_guest.py' @@ -71,6 +73,7 @@ jobs: scripts/truenas_middleware_minecraft_t6_probe.py \ scripts/truenas_middleware_collage_readback_p2.py \ scripts/qualify_vanilla263_native_lantern_stage.py \ + scripts/qualify_vanilla263_native_world_rollback.py \ scripts/admit_vanilla263_native_t6_control.py \ scripts/assert_vanilla263_native_t6_receipt.py python3 -m py_compile tests/test_truenas_t6_vanilla263_guest.py @@ -78,6 +81,7 @@ jobs: run: | set -euo pipefail python3 -m unittest -v tests/test_vanilla263_native_lantern_stage.py + python3 -m unittest -v tests/test_vanilla263_native_world_rollback.py python3 -m unittest -v tests/test_truenas_t6_vanilla263_guest.py python3 -m unittest -v tests/test_collage_p2_effective_roles.py bash -n scripts/gha_kvm_truenas_rdte.sh diff --git a/scripts/qualify_vanilla263_native_world_rollback.py b/scripts/qualify_vanilla263_native_world_rollback.py new file mode 100644 index 000000000..d8d424fda --- /dev/null +++ b/scripts/qualify_vanilla263_native_world_rollback.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Bounded G3 native-world rollback mechanics for an isolated TrueNAS RDTE guest. + +This is NOT a COLLAGE-authorized APPLY, online player authentication, generic +snapshot manager, production NAS operator, or evidence of a successful guest. +The existing native T6 runner owns the authenticated session, exact App/world, +signed artifact, stop/start calls, lifecycle receipts and final owned cleanup. +This module supplies only fixed identity/snapshot/rollback preconditions. +""" +from __future__ import annotations + +# Native guest executes scripts as top-level modules; public unit tests import +# them through the scripts package. Share the same pinned artifact constants. +try: + from .qualify_vanilla263_native_lantern_stage import WORLD, TARGET, SHA, NAME +except ImportError: + from qualify_vanilla263_native_lantern_stage import WORLD, TARGET, SHA, NAME + +APP = "rdte-t6-minecraft-vanilla-263" +DATASET = "rdtepool/minecraft-vanilla-263-t6-world" +SNAPSHOT = DATASET + "@op4g-pre-lantern" +SCHEMA = "supracraft.native-vanilla263-rdte-rollback-mechanics/1" + + +class Refused(RuntimeError): + pass + + +def require(value, code): + if not value: + raise Refused(code) + + +def app_state(call): + rows = call("app.query", [[["id", "=", APP]]]) + require(type(rows) is list and len(rows) == 1 and + type(rows[0]) is dict and rows[0].get("name", APP) == APP, + "G3_APP_OWNER_NOT_EXACT") + state = rows[0].get("state") + require(state in {"STOPPED", "RUNNING"}, "G3_APP_UNSAFE_STATE") + return state + + +def _snapshot_rows(call): + rows = call("zfs.snapshot.query", [[["id", "=", SNAPSHOT]]]) + require(type(rows) is list and len(rows) <= 1 and + all(type(row) is dict and row.get("id") == SNAPSHOT for row in rows), + "G3_SNAPSHOT_QUERY_AMBIGUOUS") + return rows + + +def _artifact_present(call): + rows = call("filesystem.listdir", [WORLD + "/datapacks"]) + require(type(rows) is list and len(rows) <= 4096 and all( + type(row) is dict and type(row.get("name")) is str and + len(row["name"]) < 260 for row in rows), + "G3_DATA_PACK_INVENTORY_UNSAFE") + # Compare only exact basename; no traversal, glob or deletion. + return any(row["name"] == NAME for row in rows) + + +def take_owned_pre_stage_snapshot(call): + """Called only after existing T6 owner App is verified STOPPED. + + Must precede the actual signed 739-byte ZIP stage. Refuse preexisting + snapshots and world release conflicts; don't rename foreign snapshots. + """ + require(app_state(call) == "STOPPED", "G3_QUIESCE_REQUIRED") + require(not _artifact_present(call), "G3_CANDIDATE_ALREADY_PRESENT") + require(_snapshot_rows(call) == [], "G3_SNAPSHOT_REPLAY_REFUSED") + row = call("zfs.snapshot.create", [{ + "dataset": DATASET, "name": SNAPSHOT.rsplit("@", 1)[1], + "recursive": False, "exclude": [], "suspend_vms": False, + "vmware_sync": False, + }]) + require(type(row) is dict and row.get("id") == SNAPSHOT, + "G3_SNAPSHOT_RESULT_NOT_EXACT") + require(len(_snapshot_rows(call)) == 1, "G3_SNAPSHOT_NOT_DURABLE") + return { + "schema": SCHEMA, "phase": "baseline_snapshotted", + "classification": "G3_DISPOSABLE_MECHANICS_ONLY", + "snapshot_owned": True, "snapshot_id_verified": True, + "candidate_sha256": SHA, "candidate_present_before_stage": False, + "world_activation_authorized_by_collage": False, + "actual_player_gameplay_verified": False, + "physical_true_nas_mutated": False, + } + + +def restore_owned_baseline_after_injected_fault(call, wait_job, wait_app, wait_status): + """Fixed rollback after independent native byte stage and app re-install. + + Caller independently verifies exact ZIP bytes and healthy running App. + This function injects an explicit failed release decision: never promote. + Stop -> rollback exact owned snapshot -> restart -> health recheck. + """ + require(app_state(call) == "RUNNING", "G3_PREFAULT_APP_NOT_RUNNING") + require(_artifact_present(call), "G3_CANDIDATE_MISSING_PREFAULT") + require(len(_snapshot_rows(call)) == 1, "G3_SNAPSHOT_NOT_DURABLE") + wait_job(call("app.stop", [APP]), "G3 pre-rollback quiesce") + require(app_state(call) == "STOPPED", "G3_STOP_UNCONFIRMED") + response = call("zfs.snapshot.rollback", [SNAPSHOT, { + "recursive": False, "recursive_clones": False, "force": False, + "recursive_rollback": False, + }]) + require(response is None or response is True, "G3_ROLLBACK_REFUSED") + require(not _artifact_present(call), "G3_ROLLBACK_LEFT_CANDIDATE") + wait_job(call("app.start", [APP]), "G3 after-rollback resume") + wait_app() + wait_status("g3-rollback") + require(app_state(call) == "RUNNING", "G3_RESTORED_APP_NOT_RUNNING") + require(not _artifact_present(call), "G3_RESTART_REINTRODUCED_CANDIDATE") + require(len(_snapshot_rows(call)) == 1, "G3_ROLLBACK_SNAPSHOT_NOT_DURABLE") + return { + "schema": SCHEMA, "phase": "injected_fault_rolled_back", + "classification": "G3_DISPOSABLE_MECHANICS_ONLY", + "snapshot_id_verified": True, "release_fault_injected": True, + "candidate_absent_after_rollback": True, + "native_app_running_after_rollback": True, + "native_game_status_rechecked": True, + "candidate_sha256": SHA, "collage_managed_deployment": False, + "world_activation_authorized_by_collage": False, + "actual_player_gameplay_verified": False, + "physical_true_nas_mutated": False, + } diff --git a/tests/test_vanilla263_native_world_rollback.py b/tests/test_vanilla263_native_world_rollback.py new file mode 100644 index 000000000..571e62b78 --- /dev/null +++ b/tests/test_vanilla263_native_world_rollback.py @@ -0,0 +1,166 @@ +"""OP4G G3 disposable world transaction cheap red/blue contract tests. + +Mock-only local transport for source validation. No native guest or AUTHORIZED +COLLAGE APPLY may be inferred from these unit tests. +""" +from __future__ import annotations +import unittest + +from scripts.qualify_vanilla263_native_world_rollback import ( + APP, DATASET, NAME, SNAPSHOT, WORLD, SCHEMA, Refused, + app_state, take_owned_pre_stage_snapshot, + restore_owned_baseline_after_injected_fault, +) + + +class SimulatedOwnedGuest: + def __init__(self): + self.state = "STOPPED" + self.pack = False + self.snapshot = False + self.call_log = [] + self.replayed = False + self.unknown_app = False + self.foreign_snapshot = False + self.forced_failure = None + + def __call__(self, method, args): + self.call_log.append((method, args)) + if method == self.forced_failure: + raise ConnectionError("UNTRUSTED SECRET MUST NEVER ESCAPE") + if method == "app.query": + return ([{"name": "foreign", "state": self.state}] if self.unknown_app + else [{"name": APP, "state": self.state}]) + if method == "filesystem.listdir": + assert args == [WORLD + "/datapacks"] + return [{"name": NAME, "type": "FILE"}] if self.pack else [] + if method == "zfs.snapshot.query": + assert args == [[["id", "=", SNAPSHOT]]] + if self.foreign_snapshot: + return [{"id": "foreign/snapshot"}] + return [{"id": SNAPSHOT}] if self.snapshot else [] + if method == "zfs.snapshot.create": + assert args[0]["dataset"] == DATASET + assert args[0]["name"] == "op4g-pre-lantern" + assert args[0]["recursive"] is False + assert args[0]["suspend_vms"] is False + assert not self.snapshot + self.snapshot = True + return {"id": SNAPSHOT} + if method == "app.stop": + assert args == [APP] + self.state = "STOPPED" + return 11 + if method == "zfs.snapshot.rollback": + assert args == [SNAPSHOT, {"recursive": False, + "recursive_clones": False, "force": False, + "recursive_rollback": False}] + assert self.state == "STOPPED" + assert self.snapshot + self.pack = False + return None + if method == "app.start": + assert args == [APP] + self.state = "RUNNING" + return 12 + raise AssertionError(method) + + +class NativeRollbackContract(unittest.TestCase): + def test_exact_stop_snapshot_stage_fault_restore_and_restart(self): + api = SimulatedOwnedGuest() + baseline = take_owned_pre_stage_snapshot(api) + self.assertEqual(baseline["schema"], SCHEMA) + self.assertFalse(baseline["world_activation_authorized_by_collage"]) + self.assertFalse(baseline["actual_player_gameplay_verified"]) + self.assertFalse(baseline["physical_true_nas_mutated"]) + api.pack = True # independent actual stage is separately admitted + api.state = "RUNNING" + events = [] + def job(i, label): + self.assertIn(i, (11, 12)) + events.append(("job", label)) + def wait_app(): + self.assertEqual(api.state, "RUNNING") + events.append(("app", "ready")) + def wait_status(which): + self.assertEqual(which, "g3-rollback") + events.append(("game", which)) + receipt = restore_owned_baseline_after_injected_fault( + api, job, wait_app, wait_status) + self.assertTrue(receipt["candidate_absent_after_rollback"]) + self.assertTrue(receipt["native_game_status_rechecked"]) + self.assertFalse(receipt["collage_managed_deployment"]) + self.assertFalse(receipt["actual_player_gameplay_verified"]) + order = [x[0] for x in api.call_log] + self.assertLess(order.index("zfs.snapshot.create"), + order.index("zfs.snapshot.rollback")) + self.assertLess(order.index("app.stop"), + order.index("zfs.snapshot.rollback")) + self.assertLess(order.index("zfs.snapshot.rollback"), + order.index("app.start")) + self.assertEqual(len([x for x in api.call_log if x[0] == + "zfs.snapshot.rollback"]), 1) + self.assertIn(("game", "g3-rollback"), events) + + def test_quiescence_owner_replay_and_existing_candidate_negatives(self): + for state, pack, snapshot, foreign, expected in ( + ("RUNNING", False, False, False, "G3_QUIESCE_REQUIRED"), + ("STOPPED", True, False, False, "G3_CANDIDATE_ALREADY_PRESENT"), + ("STOPPED", False, True, False, "G3_SNAPSHOT_REPLAY_REFUSED"), + ("STOPPED", False, False, True, "G3_APP_OWNER_NOT_EXACT"), + ): + api = SimulatedOwnedGuest() + api.state, api.pack, api.snapshot, api.unknown_app = ( + state, pack, snapshot, foreign) + with self.subTest(expected=expected): + with self.assertRaisesRegex(Refused, expected): + take_owned_pre_stage_snapshot(api) + self.assertFalse(any(x[0] == "zfs.snapshot.create" + for x in api.call_log)) + api = SimulatedOwnedGuest() + api.foreign_snapshot = True + with self.assertRaisesRegex(Refused, "G3_SNAPSHOT_QUERY_AMBIGUOUS"): + take_owned_pre_stage_snapshot(api) + + def test_rollback_refuses_absent_artifact_or_unowned_app(self): + for state, pack, snapshot, reason in ( + ("STOPPED", True, True, "G3_PREFAULT_APP_NOT_RUNNING"), + ("RUNNING", False, True, "G3_CANDIDATE_MISSING_PREFAULT"), + ("RUNNING", True, False, "G3_SNAPSHOT_NOT_DURABLE"), + ): + api = SimulatedOwnedGuest() + api.state, api.pack, api.snapshot = state, pack, snapshot + with self.subTest(reason=reason): + with self.assertRaisesRegex(Refused, reason): + restore_owned_baseline_after_injected_fault( + api, lambda *_: None, lambda: None, lambda *_: None) + self.assertNotIn("app.stop", [x[0] for x in api.call_log]) + + def test_snapshot_and_inventory_malformed_shapes_rejected_before_mutation(self): + class BadShape(SimulatedOwnedGuest): + def __init__(self, method, result): + super().__init__() + self.bad_method, self.bad_result = method, result + def __call__(self, method, args): + if method == self.bad_method: + self.call_log.append((method, args)) + return self.bad_result + return super().__call__(method, args) + for method, response in ( + ("filesystem.listdir", [{"name": "../../foreign"}] * 4097), + ("filesystem.listdir", [{"name": 5}]), + ("zfs.snapshot.query", [{"id": SNAPSHOT}, {"id": SNAPSHOT}]), + ("zfs.snapshot.query", {"id": SNAPSHOT}), + ("app.query", [{"name": APP, "state": "STOPPED"}] * 2), + ): + api = BadShape(method, response) + with self.subTest(method=method): + with self.assertRaises(Refused): + take_owned_pre_stage_snapshot(api) + self.assertNotIn("zfs.snapshot.create", + [x[0] for x in api.call_log]) + + +if __name__ == "__main__": + unittest.main() From bcf4ea46b25cb4bc639389c6128ca264b4c953aa Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:07:51 +0200 Subject: [PATCH 693/719] feat(op4g): wire fixed-owner native rollback into disposable guest only * feat(op4g): gated native pre-stage ZFS snapshot and post-reinstall injected-fault restore * feat(op4g): explicit disposable-only shell admission flag for native rollback * ci(op4g): exact one-use request-scoped native rollback admission and independent receipt gate * test(op4g): independently gate native rollback mechanics and healthy world receipt * test(op4g): independent G3 mechanics receipt cannot masquerade as managed COLLAGE APPLY * ci(op4g): native rollback opt-in runner and independent negative receipt tests --- .github/workflows/gha-kvm-system-rdte.yml | 16 +++ .../vanilla263-native-guest-static.yml | 2 + .../assert_vanilla263_native_t6_receipt.py | 40 ++++++- scripts/gha_kvm_truenas_rdte.sh | 13 +++ .../truenas_middleware_minecraft_t6_probe.py | 27 +++++ ...test_vanilla263_native_rollback_receipt.py | 102 ++++++++++++++++++ 6 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 tests/test_vanilla263_native_rollback_receipt.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 8f20a1838..49d89e209 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -94,6 +94,7 @@ jobs: truenas_t6_product: ${{ steps.scope.outputs.truenas_t6_product }} truenas_collage_readback: ${{ steps.scope.outputs.truenas_collage_readback }} truenas_lantern_stage: ${{ steps.scope.outputs.truenas_lantern_stage }} + truenas_world_rollback: ${{ steps.scope.outputs.truenas_world_rollback }} truenas_compute_fixture: ${{ steps.scope.outputs.truenas_compute_fixture }} proxmox_compute_fixture: ${{ steps.scope.outputs.proxmox_compute_fixture }} steps: @@ -124,6 +125,7 @@ jobs: truenas_t6_product="${DISPATCH_TRUENAS_T6_PRODUCT:-litellm}" truenas_collage_readback=false truenas_lantern_stage=false + truenas_world_rollback=false truenas_compute_fixture="${DISPATCH_TRUENAS_COMPUTE_FIXTURE:-none}" proxmox_compute_fixture="${DISPATCH_PROXMOX_COMPUTE_FIXTURE:-none}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then @@ -196,6 +198,13 @@ jobs: truenas_compute_fixture="$(jq -er '(.compute_fixture // "none") | select(. == "none" or . == "container-c0" or . == "vm-v0" or . == "vm-v1")' "$RUN_REQUEST_PATH")" truenas_collage_readback="$(jq -er '(.collage_readback // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" truenas_lantern_stage="$(jq -er '(.lantern_stage // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + truenas_world_rollback="$(jq -er '(.native_world_rollback // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + if [[ "$truenas_world_rollback" == "true" ]]; then + [[ "$truenas_lantern_stage" == "true" && "$truenas_collage_readback" == "true" && + "$truenas_t6_product" == "minecraft-vanilla-263" && "$truenas_version" == "25.04.2.6" && "$truenas_rung" == "t6" ]] || { + echo "native rollback only allowed in one exact G2+Lantern disposable fixture" >&2; exit 2; + } + fi if [[ "$truenas_lantern_stage" == "true" ]]; then [[ "$truenas_collage_readback" == "true" && "$truenas_t6_product" == "minecraft-vanilla-263" && "$truenas_version" == "25.04.2.6" && "$truenas_rung" == "t6" ]] || { @@ -224,6 +233,7 @@ jobs: echo "truenas_t6_product=$truenas_t6_product" >>"$GITHUB_OUTPUT" echo "truenas_collage_readback=$truenas_collage_readback" >>"$GITHUB_OUTPUT" echo "truenas_lantern_stage=$truenas_lantern_stage" >>"$GITHUB_OUTPUT" + echo "truenas_world_rollback=$truenas_world_rollback" >>"$GITHUB_OUTPUT" echo "truenas_compute_fixture=$truenas_compute_fixture" >>"$GITHUB_OUTPUT" echo "proxmox_compute_fixture=$proxmox_compute_fixture" >>"$GITHUB_OUTPUT" printf 'target scope: proxmox=%s proxmox_compute_fixture=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s truenas_compute_fixture=%s\n' "$proxmox" "$proxmox_compute_fixture" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" @@ -809,6 +819,9 @@ jobs: args+=(--vanilla-lantern-zip "$RUNNER_TEMP/lantern-native/lantern-restoration-26.3.zip" --vanilla-lantern-manifest "$RUNNER_TEMP/lantern-native/manifest.json") fi + if [[ "${{ needs.changes.outputs.truenas_world_rollback }}" == "true" ]]; then + args+=(--vanilla-world-rollback) + fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" # A green job is not a Minecraft product pass without full guest evidence. @@ -824,6 +837,9 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_lantern_stage }}" == "true" ]]; then additional+=(--require-lantern-staged) fi + if [[ "${{ needs.changes.outputs.truenas_world_rollback }}" == "true" ]]; then + additional+=(--require-native-rollback) + fi python3 scripts/assert_vanilla263_native_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" "${additional[@]}" fi if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then diff --git a/.github/workflows/vanilla263-native-guest-static.yml b/.github/workflows/vanilla263-native-guest-static.yml index 1044d4c08..a1bfac63b 100644 --- a/.github/workflows/vanilla263-native-guest-static.yml +++ b/.github/workflows/vanilla263-native-guest-static.yml @@ -8,6 +8,7 @@ on: - 'scripts/qualify_vanilla263_native_lantern_stage.py' - 'scripts/qualify_vanilla263_native_world_rollback.py' - 'tests/test_vanilla263_native_world_rollback.py' + - 'tests/test_vanilla263_native_rollback_receipt.py' - 'tests/test_vanilla263_native_lantern_stage.py' - 'scripts/assert_vanilla263_native_t6_receipt.py' - 'tests/test_truenas_t6_vanilla263_guest.py' @@ -82,6 +83,7 @@ jobs: set -euo pipefail python3 -m unittest -v tests/test_vanilla263_native_lantern_stage.py python3 -m unittest -v tests/test_vanilla263_native_world_rollback.py + python3 -m unittest -v tests/test_vanilla263_native_rollback_receipt.py python3 -m unittest -v tests/test_truenas_t6_vanilla263_guest.py python3 -m unittest -v tests/test_collage_p2_effective_roles.py bash -n scripts/gha_kvm_truenas_rdte.sh diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py index f3efb5c57..c57649ba2 100644 --- a/scripts/assert_vanilla263_native_t6_receipt.py +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -20,7 +20,8 @@ def check(root: dict, *, require_collage: bool = False, - require_lantern_staged: bool = False) -> None: + require_lantern_staged: bool = False, + require_native_rollback: bool = False) -> None: def need(condition: bool, reason: str) -> None: if not condition: raise ValueError(reason) @@ -122,6 +123,36 @@ def need(condition: bool, reason: str) -> None: stage.get("principal_authorized_to_apply") is False and stage.get("physical_truenas_apply") == "HOLD", "actual native Lantern ZIP byte stage/retention was not qualified") + if require_native_rollback: + need(require_lantern_staged and require_collage, + "native rollback requires independently qualified G2 and artifact") + rb = p.get("native_world_rollback") or {} + need(type(rb) is dict and + rb.get("schema") == "supracraft.native-vanilla263-rdte-rollback-mechanics/1" and + rb.get("classification") == "G3_DISPOSABLE_MECHANICS_ONLY" and + rb.get("phase") == "injected_fault_rolled_back" and + rb.get("snapshot_owned") is True and + rb.get("snapshot_id_verified") is True and + rb.get("candidate_present_before_stage") is False and + rb.get("release_fault_injected") is True and + rb.get("candidate_absent_after_rollback") is True and + rb.get("native_app_running_after_rollback") is True and + rb.get("native_game_status_rechecked") is True and + rb.get("candidate_sha256") == + "da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8" and + rb.get("collage_managed_deployment") is False and + rb.get("world_activation_authorized_by_collage") is False and + rb.get("actual_player_gameplay_verified") is False and + rb.get("physical_true_nas_mutated") is False, + "native disposable rollback receipt missing") + restored = (p.get("readiness") or {}).get("g3-rollback") or {} + proto = restored.get("protocol") or {} + level = restored.get("level") or {} + need(type(restored) is dict and type(proto) is dict and + proto.get("oracleSatisfied") is True and + type(level) is dict and type(level.get("size")) is int and + level["size"] > 0, + "native rollback independently healthy world not observed") if require_collage: observer = p.get("collage_readback") or {} need(type(observer) is dict and @@ -153,11 +184,13 @@ def main() -> int: p.add_argument("--receipt", required=True, type=Path) p.add_argument("--require-collage-readback", action="store_true") p.add_argument("--require-lantern-staged", action="store_true") + p.add_argument("--require-native-rollback", action="store_true") a = p.parse_args() try: check(json.loads(a.receipt.read_text(encoding="utf-8")), require_collage=a.require_collage_readback, - require_lantern_staged=a.require_lantern_staged) + require_lantern_staged=a.require_lantern_staged, + require_native_rollback=a.require_native_rollback) except (OSError, ValueError, TypeError) as ex: # Error messages generated by check() are constant, never propagate # arbitrary provider receipt fields, credentials or real addresses. @@ -171,6 +204,9 @@ def main() -> int: "independent world not retained", "guest readiness stages absent", "native App/ZFS world cleanup left residue", "guest receipt admits secrets", "guest receipt cross-target mismatch", + "native rollback requires independently qualified G2 and artifact", + "native disposable rollback receipt missing", + "native rollback independently healthy world not observed", } or str(ex).startswith(( "independent vanilla protocol not accepted at ", "independent owned world level.dat not observed at ", diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 69552ff34..b34993807 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -42,6 +42,7 @@ SESSION_MANIFEST="" VANILLA_COLLAGE_READBACK=false VANILLA_LANTERN_ZIP="" VANILLA_LANTERN_MANIFEST="" +VANILLA_WORLD_ROLLBACK=false while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -66,6 +67,7 @@ while [[ $# -gt 0 ]]; do --vanilla-collage-readback) VANILLA_COLLAGE_READBACK=true; shift ;; --vanilla-lantern-zip) VANILLA_LANTERN_ZIP="$2"; shift 2 ;; --vanilla-lantern-manifest) VANILLA_LANTERN_MANIFEST="$2"; shift 2 ;; + --vanilla-world-rollback) VANILLA_WORLD_ROLLBACK=true; shift ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -87,6 +89,12 @@ if [[ -n "$VANILLA_LANTERN_ZIP" || -n "$VANILLA_LANTERN_MANIFEST" ]]; then "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || { echo "native Lantern ZIP stage requires exact disposable vanilla263 and scoped COLLAGE readback" >&2; exit 2; } fi +if [[ "$VANILLA_WORLD_ROLLBACK" == "true" ]]; then + [[ "$VANILLA_COLLAGE_READBACK" == "true" && "$RUNG" == "t6" && + "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" && + -f "$VANILLA_LANTERN_ZIP" && -f "$VANILLA_LANTERN_MANIFEST" ]] || + { echo "native world rollback requires disposable vanilla263 G2 and exact Lantern ZIP" >&2; exit 2; } +fi if [[ "$VANILLA_COLLAGE_READBACK" == "true" ]]; then [[ "$RUNG" == "t6" && "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || { echo "vanilla scoped readback only admitted on exact 25.04.2.6/T6/vanilla263" >&2; exit 2; } @@ -1215,6 +1223,10 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then --lantern-manifest "$VANILLA_LANTERN_MANIFEST" ) fi + NATIVE_ROLLBACK_ARGS=() + if [[ "$VANILLA_WORLD_ROLLBACK" == "true" ]]; then + NATIVE_ROLLBACK_ARGS=(--native-world-rollback) + fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ --product-profile vanilla263 --native-catalog-profile 1.15.30 \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ @@ -1225,6 +1237,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then --foundry-commit "$FOUNDRY_COMMIT" --expected-system-version "$EXPECTED_SYSTEM_VERSION" \ "${NATIVE_READBACK_ARGS[@]}" \ "${NATIVE_LANTERN_ARGS[@]}" \ + "${NATIVE_ROLLBACK_ARGS[@]}" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index c2c838638..eae4a029e 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -402,6 +402,7 @@ def main() -> int: p.add_argument("--collage-source-ref") p.add_argument("--lantern-zip", type=pathlib.Path) p.add_argument("--lantern-manifest", type=pathlib.Path) + p.add_argument("--native-world-rollback", action="store_true") p.add_argument("--native-catalog-profile", choices=sorted(CATALOG_PROFILES), default="1.15.29") p.add_argument("--product-profile", choices=("legacy-paper", "vanilla263"), @@ -433,6 +434,9 @@ def main() -> int: if a.lantern_zip is not None and (not VANILLA263 or a.collage_source_dir is None): raise MinecraftT6Error("native Lantern staging needs exact vanilla263 and read-only COLLAGE source") + if a.native_world_rollback and (not VANILLA263 or + a.lantern_zip is None or a.collage_source_dir is None): + raise MinecraftT6Error("G3 native rollback requires exact vanilla263 G2 and signed Lantern ZIP") lantern_blob = None lantern_evidence = None if a.lantern_zip is not None: @@ -607,6 +611,9 @@ def delete_app(label): required = {"app.query", "app.config", "app.create", "app.delete", "app.start", "app.stop", "app.redeploy", "pool.dataset.query", "pool.dataset.create", "pool.dataset.delete", "filesystem.stat", "filesystem.setperm"} + if a.native_world_rollback: + required.update({"zfs.snapshot.create", "zfs.snapshot.query", + "zfs.snapshot.rollback", "filesystem.listdir"}) if not isinstance(methods, dict) or not required.issubset(methods): raise MinecraftT6Error("required native TrueNAS methods missing") if getapp() is not None or one("pool.dataset.query", [["id", "=", DATASET]]) is not None: @@ -688,6 +695,15 @@ def observe_collage_after_native_readiness(): first_level = payload["readiness"]["first"]["level"]["size"] wait_job(call("app.stop", [APP]), "Minecraft stop") + if a.native_world_rollback: + from qualify_vanilla263_native_world_rollback import take_owned_pre_stage_snapshot + try: + payload["native_world_rollback"] = take_owned_pre_stage_snapshot(call) + except Exception as exc: + from qualify_vanilla263_native_world_rollback import Refused + if isinstance(exc, Refused): + raise MinecraftT6Error("G3 snapshot: " + str(exc)) from None + raise MinecraftT6Error("G3 snapshot: FIXED_NATIVE_SNAPSHOT_UNAVAILABLE") from None if lantern_blob is not None: from qualify_vanilla263_native_lantern_stage import stage try: @@ -736,6 +752,17 @@ def observe_collage_after_native_readiness(): payload["lantern_artifact_stage"]["persisted_on_reinstall"] = True # The same exact App owner is rechecked by wait_app() above; # pre-stage TLS Go observation remains a separate G2 receipt. + if a.native_world_rollback: + from qualify_vanilla263_native_world_rollback import restore_owned_baseline_after_injected_fault + try: + restored = restore_owned_baseline_after_injected_fault( + call, wait_job, wait_app, wait_status) + except Exception as exc: + from qualify_vanilla263_native_world_rollback import Refused + if isinstance(exc, Refused): + raise MinecraftT6Error("G3 rollback: " + str(exc)) from None + raise MinecraftT6Error("G3 rollback: FIXED_NATIVE_RESTORE_UNAVAILABLE") from None + payload["native_world_rollback"].update(restored) read_sentinel() if first_level <= 0: raise MinecraftT6Error("world level.dat size invalid") diff --git a/tests/test_vanilla263_native_rollback_receipt.py b/tests/test_vanilla263_native_rollback_receipt.py new file mode 100644 index 000000000..164c6ac16 --- /dev/null +++ b/tests/test_vanilla263_native_rollback_receipt.py @@ -0,0 +1,102 @@ +"""OP4G independent receipt gate: cannot upgrade staged ZIP/G2 to G3 mechanics.""" +from __future__ import annotations +import copy +import unittest +from tests.test_truenas_t6_vanilla263_guest import guest_fixture +from scripts.assert_vanilla263_native_t6_receipt import check +from scripts.admit_vanilla263_native_t6_control import APP + +SHA="da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8" + +def accepted_fixture(): + root=guest_fixture() + p=root["foundry_materialization"] + p["collage_readback"]={ + "schema":"semper-supra.collage-native-readback-p2/1", + "classification":"PROVIDER_READBACK_MATCH", + "source_profile":"vanilla263", + "source_sha":"2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8", + "native_app":APP,"truenas_version":"25.04.2.6", + "world_id":"lantern-rdte", + "service_id":"11111111-1111-4111-8111-111111111111", + "ownership_fields_matched":6,"transport":"isolated_guest_tls_verified", + "credential_scope":"APPS_READ", + "credential_mutation_phase":"disposable_guest_setup_before_observation", + "observation_mutation_count":0,"world_artifact_format_verified":False, + "world_activated":False,"cloudnet_adoption_executed":False, + "physical_true_nas_mutated":False,"tls_cert_sha256":"a"*64, + } + p["lantern_artifact_stage"]={ + "schema":"supracraft.native-vanilla263-lantern-artifact-stage/1", + "classification":"REAL_NATIVE_GUEST_ARTIFACT_BYTES_STAGED", + "artifact_sha256":SHA,"download_readback_sha256":SHA, + "world_id":"lantern-rdte","native_app":APP, + "native_truenas_version":"25.04.2.6","zfs_world_owned":True, + "app_quiesced_before_write":True,"persisted_on_restart":True, + "persisted_on_reinstall":True, + "artifact_gameplay_effect_verified":False, + "collage_managed_deployment":False, + "principal_authorized_to_apply":False,"physical_truenas_apply":"HOLD", + } + p["native_world_rollback"]={ + "schema":"supracraft.native-vanilla263-rdte-rollback-mechanics/1", + "classification":"G3_DISPOSABLE_MECHANICS_ONLY", + "phase":"injected_fault_rolled_back", + "snapshot_owned":True,"snapshot_id_verified":True, + "candidate_present_before_stage":False, + "release_fault_injected":True, + "candidate_absent_after_rollback":True, + "native_app_running_after_rollback":True, + "native_game_status_rechecked":True, + "candidate_sha256":SHA,"collage_managed_deployment":False, + "world_activation_authorized_by_collage":False, + "actual_player_gameplay_verified":False, + "physical_true_nas_mutated":False, + } + p["readiness"]["g3-rollback"]=copy.deepcopy(p["readiness"]["reinstall"]) + return root + + +class NegativeReceiptTests(unittest.TestCase): + def test_source_fixture_positive_and_missing_rollback_refused(self): + root=accepted_fixture() + check(root,require_collage=True,require_lantern_staged=True, + require_native_rollback=True) + del root["foundry_materialization"]["native_world_rollback"] + with self.assertRaisesRegex(ValueError,"native disposable rollback receipt missing"): + check(root,require_collage=True,require_lantern_staged=True, + require_native_rollback=True) + + def test_cannot_promote_without_g2_or_real_artifact(self): + root=accepted_fixture() + for collage,stage in ((False,False),(True,False),(False,True)): + with self.subTest(collage=collage,stage=stage): + with self.assertRaisesRegex(ValueError,"native rollback requires"): + check(root,require_collage=collage, + require_lantern_staged=stage,require_native_rollback=True) + + def test_forged_cleanup_owner_replay_game_health_and_promotion_are_refused(self): + for parent,field,value in ( + ("native_world_rollback","snapshot_owned",False), + ("native_world_rollback","candidate_sha256","f"*64), + ("native_world_rollback","candidate_absent_after_rollback",False), + ("native_world_rollback","world_activation_authorized_by_collage",True), + ("native_world_rollback","collage_managed_deployment",True), + ("native_world_rollback","actual_player_gameplay_verified",True), + ("native_world_rollback","physical_true_nas_mutated",True), + ("native_world_rollback","native_app_running_after_rollback",False), + ("readiness","g3-rollback",{}), + ): + root=accepted_fixture() + p=root["foundry_materialization"] + if parent=="readiness": + p[parent][field]=value + else: + p[parent][field]=value + with self.subTest(field=field),self.assertRaises(ValueError): + check(root,require_collage=True, + require_lantern_staged=True,require_native_rollback=True) + + +if __name__=="__main__": + unittest.main() From 2192a237762f21fb455dd0de0a85ad4ba0ad2969 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 20:11:28 +0200 Subject: [PATCH 694/719] admit(op4g): single native ZFS snapshot and injected rollback rep 012 * admit(op4g): unique one-use 012 exact native rollback fixture config/truenas-rdte-run-request.json * admit(op4g): unique one-use 012 exact native rollback fixture tests/test_truenas_rdte_target.py * admit(op4g): unique one-use 012 exact native rollback fixture tests/test_vanilla263_t6_guest_routing.py * test(op4g): ensure 012 one-shot flag G2 prerequisites and independent native receipt gate * test(op4g): admit only explicit boolean rollback flag in exact 012 request --- config/truenas-rdte-run-request.json | 7 ++++--- tests/test_truenas_rdte_target.py | 6 ++++-- tests/test_vanilla263_t6_guest_routing.py | 23 +++++++++++++++++++++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index e37cfb5cb..7d934b90e 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,13 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4F-011 single serialized public/free disposable TrueNAS25.04.2.6 native vanilla26.3/Java25/protocol777 scoped COLLAGE G2 EJSON expiry qualification. Following consumed 010 real E11_VALIDATION_EXPIRES_AT_KEY_COUNT0 and signed strict Pydantic/native 25.04 legacy EJSON source repair, issue one scoped short-lived APPS_READ guest-only observer key using 1h UTC {'$date': epoch_ms}, no retry after ambiguity and only allowlisted field/count failure categories. Verify first native App readiness and TLS-pinned Go read-only six-marker owner observation with observation_mutation_count=0 BEFORE real 739-byte Lantern ZIP stage/stop-restart/reinstall. If observation fails, preserve bounded receipt, independently clean owned App/ZFS and STOP; if passes, complete existing exact artifact stage/readback/retain/cleanup without managed COLLAGE APPLY claims. Current 011 is one-use; no replay 006-010, no role escalation, TLS bypass, secrets or raw DDP trace, private GHA minutes, native player auth claim or physical TrueNAS APPLY.", + "purpose": "OP4G-012 one-use public/free serialized native TrueNAS 25.04.2.6 vanilla26.3 Java25/777 disposable ZFS snapshot + injected-fault restore mechanics. Original signed Foundry OCI, scoped APPS_READ TLS Go COLLAGE readback six-marker G2, exact 739-byte Lantern real ZIP stage/hash/restart/reinstall required first. Existing native App STOPPED, candidate absent, exact owned snapshot absent, then snapshot only rdtepool/minecraft-vanilla-263-t6-world@op4g-pre-lantern; after proven stage/reinstall inject release fault, stop App, roll back only that ZFS snapshot with recursive/force disabled, verify original artifact absent, restart and independent Java26.3 status/world health, clean only owned App/ZFS, zero residue. Explicit disposable mechanics ONLY: no COLLAGE-authorized APPLY, native gameplay or Microsoft/VIP identity acceptance, no private Actions minutes, no physical TrueNAS APPLY. Consumed 011 not replayed; no repeat of 012.", "collage_readback": true, - "lantern_stage": true + "lantern_stage": true, + "native_world_rollback": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 385f105e7..9c00b7f49 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,12 +92,14 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry") + "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) + self.assertIs(request.get("native_world_rollback"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", "authority_issue", "consumer_authority_issue", - "purpose", "collage_readback", "lantern_stage"}) + "purpose", "collage_readback", "lantern_stage", + "native_world_rollback"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index b27fd9df7..d69a7d9bc 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4f-011-ejson-utc-expiry") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -109,6 +109,25 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) self.assertIn('persisted_on_reinstall',source) self.assertIn('persisted_on_restart',source) + def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): + request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) + self.assertEqual(request["request_id"], + "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") + self.assertIs(request.get("native_world_rollback"),True) + self.assertIs(request.get("collage_readback"),True) + self.assertIs(request.get("lantern_stage"),True) + self.assertIn("truenas_world_rollback=false",WORKFLOW) + self.assertIn("(.native_world_rollback // false)",WORKFLOW) + self.assertIn('truenas_world_rollback=$truenas_world_rollback',WORKFLOW) + self.assertIn("--vanilla-world-rollback",WORKFLOW) + self.assertIn("--require-native-rollback",WORKFLOW) + self.assertIn("VANILLA_WORLD_ROLLBACK=false",HARNESS) + self.assertIn('"$VANILLA_COLLAGE_READBACK" == "true"',HARNESS) + probe=(ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text() + self.assertIn('--native-world-rollback',probe) + self.assertIn('take_owned_pre_stage_snapshot(call)',probe) + self.assertIn('restore_owned_baseline_after_injected_fault(',probe) + def test_no_fallback_guest_or_physical_promotion(self): self.assertNotIn('product == "minecraft-vanilla-263" && needs[\'foundry-minecraft-paper',WORKFLOW) self.assertIn("agent-dispatch-heavyweight-rdte",WORKFLOW) From 6702c9dedcb74869e015c38dc459f90e50ed6b1c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:23:40 +0200 Subject: [PATCH 695/719] feat(op4h): source-qualify exact native loopback Mineflayer client causal contract * feat(op4h): bounded protocol777 offline native-world client action worker * feat(op4h): exact loopback client validator with sanitized staged results * test(op4h): native loopback protocol777 client positive/negative and no privilege promotion * ci(op4h): cheap Python/Node synthetic player contract and negative gates * fix(op4h): support package and native guest top-level immutable overlay imports * fix(op4h): accept concrete pathlib PosixPath while retaining directory/symlink gates --- ...alify-native-loopback-gameplay-overlay.yml | 9 +- ...tive_vanilla263_loopback_player_overlay.py | 13 +- ...ify_native_vanilla263_mineflayer_causal.js | 114 +++++++++++++++++ ...ify_native_vanilla263_mineflayer_causal.py | 105 ++++++++++++++++ ...est_native_vanilla263_mineflayer_causal.py | 118 ++++++++++++++++++ 5 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 scripts/qualify_native_vanilla263_mineflayer_causal.js create mode 100644 scripts/qualify_native_vanilla263_mineflayer_causal.py create mode 100644 tests/test_native_vanilla263_mineflayer_causal.py diff --git a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml index 47629aca2..c7285e4bf 100644 --- a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml +++ b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml @@ -4,6 +4,9 @@ on: paths: - 'scripts/qualify_native_vanilla263_loopback_player_overlay.py' - 'tests/test_native_vanilla263_loopback_player_overlay.py' + - 'scripts/qualify_native_vanilla263_mineflayer_causal.py' + - 'scripts/qualify_native_vanilla263_mineflayer_causal.js' + - 'tests/test_native_vanilla263_mineflayer_causal.py' - '.github/workflows/qualify-native-loopback-gameplay-overlay.yml' workflow_dispatch: permissions: @@ -22,8 +25,12 @@ jobs: set -euo pipefail python3 -m py_compile \ scripts/qualify_native_vanilla263_loopback_player_overlay.py \ - tests/test_native_vanilla263_loopback_player_overlay.py + scripts/qualify_native_vanilla263_mineflayer_causal.py \ + tests/test_native_vanilla263_loopback_player_overlay.py \ + tests/test_native_vanilla263_mineflayer_causal.py python3 -m unittest -v tests.test_native_vanilla263_loopback_player_overlay + python3 -m unittest -v tests.test_native_vanilla263_mineflayer_causal + node --check scripts/qualify_native_vanilla263_mineflayer_causal.js bash -n scripts/gha_kvm_truenas_rdte.sh - name: Preserve operational auth and physical TrueNAS hold run: echo 'Source-only variant; immutable operational online_mode=true, RCON disabled; no QEMU guest, player, world mutation, or physical TrueNAS APPLY.' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/qualify_native_vanilla263_loopback_player_overlay.py b/scripts/qualify_native_vanilla263_loopback_player_overlay.py index 60170d01e..1c150105b 100644 --- a/scripts/qualify_native_vanilla263_loopback_player_overlay.py +++ b/scripts/qualify_native_vanilla263_loopback_player_overlay.py @@ -12,9 +12,16 @@ import re from typing import Any -from admit_vanilla263_native_t6_control import ( - APP, CONTROL_SHA, OWNER, Refused, TARGET, verify, -) +# Native guest imports from the scripts directory as top-level modules; +# repo-root cheap tests may also import the same module as scripts.*. +try: + from .admit_vanilla263_native_t6_control import ( + APP, CONTROL_SHA, OWNER, Refused, TARGET, verify, + ) +except ImportError: + from admit_vanilla263_native_t6_control import ( + APP, CONTROL_SHA, OWNER, Refused, TARGET, verify, + ) PROFILE = "minecraft-vanilla263-disposable-loopback-player/1" BOT_NAME = "SupraCraftProbe" diff --git a/scripts/qualify_native_vanilla263_mineflayer_causal.js b/scripts/qualify_native_vanilla263_mineflayer_causal.js new file mode 100644 index 000000000..76720f50c --- /dev/null +++ b/scripts/qualify_native_vanilla263_mineflayer_causal.js @@ -0,0 +1,114 @@ +'use strict' +// G4 source-only player worker for one isolated runner-owned QEMU native App. +// No Microsoft account, stock client, RCON, broader ingress, or TrueNAS API. +const fs = require('node:fs') +const mineflayer = require('mineflayer') +const { Vec3 } = require('vec3') + +const stage = process.env.SUPRACRAFT_NATIVE_STAGE +const port = Number(process.env.MC_PORT) +const output = process.env.BOT_RESULT_FILE +const admitted = ['candidate', 'rollback'].includes(stage) && + Number.isSafeInteger(port) && port > 1024 && port < 65536 && + typeof output === 'string' && output.length > 0 +if (!admitted) process.exit(2) +const result = { + schema: 'supracraft.native-vanilla263-mineflayer-causal/1', + classification: 'CLIENT_NOT_QUALIFIED', + stage, requested_version: '26.3', + protocol: 777, offline_mode: true, + synthetic_bot: 'SupraCraftProbe', + operation: 'mineflayer.placeBlock', + rcon_used: false, + backend_independent_oracle_qualified: false, + true_nas_physical_apply: false, + collage_mutation_authorized: false, + in_guest_server_world: false, + fixture_via_bounded_bot_commands: false, + placed: false, visible_effect_correct: false +} +let done = false +let bot +function save () { fs.writeFileSync(output, JSON.stringify(result, null, 2) + '\n') } +function finish (ok, code) { + if (done) return + done = true + result.classification = ok ? 'CLIENT_CAUSAL_VISIBILITY' : 'CLIENT_REFUSED' + if (!ok) result.reason_code = code + save() + try { bot?.quit('synthetic native world test complete') } catch {} + setTimeout(() => process.exit(ok ? 0 : 1), 200) +} +function read (x, y, z) { + const block = bot.blockAt(new Vec3(x, y, z)) + return block ? block.name : null +} +try { + bot = mineflayer.createBot({ + host: '127.0.0.1', port, username: 'SupraCraftProbe', + auth: 'offline', version: '26.3' + }) +} catch (_) { finish(false, 'CLIENT_CONSTRUCT_REFUSED') } +bot?.on('error', () => finish(false, 'CLIENT_ERROR')) +bot?.on('kicked', () => finish(false, 'CLIENT_KICKED')) +bot?.on('end', () => { if (!done) finish(false, 'CLIENT_ENDED') }) +bot?.once('spawn', async () => { + try { + result.connected_and_spawned = true + result.negotiated_version = bot.version + result.negotiated_protocol = bot.protocolVersion + if (bot.version !== '26.3' || bot.protocolVersion !== 777) { + return finish(false, 'CLIENT_VERSION_DRIFT') + } + const commands = [ + '/forceload add 0 0', + '/fill -2 69 -2 4 69 3 minecraft:stone', + '/fill -2 70 -2 4 72 3 minecraft:air', + '/setblock 0 70 0 minecraft:air', + '/setblock 2 70 0 minecraft:iron_bars', + '/setblock 1 70 1 minecraft:black_concrete', + '/gamemode survival SupraCraftProbe', + '/give SupraCraftProbe minecraft:redstone_block 1', + '/tp SupraCraftProbe 0.5 70.0 2.5 180 0' + ] + // This single synthetic offline bot must be an OP under the exact + // source-admitted QEMU-only overlay, never under the normal online App. + for (const command of commands) { + bot.chat(command) + await bot.waitForTicks(8) + } + result.fixture_via_bounded_bot_commands = true + await bot.waitForTicks(20) + const before = read(0, 70, 0) + const support = read(0, 69, 0) + if (before !== 'air' || support !== 'stone') { + return finish(false, 'CLIENT_FIXTURE_NOT_PRESENT') + } + const deadline = Date.now() + 20000 + let core + while (Date.now() < deadline) { + core = bot.inventory.items().find(item => item.name === 'redstone_block') + if (core) break + await bot.waitForTicks(2) + } + if (!core) return finish(false, 'CLIENT_ITEM_NOT_GRANTED') + await bot.equip(core, 'hand') + await bot.waitForTicks(6) + await bot.placeBlock(bot.blockAt(new Vec3(0, 69, 0)), new Vec3(0, 1, 0)) + await bot.waitForTicks(25) + const after = read(0, 70, 0) + const access = read(2, 70, 0) + const feedback = read(1, 70, 1) + result.placed = after === 'redstone_block' + result.visible_effect_correct = result.placed && + (stage === 'candidate' + ? access === 'air' && feedback === 'sea_lantern' + : access === 'iron_bars' && feedback === 'black_concrete') + result.expected_datapack_present = stage === 'candidate' + if (!result.visible_effect_correct) { + return finish(false, 'CLIENT_CAUSAL_EFFECT_ABSENT') + } + finish(true) + } catch (_) { finish(false, 'CLIENT_EXECUTION_REFUSED') } +}) +setTimeout(() => finish(false, 'CLIENT_TIMEOUT'), 120000) diff --git a/scripts/qualify_native_vanilla263_mineflayer_causal.py b/scripts/qualify_native_vanilla263_mineflayer_causal.py new file mode 100644 index 000000000..ec1a14494 --- /dev/null +++ b/scripts/qualify_native_vanilla263_mineflayer_causal.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Protocol777 client-side causal action validator for disposable native TrueNAS. + +Source and narrow runner-local worker seam only. A Mineflayer packet is NOT +an independent server world oracle, authenticated production client or COLLAGE +deployment authority. Actual guest admission and backend proof remain separate. +""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess + +try: + from .qualify_native_vanilla263_loopback_player_overlay import runner_owned_loopback_forward +except ImportError: + from qualify_native_vanilla263_loopback_player_overlay import runner_owned_loopback_forward + +SCHEMA = "supracraft.native-vanilla263-mineflayer-causal/1" +STAGES = ("candidate", "rollback") +WORKER = Path(__file__).with_name("qualify_native_vanilla263_mineflayer_causal.js") + + +class Refused(RuntimeError): + pass + + +def require(ok: bool, reason: str) -> None: + if not ok: + raise Refused(reason) + + +def validate(receipt: object, stage: str) -> dict: + require(stage in STAGES and type(receipt) is dict and + receipt.get("schema") == SCHEMA and + receipt.get("classification") == "CLIENT_CAUSAL_VISIBILITY" and + receipt.get("stage") == stage and + receipt.get("requested_version") == "26.3" and + receipt.get("negotiated_version") == "26.3" and + type(receipt.get("protocol")) is int and receipt["protocol"] == 777 and + type(receipt.get("negotiated_protocol")) is int and + receipt["negotiated_protocol"] == 777 and + receipt.get("offline_mode") is True and + receipt.get("synthetic_bot") == "SupraCraftProbe" and + receipt.get("operation") == "mineflayer.placeBlock" and + receipt.get("connected_and_spawned") is True and + receipt.get("fixture_via_bounded_bot_commands") is True and + receipt.get("placed") is True and + receipt.get("visible_effect_correct") is True and + receipt.get("expected_datapack_present") is (stage == "candidate") and + receipt.get("rcon_used") is False and + receipt.get("backend_independent_oracle_qualified") is False and + receipt.get("true_nas_physical_apply") is False and + receipt.get("collage_mutation_authorized") is False and + receipt.get("in_guest_server_world") is False and + "error" not in receipt and "reason_code" not in receipt, + "NATIVE_OFFLINE_PLAYER_CLIENT_CAUSAL_UNQUALIFIED") + return { + "schema": SCHEMA, + "classification": "CLIENT_CAUSAL_VISIBILITY_ONLY", + "stage": stage, + "client_protocol777_connected_and_spawned": True, + "offline_synthetic_player_caused_core_placement": True, + "client_saw_expected_datapack_effect": stage == "candidate", + "client_saw_expected_no_datapack_effect": stage == "rollback", + "native_guest_backend_independent_oracle_qualified": False, + "stock_mojang_client_qualified": False, + "online_account_qualified": False, + "collage_mutation_authorized": False, + "physical_true_nas_apply": "HOLD", + } + + +def run(*, stage: str, forward: str, state_dir: Path, node_path: Path) -> dict: + require(stage in STAGES, "UNSUPPORTED_CLIENT_STAGE") + host_port = runner_owned_loopback_forward(forward) + require(isinstance(state_dir, Path) and state_dir.is_dir() and not state_dir.is_symlink(), + "PLAYER_STATE_OWNER_MISSING") + require(isinstance(node_path, Path) and node_path.is_dir() and + not node_path.is_symlink(), "PINNED_CLIENT_DEPENDENCIES_MISSING") + require(WORKER.is_file() and not WORKER.is_symlink(), + "PINNED_CLIENT_WORKER_MISSING") + output = state_dir / ("native-mineflayer-" + stage + ".json") + require(not output.exists() and not output.is_symlink(), + "PLAYER_RECEIPT_REPLAY_REFUSED") + env = dict(os.environ, MC_PORT=str(host_port), + SUPRACRAFT_NATIVE_STAGE=stage, BOT_RESULT_FILE=str(output), + NODE_PATH=str(node_path)) + try: + result = subprocess.run( + ["node", str(WORKER)], cwd=str(WORKER.parent), env=env, + capture_output=True, text=True, timeout=145, check=False, + ) + except (OSError, subprocess.TimeoutExpired): + raise Refused("NATIVE_CLIENT_RUNTIME_UNAVAILABLE") from None + # Never expose the worker's stdout/stderr, packets, kicks or provider errors. + require(result.returncode == 0 and output.is_file() and + not output.is_symlink() and 0 < output.stat().st_size < 8192, + "NATIVE_CLIENT_RUNTIME_REFUSED") + try: + doc = json.loads(output.read_text(encoding="utf-8")) + except (ValueError, UnicodeError, OSError): + raise Refused("NATIVE_CLIENT_RECEIPT_UNREADABLE") from None + return validate(doc, stage) diff --git a/tests/test_native_vanilla263_mineflayer_causal.py b/tests/test_native_vanilla263_mineflayer_causal.py new file mode 100644 index 000000000..6aee3c497 --- /dev/null +++ b/tests/test_native_vanilla263_mineflayer_causal.py @@ -0,0 +1,118 @@ +"""OP4H source and adversarial only; no real native client or KVM admission.""" +from __future__ import annotations +import copy +from pathlib import Path +import subprocess +import tempfile +import unittest +from unittest.mock import patch + +from scripts import qualify_native_vanilla263_mineflayer_causal as target + + +def good(stage): + return { + "schema":target.SCHEMA, "classification":"CLIENT_CAUSAL_VISIBILITY", + "stage":stage,"requested_version":"26.3","negotiated_version":"26.3", + "protocol":777,"negotiated_protocol":777, + "offline_mode":True,"synthetic_bot":"SupraCraftProbe", + "operation":"mineflayer.placeBlock", + "connected_and_spawned":True, + "fixture_via_bounded_bot_commands":True, + "placed":True,"visible_effect_correct":True, + "expected_datapack_present":stage=="candidate", + "rcon_used":False,"backend_independent_oracle_qualified":False, + "true_nas_physical_apply":False, + "collage_mutation_authorized":False,"in_guest_server_world":False, + } + + +class ClientSourceTests(unittest.TestCase): + def test_candidate_and_rollback_effects_are_separate_and_no_authority(self): + for stage in target.STAGES: + receipt=target.validate(good(stage),stage) + self.assertEqual(receipt["classification"],"CLIENT_CAUSAL_VISIBILITY_ONLY") + self.assertFalse(receipt["native_guest_backend_independent_oracle_qualified"]) + self.assertFalse(receipt["collage_mutation_authorized"]) + self.assertEqual(receipt["physical_true_nas_apply"],"HOLD") + self.assertEqual(receipt["client_saw_expected_datapack_effect"], + stage=="candidate") + self.assertEqual(receipt["client_saw_expected_no_datapack_effect"], + stage=="rollback") + + def test_forged_success_protocol_bot_gameplay_and_privilege_are_refused(self): + for field,value in ( + ("classification","SOURCE_ONLY"),("schema","untrusted"), + ("requested_version","26.2"),("negotiated_version","1.21.11"), + ("protocol",774),("negotiated_protocol",True), + ("offline_mode",False),("synthetic_bot","OtherPerson"), + ("operation","claim_without_packet"),("connected_and_spawned",False), + ("fixture_via_bounded_bot_commands",False), + ("placed",False),("visible_effect_correct",False), + ("expected_datapack_present",False), + ("rcon_used",True),("backend_independent_oracle_qualified",True), + ("true_nas_physical_apply",True), + ("collage_mutation_authorized",True),("in_guest_server_world",True), + ): + bad=good("candidate") + bad[field]=value + with self.subTest(field=field), self.assertRaises(target.Refused): + target.validate(bad,"candidate") + bad=good("candidate") + bad["reason_code"]="SECRET_OR_FALSE_POSITIVE" + with self.assertRaises(target.Refused): + target.validate(bad,"candidate") + with self.assertRaises(target.Refused): + target.validate(good("candidate"),"rollback") + + def test_requires_owned_loopback_and_no_preexisting_result(self): + with tempfile.TemporaryDirectory() as tmp: + base=Path(tmp) + runtime=base/"node_modules" + runtime.mkdir() + for bad in ( + "tcp:0.0.0.0:45451-:25580", + "tcp:127.0.0.1:45451-:25565", + "tcp:127.0.0.1:22-:25580", + ): + with self.subTest(bad=bad),self.assertRaises(Exception): + target.run(stage="candidate",forward=bad, + state_dir=base,node_path=runtime) + (base/"native-mineflayer-candidate.json").write_text("{}") + with self.assertRaisesRegex(target.Refused,"REPLAY_REFUSED"): + target.run(stage="candidate", + forward="tcp:127.0.0.1:45451-:25580", + state_dir=base,node_path=runtime) + + def test_runtime_drops_raw_output_and_never_uses_auth_or_network_arguments(self): + with tempfile.TemporaryDirectory() as tmp: + root=Path(tmp); deps=root/"node_modules"; deps.mkdir() + def fake(cmd,**kwargs): + self.assertEqual(cmd,["node",str(target.WORKER)]) + env=kwargs["env"] + self.assertEqual(env["MC_PORT"],"45451") + self.assertEqual(env["SUPRACRAFT_NATIVE_STAGE"],"rollback") + self.assertEqual(env["NODE_PATH"],str(deps)) + self.assertNotIn("secret",str(cmd)) + import json + Path(env["BOT_RESULT_FILE"]).write_text(json.dumps(good("rollback"))) + return subprocess.CompletedProcess(cmd,0,"SHOULD NEVER PUBLISH","SECRET") + with patch.object(target.subprocess,"run",side_effect=fake): + result=target.run(stage="rollback",node_path=deps,state_dir=root, + forward="tcp:127.0.0.1:45451-:25580") + self.assertNotIn("SECRET",str(result)) + self.assertEqual(result["stage"],"rollback") + self.assertFalse(result["stock_mojang_client_qualified"]) + + def test_absent_client_result_or_nonzero_exit_is_never_accepted(self): + with tempfile.TemporaryDirectory() as tmp: + root=Path(tmp); deps=root/"node_modules"; deps.mkdir() + with patch.object(target.subprocess,"run",return_value= + subprocess.CompletedProcess(["node"],1,"secret","trace")): + with self.assertRaisesRegex(target.Refused,"RUNTIME_REFUSED"): + target.run(stage="candidate",node_path=deps,state_dir=root, + forward="tcp:127.0.0.1:45451-:25580") + + +if __name__=="__main__": + unittest.main() From 03fcf87d85de475215171696070dd16f8f2ccc38 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:30:40 +0200 Subject: [PATCH 696/719] feat(op4h): opt-in native offline player candidate and postrollback client visibility * feat(op4h): exact opt-in offline Mineflayer candidate and post-ZFS-rollback same native App * feat(op4h): QEMU loopback-only exact offline player path through existing native guest * ci(op4h): one-shot opt-in native synthetic Mineflayer client with pinned Node build * test(op4h): separate client-visible native positive/rollback oracle without backend promotion * test(op4h): independent native client receipt source negatives and no G4 backend inflation * ci(op4h): fail-closed native guest player receipt and JS syntax cheap gates * test(op4h): synthetic native player default-off and exact loopback/G3 request routing * fix(op4h): independently verify exact offline OP/no-RCON native App config before gameplay --- .github/workflows/gha-kvm-system-rdte.yml | 31 ++++++ .../vanilla263-native-guest-static.yml | 5 + .../assert_vanilla263_native_t6_receipt.py | 43 +++++++- scripts/gha_kvm_truenas_rdte.sh | 21 ++++ .../truenas_middleware_minecraft_t6_probe.py | 63 ++++++++++++ ..._native_vanilla263_player_guest_receipt.py | 97 +++++++++++++++++++ tests/test_vanilla263_t6_guest_routing.py | 24 +++++ 7 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 tests/test_native_vanilla263_player_guest_receipt.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 49d89e209..f4be99a18 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -95,6 +95,7 @@ jobs: truenas_collage_readback: ${{ steps.scope.outputs.truenas_collage_readback }} truenas_lantern_stage: ${{ steps.scope.outputs.truenas_lantern_stage }} truenas_world_rollback: ${{ steps.scope.outputs.truenas_world_rollback }} + truenas_native_offline_player: ${{ steps.scope.outputs.truenas_native_offline_player }} truenas_compute_fixture: ${{ steps.scope.outputs.truenas_compute_fixture }} proxmox_compute_fixture: ${{ steps.scope.outputs.proxmox_compute_fixture }} steps: @@ -126,6 +127,7 @@ jobs: truenas_collage_readback=false truenas_lantern_stage=false truenas_world_rollback=false + truenas_native_offline_player=false truenas_compute_fixture="${DISPATCH_TRUENAS_COMPUTE_FIXTURE:-none}" proxmox_compute_fixture="${DISPATCH_PROXMOX_COMPUTE_FIXTURE:-none}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then @@ -199,6 +201,14 @@ jobs: truenas_collage_readback="$(jq -er '(.collage_readback // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" truenas_lantern_stage="$(jq -er '(.lantern_stage // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" truenas_world_rollback="$(jq -er '(.native_world_rollback // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + truenas_native_offline_player="$(jq -er '(.native_offline_player // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + if [[ "$truenas_native_offline_player" == "true" ]]; then + [[ "$truenas_world_rollback" == "true" && "$truenas_lantern_stage" == "true" && + "$truenas_collage_readback" == "true" && "$truenas_version" == "25.04.2.6" && + "$truenas_rung" == "t6" && "$truenas_t6_product" == "minecraft-vanilla-263" ]] || { + echo "G4 only admitted in exact disposable native G2+G3 player profile" >&2; exit 2; + } + fi if [[ "$truenas_world_rollback" == "true" ]]; then [[ "$truenas_lantern_stage" == "true" && "$truenas_collage_readback" == "true" && "$truenas_t6_product" == "minecraft-vanilla-263" && "$truenas_version" == "25.04.2.6" && "$truenas_rung" == "t6" ]] || { @@ -234,6 +244,7 @@ jobs: echo "truenas_collage_readback=$truenas_collage_readback" >>"$GITHUB_OUTPUT" echo "truenas_lantern_stage=$truenas_lantern_stage" >>"$GITHUB_OUTPUT" echo "truenas_world_rollback=$truenas_world_rollback" >>"$GITHUB_OUTPUT" + echo "truenas_native_offline_player=$truenas_native_offline_player" >>"$GITHUB_OUTPUT" echo "truenas_compute_fixture=$truenas_compute_fixture" >>"$GITHUB_OUTPUT" echo "proxmox_compute_fixture=$proxmox_compute_fixture" >>"$GITHUB_OUTPUT" printf 'target scope: proxmox=%s proxmox_compute_fixture=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s truenas_compute_fixture=%s\n' "$proxmox" "$proxmox_compute_fixture" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" @@ -432,6 +443,19 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false + - name: Set up immutable 26.3 experimental client toolchain for native G4 + if: needs.changes.outputs.truenas_native_offline_player == 'true' + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + - name: Build pre-qualified 26.3 Mineflayer client on public runner only + if: needs.changes.outputs.truenas_native_offline_player == 'true' + run: | + set -euo pipefail + test "$(git -C .deps/minecraft-product rev-parse HEAD)" = 4df9fbbfea0ae0e824719db367bd551349a86378 + bash .deps/minecraft-product/tools/install_experimental_prismarine_26_3.sh "$RUNNER_TEMP/native-prismarine" + test -d "$RUNNER_TEMP/native-prismarine/runtime/node_modules/mineflayer" + node --check scripts/qualify_native_vanilla263_mineflayer_causal.js - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -822,6 +846,10 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_world_rollback }}" == "true" ]]; then args+=(--vanilla-world-rollback) fi + if [[ "${{ needs.changes.outputs.truenas_native_offline_player }}" == "true" ]]; then + args+=(--vanilla-native-player + --vanilla-player-node-path "$RUNNER_TEMP/native-prismarine/runtime/node_modules") + fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" # A green job is not a Minecraft product pass without full guest evidence. @@ -840,6 +868,9 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_world_rollback }}" == "true" ]]; then additional+=(--require-native-rollback) fi + if [[ "${{ needs.changes.outputs.truenas_native_offline_player }}" == "true" ]]; then + additional+=(--require-native-client-visibility) + fi python3 scripts/assert_vanilla263_native_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" "${additional[@]}" fi if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then diff --git a/.github/workflows/vanilla263-native-guest-static.yml b/.github/workflows/vanilla263-native-guest-static.yml index a1bfac63b..71a0a3350 100644 --- a/.github/workflows/vanilla263-native-guest-static.yml +++ b/.github/workflows/vanilla263-native-guest-static.yml @@ -9,6 +9,9 @@ on: - 'scripts/qualify_vanilla263_native_world_rollback.py' - 'tests/test_vanilla263_native_world_rollback.py' - 'tests/test_vanilla263_native_rollback_receipt.py' + - 'scripts/qualify_native_vanilla263_mineflayer_causal.py' + - 'scripts/qualify_native_vanilla263_mineflayer_causal.js' + - 'tests/test_native_vanilla263_player_guest_receipt.py' - 'tests/test_vanilla263_native_lantern_stage.py' - 'scripts/assert_vanilla263_native_t6_receipt.py' - 'tests/test_truenas_t6_vanilla263_guest.py' @@ -84,6 +87,8 @@ jobs: python3 -m unittest -v tests/test_vanilla263_native_lantern_stage.py python3 -m unittest -v tests/test_vanilla263_native_world_rollback.py python3 -m unittest -v tests/test_vanilla263_native_rollback_receipt.py + python3 -m unittest -v tests/test_native_vanilla263_player_guest_receipt.py + node --check scripts/qualify_native_vanilla263_mineflayer_causal.js python3 -m unittest -v tests/test_truenas_t6_vanilla263_guest.py python3 -m unittest -v tests/test_collage_p2_effective_roles.py bash -n scripts/gha_kvm_truenas_rdte.sh diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py index c57649ba2..74ce2804e 100644 --- a/scripts/assert_vanilla263_native_t6_receipt.py +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -21,7 +21,8 @@ def check(root: dict, *, require_collage: bool = False, require_lantern_staged: bool = False, - require_native_rollback: bool = False) -> None: + require_native_rollback: bool = False, + require_native_client_visibility: bool = False) -> None: def need(condition: bool, reason: str) -> None: if not condition: raise ValueError(reason) @@ -123,6 +124,39 @@ def need(condition: bool, reason: str) -> None: stage.get("principal_authorized_to_apply") is False and stage.get("physical_truenas_apply") == "HOLD", "actual native Lantern ZIP byte stage/retention was not qualified") + if require_native_client_visibility: + need(require_native_rollback and require_lantern_staged and require_collage, + "native client requires exact G2 and G3") + profile = p.get("native_player_profile") or {} + gameplay = p.get("native_player_gameplay") or {} + need(type(profile) is dict and profile.get("schema") == + "minecraft-vanilla263-disposable-loopback-player/1" and + profile.get("classification") == "DISPOSABLE_OVERLAY_BOUND" and + profile.get("rcon_enabled") is False and + profile.get("client_online_account_qualified") is False and + profile.get("physical_truenas_apply") == "HOLD" and + type(gameplay) is dict and + gameplay.get("same_native_guest_client_causal_visibility") is True and + gameplay.get("backend_independent_world_oracle_pass") is False and + gameplay.get("collage_managed_deployment") is False and + gameplay.get("physical_true_nas_apply") == "HOLD", + "native offline client path not independently verified") + for name in ("candidate", "rollback"): + client = gameplay.get(name) or {} + need(type(client) is dict and client.get("schema") == + "supracraft.native-vanilla263-mineflayer-causal/1" and + client.get("classification") == "CLIENT_CAUSAL_VISIBILITY_ONLY" and + client.get("stage") == name and + client.get("client_protocol777_connected_and_spawned") is True and + client.get("offline_synthetic_player_caused_core_placement") is True and + client.get("client_saw_expected_datapack_effect") is (name == "candidate") and + client.get("client_saw_expected_no_datapack_effect") is (name == "rollback") and + client.get("native_guest_backend_independent_oracle_qualified") is False and + client.get("stock_mojang_client_qualified") is False and + client.get("online_account_qualified") is False and + client.get("collage_mutation_authorized") is False and + client.get("physical_true_nas_apply") == "HOLD", + "native offline client causal stage missing") if require_native_rollback: need(require_lantern_staged and require_collage, "native rollback requires independently qualified G2 and artifact") @@ -185,12 +219,14 @@ def main() -> int: p.add_argument("--require-collage-readback", action="store_true") p.add_argument("--require-lantern-staged", action="store_true") p.add_argument("--require-native-rollback", action="store_true") + p.add_argument("--require-native-client-visibility", action="store_true") a = p.parse_args() try: check(json.loads(a.receipt.read_text(encoding="utf-8")), require_collage=a.require_collage_readback, require_lantern_staged=a.require_lantern_staged, - require_native_rollback=a.require_native_rollback) + require_native_rollback=a.require_native_rollback, + require_native_client_visibility=a.require_native_client_visibility) except (OSError, ValueError, TypeError) as ex: # Error messages generated by check() are constant, never propagate # arbitrary provider receipt fields, credentials or real addresses. @@ -207,6 +243,9 @@ def main() -> int: "native rollback requires independently qualified G2 and artifact", "native disposable rollback receipt missing", "native rollback independently healthy world not observed", + "native client requires exact G2 and G3", + "native offline client path not independently verified", + "native offline client causal stage missing", } or str(ex).startswith(( "independent vanilla protocol not accepted at ", "independent owned world level.dat not observed at ", diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index b34993807..9c0342fcc 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -43,6 +43,8 @@ VANILLA_COLLAGE_READBACK=false VANILLA_LANTERN_ZIP="" VANILLA_LANTERN_MANIFEST="" VANILLA_WORLD_ROLLBACK=false +VANILLA_NATIVE_PLAYER=false +VANILLA_PLAYER_NODE_PATH="" while [[ $# -gt 0 ]]; do case "$1" in --out) OUT="$2"; shift 2 ;; @@ -68,6 +70,8 @@ while [[ $# -gt 0 ]]; do --vanilla-lantern-zip) VANILLA_LANTERN_ZIP="$2"; shift 2 ;; --vanilla-lantern-manifest) VANILLA_LANTERN_MANIFEST="$2"; shift 2 ;; --vanilla-world-rollback) VANILLA_WORLD_ROLLBACK=true; shift ;; + --vanilla-native-player) VANILLA_NATIVE_PLAYER=true; shift ;; + --vanilla-player-node-path) VANILLA_PLAYER_NODE_PATH="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac @@ -89,6 +93,14 @@ if [[ -n "$VANILLA_LANTERN_ZIP" || -n "$VANILLA_LANTERN_MANIFEST" ]]; then "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || { echo "native Lantern ZIP stage requires exact disposable vanilla263 and scoped COLLAGE readback" >&2; exit 2; } fi +if [[ "$VANILLA_NATIVE_PLAYER" == "true" ]]; then + [[ "$VANILLA_WORLD_ROLLBACK" == "true" && -d "$VANILLA_PLAYER_NODE_PATH" && + "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || + { echo "G4 synthetic client requires exact disposable native G3 and pinned Node dependencies" >&2; exit 2; } +fi +if [[ "$VANILLA_NATIVE_PLAYER" != "true" && -n "$VANILLA_PLAYER_NODE_PATH" ]]; then + echo "G4 client dependencies without explicit disposable admission" >&2; exit 2 +fi if [[ "$VANILLA_WORLD_ROLLBACK" == "true" ]]; then [[ "$VANILLA_COLLAGE_READBACK" == "true" && "$RUNG" == "t6" && "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" && @@ -1227,6 +1239,14 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then if [[ "$VANILLA_WORLD_ROLLBACK" == "true" ]]; then NATIVE_ROLLBACK_ARGS=(--native-world-rollback) fi + NATIVE_PLAYER_ARGS=() + if [[ "$VANILLA_NATIVE_PLAYER" == "true" ]]; then + [[ "$MINECRAFT_HOSTFWD" == ,hostfwd=tcp:127.0.0.1:*"-:25580" ]] || + { echo "G4 missing runner-owned QEMU localhost-only Minecraft forward" >&2; exit 2; } + NATIVE_PLAYER_ARGS=(--native-offline-player + --native-player-forward "${MINECRAFT_HOSTFWD#,hostfwd=}" + --native-player-node-path "$VANILLA_PLAYER_NODE_PATH") + fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ --product-profile vanilla263 --native-catalog-profile 1.15.30 \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ @@ -1238,6 +1258,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then "${NATIVE_READBACK_ARGS[@]}" \ "${NATIVE_LANTERN_ARGS[@]}" \ "${NATIVE_ROLLBACK_ARGS[@]}" \ + "${NATIVE_PLAYER_ARGS[@]}" \ --out "$FOUNDRY_OUT" --timeout 8 --job-timeout 600 --state-timeout 480 >/dev/null 2>&1 || true elif [[ "$T6_PRODUCT" == "foliorelay" ]]; then python3 "$SCRIPT_DIR/truenas_middleware_foliorelay_t6_probe.py" \ diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index eae4a029e..f7edc3bd5 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -403,6 +403,9 @@ def main() -> int: p.add_argument("--lantern-zip", type=pathlib.Path) p.add_argument("--lantern-manifest", type=pathlib.Path) p.add_argument("--native-world-rollback", action="store_true") + p.add_argument("--native-offline-player", action="store_true") + p.add_argument("--native-player-forward") + p.add_argument("--native-player-node-path", type=pathlib.Path) p.add_argument("--native-catalog-profile", choices=sorted(CATALOG_PROFILES), default="1.15.29") p.add_argument("--product-profile", choices=("legacy-paper", "vanilla263"), @@ -437,6 +440,12 @@ def main() -> int: if a.native_world_rollback and (not VANILLA263 or a.lantern_zip is None or a.collage_source_dir is None): raise MinecraftT6Error("G3 native rollback requires exact vanilla263 G2 and signed Lantern ZIP") + if a.native_offline_player and (not a.native_world_rollback or + not a.native_player_forward or a.native_player_node_path is None): + raise MinecraftT6Error("G4 client requires exact G3 rollback and owned loopback") + if not a.native_offline_player and (a.native_player_forward or + a.native_player_node_path is not None): + raise MinecraftT6Error("G4 client arguments without explicit admission") lantern_blob = None lantern_evidence = None if a.lantern_zip is not None: @@ -450,6 +459,30 @@ def main() -> int: if a.expected_system_version != "TrueNAS-" + EXPECTED_TARGET: raise MinecraftT6Error("requested exact system version drift") desired = runtime_values(control) + if a.native_offline_player: + from qualify_native_vanilla263_loopback_player_overlay import ( + BOT_NAME, PROFILE, derive, + ) + admission = { + "schema": PROFILE, "rdte_disposable": True, + "experiment": "offline-mineflayer-player-causal", + "physical_truenas_apply": "HOLD", + "operational_selection": False, "external_ingress": False, + "qemu_network": "user", "host_forward": a.native_player_forward, + "guest_target": "TrueNAS-" + EXPECTED_TARGET, + "authorized_bot": BOT_NAME, + } + overlay = derive(control, admission) + if overlay["host_port"] != a.status_port: + raise MinecraftT6Error("G4 client and actual native QEMU port differ") + desired = overlay["values"] + payload["native_player_profile"] = { + "schema": PROFILE, "classification": "DISPOSABLE_OVERLAY_BOUND", + "operational_selection": False, + "physical_truenas_apply": "HOLD", + "client_online_account_qualified": False, + "rcon_enabled": False, + } sentinel_bytes = (("SUPRACRAFT-VANILLA263-T6:" if VANILLA263 else "SUPRACRAFT-PAPER-T6:") + control["control_sha256"] + "\n").encode("ascii") payload["foundry_ref"] = a.foundry_commit payload["control_sha256"] = control["control_sha256"] @@ -495,6 +528,19 @@ def wait_app(): payload["observed_image_reference"] = matching_image payload["image_digest_qualification"] = "SOURCE_AND_GUEST_REF_MATCH" cfg = call("app.config", [APP]) + if a.native_offline_player: + # Never grant OP or disable online account auth in an + # unconfirmed App. Read exact isolated source overlay, + # no raw config/provider fields in public receipts. + cm = cfg.get("minecraft") if isinstance(cfg, dict) else None + if (not isinstance(cm, dict) or + cm.get("online_mode") is not False or + cm.get("ops") != ["SupraCraftProbe"] or + cm.get("enable_rcon") is not False or + cm.get("rcon_password") != "" or + cm.get("whitelist") != []): + raise MinecraftT6Error( + "disposable native player auth/OP profile mismatch") match_vector = owner_readback_vector(cfg, desired) if all(match_vector.values()): return app @@ -752,6 +798,17 @@ def observe_collage_after_native_readiness(): payload["lantern_artifact_stage"]["persisted_on_reinstall"] = True # The same exact App owner is rechecked by wait_app() above; # pre-stage TLS Go observation remains a separate G2 receipt. + if a.native_offline_player: + from qualify_native_vanilla263_mineflayer_causal import run as run_player + payload["native_player_gameplay"] = { + "candidate": run_player(stage="candidate", + forward=a.native_player_forward, state_dir=a.out.parent, + node_path=a.native_player_node_path), + "same_native_guest_client_causal_visibility": False, + "backend_independent_world_oracle_pass": False, + "collage_managed_deployment": False, + "physical_true_nas_apply": "HOLD", + } if a.native_world_rollback: from qualify_vanilla263_native_world_rollback import restore_owned_baseline_after_injected_fault try: @@ -763,6 +820,12 @@ def observe_collage_after_native_readiness(): raise MinecraftT6Error("G3 rollback: " + str(exc)) from None raise MinecraftT6Error("G3 rollback: FIXED_NATIVE_RESTORE_UNAVAILABLE") from None payload["native_world_rollback"].update(restored) + if a.native_offline_player: + from qualify_native_vanilla263_mineflayer_causal import run as run_player + payload["native_player_gameplay"]["rollback"] = run_player( + stage="rollback", forward=a.native_player_forward, + state_dir=a.out.parent, node_path=a.native_player_node_path) + payload["native_player_gameplay"]["same_native_guest_client_causal_visibility"] = True read_sentinel() if first_level <= 0: raise MinecraftT6Error("world level.dat size invalid") diff --git a/tests/test_native_vanilla263_player_guest_receipt.py b/tests/test_native_vanilla263_player_guest_receipt.py new file mode 100644 index 000000000..eb2d8ba35 --- /dev/null +++ b/tests/test_native_vanilla263_player_guest_receipt.py @@ -0,0 +1,97 @@ +"""G4 source-only forged native-client visibility receipt negative tests. + +Never substitute this JSON unit fixture for live guest + backend + game proof. +""" +from __future__ import annotations +import copy +import unittest + +from tests.test_vanilla263_native_rollback_receipt import accepted_fixture +from scripts.assert_vanilla263_native_t6_receipt import check + +SCHEMA="supracraft.native-vanilla263-mineflayer-causal/1" + + +def player(stage): + return { + "schema":SCHEMA, + "classification":"CLIENT_CAUSAL_VISIBILITY_ONLY", + "stage":stage, + "client_protocol777_connected_and_spawned":True, + "offline_synthetic_player_caused_core_placement":True, + "client_saw_expected_datapack_effect":stage=="candidate", + "client_saw_expected_no_datapack_effect":stage=="rollback", + "native_guest_backend_independent_oracle_qualified":False, + "stock_mojang_client_qualified":False, + "online_account_qualified":False, + "collage_mutation_authorized":False, + "physical_true_nas_apply":"HOLD", + } + + +def guest(): + r=accepted_fixture() + p=r["foundry_materialization"] + p["native_player_profile"]={ + "schema":"minecraft-vanilla263-disposable-loopback-player/1", + "classification":"DISPOSABLE_OVERLAY_BOUND", + "rcon_enabled":False, + "client_online_account_qualified":False, + "physical_truenas_apply":"HOLD", + } + p["native_player_gameplay"]={ + "same_native_guest_client_causal_visibility":True, + "backend_independent_world_oracle_pass":False, + "collage_managed_deployment":False, + "physical_true_nas_apply":"HOLD", + "candidate":player("candidate"), + "rollback":player("rollback"), + } + return r + + +class PlayerGuestReceiptSourceTests(unittest.TestCase): + def admit(self, row): + return check(row,require_collage=True, + require_lantern_staged=True,require_native_rollback=True, + require_native_client_visibility=True) + + def test_synthetic_positive_preserves_false_claims(self): + self.admit(guest()) + + def test_client_cannot_skip_g2_g3(self): + with self.assertRaisesRegex(ValueError,"native client requires exact G2 and G3"): + check(guest(),require_collage=True,require_lantern_staged=True, + require_native_client_visibility=True) + + def test_missing_and_forged_client_result_refused(self): + for group,field,bad in ( + ("profile","rcon_enabled",True), + ("profile","client_online_account_qualified",True), + ("profile","physical_truenas_apply","APPLY"), + ("gameplay","same_native_guest_client_causal_visibility",False), + ("gameplay","backend_independent_world_oracle_pass",True), + ("gameplay","collage_managed_deployment",True), + ("client","stage","rollback"), + ("client","client_protocol777_connected_and_spawned",False), + ("client","offline_synthetic_player_caused_core_placement",False), + ("client","client_saw_expected_datapack_effect",False), + ("client","collage_mutation_authorized",True), + ("client","online_account_qualified",True), + ("client","native_guest_backend_independent_oracle_qualified",True), + ): + d=guest() + p=d["foundry_materialization"] + if group=="profile":p["native_player_profile"][field]=bad + elif group=="gameplay":p["native_player_gameplay"][field]=bad + else:p["native_player_gameplay"]["candidate"][field]=bad + with self.subTest(group=group,field=field): + with self.assertRaises(ValueError):self.admit(d) + d=guest() + del d["foundry_materialization"]["native_player_gameplay"]["rollback"] + with self.assertRaisesRegex(ValueError,"native offline client causal stage missing"): + self.admit(d) + + +if __name__=="__main__": + unittest.main() diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index d69a7d9bc..60cea4397 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -128,6 +128,30 @@ def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): self.assertIn('take_owned_pre_stage_snapshot(call)',probe) self.assertIn('restore_owned_baseline_after_injected_fault(',probe) + def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(self): + request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) + # 012 is consumed; source qualification of G4 cannot replay it. + self.assertEqual(request["request_id"], + "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") + self.assertFalse(request.get("native_offline_player",False)) + self.assertIn("truenas_native_offline_player=false",WORKFLOW) + self.assertIn("(.native_offline_player // false)",WORKFLOW) + self.assertIn("truenas_native_offline_player=$truenas_native_offline_player",WORKFLOW) + self.assertIn("--vanilla-native-player",WORKFLOW) + self.assertIn("--vanilla-player-node-path",WORKFLOW) + self.assertIn("--require-native-client-visibility",WORKFLOW) + self.assertIn("VANILLA_NATIVE_PLAYER=false",HARNESS) + self.assertIn("MINECRAFT_HOSTFWD",HARNESS) + self.assertIn('"${MINECRAFT_HOSTFWD#,hostfwd=}"',HARNESS) + probe=(ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text() + self.assertIn("--native-offline-player",probe) + self.assertIn("overlay = derive(control, admission)",probe) + self.assertIn('stage="candidate"',probe) + self.assertIn('stage="rollback"',probe) + self.assertIn("if a.native_offline_player",probe) + assert_path=(ROOT/"scripts/assert_vanilla263_native_t6_receipt.py").read_text() + self.assertIn("native_guest_backend_independent_oracle_qualified",assert_path) + def test_no_fallback_guest_or_physical_promotion(self): self.assertNotIn('product == "minecraft-vanilla-263" && needs[\'foundry-minecraft-paper',WORKFLOW) self.assertIn("agent-dispatch-heavyweight-rdte",WORKFLOW) From e767642d2cb036ad4d9f7c2745355e91d8d1c0ef Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:33:32 +0200 Subject: [PATCH 697/719] admit(op4h): one isolated native Mineflayer causal visible test 013 * admit(op4h): unique 013 exact native synthetic client edge config/truenas-rdte-run-request.json * admit(op4h): unique 013 exact native synthetic client edge tests/test_truenas_rdte_target.py * admit(op4h): unique 013 exact native synthetic client edge tests/test_vanilla263_t6_guest_routing.py --- config/truenas-rdte-run-request.json | 7 ++++--- tests/test_truenas_rdte_target.py | 5 +++-- tests/test_vanilla263_t6_guest_routing.py | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 7d934b90e..946343542 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,13 +1,14 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4G-012 one-use public/free serialized native TrueNAS 25.04.2.6 vanilla26.3 Java25/777 disposable ZFS snapshot + injected-fault restore mechanics. Original signed Foundry OCI, scoped APPS_READ TLS Go COLLAGE readback six-marker G2, exact 739-byte Lantern real ZIP stage/hash/restart/reinstall required first. Existing native App STOPPED, candidate absent, exact owned snapshot absent, then snapshot only rdtepool/minecraft-vanilla-263-t6-world@op4g-pre-lantern; after proven stage/reinstall inject release fault, stop App, roll back only that ZFS snapshot with recursive/force disabled, verify original artifact absent, restart and independent Java26.3 status/world health, clean only owned App/ZFS, zero residue. Explicit disposable mechanics ONLY: no COLLAGE-authorized APPLY, native gameplay or Microsoft/VIP identity acceptance, no private Actions minutes, no physical TrueNAS APPLY. Consumed 011 not replayed; no repeat of 012.", + "purpose": "OP4H-013 unique serialized public/free QEMU TrueNAS25.04.2.6 native Minecraft Java25/vanilla26.3/protocol777 synthetic Mineflayer client-causal positive/negative rep. Same immutable signed Foundry and exact 739-byte Lantern ZIP, TLS verified read-only Go COLLAGE six marker G2, actual native owned ZFS snapshot/injected rollback G3. ONLY for disposable isolated QEMU user-net runner-local 127.0.0.1 ephemeral forwarding to guest :25580; derived online_mode=false one offline operator SupraCraftProbe; no RCON; only same native App. Real Mineflayer protocol777 logs in, places core, observes sea lantern/access opening with pack; rollback exact owned ZFS snapshot; in same native App bot repeats core placement and sees no datapack effect. Independent receipts must still report backend world oracle=false, COLLAGE managed deployment=false, Mojang online identity=false and physical TrueNAS APPLY HOLD. One-use 013; do not replay 006-012 or 013, no broad privilege or external ingress, no private GHA minutes.", "collage_readback": true, "lantern_stage": true, - "native_world_rollback": true + "native_world_rollback": true, + "native_offline_player": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 9c00b7f49..50309ebc5 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,14 +92,15 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") + "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) + self.assertIs(request.get("native_offline_player"), True) self.assertIs(request.get("native_world_rollback"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", "authority_issue", "consumer_authority_issue", "purpose", "collage_readback", "lantern_stage", - "native_world_rollback"}) + "native_world_rollback", "native_offline_player"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 60cea4397..bf557228c 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -112,7 +112,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") + "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -132,8 +132,8 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4g-012-owned-snapshot-rollback") - self.assertFalse(request.get("native_offline_player",False)) + "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") + self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) self.assertIn("truenas_native_offline_player=$truenas_native_offline_player",WORKFLOW) From 12e87257e5f5795cdb073166b251616f2bfd9a9c Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:41:27 +0200 Subject: [PATCH 698/719] fix(op4h): put pinned synthetic client install in native TrueNAS G4 job * fix(op4h): build Mineflayer dependencies in TrueNAS guest job, not Proxmox * test: catch wrong native-client workflow job placement --- .github/workflows/gha-kvm-system-rdte.yml | 26 +++++++++++------------ tests/test_vanilla263_t6_guest_routing.py | 14 ++++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index f4be99a18..c0ac9e33e 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -443,19 +443,6 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false - - name: Set up immutable 26.3 experimental client toolchain for native G4 - if: needs.changes.outputs.truenas_native_offline_player == 'true' - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 - with: - node-version: '22' - - name: Build pre-qualified 26.3 Mineflayer client on public runner only - if: needs.changes.outputs.truenas_native_offline_player == 'true' - run: | - set -euo pipefail - test "$(git -C .deps/minecraft-product rev-parse HEAD)" = 4df9fbbfea0ae0e824719db367bd551349a86378 - bash .deps/minecraft-product/tools/install_experimental_prismarine_26_3.sh "$RUNNER_TEMP/native-prismarine" - test -d "$RUNNER_TEMP/native-prismarine/runtime/node_modules/mineflayer" - node --check scripts/qualify_native_vanilla263_mineflayer_causal.js - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail @@ -756,6 +743,19 @@ jobs: python3 -m unittest -v tests.test_collage_catalog_preflight python3 scripts/check_collage_catalog_preflight.py --expected 1.15.30 + - name: Set up immutable 26.3 experimental client toolchain for native G4 + if: needs.changes.outputs.truenas_native_offline_player == 'true' + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: '22' + - name: Build pre-qualified 26.3 Mineflayer client on public runner only + if: needs.changes.outputs.truenas_native_offline_player == 'true' + run: | + set -euo pipefail + test "$(git -C .deps/minecraft-product rev-parse HEAD)" = 4df9fbbfea0ae0e824719db367bd551349a86378 + bash .deps/minecraft-product/tools/install_experimental_prismarine_26_3.sh "$RUNNER_TEMP/native-prismarine" + test -d "$RUNNER_TEMP/native-prismarine/runtime/node_modules/mineflayer" + node --check scripts/qualify_native_vanilla263_mineflayer_causal.js - name: Ensure bounded QEMU/KVM prerequisites run: | set -euo pipefail diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index bf557228c..54e7c9940 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -152,6 +152,20 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se assert_path=(ROOT/"scripts/assert_vanilla263_native_t6_receipt.py").read_text() self.assertIn("native_guest_backend_independent_oracle_qualified",assert_path) + def test_player_dependencies_are_installed_in_native_guest_job(self): + top, native = WORKFLOW.split(" truenas-26-beta3:", 1) + setup = "Set up immutable 26.3 experimental client toolchain for native G4" + install = "Build pre-qualified 26.3 Mineflayer client on public runner only" + for name in (setup, install): + self.assertEqual(WORKFLOW.count(name), 1) + self.assertNotIn(name, top) + self.assertIn(name, native) + self.assertLess(native.index(install), + native.index("Ensure bounded QEMU/KVM prerequisites")) + self.assertLess(native.index("Ensure bounded QEMU/KVM prerequisites"), + native.index("Exercise pinned TrueNAS installer target")) + self.assertIn("--require-native-client-visibility", native) + def test_no_fallback_guest_or_physical_promotion(self): self.assertNotIn('product == "minecraft-vanilla-263" && needs[\'foundry-minecraft-paper',WORKFLOW) self.assertIn("agent-dispatch-heavyweight-rdte",WORKFLOW) From db06d78663183dd27783dfa4ceecd8261bd88875 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:45:22 +0200 Subject: [PATCH 699/719] admit(op4h): unique corrected native client one-shot 014 * admit(op4h): new one-shot 014 corrected native client lane config/truenas-rdte-run-request.json * admit(op4h): new one-shot 014 corrected native client lane tests/test_truenas_rdte_target.py * admit(op4h): new one-shot 014 corrected native client lane tests/test_vanilla263_t6_guest_routing.py --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 946343542..9e06c5ac8 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4H-013 unique serialized public/free QEMU TrueNAS25.04.2.6 native Minecraft Java25/vanilla26.3/protocol777 synthetic Mineflayer client-causal positive/negative rep. Same immutable signed Foundry and exact 739-byte Lantern ZIP, TLS verified read-only Go COLLAGE six marker G2, actual native owned ZFS snapshot/injected rollback G3. ONLY for disposable isolated QEMU user-net runner-local 127.0.0.1 ephemeral forwarding to guest :25580; derived online_mode=false one offline operator SupraCraftProbe; no RCON; only same native App. Real Mineflayer protocol777 logs in, places core, observes sea lantern/access opening with pack; rollback exact owned ZFS snapshot; in same native App bot repeats core placement and sees no datapack effect. Independent receipts must still report backend world oracle=false, COLLAGE managed deployment=false, Mojang online identity=false and physical TrueNAS APPLY HOLD. One-use 013; do not replay 006-012 or 013, no broad privilege or external ingress, no private GHA minutes.", + "purpose": "OP4H-014 new single-use serialized public disposable native TrueNAS25.04.2.6 Minecraft26.3 Java25/777 Mineflayer causal client after 013 harness-only failure. Corrected public TrueNAS job builds pinned Prismarine/Mineflayer Node dependencies before guest exercise; 013 consumed and NOT replayed. Same signed Foundry OCI, scoped APPS_READ TLS Go COLLAGE native owner G2, 739-byte exact Lantern ZIP, owned ZFS pre-stage snapshot and injected-fault/rollback G3. Distinct QEMU user-network host-fwd only runner-owned 127.0.0.1 ephemeral to guest :25580, offline operator SupraCraftProbe only, no RCON, no external ingress. Candidate bot placement/sea-lantern and postrollback identical client action/no datapack effect in SAME native App. Independent receipt requires client-visible stages but must not misrepresent independent native backend oracle, managed COLLAGE APPLY or online auth. Owned App/world cleanup zero residue, no physical TrueNAS APPLY/private GHA/production authority. Unique 014 consumed on launch, no replay of 013 or 014.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 50309ebc5..a4c44689b 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") + "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 54e7c9940..23e875f57 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -112,7 +112,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") + "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -132,7 +132,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-013-native-mineflayer-loopback") + "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) From 189bd93e1aa1bc409efe4638a6a4a638fd657005 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:36:35 +0200 Subject: [PATCH 700/719] fix(op4h): use offline UUID for native synthetic operator; retain bounded startup census * fix(op4h): pin offline UUIDv3 in native OPS to avoid Mojang username resolution before startup * fix(op4h): verify actual native App binds exact synthetic offline UUID operator * test(op4h): enforce Java offline UUIDv3 and forbid nickname in native startup OPS * diag(op4h): add safe bounded native startup state/count census without raw logs * diag(op4h): persist bounded app lifecycle/health-state census on failed startup * test(op4h): startup diagnosis reveals only allowlisted state counts, never provider metadata --- ...tive_vanilla263_loopback_player_overlay.py | 39 +++++++++++++++- .../truenas_middleware_minecraft_t6_probe.py | 13 +++++- ...tive_vanilla263_loopback_player_overlay.py | 45 ++++++++++++++++++- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/scripts/qualify_native_vanilla263_loopback_player_overlay.py b/scripts/qualify_native_vanilla263_loopback_player_overlay.py index 1c150105b..88bef894c 100644 --- a/scripts/qualify_native_vanilla263_loopback_player_overlay.py +++ b/scripts/qualify_native_vanilla263_loopback_player_overlay.py @@ -10,6 +10,7 @@ import hashlib import json import re +import uuid from typing import Any # Native guest imports from the scripts directory as top-level modules; @@ -25,6 +26,10 @@ PROFILE = "minecraft-vanilla263-disposable-loopback-player/1" BOT_NAME = "SupraCraftProbe" +# Minecraft offline UUIDv3 from MD5("OfflinePlayer:" + synthetic name). +# itzg/minecraft-server:2026.10.0 runs `mc-image-helper manage-users` +# before JVM boot; offline-mode OPS must be a UUID, not a non-Mojang name. +BOT_OFFLINE_UUID = "f4469b77-cc14-3c07-95f3-0b0c3f151bd2" WORLD = "/mnt/rdtepool/minecraft-vanilla-263-t6-world" HOST_FORWARD = "tcp:127.0.0.1:25580-:25580" @@ -49,6 +54,33 @@ def require(cond: bool, code: str) -> None: if not cond: raise Refused(code) +def redacted_native_startup_census(app: object) -> dict[str, Any]: + """Bounded state/count only; never copy App metadata, paths, ports or logs. + + Frozen TrueNAS25.04.2.6 app.query reports container state through + active_workloads.container_details. An unhealthy App can remain DEPLOYING + (healthcheck STARTING) without ever raising a terminal job failure. + """ + allowed = {"RUNNING", "DEPLOYING", "STOPPED", "STOPPING", "CRASHED"} + container_states = {"created", "starting", "running", "exited", "crashed"} + state = app.get("state") if type(app) is dict else None + state = state if type(state) is str and state in allowed else "UNKNOWN" + active = app.get("active_workloads") if type(app) is dict else None + details = active.get("container_details") if type(active) is dict else None + if type(details) is not list: + details = [] + totals = {k: 0 for k in sorted(container_states)} + for item in details[:16]: + value = item.get("state") if type(item) is dict else None + if type(value) is str and value in totals: + totals[value] += 1 + return { + "native_app_state": state, + "container_states": totals, + "container_census_truncated": len(details) > 16, + } + + def derive(control: dict, admission: dict) -> dict[str, Any]: """Prepare a synthetic-world profile only; no RPC, filesystem, or guest.""" verify(control) @@ -86,14 +118,17 @@ def derive(control: dict, admission: dict) -> dict[str, Any]: # synthetic client identity and granting that bot in-game commands. # No RCON or remote management listener introduced, and all six # COLLAGE owner labels remain exact. + require(str(uuid.UUID(bytes=hashlib.md5( + ("OfflinePlayer:" + BOT_NAME).encode("utf-8")).digest(), version=3)) + == BOT_OFFLINE_UUID, "DISPOSABLE_PLAYER_OFFLINE_UUID_DRIFT") mc["online_mode"] = False - mc["ops"] = [BOT_NAME] + mc["ops"] = [BOT_OFFLINE_UUID] # Identical site bindings as accepted guest native app probe. values["storage"]["additional_storage"][0]["host_path_config"]["path"] = WORLD values["network"]["server_port"]["port_number"] = 25580 mc.pop("seed", None) if mc.get("seed") is None else None require(mc["enable_rcon"] is False and mc["rcon_password"] == "" and - mc["whitelist"] == [] and mc["ops"] == [BOT_NAME] and + mc["whitelist"] == [] and mc["ops"] == [BOT_OFFLINE_UUID] and mc["online_mode"] is False and {x["name"]: x["value"] for x in mc["additional_envs"]} == OWNER, "DISPOSABLE_PLAYER_OVERLAY_ESCAPED_ALLOWLIST") diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index f7edc3bd5..93d36613e 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -461,7 +461,7 @@ def main() -> int: desired = runtime_values(control) if a.native_offline_player: from qualify_native_vanilla263_loopback_player_overlay import ( - BOT_NAME, PROFILE, derive, + BOT_NAME, BOT_OFFLINE_UUID, PROFILE, derive, ) admission = { "schema": PROFILE, "rdte_disposable": True, @@ -520,6 +520,15 @@ def wait_app(): deadline = time.monotonic() + a.state_timeout while time.monotonic() < deadline: app = getapp() + if a.native_offline_player: + from qualify_native_vanilla263_loopback_player_overlay import ( + redacted_native_startup_census, + ) + witness = redacted_native_startup_census(app) + history = payload.setdefault("native_startup_states_seen", []) + if witness["native_app_state"] not in history: + history.append(witness["native_app_state"]) + payload["native_startup_last_census"] = witness if isinstance(app, dict) and app.get("state") == "RUNNING": matching_image = qualified_native_image(app, image_fragment=CATALOG_PROFILES[a.native_catalog_profile]["image"]) @@ -535,7 +544,7 @@ def wait_app(): cm = cfg.get("minecraft") if isinstance(cfg, dict) else None if (not isinstance(cm, dict) or cm.get("online_mode") is not False or - cm.get("ops") != ["SupraCraftProbe"] or + cm.get("ops") != [BOT_OFFLINE_UUID] or cm.get("enable_rcon") is not False or cm.get("rcon_password") != "" or cm.get("whitelist") != []): diff --git a/tests/test_native_vanilla263_loopback_player_overlay.py b/tests/test_native_vanilla263_loopback_player_overlay.py index f53c46e7d..c6ee03cdf 100644 --- a/tests/test_native_vanilla263_loopback_player_overlay.py +++ b/tests/test_native_vanilla263_loopback_player_overlay.py @@ -62,7 +62,8 @@ def test_synthetic_player_overlay_preserves_operational_control_and_is_source_on self.assertEqual(result["immutable_base_control_sha256"],overlay.CONTROL_SHA) minecraft=result["values"]["minecraft"] self.assertIs(minecraft["online_mode"],False) - self.assertEqual(minecraft["ops"],["SupraCraftProbe"]) + self.assertEqual(minecraft["ops"],[overlay.BOT_OFFLINE_UUID]) + self.assertNotIn(overlay.BOT_NAME,minecraft["ops"]) self.assertFalse(minecraft["enable_rcon"]) self.assertEqual(minecraft["rcon_password"],"") self.assertFalse(minecraft["whitelist"]) @@ -77,6 +78,48 @@ def test_synthetic_player_overlay_preserves_operational_control_and_is_source_on self.assertEqual(result["physical_truenas_apply"],"HOLD") self.assertEqual(len(result["derived_runtime_values_sha256"]),64) + def test_offline_uuid_v3_operator_is_not_an_unresolvable_mojang_name(self): + import hashlib, uuid + expected=str(uuid.UUID(bytes=hashlib.md5( + ("OfflinePlayer:" + overlay.BOT_NAME).encode()).digest(), version=3)) + self.assertEqual(expected,"f4469b77-cc14-3c07-95f3-0b0c3f151bd2") + self.assertEqual(expected,overlay.BOT_OFFLINE_UUID) + self.assertEqual(derive()["values"]["minecraft"]["ops"],[expected]) + self.assertEqual(derive()["bot"],overlay.BOT_NAME) + self.assertFalse(derive()["values"]["minecraft"]["online_mode"]) + self.assertFalse(derive()["values"]["minecraft"]["enable_rcon"]) + + def test_read_only_native_startup_census_never_rediscloses_metadata(self): + sample={ + "state":"DEPLOYING", + "name":"private-site", + "active_workloads":{"container_details":[ + {"state":"starting","id":"opaque-secret-container", + "image":"private.registry/password"}, + {"state":"created","port_config":[{"host_ip":"192.0.2.10"}]}, + {"state":"exited","volume_mounts":[{"source":"/secret/path"}]}, + ]}, + } + report=overlay.redacted_native_startup_census(sample) + self.assertEqual(report["native_app_state"],"DEPLOYING") + self.assertEqual(report["container_states"]["starting"],1) + self.assertEqual(report["container_states"]["created"],1) + self.assertEqual(report["container_states"]["exited"],1) + self.assertFalse(report["container_census_truncated"]) + self.assertNotIn("opaque-secret",str(report)) + self.assertNotIn("password",str(report)) + self.assertNotIn("192.0.2.10",str(report)) + self.assertNotIn("/secret",str(report)) + self.assertEqual(overlay.redacted_native_startup_census(None)["native_app_state"], + "UNKNOWN") + self.assertEqual(overlay.redacted_native_startup_census( + {"state":"UNKNOWN", "active_workloads":{"container_details":[]}} + )["native_app_state"],"UNKNOWN") + self.assertTrue(overlay.redacted_native_startup_census( + {"state":"STOPPED", "active_workloads":{"container_details":[ + {"state":"running"}]*17}} + )["container_census_truncated"]) + def test_runner_owned_ephemeral_forward_matches_native_harness(self): req=admission() req["host_forward"]="tcp:127.0.0.1:45451-:25580" From 71c36ffb4f2d1245c1f483acfd4038f4d1e6995e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 00:39:32 +0200 Subject: [PATCH 701/719] admit(op4h): one disposable native Mineflayer offline UUID 015 qualification * admit(op4h): unique 015 offline UUID synthetic native Minecraft config/truenas-rdte-run-request.json * admit(op4h): unique 015 offline UUID synthetic native Minecraft tests/test_truenas_rdte_target.py * admit(op4h): unique 015 offline UUID synthetic native Minecraft tests/test_vanilla263_t6_guest_routing.py --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 9e06c5ac8..857dd27af 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4H-014 new single-use serialized public disposable native TrueNAS25.04.2.6 Minecraft26.3 Java25/777 Mineflayer causal client after 013 harness-only failure. Corrected public TrueNAS job builds pinned Prismarine/Mineflayer Node dependencies before guest exercise; 013 consumed and NOT replayed. Same signed Foundry OCI, scoped APPS_READ TLS Go COLLAGE native owner G2, 739-byte exact Lantern ZIP, owned ZFS pre-stage snapshot and injected-fault/rollback G3. Distinct QEMU user-network host-fwd only runner-owned 127.0.0.1 ephemeral to guest :25580, offline operator SupraCraftProbe only, no RCON, no external ingress. Candidate bot placement/sea-lantern and postrollback identical client action/no datapack effect in SAME native App. Independent receipt requires client-visible stages but must not misrepresent independent native backend oracle, managed COLLAGE APPLY or online auth. Owned App/world cleanup zero residue, no physical TrueNAS APPLY/private GHA/production authority. Unique 014 consumed on launch, no replay of 013 or 014.", + "purpose": "OP4H-015: fresh serialized disposable TrueNAS25.04.2.6 native Minecraft26.3 Java25/777 Mineflayer client causal rep after 014 offline OPS synthetic username made native Minecraft App fail healthy startup; source-only repair PR #468 pins Java offline UUIDv3 f4469b77-cc14-3c07-95f3-0b0c3f151bd2 for exact QEMU-only bot SupraCraftProbe. Same signed official Minecraft App1.15.30 Foundry OCI, exact 739-byte Lantern SHA256, TLS Go scoped APPS_READ G2, owned ZFS snapshot/fault rollback G3. New safe bounded startup state/count census without raw logs, never infer provider secrets. QEMU usernet ephemeral host 127.0.0.1 to guest25580, no external ingress, one synthetic offline OP with RCON disabled, no production online identity or permissions. Test actual Mineflayer core placement candidate positive and same action postrollback negative in SAME guest/App, retain owned cleanup. Client visual packet oracle not independent backend-world proof; managed COLLAGE mutation, VIP/Microsoft auth, full Two Rivers and physical TrueNAS APPLY held. One-use 015; never replay 013/014/015 or overlap guests.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index a4c44689b..17d18a49b 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") + "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 23e875f57..1f1d4b75b 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -112,7 +112,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") + "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -132,7 +132,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-014-player-job-corrected") + "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) From 6705d49f26e2c691a3bde49679e367b066a5b1a5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:57:10 +0200 Subject: [PATCH 702/719] test(op4h): pin actual Java26.3 offline UUID image helper with no network * test(op4h): run pinned image offline UUID operator helper without TrueNAS/KVM * ci(op4h): public cheap OCI helper preflight to guard offline synthetic operator startup --- .../qualify-native-263-offline-ops-image.yml | 32 +++++++++++++ ...ify_native_vanilla263_offline_ops_image.sh | 46 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 .github/workflows/qualify-native-263-offline-ops-image.yml create mode 100644 scripts/qualify_native_vanilla263_offline_ops_image.sh diff --git a/.github/workflows/qualify-native-263-offline-ops-image.yml b/.github/workflows/qualify-native-263-offline-ops-image.yml new file mode 100644 index 000000000..fa6172f74 --- /dev/null +++ b/.github/workflows/qualify-native-263-offline-ops-image.yml @@ -0,0 +1,32 @@ +name: Native Minecraft 26.3 offline UUID image helper preflight +on: + pull_request: + paths: + - 'scripts/qualify_native_vanilla263_offline_ops_image.sh' + - 'scripts/qualify_native_vanilla263_loopback_player_overlay.py' + - '.github/workflows/qualify-native-263-offline-ops-image.yml' + workflow_dispatch: +permissions: + contents: read +jobs: + pinned-image-offline-operator: + if: github.event.repository.private == false + runs-on: ubuntu-24.04 + timeout-minutes: 8 + steps: + - name: Exact checked-out source, no credentials persisted + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - name: Validate bounded offline username-to-UUID source + run: | + set -euo pipefail + bash -n scripts/qualify_native_vanilla263_offline_ops_image.sh + python3 -m unittest -v tests.test_native_vanilla263_loopback_player_overlay + - name: Use pinned actual public image helper, no Minecraft, no guest + run: | + set -euo pipefail + bash scripts/qualify_native_vanilla263_offline_ops_image.sh + - name: Preserve boundary + run: | + echo 'Pinned image offline UUID operator configuration only. No native TrueNAS, live player, backend world witness, principal mutation authorization or physical APPLY.' >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/qualify_native_vanilla263_offline_ops_image.sh b/scripts/qualify_native_vanilla263_offline_ops_image.sh new file mode 100644 index 000000000..8197f81e0 --- /dev/null +++ b/scripts/qualify_native_vanilla263_offline_ops_image.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Source-level preflight of the exact disposable G4 synthetic OP conversion. +# Does not launch Minecraft, a TrueNAS guest, or an operational server. +set -euo pipefail +IMAGE='ghcr.io/itzg/minecraft-server:2026.10.0-java25@sha256:5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da' +UUID='f4469b77-cc14-3c07-95f3-0b0c3f151bd2' +NAME='SupraCraftProbe' +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +command -v docker >/dev/null || { echo 'PRECHECK_DOCKER_MISSING'; exit 2; } +command -v python3 >/dev/null || { echo 'PRECHECK_PYTHON_MISSING'; exit 2; } +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +mkdir -p "$tmp/out" +python3 - "$ROOT" "$UUID" "$NAME" <<'PY' +import hashlib, pathlib, sys, uuid +root,expected,name=sys.argv[1:] +value=str(uuid.UUID(bytes=hashlib.md5(("OfflinePlayer:"+name).encode()).digest(),version=3)) +assert value==expected, "synthetic offline identity changed" +src=(pathlib.Path(root)/"scripts/qualify_native_vanilla263_loopback_player_overlay.py").read_text() +assert f'BOT_OFFLINE_UUID = "{expected}"' in src, "disposable overlay drift" +assert 'mc["ops"] = [BOT_OFFLINE_UUID]' in src, "unresolved username used as OPS" +PY +# No platform or provider credentials; no raw container startup logs/identities. +# Bounded actual pinned-image helper: do not substitute a source-only JSON mock. +timeout 240s docker pull "$IMAGE" >/dev/null 2>&1 || { + echo 'IMAGE_PULL_NOT_QUALIFIED'; exit 2; +} +timeout 90s docker run --rm --network none --user 0:0 \ + -e ONLINE_MODE=FALSE -v "$tmp/out:/out" \ + --entrypoint mc-image-helper "$IMAGE" \ + manage-users --type=JAVA_OPS --version=26.3 \ + --existing=SYNCHRONIZE --output-directory=/out "$UUID" \ + >/dev/null 2>&1 || { + echo 'PINNED_OFFLINE_UUID_IMAGE_HELPER_REFUSED'; exit 2; +} +python3 - "$tmp/out/ops.json" "$UUID" <<'PY' +import json, pathlib, sys, uuid +p=pathlib.Path(sys.argv[1]) +assert p.is_file() and 0 < p.stat().st_size < 8192, "no bounded operators file" +doc=json.loads(p.read_text()) +assert type(doc) is list and len(doc)==1, "not exactly one synthetic OP" +entry=doc[0] +assert type(entry) is dict and uuid.UUID(entry["uuid"])==uuid.UUID(sys.argv[2]), "wrong offline identity" +assert entry["level"]==4 and entry["bypassesPlayerLimit"] is False, "unexpected operator privileges" +PY +echo 'PINNED_OFFLINE_UUID_IMAGE_HELPER_SOURCE_QUALIFIED' From c84ab5c1bbfa4f5ed11c30a4f83c9b10ff490ed9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:39:42 +0200 Subject: [PATCH 703/719] feat(G4): bounded Anvil candidate/rollback source decoder (no native claim) * feat(G4): bounded independent Anvil block-state decoder without guest authority * test(G4): strict synthetic Anvil candidate/rollback and malformed-region negatives * ci(G4): add bounded Anvil NBT source/adversarial gates to existing public CPU job * fix(G4 CI): single shell continuation before Anvil parser static checks * fix(G4): fail fast and distinguish identical candidate/rollback Anvil replay --- ...alify-native-loopback-gameplay-overlay.yml | 7 +- .../qualify_native_vanilla263_anvil_blocks.py | 207 ++++++++++++++++++ tests/test_native_vanilla263_anvil_blocks.py | 140 ++++++++++++ 3 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 scripts/qualify_native_vanilla263_anvil_blocks.py create mode 100644 tests/test_native_vanilla263_anvil_blocks.py diff --git a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml index c7285e4bf..a2084dabf 100644 --- a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml +++ b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml @@ -7,6 +7,8 @@ on: - 'scripts/qualify_native_vanilla263_mineflayer_causal.py' - 'scripts/qualify_native_vanilla263_mineflayer_causal.js' - 'tests/test_native_vanilla263_mineflayer_causal.py' + - 'scripts/qualify_native_vanilla263_anvil_blocks.py' + - 'tests/test_native_vanilla263_anvil_blocks.py' - '.github/workflows/qualify-native-loopback-gameplay-overlay.yml' workflow_dispatch: permissions: @@ -27,9 +29,12 @@ jobs: scripts/qualify_native_vanilla263_loopback_player_overlay.py \ scripts/qualify_native_vanilla263_mineflayer_causal.py \ tests/test_native_vanilla263_loopback_player_overlay.py \ - tests/test_native_vanilla263_mineflayer_causal.py + tests/test_native_vanilla263_mineflayer_causal.py \ + scripts/qualify_native_vanilla263_anvil_blocks.py \ + tests/test_native_vanilla263_anvil_blocks.py python3 -m unittest -v tests.test_native_vanilla263_loopback_player_overlay python3 -m unittest -v tests.test_native_vanilla263_mineflayer_causal + python3 -m unittest -v tests.test_native_vanilla263_anvil_blocks node --check scripts/qualify_native_vanilla263_mineflayer_causal.js bash -n scripts/gha_kvm_truenas_rdte.sh - name: Preserve operational auth and physical TrueNAS hold diff --git a/scripts/qualify_native_vanilla263_anvil_blocks.py b/scripts/qualify_native_vanilla263_anvil_blocks.py new file mode 100644 index 000000000..83f0255d6 --- /dev/null +++ b/scripts/qualify_native_vanilla263_anvil_blocks.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Bounded independent Minecraft Anvil region *source* decoder, G4 candidate. + +Accepts raw region bytes only. Admission of bytes as live SERVER-OWNED world +requires a separate authenticated fixed-path native TrueNAS read at explicit +quiesced checkpoints. This module alone NEVER asserts backend world proof, +COLLAGE APPLY, authenticated clients or a successful guest. +""" +from __future__ import annotations + +import hashlib +import math +import struct +import zlib + +SCHEMA = "supracraft.native-vanilla263-server-owned-anvil/1" +MAX_REGION = 64 * 1024 * 1024 +MAX_CHUNK = 16 * 1024 * 1024 +MAX_ITEMS = 1_000_000 +COORDINATES = ((0, 70, 0), (2, 70, 0), (1, 70, 1)) +EXPECTED = { + "candidate": ("minecraft:redstone_block", "minecraft:air", "minecraft:sea_lantern"), + "rollback": ("minecraft:redstone_block", "minecraft:iron_bars", "minecraft:black_concrete"), +} + + +class Refused(ValueError): + pass + + +def require(ok, code): + if not ok: + raise Refused(code) + + +class NBT: + def __init__(self, data: bytes): + require(type(data) is bytes and 0 < len(data) <= MAX_CHUNK, "NBT_SIZE") + self.data, self.i = data, 0 + + def raw(self, n): + require(type(n) is int and 0 <= n <= len(self.data) - self.i, + "NBT_TRUNCATED") + result = self.data[self.i:self.i+n] + self.i += n + return result + + def num(self, fmt): + return struct.unpack(">" + fmt, self.raw(struct.calcsize(">" + fmt)))[0] + + def string(self): + n = self.num("H") + require(n <= 4096, "NBT_STRING_TOO_LONG") + try: + return self.raw(n).decode("utf-8", "strict") + except UnicodeDecodeError: + raise Refused("NBT_UTF8_INVALID") from None + + def payload(self, tag, depth=0): + require(depth <= 24, "NBT_RECURSION") + if tag == 1: return self.num("b") + if tag == 2: return self.num("h") + if tag == 3: return self.num("i") + if tag == 4: return self.num("q") + if tag == 5: return self.num("f") + if tag == 6: return self.num("d") + if tag == 8: return self.string() + if tag in (7, 11, 12): + n = self.num("i") + width, fmt = {7:(1,"b"), 11:(4,"i"), 12:(8,"q")}[tag] + require(0 <= n <= MAX_ITEMS and n*width <= len(self.data)-self.i, + "NBT_ARRAY_BOUNDS") + return [self.num(fmt) for _ in range(n)] + if tag == 9: + nested = self.num("B") + n = self.num("i") + require(1 <= nested <= 12 and 0 <= n <= MAX_ITEMS, + "NBT_LIST_BOUNDS") + return [self.payload(nested, depth+1) for _ in range(n)] + if tag == 10: + value = {} + for _ in range(MAX_ITEMS): + nested = self.num("B") + if nested == 0: return value + require(1 <= nested <= 12, "NBT_TAG_UNSUPPORTED") + name = self.string() + require(name not in value, "NBT_DUPLICATE_NAME") + value[name] = self.payload(nested, depth+1) + raise Refused("NBT_COMPOUND_TOO_LARGE") + raise Refused("NBT_TAG_UNSUPPORTED") + + def document(self): + require(self.num("B") == 10, "NBT_ROOT_NOT_COMPOUND") + self.string() + result = self.payload(10) + require(self.i == len(self.data), "NBT_TRAILING_BYTES") + return result + + +def chunk_from_region(blob: bytes): + require(type(blob) is bytes and 8192 <= len(blob) <= MAX_REGION, + "REGION_FILE_BOUNDS") + # Fixed region r.0.0.mca, chunk (0,0); no path or caller-controlled index. + entry = int.from_bytes(blob[0:4], "big") + sector, count = entry >> 8, entry & 255 + require(sector >= 2 and count > 0 and sector*4096 < len(blob) and + (sector+count)*4096 <= len(blob), "REGION_CHUNK_NOT_OWNED") + start = sector*4096 + n = int.from_bytes(blob[start:start+4], "big") + require(1 < n <= count*4096-4 and n <= MAX_CHUNK, + "REGION_CHUNK_LENGTH_INVALID") + # Only inline zlib Anvil: refuse external sidecars, other compressors, + # header overlap, malformed stream and zip bombs. + require(blob[start+4] == 2, "REGION_COMPRESSION_UNSUPPORTED") + packed = blob[start+5:start+4+n] + require(len(packed) == n-1, "REGION_TRUNCATED") + try: + inflater = zlib.decompressobj() + decoded = inflater.decompress(packed, MAX_CHUNK+1) + decoded += inflater.flush(MAX_CHUNK+1-len(decoded)) + require(inflater.eof and not inflater.unused_data and + not inflater.unconsumed_tail and 0 < len(decoded) <= MAX_CHUNK, + "REGION_DECOMPRESS_BOUNDS") + except (zlib.error, ValueError): + raise Refused("REGION_DECOMPRESS_BOUNDS") from None + root = NBT(decoded).document() + require(type(root) is dict and root.get("xPos") == 0 and root.get("zPos") == 0, + "REGION_WRONG_CHUNK") + return root + + +def block_at(root: dict, xyz: tuple[int, int, int]): + x, y, z = xyz + require(0 <= x <= 15 and 0 <= z <= 15 and y == 70, + "FIXTURE_COORDINATE_DRIFT") + sections = root.get("sections") + require(type(sections) is list and len(sections) <= 32, "SECTION_LIST_INVALID") + section_y = y // 16 + selected = [v for v in sections if type(v) is dict and v.get("Y") == section_y] + require(len(selected) == 1, "SECTION_ID_AMBIGUOUS") + states = selected[0].get("block_states") + require(type(states) is dict and type(states.get("palette")) is list, + "BLOCK_PALETTE_MISSING") + palette = states["palette"] + require(1 <= len(palette) <= 4096 and all(type(p) is dict and + type(p.get("Name")) is str and + p["Name"].startswith("minecraft:") for p in palette), + "BLOCK_PALETTE_INVALID") + if len(palette) == 1: + # A uniform section has no palette data. Nonempty data for uniform + # sections is unusual, but must still contain valid zero indices. + data = states.get("data", []) + else: + data = states.get("data") + require(type(data) is list and data, "BLOCK_DATA_REQUIRED") + require(type(data) is list and len(data) <= 1024 and + all(type(v) is int and -(1<<63) <= v < (1<<63) for v in data), + "BLOCK_DATA_INVALID") + if not data: + require(len(palette) == 1, "BLOCK_DATA_REQUIRED") + return palette[0]["Name"] + bits = max(4, math.ceil(math.log2(len(palette)))) + per_long = 64 // bits + idx = ((y & 15) << 8) | (z << 4) | x + position, shift = divmod(idx, per_long) + require(position < len(data), "BLOCK_DATA_TRUNCATED") + number = ((data[position] & ((1<<64)-1)) >> (shift*bits)) & ((1< dict: + require(stage in EXPECTED, "STAGE_NOT_ADMITTED") + root = chunk_from_region(region_bytes) + actual = tuple(block_at(root, c) for c in COORDINATES) + require(actual == EXPECTED[stage], "SERVER_BLOCK_CONSEQUENCE_ABSENT") + return { + "schema": SCHEMA, "classification": "UNTRUSTED_REGION_BYTES_DECODED_ONLY", + "stage": stage, "region_sha256": hashlib.sha256(region_bytes).hexdigest(), + "coordinates": [list(c) for c in COORDINATES], + "block_state_expectations_matched": True, + "native_guest_server_owned_bytes_proven": False, + "server_quiescence_proven": False, + "backend_independent_world_oracle_pass": False, + "collage_mutation_authorized": False, + "physical_true_nas_apply": "HOLD", + } + + +def compare(candidate: bytes, rollback: bytes) -> dict: + require(type(candidate) is bytes and type(rollback) is bytes and + 8192 <= len(candidate) <= MAX_REGION and + 8192 <= len(rollback) <= MAX_REGION, "REGION_FILE_BOUNDS") + require(candidate != rollback, "CANDIDATE_ROLLBACK_SAME_REGION_REPLAY") + c, r = inspect(candidate, "candidate"), inspect(rollback, "rollback") + return { + "schema": SCHEMA, + "classification": "SOURCE_ONLY_DUAL_ANVIL_BLOCK_DECODER", + "candidate_region_sha256": c["region_sha256"], + "rollback_region_sha256": r["region_sha256"], + "block_states_differ_exactly_as_expected": True, + "server_origin_authenticated": False, + "world_quiescence_observed": False, + "backend_independent_world_oracle_pass": False, + "next_gate": "LIVE_QUIESCED_GUEST_FIXED_REGION_DOWNLOAD_AND_PREACTION_CONTROL", + "physical_true_nas_apply": "HOLD", + } diff --git a/tests/test_native_vanilla263_anvil_blocks.py b/tests/test_native_vanilla263_anvil_blocks.py new file mode 100644 index 000000000..db7a4b659 --- /dev/null +++ b/tests/test_native_vanilla263_anvil_blocks.py @@ -0,0 +1,140 @@ +"""Synthetic G4 Anvil block-state decoder tests; never a live guest proof.""" +from __future__ import annotations + +import math +import struct +import unittest +import zlib + +from scripts.qualify_native_vanilla263_anvil_blocks import Refused, compare, inspect + +PALETTE = ("minecraft:redstone_block", "minecraft:air", + "minecraft:sea_lantern", "minecraft:iron_bars", "minecraft:black_concrete") + + +def nbt_string(s): + b = s.encode() + return struct.pack(">H", len(b)) + b + + +def tag(number, name, payload): + return bytes((number,)) + nbt_string(name) + payload + + +def compound(*children): + return b"".join(children) + b"\x00" + + +def build(stage="candidate", *, x_pos=0, z_pos=0, overrides=None): + positions = {((70 & 15) << 8) | (z << 4) | x: value + for (x, z, value) in ( + (0, 0, 0), (2, 0, 1 if stage == "candidate" else 3), + (1, 1, 2 if stage == "candidate" else 4))} + positions.update(overrides or {}) + bits, per_long = max(4, math.ceil(math.log2(len(PALETTE)))), 16 + # Default air; section 4 consists of exactly 4096 block indices. + idxs = [positions.get(i, 1) for i in range(4096)] + longs = [] + for block in range(0, 4096, per_long): + value = sum(idxs[block + i] << (bits*i) + for i in range(per_long)) + if value >= 1 << 63: + value -= 1 << 64 + longs.append(value) + pal = b"".join(compound(tag(8, "Name", nbt_string(n))) for n in PALETTE) + states = compound( + tag(9, "palette", b"\x0a" + struct.pack(">i", len(PALETTE)) + pal), + tag(12, "data", struct.pack(">i", len(longs)) + + b"".join(struct.pack(">q", n) for n in longs)), + ) + sections = compound( + tag(1, "Y", struct.pack(">b", 4)), + tag(10, "block_states", states), + ) + root = compound( + tag(3, "xPos", struct.pack(">i", x_pos)), + tag(3, "zPos", struct.pack(">i", z_pos)), + tag(9, "sections", b"\x0a" + struct.pack(">i", 1) + sections), + ) + doc = b"\x0a" + nbt_string("") + root + packed = zlib.compress(doc, 9) + count = max(1, math.ceil((len(packed) + 5) / 4096)) + region = bytearray((2 + count) * 4096) + region[0:4] = ((2 << 8) | count).to_bytes(4, "big") + region[2*4096:2*4096+4] = (1+len(packed)).to_bytes(4, "big") + region[2*4096+4] = 2 + region[2*4096+5:2*4096+5+len(packed)] = packed + return bytes(region) + + +class BlockRegionTests(unittest.TestCase): + def test_candidate_rollback_differ_source_only(self): + result = compare(build("candidate"), build("rollback")) + self.assertEqual(result["classification"], + "SOURCE_ONLY_DUAL_ANVIL_BLOCK_DECODER") + self.assertFalse(result["server_origin_authenticated"]) + self.assertFalse(result["backend_independent_world_oracle_pass"]) + self.assertEqual(result["physical_true_nas_apply"], "HOLD") + + def test_individual_stages_not_backend_proof(self): + for stage in ("candidate", "rollback"): + result = inspect(build(stage), stage) + self.assertTrue(result["block_state_expectations_matched"]) + self.assertFalse(result["native_guest_server_owned_bytes_proven"]) + self.assertFalse(result["server_quiescence_proven"]) + self.assertFalse(result["backend_independent_world_oracle_pass"]) + + def reject(self, blob, stage, code): + with self.assertRaises(Refused) as ex: + inspect(blob, stage) + self.assertEqual(str(ex.exception), code) + + def test_wrong_stage_and_replayed_same_region(self): + self.reject(build(), "rollback", "SERVER_BLOCK_CONSEQUENCE_ABSENT") + self.reject(build(), "unknown", "STAGE_NOT_ADMITTED") + with self.assertRaisesRegex(Refused, "SAME_REGION_REPLAY"): + compare(build(), build()) + + def test_wrong_or_stale_placed_core_negative(self): + idx = ((70 & 15) << 8) | (0 << 4) | 0 + self.reject(build(overrides={idx: 1}), "candidate", + "SERVER_BLOCK_CONSEQUENCE_ABSENT") + + def test_wrong_effect_negative(self): + access_idx = ((70 & 15) << 8) | (0 << 4) | 2 + self.reject(build(overrides={access_idx: 3}), "candidate", + "SERVER_BLOCK_CONSEQUENCE_ABSENT") + + def test_wrong_chunk_and_dimension_not_admitted(self): + self.reject(build(x_pos=1), "candidate", "REGION_WRONG_CHUNK") + self.reject(build(z_pos=-1), "candidate", "REGION_WRONG_CHUNK") + + def test_truncated_or_empty_region(self): + self.reject(b"", "candidate", "REGION_FILE_BOUNDS") + self.reject(build()[:8191], "candidate", "REGION_FILE_BOUNDS") + + def test_corrupted_header_and_external_compression(self): + blob = bytearray(build()) + blob[0:4] = b"\x00\x00\x00\x00" + self.reject(bytes(blob), "candidate", "REGION_CHUNK_NOT_OWNED") + blob = bytearray(build()) + blob[8192+4] = 0x82 + self.reject(bytes(blob), "candidate", "REGION_COMPRESSION_UNSUPPORTED") + blob = bytearray(build()) + blob[8192:8196] = (99999999).to_bytes(4, "big") + self.reject(bytes(blob), "candidate", "REGION_CHUNK_LENGTH_INVALID") + + def test_bad_decompression_rejected(self): + blob = bytearray(build()) + blob[8192+5:8192+9] = b"xxxx" + self.reject(bytes(blob), "candidate", "REGION_DECOMPRESS_BOUNDS") + + def test_reject_foreign_or_unqualified_blobs(self): + with self.assertRaisesRegex(Refused, "REGION_FILE_BOUNDS"): + compare(build(), b"caller-supplied-block-list") + self.assertNotIn("native_guest_backend_independent_oracle_qualified", + inspect(build(), "candidate")) + + +if __name__ == "__main__": + unittest.main() From aae3919f9d2765075706e6588a9776f7eb4fffbd Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:43:45 +0200 Subject: [PATCH 704/719] feat(G4): fixed owned Anvil region readback source seam (no live guest claim) * feat(G4): strict fixed-path quiesced middleware Anvil fetch seam, no guest admission * test(G4): read-only fixed guest path, quiesce, forged route, file drift negatives * ci(G4): adversarial fixed-path guest download seam on existing cheap runner * fix(G4 test): avoid overriding unittest TestCase.run with adapter helper --- ...alify-native-loopback-gameplay-overlay.yml | 7 +- ..._native_vanilla263_anvil_guest_readback.py | 66 ++++++++++++ ..._native_vanilla263_anvil_guest_readback.py | 100 ++++++++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 scripts/qualify_native_vanilla263_anvil_guest_readback.py create mode 100644 tests/test_native_vanilla263_anvil_guest_readback.py diff --git a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml index a2084dabf..a9f7f1e00 100644 --- a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml +++ b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml @@ -9,6 +9,8 @@ on: - 'tests/test_native_vanilla263_mineflayer_causal.py' - 'scripts/qualify_native_vanilla263_anvil_blocks.py' - 'tests/test_native_vanilla263_anvil_blocks.py' + - 'scripts/qualify_native_vanilla263_anvil_guest_readback.py' + - 'tests/test_native_vanilla263_anvil_guest_readback.py' - '.github/workflows/qualify-native-loopback-gameplay-overlay.yml' workflow_dispatch: permissions: @@ -31,10 +33,13 @@ jobs: tests/test_native_vanilla263_loopback_player_overlay.py \ tests/test_native_vanilla263_mineflayer_causal.py \ scripts/qualify_native_vanilla263_anvil_blocks.py \ - tests/test_native_vanilla263_anvil_blocks.py + tests/test_native_vanilla263_anvil_blocks.py \ + scripts/qualify_native_vanilla263_anvil_guest_readback.py \ + tests/test_native_vanilla263_anvil_guest_readback.py python3 -m unittest -v tests.test_native_vanilla263_loopback_player_overlay python3 -m unittest -v tests.test_native_vanilla263_mineflayer_causal python3 -m unittest -v tests.test_native_vanilla263_anvil_blocks + python3 -m unittest -v tests.test_native_vanilla263_anvil_guest_readback node --check scripts/qualify_native_vanilla263_mineflayer_causal.js bash -n scripts/gha_kvm_truenas_rdte.sh - name: Preserve operational auth and physical TrueNAS hold diff --git a/scripts/qualify_native_vanilla263_anvil_guest_readback.py b/scripts/qualify_native_vanilla263_anvil_guest_readback.py new file mode 100644 index 000000000..3ac23496b --- /dev/null +++ b/scripts/qualify_native_vanilla263_anvil_guest_readback.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Bounded G4 quiesced native world Anvil file readback seam (source-only). + +This is a fixed-path read-only adapter for the existing disposable native T6 +DDP session and its already admitted owned App/world. It has no network client, +credential issuance, provider mutation or disk export. Neither a synthetic +test nor a caller-provided bytes callback is an independent live guest receipt. +""" +from __future__ import annotations + +try: + from .qualify_native_vanilla263_anvil_blocks import ( + MAX_REGION, SCHEMA, inspect, require) + from .qualify_vanilla263_native_world_rollback import ( + APP, WORLD, app_state) +except ImportError: + from qualify_native_vanilla263_anvil_blocks import ( + MAX_REGION, SCHEMA, inspect, require) + from qualify_vanilla263_native_world_rollback import ( + APP, WORLD, app_state) + +PATH = WORLD + "/region/r.0.0.mca" +STAGES = ("candidate", "rollback") + + +def capture(*, call, wait_job, fetch_bytes, stage: str) -> dict: + """Only after existing *actual* guest runner verified owned App then stop. + + The runner MUST independently bind the current authenticated DDP session, + exact native service/world, player action, signed request, and App stop. + It must not publish the raw Anvil region, signed session or download URL. + """ + require(stage in STAGES, "BACKEND_STAGE_NOT_ADMITTED") + require(app_state(call) == "STOPPED", "BACKEND_QUIESCENCE_NOT_OBSERVED") + stat = call("filesystem.stat", [PATH]) + require(type(stat) is dict and stat.get("type") == "FILE" and + type(stat.get("size")) is int and + 8192 <= stat["size"] <= MAX_REGION, "BACKEND_REGION_FILE_NOT_EXACT") + row = call("core.download", [ + "filesystem.get", [PATH], "g4-owned-region-" + stage + ".mca", True]) + require(type(row) is list and len(row) == 2 and + type(row[0]) is int and row[0] > 0 and + type(row[1]) is str and + row[1].startswith("/_download/") and + len(row[1]) <= 300 and + ".." not in row[1] and "?" not in row[1] and "#" not in row[1], + "BACKEND_DOWNLOAD_ROUTE_INVALID") + wait_job(row[0], "G4 read-only owned Anvil file download") + blob = fetch_bytes(row[1]) + require(type(blob) is bytes and len(blob) == stat["size"], + "BACKEND_DOWNLOADED_FILE_DRIFT") + result = inspect(blob, stage) + # The host transfer API is separate from independent receipt admission: + # this module cannot prove the caller ran under authorized guest identity. + result.update({ + "classification": "SOURCE_ONLY_OWNED_ANVIL_DOWNLOAD_SEAM", + "downloaded_file_size": len(blob), + "requested_native_app": APP, + "requested_native_world_file_fixed": True, + "read_only_middleware_calls": True, + "live_guest_identity_independently_verified": False, + "backend_independent_world_oracle_pass": False, + "raw_region_published": False, + "next_gate": "EXACT_LIVE_GUEST_RUNNER_AND_INDEPENDENT_RECEIPT_BINDING", + }) + return result diff --git a/tests/test_native_vanilla263_anvil_guest_readback.py b/tests/test_native_vanilla263_anvil_guest_readback.py new file mode 100644 index 000000000..46f31604e --- /dev/null +++ b/tests/test_native_vanilla263_anvil_guest_readback.py @@ -0,0 +1,100 @@ +"""Stubbed middleware admission and negative surface tests; NOT a live guest.""" +from __future__ import annotations + +import unittest + +from scripts.qualify_native_vanilla263_anvil_guest_readback import capture, PATH +from scripts.qualify_native_vanilla263_anvil_blocks import Refused +from scripts.qualify_vanilla263_native_world_rollback import APP +from tests.test_native_vanilla263_anvil_blocks import build + + +class NativeReadbackTests(unittest.TestCase): + def setUp(self): + self.blob = build("candidate") + self.state = "STOPPED" + self.stat = {"type": "FILE", "size": len(self.blob)} + self.token = "/_download/synthetic-test-opaque" + self.record = [] + self.downloaded = [] + + def call(self, name, args): + self.record.append((name, args)) + if name == "app.query": + self.assertEqual(args, [[["id", "=", APP]]]) + return [{"name": APP, "state": self.state}] + if name == "filesystem.stat": + self.assertEqual(args, [PATH]) + return self.stat + if name == "core.download": + self.assertEqual(args, ["filesystem.get", [PATH], + "g4-owned-region-candidate.mca", True]) + return [1001, self.token] + self.fail("unexpected middleware RPC: " + name) + + def wait(self, job, label): + self.downloaded.append((job, label)) + + def fetch(self, token): + self.assertEqual(token, self.token) + return self.blob + + def capture_stage(self, stage="candidate"): + return capture(call=self.call, wait_job=self.wait, + fetch_bytes=self.fetch, stage=stage) + + def reject(self, reason): + with self.assertRaises(Refused) as cm: + self.capture_stage() + self.assertEqual(str(cm.exception), reason) + + def test_fixed_native_quiesced_read_only_path_and_nonclaim(self): + result = self.capture_stage() + self.assertEqual(result["classification"], + "SOURCE_ONLY_OWNED_ANVIL_DOWNLOAD_SEAM") + self.assertEqual(result["downloaded_file_size"], len(self.blob)) + self.assertTrue(result["requested_native_world_file_fixed"]) + self.assertFalse(result["live_guest_identity_independently_verified"]) + self.assertFalse(result["backend_independent_world_oracle_pass"]) + self.assertFalse(result["raw_region_published"]) + self.assertEqual(result["physical_true_nas_apply"], "HOLD") + self.assertEqual([name for name, _ in self.record], + ["app.query", "filesystem.stat", "core.download"]) + self.assertEqual(self.downloaded, [(1001, "G4 read-only owned Anvil file download")]) + + def test_wrong_stage_has_zero_provider_calls(self): + with self.assertRaisesRegex(Refused, "BACKEND_STAGE_NOT_ADMITTED"): + self.capture_stage("other") + self.assertEqual(self.record, []) + + def test_refuse_running_app_before_file_read(self): + self.state = "RUNNING" + self.reject("BACKEND_QUIESCENCE_NOT_OBSERVED") + self.assertEqual([name for name, _ in self.record], ["app.query"]) + + def test_refuse_missing_or_unsafe_file_stat(self): + for attr in ({"type": "DIRECTORY", "size": len(self.blob)}, + {"type": "FILE", "size": 0}, {"type": "FILE", "size": True}, + None): + with self.subTest(stat=attr): + self.stat = attr + self.reject("BACKEND_REGION_FILE_NOT_EXACT") + + def test_refuse_injected_redirect_or_query(self): + for path in ("https://foreign.invalid/a", "/_download/../secrets", + "/_download/a?x=secret", "/_download/a#fragment", "/outside"): + with self.subTest(path=path): + self.token = path + self.reject("BACKEND_DOWNLOAD_ROUTE_INVALID") + + def test_refuse_wrong_file_bytes(self): + self.blob = self.blob[:-1] + self.reject("BACKEND_DOWNLOADED_FILE_DRIFT") + + def test_refuse_wrong_effect_inside_download(self): + self.blob = build("rollback") + self.reject("SERVER_BLOCK_CONSEQUENCE_ABSENT") + + +if __name__ == "__main__": + unittest.main() From 34415f7c9e5a4c0f8f77b5867249b1b4cd4c1c0e Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:06:53 +0200 Subject: [PATCH 705/719] feat(G4): three checkpoint native world witness source (no new guest admission) * feat(G4): check independent clean pre-action Anvil fixture state * feat(G4): add fixed preaction sample to owner/quiesce source seam * feat(G4): preserve established client gameplay and add bounded fixture-only setup phase * feat(G4): validate independent preaction client fixture with no false causal claim * feat(G4): integrate preaction candidate rollback Anvil checkpoints into native T6 guest * feat(G4): explicit runner-level native backend region admission flag gated by client * feat(G4): opt-in backend witness from new run manifest through router/verifier * feat(G4): independent three-checkpoint backend receipt admission with strict negative boundaries * test(G4): enforce independent empty-core preaction world negative * test(G4): independent guest receipt three-stage forgery replay owner and rollback rejection * test(G4): preserve old two-stage acceptance and test bounded client-only preparation * ci(G4): exercise full three-checkpoint receipt anti-forgery on free public CPU * test(G4): import canonical native fixture before strict standalone verifier * test(G4): prove new region gate is opt-in and cannot replay 015 * test(G4): avoid formatting-sensitive source regression assertion --- .github/workflows/gha-kvm-system-rdte.yml | 14 ++ ...alify-native-loopback-gameplay-overlay.yml | 11 +- .../assert_vanilla263_native_t6_receipt.py | 71 +++++++++- scripts/gha_kvm_truenas_rdte.sh | 8 ++ .../qualify_native_vanilla263_anvil_blocks.py | 2 + ..._native_vanilla263_anvil_guest_readback.py | 3 +- ...ify_native_vanilla263_mineflayer_causal.js | 15 +- ...ify_native_vanilla263_mineflayer_causal.py | 35 ++++- .../truenas_middleware_minecraft_t6_probe.py | 65 +++++++++ tests/test_native_vanilla263_anvil_blocks.py | 12 +- ...lla263_backend_three_checkpoint_receipt.py | 133 ++++++++++++++++++ ...est_native_vanilla263_mineflayer_causal.py | 18 ++- tests/test_vanilla263_t6_guest_routing.py | 24 ++++ 13 files changed, 400 insertions(+), 11 deletions(-) create mode 100644 tests/test_native_vanilla263_backend_three_checkpoint_receipt.py diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index c0ac9e33e..1fb2ef636 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -96,6 +96,7 @@ jobs: truenas_lantern_stage: ${{ steps.scope.outputs.truenas_lantern_stage }} truenas_world_rollback: ${{ steps.scope.outputs.truenas_world_rollback }} truenas_native_offline_player: ${{ steps.scope.outputs.truenas_native_offline_player }} + truenas_native_backend_region: ${{ steps.scope.outputs.truenas_native_backend_region }} truenas_compute_fixture: ${{ steps.scope.outputs.truenas_compute_fixture }} proxmox_compute_fixture: ${{ steps.scope.outputs.proxmox_compute_fixture }} steps: @@ -128,6 +129,7 @@ jobs: truenas_lantern_stage=false truenas_world_rollback=false truenas_native_offline_player=false + truenas_native_backend_region=false truenas_compute_fixture="${DISPATCH_TRUENAS_COMPUTE_FIXTURE:-none}" proxmox_compute_fixture="${DISPATCH_PROXMOX_COMPUTE_FIXTURE:-none}" if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then @@ -202,6 +204,11 @@ jobs: truenas_lantern_stage="$(jq -er '(.lantern_stage // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" truenas_world_rollback="$(jq -er '(.native_world_rollback // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" truenas_native_offline_player="$(jq -er '(.native_offline_player // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + truenas_native_backend_region="$(jq -er '(.native_backend_region // false) | select(. == true or . == false)' "$RUN_REQUEST_PATH")" + if [[ "$truenas_native_backend_region" == "true" && + "$truenas_native_offline_player" != "true" ]]; then + echo "G4 region requires native offline player" >&2; exit 2 + fi if [[ "$truenas_native_offline_player" == "true" ]]; then [[ "$truenas_world_rollback" == "true" && "$truenas_lantern_stage" == "true" && "$truenas_collage_readback" == "true" && "$truenas_version" == "25.04.2.6" && @@ -245,6 +252,7 @@ jobs: echo "truenas_lantern_stage=$truenas_lantern_stage" >>"$GITHUB_OUTPUT" echo "truenas_world_rollback=$truenas_world_rollback" >>"$GITHUB_OUTPUT" echo "truenas_native_offline_player=$truenas_native_offline_player" >>"$GITHUB_OUTPUT" + echo "truenas_native_backend_region=$truenas_native_backend_region" >>"$GITHUB_OUTPUT" echo "truenas_compute_fixture=$truenas_compute_fixture" >>"$GITHUB_OUTPUT" echo "proxmox_compute_fixture=$proxmox_compute_fixture" >>"$GITHUB_OUTPUT" printf 'target scope: proxmox=%s proxmox_compute_fixture=%s truenas=%s qemu_topology=%s truenas_version=%s truenas_rung=%s t6_product=%s truenas_compute_fixture=%s\n' "$proxmox" "$proxmox_compute_fixture" "$truenas" "$qemu_topology" "$truenas_version" "$truenas_rung" "$truenas_t6_product" "$truenas_compute_fixture" @@ -850,6 +858,9 @@ jobs: args+=(--vanilla-native-player --vanilla-player-node-path "$RUNNER_TEMP/native-prismarine/runtime/node_modules") fi + if [[ "${{ needs.changes.outputs.truenas_native_backend_region }}" == "true" ]]; then + args+=(--vanilla-backend-region) + fi bash scripts/gha_kvm_truenas_rdte.sh "${args[@]}" python3 -m json.tool "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" # A green job is not a Minecraft product pass without full guest evidence. @@ -871,6 +882,9 @@ jobs: if [[ "${{ needs.changes.outputs.truenas_native_offline_player }}" == "true" ]]; then additional+=(--require-native-client-visibility) fi + if [[ "${{ needs.changes.outputs.truenas_native_backend_region }}" == "true" ]]; then + additional+=(--require-native-backend-region) + fi python3 scripts/assert_vanilla263_native_t6_receipt.py --receipt "$RUNNER_TEMP/gha-kvm-system-rdte/truenas-receipt.json" "${additional[@]}" fi if [[ "${{ needs.changes.outputs.truenas_rung }}" == "t6" && "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then diff --git a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml index a9f7f1e00..1d4d60594 100644 --- a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml +++ b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml @@ -11,6 +11,11 @@ on: - 'tests/test_native_vanilla263_anvil_blocks.py' - 'scripts/qualify_native_vanilla263_anvil_guest_readback.py' - 'tests/test_native_vanilla263_anvil_guest_readback.py' + - 'tests/test_native_vanilla263_backend_three_checkpoint_receipt.py' + - 'scripts/truenas_middleware_minecraft_t6_probe.py' + - 'scripts/assert_vanilla263_native_t6_receipt.py' + - 'scripts/gha_kvm_truenas_rdte.sh' + - '.github/workflows/gha-kvm-system-rdte.yml' - '.github/workflows/qualify-native-loopback-gameplay-overlay.yml' workflow_dispatch: permissions: @@ -35,11 +40,15 @@ jobs: scripts/qualify_native_vanilla263_anvil_blocks.py \ tests/test_native_vanilla263_anvil_blocks.py \ scripts/qualify_native_vanilla263_anvil_guest_readback.py \ - tests/test_native_vanilla263_anvil_guest_readback.py + tests/test_native_vanilla263_anvil_guest_readback.py \ + tests/test_native_vanilla263_backend_three_checkpoint_receipt.py \ + scripts/truenas_middleware_minecraft_t6_probe.py \ + scripts/assert_vanilla263_native_t6_receipt.py python3 -m unittest -v tests.test_native_vanilla263_loopback_player_overlay python3 -m unittest -v tests.test_native_vanilla263_mineflayer_causal python3 -m unittest -v tests.test_native_vanilla263_anvil_blocks python3 -m unittest -v tests.test_native_vanilla263_anvil_guest_readback + python3 -m unittest -v tests.test_native_vanilla263_backend_three_checkpoint_receipt node --check scripts/qualify_native_vanilla263_mineflayer_causal.js bash -n scripts/gha_kvm_truenas_rdte.sh - name: Preserve operational auth and physical TrueNAS hold diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py index 74ce2804e..78557c970 100644 --- a/scripts/assert_vanilla263_native_t6_receipt.py +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -22,7 +22,8 @@ def check(root: dict, *, require_collage: bool = False, require_lantern_staged: bool = False, require_native_rollback: bool = False, - require_native_client_visibility: bool = False) -> None: + require_native_client_visibility: bool = False, + require_native_backend_region: bool = False) -> None: def need(condition: bool, reason: str) -> None: if not condition: raise ValueError(reason) @@ -157,6 +158,70 @@ def need(condition: bool, reason: str) -> None: client.get("collage_mutation_authorized") is False and client.get("physical_true_nas_apply") == "HOLD", "native offline client causal stage missing") + if require_native_backend_region: + need(require_native_client_visibility and require_native_rollback and + require_collage and require_lantern_staged, + "backend world region requires G2/G3 and same native client") + proof = p.get("native_backend_world") or {} + need(type(proof) is dict and + proof.get("schema") == "supracraft.native-vanilla263-live-backend-region/1" and + proof.get("classification") == "LIVE_NATIVE_BACKEND_THREE_CHECKPOINTS" and + proof.get("same_owned_guest_three_checkpoints") is True and + proof.get("backend_independent_world_oracle_pass") is True and + proof.get("collage_mutation_authorized") is False and + proof.get("physical_true_nas_apply") == "HOLD", + "live three checkpoint backend admission unproven") + prep = proof.get("preparation") or {} + need(type(prep) is dict and + prep.get("schema") == "supracraft.native-vanilla263-mineflayer-causal/1" and + prep.get("classification") == "FIXED_PLAYER_FIXTURE_PREPARATION_ONLY" and + prep.get("stage") == "candidate-prep" and + prep.get("client_protocol777_connected_and_spawned") is True and + prep.get("client_fixture_prepared_without_core_placement") is True and + prep.get("native_guest_backend_independent_oracle_qualified") is False and + prep.get("collage_mutation_authorized") is False and + prep.get("physical_true_nas_apply") == "HOLD", + "independent backend preaction preparation absent") + blocks = { + "preaction": ["minecraft:air", "minecraft:iron_bars", "minecraft:black_concrete"], + "candidate": ["minecraft:redstone_block", "minecraft:air", "minecraft:sea_lantern"], + "rollback": ["minecraft:redstone_block", "minecraft:iron_bars", "minecraft:black_concrete"], + } + hashes = set() + for stage, expected in blocks.items(): + observed = proof.get(stage) or {} + digest = observed.get("region_sha256") + need(type(observed) is dict and + observed.get("schema") == "supracraft.native-vanilla263-server-owned-anvil/1" and + observed.get("classification") == "LIVE_NATIVE_STOPPED_OWNER_ANVIL_READBACK" and + observed.get("stage") == stage and + observed.get("block_states") == expected and + observed.get("coordinates") == [[0,70,0],[2,70,0],[1,70,1]] and + observed.get("block_state_expectations_matched") is True and + observed.get("requested_native_app") == APP and + observed.get("requested_native_world") == + "rdtepool/minecraft-vanilla-263-t6-world" and + observed.get("requested_native_world_file_fixed") is True and + observed.get("read_only_middleware_calls") is True and + observed.get("live_guest_identity_independently_verified") is True and + observed.get("native_guest_server_owned_bytes_proven") is True and + observed.get("server_quiescence_proven") is True and + observed.get("raw_region_published") is False and + observed.get("collage_mutation_authorized") is False and + observed.get("physical_true_nas_apply") == "HOLD" and + type(observed.get("downloaded_file_size")) is int and + 8192 <= observed["downloaded_file_size"] <= 64*1024*1024 and + type(digest) is str and len(digest) == 64 and + all(c in "0123456789abcdef" for c in digest), + "native server-owned Anvil stage missing or corrupted: " + stage) + hashes.add(digest) + readiness = (p.get("readiness") or {}).get("g4-" + stage) or {} + need(type(readiness) is dict and + (readiness.get("protocol") or {}).get("oracleSatisfied") is True and + type((readiness.get("level") or {}).get("size")) is int and + readiness["level"]["size"] > 0, + "native backend stage restart/health missing: " + stage) + need(len(hashes) == 3, "backend world snapshot hash replay") if require_native_rollback: need(require_lantern_staged and require_collage, "native rollback requires independently qualified G2 and artifact") @@ -220,13 +285,15 @@ def main() -> int: p.add_argument("--require-lantern-staged", action="store_true") p.add_argument("--require-native-rollback", action="store_true") p.add_argument("--require-native-client-visibility", action="store_true") + p.add_argument("--require-native-backend-region", action="store_true") a = p.parse_args() try: check(json.loads(a.receipt.read_text(encoding="utf-8")), require_collage=a.require_collage_readback, require_lantern_staged=a.require_lantern_staged, require_native_rollback=a.require_native_rollback, - require_native_client_visibility=a.require_native_client_visibility) + require_native_client_visibility=a.require_native_client_visibility, + require_native_backend_region=a.require_native_backend_region) except (OSError, ValueError, TypeError) as ex: # Error messages generated by check() are constant, never propagate # arbitrary provider receipt fields, credentials or real addresses. diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 9c0342fcc..bb91bea9b 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -44,6 +44,7 @@ VANILLA_LANTERN_ZIP="" VANILLA_LANTERN_MANIFEST="" VANILLA_WORLD_ROLLBACK=false VANILLA_NATIVE_PLAYER=false +VANILLA_BACKEND_REGION=false VANILLA_PLAYER_NODE_PATH="" while [[ $# -gt 0 ]]; do case "$1" in @@ -71,6 +72,7 @@ while [[ $# -gt 0 ]]; do --vanilla-lantern-manifest) VANILLA_LANTERN_MANIFEST="$2"; shift 2 ;; --vanilla-world-rollback) VANILLA_WORLD_ROLLBACK=true; shift ;; --vanilla-native-player) VANILLA_NATIVE_PLAYER=true; shift ;; + --vanilla-backend-region) VANILLA_BACKEND_REGION=true; shift ;; --vanilla-player-node-path) VANILLA_PLAYER_NODE_PATH="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; @@ -98,6 +100,9 @@ if [[ "$VANILLA_NATIVE_PLAYER" == "true" ]]; then "$T6_PRODUCT" == "minecraft-vanilla-263" && "$VERSION" == "25.04.2.6" ]] || { echo "G4 synthetic client requires exact disposable native G3 and pinned Node dependencies" >&2; exit 2; } fi +if [[ "$VANILLA_BACKEND_REGION" == "true" && "$VANILLA_NATIVE_PLAYER" != "true" ]]; then + echo "G4 region requires native offline player and separate G2/G3" >&2; exit 2 +fi if [[ "$VANILLA_NATIVE_PLAYER" != "true" && -n "$VANILLA_PLAYER_NODE_PATH" ]]; then echo "G4 client dependencies without explicit disposable admission" >&2; exit 2 fi @@ -1247,6 +1252,9 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then --native-player-forward "${MINECRAFT_HOSTFWD#,hostfwd=}" --native-player-node-path "$VANILLA_PLAYER_NODE_PATH") fi + if [[ "$VANILLA_BACKEND_REGION" == "true" ]]; then + NATIVE_PLAYER_ARGS+=(--native-backend-region) + fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ --product-profile vanilla263 --native-catalog-profile 1.15.30 \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ diff --git a/scripts/qualify_native_vanilla263_anvil_blocks.py b/scripts/qualify_native_vanilla263_anvil_blocks.py index 83f0255d6..271e88750 100644 --- a/scripts/qualify_native_vanilla263_anvil_blocks.py +++ b/scripts/qualify_native_vanilla263_anvil_blocks.py @@ -19,6 +19,7 @@ MAX_ITEMS = 1_000_000 COORDINATES = ((0, 70, 0), (2, 70, 0), (1, 70, 1)) EXPECTED = { + "preaction": ("minecraft:air", "minecraft:iron_bars", "minecraft:black_concrete"), "candidate": ("minecraft:redstone_block", "minecraft:air", "minecraft:sea_lantern"), "rollback": ("minecraft:redstone_block", "minecraft:iron_bars", "minecraft:black_concrete"), } @@ -179,6 +180,7 @@ def inspect(region_bytes: bytes, stage: str) -> dict: "stage": stage, "region_sha256": hashlib.sha256(region_bytes).hexdigest(), "coordinates": [list(c) for c in COORDINATES], "block_state_expectations_matched": True, + "block_states": list(actual), "native_guest_server_owned_bytes_proven": False, "server_quiescence_proven": False, "backend_independent_world_oracle_pass": False, diff --git a/scripts/qualify_native_vanilla263_anvil_guest_readback.py b/scripts/qualify_native_vanilla263_anvil_guest_readback.py index 3ac23496b..215cff1f0 100644 --- a/scripts/qualify_native_vanilla263_anvil_guest_readback.py +++ b/scripts/qualify_native_vanilla263_anvil_guest_readback.py @@ -20,7 +20,7 @@ APP, WORLD, app_state) PATH = WORLD + "/region/r.0.0.mca" -STAGES = ("candidate", "rollback") +STAGES = ("preaction", "candidate", "rollback") def capture(*, call, wait_job, fetch_bytes, stage: str) -> dict: @@ -57,6 +57,7 @@ def capture(*, call, wait_job, fetch_bytes, stage: str) -> dict: "downloaded_file_size": len(blob), "requested_native_app": APP, "requested_native_world_file_fixed": True, + "requested_native_world": "rdtepool/minecraft-vanilla-263-t6-world", "read_only_middleware_calls": True, "live_guest_identity_independently_verified": False, "backend_independent_world_oracle_pass": False, diff --git a/scripts/qualify_native_vanilla263_mineflayer_causal.js b/scripts/qualify_native_vanilla263_mineflayer_causal.js index 76720f50c..cd0b61ee5 100644 --- a/scripts/qualify_native_vanilla263_mineflayer_causal.js +++ b/scripts/qualify_native_vanilla263_mineflayer_causal.js @@ -6,9 +6,10 @@ const mineflayer = require('mineflayer') const { Vec3 } = require('vec3') const stage = process.env.SUPRACRAFT_NATIVE_STAGE +const prepare = stage === 'candidate-prep' const port = Number(process.env.MC_PORT) const output = process.env.BOT_RESULT_FILE -const admitted = ['candidate', 'rollback'].includes(stage) && +const admitted = ['candidate', 'rollback', 'candidate-prep'].includes(stage) && Number.isSafeInteger(port) && port > 1024 && port < 65536 && typeof output === 'string' && output.length > 0 if (!admitted) process.exit(2) @@ -18,7 +19,7 @@ const result = { stage, requested_version: '26.3', protocol: 777, offline_mode: true, synthetic_bot: 'SupraCraftProbe', - operation: 'mineflayer.placeBlock', + operation: prepare ? 'mineflayer.chat.fixedFixture' : 'mineflayer.placeBlock', rcon_used: false, backend_independent_oracle_qualified: false, true_nas_physical_apply: false, @@ -33,7 +34,7 @@ function save () { fs.writeFileSync(output, JSON.stringify(result, null, 2) + '\ function finish (ok, code) { if (done) return done = true - result.classification = ok ? 'CLIENT_CAUSAL_VISIBILITY' : 'CLIENT_REFUSED' + result.classification = ok ? (prepare ? 'CLIENT_FIXTURE_PREPARED' : 'CLIENT_CAUSAL_VISIBILITY') : 'CLIENT_REFUSED' if (!ok) result.reason_code = code save() try { bot?.quit('synthetic native world test complete') } catch {} @@ -84,6 +85,14 @@ bot?.once('spawn', async () => { if (before !== 'air' || support !== 'stone') { return finish(false, 'CLIENT_FIXTURE_NOT_PRESENT') } + if (prepare) { + result.expected_datapack_present = true + result.visible_effect_correct = before === 'air' && support === 'stone' && + read(2, 70, 0) === 'iron_bars' && + read(1, 70, 1) === 'black_concrete' + return finish(result.visible_effect_correct, + 'CLIENT_FIXTURE_PREPARATION_FAILED') + } const deadline = Date.now() + 20000 let core while (Date.now() < deadline) { diff --git a/scripts/qualify_native_vanilla263_mineflayer_causal.py b/scripts/qualify_native_vanilla263_mineflayer_causal.py index ec1a14494..16a31cd4a 100644 --- a/scripts/qualify_native_vanilla263_mineflayer_causal.py +++ b/scripts/qualify_native_vanilla263_mineflayer_causal.py @@ -18,7 +18,7 @@ from qualify_native_vanilla263_loopback_player_overlay import runner_owned_loopback_forward SCHEMA = "supracraft.native-vanilla263-mineflayer-causal/1" -STAGES = ("candidate", "rollback") +STAGES = ("candidate", "rollback", "candidate-prep") WORKER = Path(__file__).with_name("qualify_native_vanilla263_mineflayer_causal.js") @@ -32,6 +32,39 @@ def require(ok: bool, reason: str) -> None: def validate(receipt: object, stage: str) -> dict: + if stage == "candidate-prep": + require(type(receipt) is dict and + receipt.get("schema") == SCHEMA and + receipt.get("classification") == "CLIENT_FIXTURE_PREPARED" and + receipt.get("stage") == stage and + receipt.get("requested_version") == "26.3" and + receipt.get("negotiated_version") == "26.3" and + type(receipt.get("protocol")) is int and receipt["protocol"] == 777 and + type(receipt.get("negotiated_protocol")) is int and + receipt["negotiated_protocol"] == 777 and + receipt.get("offline_mode") is True and + receipt.get("synthetic_bot") == "SupraCraftProbe" and + receipt.get("operation") == "mineflayer.chat.fixedFixture" and + receipt.get("connected_and_spawned") is True and + receipt.get("fixture_via_bounded_bot_commands") is True and + receipt.get("placed") is False and + receipt.get("visible_effect_correct") is True and + receipt.get("expected_datapack_present") is True and + receipt.get("rcon_used") is False and + receipt.get("backend_independent_oracle_qualified") is False and + receipt.get("true_nas_physical_apply") is False and + receipt.get("collage_mutation_authorized") is False and + receipt.get("in_guest_server_world") is False and + "error" not in receipt and "reason_code" not in receipt, + "G4_PREACTION_FIXTURE_UNQUALIFIED") + return { + "schema": SCHEMA, "classification": "FIXED_PLAYER_FIXTURE_PREPARATION_ONLY", + "stage": stage, "client_protocol777_connected_and_spawned": True, + "client_fixture_prepared_without_core_placement": True, + "native_guest_backend_independent_oracle_qualified": False, + "collage_mutation_authorized": False, + "physical_true_nas_apply": "HOLD", + } require(stage in STAGES and type(receipt) is dict and receipt.get("schema") == SCHEMA and receipt.get("classification") == "CLIENT_CAUSAL_VISIBILITY" and diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 93d36613e..ee603093e 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -404,6 +404,7 @@ def main() -> int: p.add_argument("--lantern-manifest", type=pathlib.Path) p.add_argument("--native-world-rollback", action="store_true") p.add_argument("--native-offline-player", action="store_true") + p.add_argument("--native-backend-region", action="store_true") p.add_argument("--native-player-forward") p.add_argument("--native-player-node-path", type=pathlib.Path) p.add_argument("--native-catalog-profile", choices=sorted(CATALOG_PROFILES), @@ -443,6 +444,10 @@ def main() -> int: if a.native_offline_player and (not a.native_world_rollback or not a.native_player_forward or a.native_player_node_path is None): raise MinecraftT6Error("G4 client requires exact G3 rollback and owned loopback") + if a.native_backend_region and (not a.native_offline_player or + not a.native_world_rollback or not VANILLA263 or + a.collage_source_dir is None): + raise MinecraftT6Error("G4 backend region requires exact native G2/G3 and player") if not a.native_offline_player and (a.native_player_forward or a.native_player_node_path is not None): raise MinecraftT6Error("G4 client arguments without explicit admission") @@ -807,6 +812,46 @@ def observe_collage_after_native_readiness(): payload["lantern_artifact_stage"]["persisted_on_reinstall"] = True # The same exact App owner is rechecked by wait_app() above; # pre-stage TLS Go observation remains a separate G2 receipt. + if a.native_backend_region: + from qualify_native_vanilla263_anvil_guest_readback import capture + from qualify_native_vanilla263_mineflayer_causal import run as run_player + payload["native_backend_world"] = { + "schema": "supracraft.native-vanilla263-live-backend-region/1", + "classification": "INCOMPLETE_NATIVE_BACKEND_REGION", + "backend_independent_world_oracle_pass": False, + "physical_true_nas_apply": "HOLD", + "collage_mutation_authorized": False, + } + payload["native_backend_world"]["preparation"] = run_player( + stage="candidate-prep", forward=a.native_player_forward, + state_dir=a.out.parent, node_path=a.native_player_node_path) + def sample_owned_backend(stage): + # A separate native App stop after the actual client action + # flushes server-owned region files. Never decode packets as + # if they were an authoritative backend world state. + wait_job(call("app.stop", [APP]), "G4 backend quiesce " + stage) + cfg = call("app.config", [APP]) + if not owned_config(cfg, desired): + raise MinecraftT6Error("G4 region owner drift") + if call("system.version", []) != a.expected_system_version: + raise MinecraftT6Error("G4 region target drift") + obs = capture(call=call, wait_job=wait_job, + fetch_bytes=lambda p: middleware_http_bytes( + a.host, a.port, a.tls, p, timeout=a.timeout), + stage=stage) + # Upgraded only in this signed, actual guest runner after + # real DDP owner/session, stop, bounded fixed-path download + # and independent Anvil block decoding all succeeded. + obs["classification"] = "LIVE_NATIVE_STOPPED_OWNER_ANVIL_READBACK" + obs["live_guest_identity_independently_verified"] = True + obs["server_quiescence_proven"] = True + obs["native_guest_server_owned_bytes_proven"] = True + wait_job(call("app.start", [APP]), "G4 backend restart " + stage) + wait_app() + wait_status("g4-" + stage) + return obs + payload["native_backend_world"]["preaction"] = sample_owned_backend( + "preaction") if a.native_offline_player: from qualify_native_vanilla263_mineflayer_causal import run as run_player payload["native_player_gameplay"] = { @@ -818,6 +863,9 @@ def observe_collage_after_native_readiness(): "collage_managed_deployment": False, "physical_true_nas_apply": "HOLD", } + if a.native_backend_region: + payload["native_backend_world"]["candidate"] = sample_owned_backend( + "candidate") if a.native_world_rollback: from qualify_vanilla263_native_world_rollback import restore_owned_baseline_after_injected_fault try: @@ -835,6 +883,23 @@ def observe_collage_after_native_readiness(): stage="rollback", forward=a.native_player_forward, state_dir=a.out.parent, node_path=a.native_player_node_path) payload["native_player_gameplay"]["same_native_guest_client_causal_visibility"] = True + if a.native_backend_region: + payload["native_backend_world"]["rollback"] = sample_owned_backend( + "rollback") + stages = payload["native_backend_world"] + p0, p1, p2 = (stages[name] for name in + ("preaction", "candidate", "rollback")) + if len({p0["region_sha256"], p1["region_sha256"], + p2["region_sha256"]}) != 3: + raise MinecraftT6Error("G4 region state replay or noncausal stage") + if not all(p.get("live_guest_identity_independently_verified") + is True and p.get("server_quiescence_proven") is True + and p.get("native_guest_server_owned_bytes_proven") is True + for p in (p0, p1, p2)): + raise MinecraftT6Error("G4 missing independent backend checkpoints") + stages["classification"] = "LIVE_NATIVE_BACKEND_THREE_CHECKPOINTS" + stages["same_owned_guest_three_checkpoints"] = True + stages["backend_independent_world_oracle_pass"] = True read_sentinel() if first_level <= 0: raise MinecraftT6Error("world level.dat size invalid") diff --git a/tests/test_native_vanilla263_anvil_blocks.py b/tests/test_native_vanilla263_anvil_blocks.py index db7a4b659..299f5a25b 100644 --- a/tests/test_native_vanilla263_anvil_blocks.py +++ b/tests/test_native_vanilla263_anvil_blocks.py @@ -28,7 +28,7 @@ def compound(*children): def build(stage="candidate", *, x_pos=0, z_pos=0, overrides=None): positions = {((70 & 15) << 8) | (z << 4) | x: value for (x, z, value) in ( - (0, 0, 0), (2, 0, 1 if stage == "candidate" else 3), + (0, 0, 1 if stage == "preaction" else 0), (2, 0, 1 if stage == "candidate" else 3), (1, 1, 2 if stage == "candidate" else 4))} positions.update(overrides or {}) bits, per_long = max(4, math.ceil(math.log2(len(PALETTE)))), 16 @@ -77,7 +77,7 @@ def test_candidate_rollback_differ_source_only(self): self.assertEqual(result["physical_true_nas_apply"], "HOLD") def test_individual_stages_not_backend_proof(self): - for stage in ("candidate", "rollback"): + for stage in ("preaction", "candidate", "rollback"): result = inspect(build(stage), stage) self.assertTrue(result["block_state_expectations_matched"]) self.assertFalse(result["native_guest_server_owned_bytes_proven"]) @@ -89,6 +89,14 @@ def reject(self, blob, stage, code): inspect(blob, stage) self.assertEqual(str(ex.exception), code) + def test_clean_preaction_backend_fixture_must_be_absent_redstone(self): + control = inspect(build("preaction"), "preaction") + self.assertEqual(control["block_states"], [ + "minecraft:air", "minecraft:iron_bars", "minecraft:black_concrete"]) + core_idx = ((70 & 15) << 8) + self.reject(build("preaction", overrides={core_idx: 0}), "preaction", + "SERVER_BLOCK_CONSEQUENCE_ABSENT") + def test_wrong_stage_and_replayed_same_region(self): self.reject(build(), "rollback", "SERVER_BLOCK_CONSEQUENCE_ABSENT") self.reject(build(), "unknown", "STAGE_NOT_ADMITTED") diff --git a/tests/test_native_vanilla263_backend_three_checkpoint_receipt.py b/tests/test_native_vanilla263_backend_three_checkpoint_receipt.py new file mode 100644 index 000000000..55a19f348 --- /dev/null +++ b/tests/test_native_vanilla263_backend_three_checkpoint_receipt.py @@ -0,0 +1,133 @@ +"""G4 synthetic JSON adjudication: cannot infer live native world from packets.""" +from __future__ import annotations +import copy +import unittest + +from tests.test_native_vanilla263_player_guest_receipt import guest +from scripts.assert_vanilla263_native_t6_receipt import check +from scripts.admit_vanilla263_native_t6_control import APP + +BLOCKS = { + "preaction": ["minecraft:air", "minecraft:iron_bars", "minecraft:black_concrete"], + "candidate": ["minecraft:redstone_block", "minecraft:air", "minecraft:sea_lantern"], + "rollback": ["minecraft:redstone_block", "minecraft:iron_bars", "minecraft:black_concrete"], +} + + +def backend_fixture(): + root = guest() + p = root["foundry_materialization"] + p["native_backend_world"] = { + "schema": "supracraft.native-vanilla263-live-backend-region/1", + "classification": "LIVE_NATIVE_BACKEND_THREE_CHECKPOINTS", + "same_owned_guest_three_checkpoints": True, + "backend_independent_world_oracle_pass": True, + "collage_mutation_authorized": False, + "physical_true_nas_apply": "HOLD", + "preparation": { + "schema": "supracraft.native-vanilla263-mineflayer-causal/1", + "classification": "FIXED_PLAYER_FIXTURE_PREPARATION_ONLY", + "stage": "candidate-prep", + "client_protocol777_connected_and_spawned": True, + "client_fixture_prepared_without_core_placement": True, + "native_guest_backend_independent_oracle_qualified": False, + "collage_mutation_authorized": False, + "physical_true_nas_apply": "HOLD", + }, + } + for i, (stage, block_states) in enumerate(BLOCKS.items()): + p["native_backend_world"][stage] = { + "schema": "supracraft.native-vanilla263-server-owned-anvil/1", + "classification": "LIVE_NATIVE_STOPPED_OWNER_ANVIL_READBACK", + "stage": stage, "region_sha256": str(i+1)*64, + "coordinates": [[0,70,0],[2,70,0],[1,70,1]], + "block_states": block_states, "block_state_expectations_matched": True, + "requested_native_app": APP, + "requested_native_world": "rdtepool/minecraft-vanilla-263-t6-world", + "requested_native_world_file_fixed": True, + "read_only_middleware_calls": True, + "live_guest_identity_independently_verified": True, + "native_guest_server_owned_bytes_proven": True, + "server_quiescence_proven": True, + "raw_region_published": False, + "collage_mutation_authorized": False, + "physical_true_nas_apply": "HOLD", + "downloaded_file_size": 12288, + } + p["readiness"]["g4-" + stage] = copy.deepcopy(p["readiness"]["reinstall"]) + return root + + +def admit(root, backend=True): + return check(root, require_collage=True, + require_lantern_staged=True, require_native_rollback=True, + require_native_client_visibility=True, + require_native_backend_region=backend) + + +class ThreeCheckpointReceiptTests(unittest.TestCase): + def test_synthetic_shape_and_falsely_upgraded_015_refused(self): + admit(backend_fixture()) # mocked JSON validation, NOT live acceptance + old = guest() + admit(old, backend=False) # 015 client-only acceptance preserved + with self.assertRaises(ValueError): + admit(old) + + def test_backend_requires_all_prior_gates(self): + with self.assertRaisesRegex(ValueError, "backend world region requires G2/G3"): + check(backend_fixture(), require_native_backend_region=True) + + def test_no_forged_guest_identity_or_server_owned_file(self): + for stage in BLOCKS: + for field, bad in ( + ("block_states", ["minecraft:redstone_block"]*3), + ("stage", "rollback" if stage != "rollback" else "candidate"), + ("region_sha256", "nothex"), + ("coordinates", [[100,70,0]]), + ("requested_native_world", "foreign-world"), + ("requested_native_app", "foreign"), + ("live_guest_identity_independently_verified", False), + ("native_guest_server_owned_bytes_proven", False), + ("server_quiescence_proven", False), + ("raw_region_published", True), + ("read_only_middleware_calls", False), + ("downloaded_file_size", 0), + ("collage_mutation_authorized", True), + ): + with self.subTest(stage=stage, field=field): + doc=backend_fixture() + doc["foundry_materialization"]["native_backend_world"][stage][field]=bad + with self.assertRaises(ValueError): + admit(doc) + + def test_replay_and_absent_clean_control_refused(self): + doc=backend_fixture() + proof=doc["foundry_materialization"]["native_backend_world"] + proof["rollback"]["region_sha256"]=proof["candidate"]["region_sha256"] + with self.assertRaisesRegex(ValueError, "hash replay"): + admit(doc) + doc=backend_fixture() + del doc["foundry_materialization"]["native_backend_world"]["preaction"] + with self.assertRaises(ValueError): admit(doc) + doc=backend_fixture() + del doc["foundry_materialization"]["readiness"]["g4-candidate"] + with self.assertRaisesRegex(ValueError,"restart/health missing"): admit(doc) + + def test_actor_authority_does_not_inherit_backend_pass(self): + doc=backend_fixture() + doc["foundry_materialization"]["native_backend_world"]["physical_true_nas_apply"]="READY" + with self.assertRaises(ValueError):admit(doc) + doc=backend_fixture() + doc["foundry_materialization"]["native_backend_world"]["collage_mutation_authorized"]=True + with self.assertRaises(ValueError):admit(doc) + + def test_preparation_must_have_no_claimed_backend_proof(self): + for field, bad in (("stage","rollback"),("client_fixture_prepared_without_core_placement",False), + ("native_guest_backend_independent_oracle_qualified", True)): + doc=backend_fixture() + doc["foundry_materialization"]["native_backend_world"]["preparation"][field]=bad + with self.assertRaises(ValueError):admit(doc) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_native_vanilla263_mineflayer_causal.py b/tests/test_native_vanilla263_mineflayer_causal.py index 6aee3c497..fd096a3d0 100644 --- a/tests/test_native_vanilla263_mineflayer_causal.py +++ b/tests/test_native_vanilla263_mineflayer_causal.py @@ -29,7 +29,7 @@ def good(stage): class ClientSourceTests(unittest.TestCase): def test_candidate_and_rollback_effects_are_separate_and_no_authority(self): - for stage in target.STAGES: + for stage in ("candidate", "rollback"): receipt=target.validate(good(stage),stage) self.assertEqual(receipt["classification"],"CLIENT_CAUSAL_VISIBILITY_ONLY") self.assertFalse(receipt["native_guest_backend_independent_oracle_qualified"]) @@ -40,6 +40,22 @@ def test_candidate_and_rollback_effects_are_separate_and_no_authority(self): self.assertEqual(receipt["client_saw_expected_no_datapack_effect"], stage=="rollback") + def test_preaction_stages_only_prepare_client_fixture(self): + pre=good("candidate-prep") + pre.update(classification="CLIENT_FIXTURE_PREPARED", + operation="mineflayer.chat.fixedFixture", + placed=False, expected_datapack_present=True) + result=target.validate(pre,"candidate-prep") + self.assertEqual(result["classification"],"FIXED_PLAYER_FIXTURE_PREPARATION_ONLY") + self.assertTrue(result["client_fixture_prepared_without_core_placement"]) + self.assertFalse(result["native_guest_backend_independent_oracle_qualified"]) + for field,value in (("placed",True),("operation","mineflayer.placeBlock"), + ("backend_independent_oracle_qualified",True), + ("expected_datapack_present",False),("rcon_used",True)): + bad=dict(pre, **{field:value}) + with self.subTest(field=field),self.assertRaises(target.Refused): + target.validate(bad,"candidate-prep") + def test_forged_success_protocol_bot_gameplay_and_privilege_are_refused(self): for field,value in ( ("classification","SOURCE_ONLY"),("schema","untrusted"), diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 1f1d4b75b..bf36ef425 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -152,6 +152,30 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se assert_path=(ROOT/"scripts/assert_vanilla263_native_t6_receipt.py").read_text() self.assertIn("native_guest_backend_independent_oracle_qualified",assert_path) + def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): + request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) + self.assertNotIn("native_backend_region", request) + self.assertEqual(request["request_id"], + "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") + for needle in ("truenas_native_backend_region=false", + "(.native_backend_region // false)", + "truenas_native_backend_region=$truenas_native_backend_region", + "--vanilla-backend-region", "--require-native-backend-region"): + self.assertIn(needle, WORKFLOW) + for needle in ("VANILLA_BACKEND_REGION=false", + "--vanilla-backend-region", + '--native-backend-region', + '"$VANILLA_BACKEND_REGION" == "true"'): + self.assertTrue(needle in HARNESS or + needle in (ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text(), + needle) + probe=(ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text() + for stage in ("preaction","candidate","rollback"): + self.assertIn('native_backend_world"]["'+stage+'"]', probe) + self.assertIn("backend_independent_world_oracle_pass", probe) + self.assertIn("owned_config(cfg, desired)", probe) + self.assertNotIn("native_backend_region", str(request)) + def test_player_dependencies_are_installed_in_native_guest_job(self): top, native = WORKFLOW.split(" truenas-26-beta3:", 1) setup = "Set up immutable 26.3 experimental client toolchain for native G4" From 6c6e40e34fe15a52d73ec6cf9fd0d7c385a2db5a Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:11:54 +0200 Subject: [PATCH 706/719] test(OP4I-016): admit one fresh native Anvil backend three-checkpoint guest * admit candidate OP4I-016: distinct one-shot three-checkpoint backend native request, unmerged * test(OP4I-016): bind exactly one immutable fresh backend-world request ID * test(OP4I-016): bind exactly one immutable fresh backend-world request ID --- config/truenas-rdte-run-request.json | 7 ++++--- tests/test_truenas_rdte_target.py | 5 +++-- tests/test_vanilla263_t6_guest_routing.py | 14 +++++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 857dd27af..86ec5b93d 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,14 +1,15 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4H-015: fresh serialized disposable TrueNAS25.04.2.6 native Minecraft26.3 Java25/777 Mineflayer client causal rep after 014 offline OPS synthetic username made native Minecraft App fail healthy startup; source-only repair PR #468 pins Java offline UUIDv3 f4469b77-cc14-3c07-95f3-0b0c3f151bd2 for exact QEMU-only bot SupraCraftProbe. Same signed official Minecraft App1.15.30 Foundry OCI, exact 739-byte Lantern SHA256, TLS Go scoped APPS_READ G2, owned ZFS snapshot/fault rollback G3. New safe bounded startup state/count census without raw logs, never infer provider secrets. QEMU usernet ephemeral host 127.0.0.1 to guest25580, no external ingress, one synthetic offline OP with RCON disabled, no production online identity or permissions. Test actual Mineflayer core placement candidate positive and same action postrollback negative in SAME guest/App, retain owned cleanup. Client visual packet oracle not independent backend-world proof; managed COLLAGE mutation, VIP/Microsoft auth, full Two Rivers and physical TrueNAS APPLY held. One-use 015; never replay 013/014/015 or overlap guests.", + "purpose": "OP4I-016 one-shot real disposable TrueNAS25.04.2.6 native community Minecraft App1.15.30 Java25 vanilla26.3/protocol777 authenticated guest DDP G4 backend-world cause test. EXACT signed immutable Foundry pinned itzg Java25 image and original 739-byte Lantern ZIP (SHA256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8), APPS_READ TLS Go six marker owner, fixed owned ZFS snapshot/release-fault rollback. Same runner-only QEMU 127.0.0.1 ephemeral port synthetic offline UUIDv3 OP Mineflayer, RCON disabled, canonical operational online authentication remains true. New three stopped-App read-only fixed r.0.0.mca region checkpoints: clean preaction fixture empty core, candidate redstone/sea lantern opened gate, rollback same action with iron bars/black concrete. Independent server-owned Anvil decoder fixed protected coordinates, source/receipt verifier exact hashes and negative/cached/foreign/replay/clean owned final cleanup. Distinct request 016 consumed once; do not replay 011-015, no other concurrent heavyweight guest. Does not authorize COLLAGE APPLY, real Mojang/stock/VIP, full Two Rivers, operations or physical TrueNAS.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, - "native_offline_player": true + "native_offline_player": true, + "native_backend_region": true } diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 17d18a49b..bb3df0ec4 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,15 +92,16 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") + "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) self.assertIs(request.get("native_world_rollback"), True) + self.assertIs(request.get("native_backend_region"), True) self.assertEqual(set(request), {"schema", "request_id", "version", "rung", "product", "authority_issue", "consumer_authority_issue", "purpose", "collage_readback", "lantern_stage", - "native_world_rollback", "native_offline_player"}) + "native_world_rollback", "native_offline_player", "native_backend_region"}) if request.get("product") == "wow-sidecar": self.assertEqual(request.get("consumer_authority_issue"), 276) if request.get("product") == "garm": diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index bf36ef425..775687ac1 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,7 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +77,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -112,7 +112,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") + "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -132,7 +132,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") + "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) @@ -154,9 +154,9 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) - self.assertNotIn("native_backend_region", request) + self.assertIs(request.get("native_backend_region"), True) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4h-015-offline-uuid-player") + "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") for needle in ("truenas_native_backend_region=false", "(.native_backend_region // false)", "truenas_native_backend_region=$truenas_native_backend_region", @@ -174,7 +174,7 @@ def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): self.assertIn('native_backend_world"]["'+stage+'"]', probe) self.assertIn("backend_independent_world_oracle_pass", probe) self.assertIn("owned_config(cfg, desired)", probe) - self.assertNotIn("native_backend_region", str(request)) + self.assertIn("native_backend_region", str(request)) def test_player_dependencies_are_installed_in_native_guest_job(self): top, native = WORKFLOW.split(" truenas-26-beta3:", 1) From c58e8edc435c48830d491856e42541a581fdb98b Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:38:43 +0200 Subject: [PATCH 707/719] fix(G4): bounded phase census for failed native Anvil readback (no guest replay) * feat(G4): emit fixed allowlisted readback phase codes without provider payload * feat(G4): fail-closed checkpoint phase census through native stop owner stat/download/restart * test(G4): differentiate stopped-owner stat/download/job/HTTP faults using only fixed phase codes --- ..._native_vanilla263_anvil_guest_readback.py | 29 +++++++- .../truenas_middleware_minecraft_t6_probe.py | 40 ++++++++++- ..._native_vanilla263_anvil_guest_readback.py | 72 ++++++++++++++++++- 3 files changed, 138 insertions(+), 3 deletions(-) diff --git a/scripts/qualify_native_vanilla263_anvil_guest_readback.py b/scripts/qualify_native_vanilla263_anvil_guest_readback.py index 215cff1f0..ddeb4ce48 100644 --- a/scripts/qualify_native_vanilla263_anvil_guest_readback.py +++ b/scripts/qualify_native_vanilla263_anvil_guest_readback.py @@ -21,9 +21,24 @@ PATH = WORLD + "/region/r.0.0.mca" STAGES = ("preaction", "candidate", "rollback") +# Only these literal, non-sensitive phases may escape as a public diagnostic. +PHASES = ( + "app_state_query", "app_stopped_verified", + "file_stat", "file_stat_verified", + "download_request", "download_route_verified", + "download_job", "download_job_completed", + "bytes_fetch", "bytes_verified", "anvil_decode", "anvil_verified", +) -def capture(*, call, wait_job, fetch_bytes, stage: str) -> dict: +def record_phase(stage: str, phase: str, reporter=None) -> None: + require(stage in STAGES and phase in PHASES, "BACKEND_PHASE_CODE_INVALID") + if reporter is not None: + reporter(stage + "." + phase) + + + +def capture(*, call, wait_job, fetch_bytes, stage: str, on_phase=None) -> dict: """Only after existing *actual* guest runner verified owned App then stop. The runner MUST independently bind the current authenticated DDP session, @@ -31,11 +46,16 @@ def capture(*, call, wait_job, fetch_bytes, stage: str) -> dict: It must not publish the raw Anvil region, signed session or download URL. """ require(stage in STAGES, "BACKEND_STAGE_NOT_ADMITTED") + record_phase(stage, "app_state_query", on_phase) require(app_state(call) == "STOPPED", "BACKEND_QUIESCENCE_NOT_OBSERVED") + record_phase(stage, "app_stopped_verified", on_phase) + record_phase(stage, "file_stat", on_phase) stat = call("filesystem.stat", [PATH]) require(type(stat) is dict and stat.get("type") == "FILE" and type(stat.get("size")) is int and 8192 <= stat["size"] <= MAX_REGION, "BACKEND_REGION_FILE_NOT_EXACT") + record_phase(stage, "file_stat_verified", on_phase) + record_phase(stage, "download_request", on_phase) row = call("core.download", [ "filesystem.get", [PATH], "g4-owned-region-" + stage + ".mca", True]) require(type(row) is list and len(row) == 2 and @@ -45,11 +65,18 @@ def capture(*, call, wait_job, fetch_bytes, stage: str) -> dict: len(row[1]) <= 300 and ".." not in row[1] and "?" not in row[1] and "#" not in row[1], "BACKEND_DOWNLOAD_ROUTE_INVALID") + record_phase(stage, "download_route_verified", on_phase) + record_phase(stage, "download_job", on_phase) wait_job(row[0], "G4 read-only owned Anvil file download") + record_phase(stage, "download_job_completed", on_phase) + record_phase(stage, "bytes_fetch", on_phase) blob = fetch_bytes(row[1]) require(type(blob) is bytes and len(blob) == stat["size"], "BACKEND_DOWNLOADED_FILE_DRIFT") + record_phase(stage, "bytes_verified", on_phase) + record_phase(stage, "anvil_decode", on_phase) result = inspect(blob, stage) + record_phase(stage, "anvil_verified", on_phase) # The host transfer API is separate from independent receipt admission: # this module cannot prove the caller ran under authorized guest identity. result.update({ diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index ee603093e..4eb2da897 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -822,6 +822,36 @@ def observe_collage_after_native_readiness(): "physical_true_nas_apply": "HOLD", "collage_mutation_authorized": False, } + # All emitted codes are fixed source literals, never DDP errors. + def g4_phase(code): + if code not in { + "fixture_preparation", "preaction.stop", "candidate.stop", + "rollback.stop", "preaction.stopped", "candidate.stopped", + "rollback.stopped", "preaction.owner_query", + "candidate.owner_query", "rollback.owner_query", + "preaction.owner_verified", "candidate.owner_verified", + "rollback.owner_verified", "preaction.version_query", + "candidate.version_query", "rollback.version_query", + "preaction.version_verified", "candidate.version_verified", + "rollback.version_verified", "preaction.restart", + "candidate.restart", "rollback.restart", + "preaction.restarted", "candidate.restarted", + "rollback.restarted", + } and code not in { + st + "." + ph + for st in ("preaction", "candidate", "rollback") + for ph in ( + "app_state_query", "app_stopped_verified", + "file_stat", "file_stat_verified", + "download_request", "download_route_verified", + "download_job", "download_job_completed", + "bytes_fetch", "bytes_verified", + "anvil_decode", "anvil_verified", + ) + }: + raise MinecraftT6Error("G4 phase code not admitted") + payload["native_backend_world"]["last_backend_phase"] = code + g4_phase("fixture_preparation") payload["native_backend_world"]["preparation"] = run_player( stage="candidate-prep", forward=a.native_player_forward, state_dir=a.out.parent, node_path=a.native_player_node_path) @@ -829,16 +859,22 @@ def sample_owned_backend(stage): # A separate native App stop after the actual client action # flushes server-owned region files. Never decode packets as # if they were an authoritative backend world state. + g4_phase(stage + ".stop") wait_job(call("app.stop", [APP]), "G4 backend quiesce " + stage) + g4_phase(stage + ".stopped") + g4_phase(stage + ".owner_query") cfg = call("app.config", [APP]) if not owned_config(cfg, desired): raise MinecraftT6Error("G4 region owner drift") + g4_phase(stage + ".owner_verified") + g4_phase(stage + ".version_query") if call("system.version", []) != a.expected_system_version: raise MinecraftT6Error("G4 region target drift") + g4_phase(stage + ".version_verified") obs = capture(call=call, wait_job=wait_job, fetch_bytes=lambda p: middleware_http_bytes( a.host, a.port, a.tls, p, timeout=a.timeout), - stage=stage) + stage=stage, on_phase=g4_phase) # Upgraded only in this signed, actual guest runner after # real DDP owner/session, stop, bounded fixed-path download # and independent Anvil block decoding all succeeded. @@ -846,9 +882,11 @@ def sample_owned_backend(stage): obs["live_guest_identity_independently_verified"] = True obs["server_quiescence_proven"] = True obs["native_guest_server_owned_bytes_proven"] = True + g4_phase(stage + ".restart") wait_job(call("app.start", [APP]), "G4 backend restart " + stage) wait_app() wait_status("g4-" + stage) + g4_phase(stage + ".restarted") return obs payload["native_backend_world"]["preaction"] = sample_owned_backend( "preaction") diff --git a/tests/test_native_vanilla263_anvil_guest_readback.py b/tests/test_native_vanilla263_anvil_guest_readback.py index 46f31604e..e36ded493 100644 --- a/tests/test_native_vanilla263_anvil_guest_readback.py +++ b/tests/test_native_vanilla263_anvil_guest_readback.py @@ -3,7 +3,7 @@ import unittest -from scripts.qualify_native_vanilla263_anvil_guest_readback import capture, PATH +from scripts.qualify_native_vanilla263_anvil_guest_readback import capture, PATH, record_phase from scripts.qualify_native_vanilla263_anvil_blocks import Refused from scripts.qualify_vanilla263_native_world_rollback import APP from tests.test_native_vanilla263_anvil_blocks import build @@ -96,5 +96,75 @@ def test_refuse_wrong_effect_inside_download(self): self.reject("SERVER_BLOCK_CONSEQUENCE_ABSENT") + def test_phase_census_is_literal_and_contains_no_provider_values(self): + seen = [] + got = capture(call=self.call, wait_job=self.wait, fetch_bytes=self.fetch, + stage="candidate", on_phase=seen.append) + self.assertTrue(got["block_state_expectations_matched"]) + self.assertEqual(seen, [ + "candidate.app_state_query", + "candidate.app_stopped_verified", + "candidate.file_stat", + "candidate.file_stat_verified", + "candidate.download_request", + "candidate.download_route_verified", + "candidate.download_job", + "candidate.download_job_completed", + "candidate.bytes_fetch", + "candidate.bytes_verified", + "candidate.anvil_decode", + "candidate.anvil_verified", + ]) + self.assertNotIn("/mnt/", str(seen)) + self.assertNotIn("/_download/", str(seen)) + self.assertNotIn("credential", str(seen)) + + def test_native_rpc_failure_identifies_operation_not_exception(self): + for fail_method, last_phase in ( + ("app.query", "candidate.app_state_query"), + ("filesystem.stat", "candidate.file_stat"), + ("core.download", "candidate.download_request"), + ): + with self.subTest(fail_method=fail_method): + previous = self.call + phases = [] + def injected(method, args): + if method == fail_method: + raise RuntimeError("secret-password provider-path /mnt/private") + return previous(method, args) + with self.assertRaises(RuntimeError): + capture(call=injected, wait_job=self.wait, fetch_bytes=self.fetch, + stage="candidate", on_phase=phases.append) + self.assertEqual(phases[-1], last_phase) + self.assertNotIn("secret-password", str(phases)) + self.assertNotIn("/mnt/private", str(phases)) + + def test_download_job_and_http_failures_are_distinct(self): + cases = ( + (lambda job, label: (_ for _ in ()).throw( + RuntimeError("untrusted job exception")), self.fetch, + "candidate.download_job"), + (self.wait, lambda token: (_ for _ in ()).throw( + RuntimeError("untrusted URL exception")), "candidate.bytes_fetch"), + ) + for wait, fetch, wanted in cases: + with self.subTest(wanted=wanted): + phases = [] + with self.assertRaises(RuntimeError): + capture(call=self.call, wait_job=wait, fetch_bytes=fetch, + stage="candidate", on_phase=phases.append) + self.assertEqual(phases[-1], wanted) + self.assertNotIn("untrusted", str(phases)) + + def test_unknown_phase_and_stage_not_emitted(self): + phases = [] + with self.assertRaisesRegex(Refused, "BACKEND_PHASE_CODE_INVALID"): + record_phase("candidate", "secret=PASSWORD", phases.append) + with self.assertRaisesRegex(Refused, "BACKEND_PHASE_CODE_INVALID"): + record_phase("foreign", "download_request", phases.append) + self.assertEqual(phases, []) + + + if __name__ == "__main__": unittest.main() From 583a7377bbb35a3e1d9fae23b6f08900f82c1955 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:44:14 +0200 Subject: [PATCH 708/719] feat(G4): qualify exact 1.15.31 Foundry signed source and native consumer (no guest) * feat(vanilla): stage exact 1.15.31 public Foundry source/consumer boundary; digest pending main receipt * ci(vanilla): exact 1.15.31 Foundry merged checkout/render/export for immutable consumer * test(vanilla): 1.15.31 producer-facing immutable SHA-bound source fixtures * feat(vanilla): exact 1.15.31 native catalog profile isolated from legacy Paper 1.15.30 * verify(vanilla): require new exact 1.15.31 native guest shape without promoting historical 015 * feat(vanilla): select new native App1.15.31 only under exact disposable vanilla263 shape * fix(vanilla): pin independently verified exact Foundry 11531 control digest at 5ee204df * ci(vanilla): assert 1.15.31 merged-main immutable control digest * ci(vanilla): call exact 1.15.31 Foundry exporter; preserve Paper 1.15.30 gate * test(vanilla): migrate only pinned exact 1.15.31 producer and guest fixture negatives * test(vanilla): migrate only pinned exact 1.15.31 producer and guest fixture negatives * test(vanilla): migrate only pinned exact 1.15.31 producer and guest fixture negatives * fix(native G4): tie runner-caller Foundry revision to verified 1.15.31 producer * qualify(vanilla): independently admit exact native App 1.15.31 signed source only * fix(regression): preserve independent COLLAGE Paper 1.15.30 native profile * fix(source): assert 016 immutable consumed identity, not stale old Paper product * ci(vanilla): source-only producer/consumer receipt exactly new 1.15.31 merged Foundry --- .github/workflows/gha-kvm-system-rdte.yml | 18 ++++++++++++------ .../qualify-vanilla263-native-admission.yml | 3 ++- ...lify-vanilla263-native-control-consumer.yml | 14 +++++++------- .../vanilla263-t6-source-admission.yml | 12 ++++++------ scripts/admit_vanilla263_native_t6_control.py | 14 +++++++------- scripts/assert_vanilla263_native_t6_receipt.py | 2 +- scripts/gha_kvm_truenas_rdte.sh | 4 ++-- scripts/qualify_vanilla263_native_admission.py | 6 +++--- .../truenas_middleware_minecraft_t6_probe.py | 4 ++-- tests/test_truenas_t6_vanilla263_guest.py | 4 ++-- tests/test_vanilla263_native_admission.py | 6 +++--- tests/test_vanilla263_t6_guest_routing.py | 10 +++++----- tests/test_vanilla263_t6_source_admission.py | 4 ++-- 13 files changed, 54 insertions(+), 47 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index 1fb2ef636..daba0c616 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -337,9 +337,9 @@ jobs: foundry-minecraft-vanilla263-t6-control: needs: [changes, contract] if: needs.contract.result == 'success' && needs.changes.outputs.truenas == 'true' && needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_version == '25.04.2.6' - uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-vanilla-263-t6-control.yml@29ec6cba7df51e316b0329926aa85807805dd07b + uses: SemperSupra/truenas-app-foundry/.github/workflows/export-minecraft-vanilla-263-11531-t6-control.yml@5ee204df205edfd531304441fd1a6a24004557b1 with: - foundry_ref: 29ec6cba7df51e316b0329926aa85807805dd07b + foundry_ref: 5ee204df205edfd531304441fd1a6a24004557b1 target_version: "25.04.2.6" permissions: contents: read @@ -579,7 +579,7 @@ jobs: if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: - name: minecraft-vanilla263-java25-truenas-250426-t6-source-candidate + name: minecraft-vanilla263-truenas-250426-11531-signed-source-candidate path: ${{ runner.temp }}/gha-kvm-system-rdte/foundry-control - name: Independently verify immutable native vanilla control pre-KVM if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' @@ -744,12 +744,18 @@ jobs: with: name: garm-provider-truenas-container-pre-b4-fixture path: ${{ runner.temp }}/gha-kvm-system-rdte/container-pre-b4-fixture - - name: Refuse floating upstream Minecraft catalog before heavyweight KVM - if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' || needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' + - name: Refuse floating historical COLLAGE Paper 1.15.30 catalog before KVM + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'collage-native-paper' run: | set -euo pipefail python3 -m unittest -v tests.test_collage_catalog_preflight python3 scripts/check_collage_catalog_preflight.py --expected 1.15.30 + - name: Refuse retired native vanilla 1.15.30; require current pinned 1.15.31 + if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' + run: | + set -euo pipefail + python3 -m unittest -v tests.test_collage_catalog_preflight + python3 scripts/check_collage_catalog_preflight.py --expected 1.15.31 - name: Set up immutable 26.3 experimental client toolchain for native G4 if: needs.changes.outputs.truenas_native_offline_player == 'true' @@ -830,7 +836,7 @@ jobs: elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "collage-native-paper" ]]; then foundry_commit="403321031b6d4e66aa64a8299fd7621eee290b0d" elif [[ "${{ needs.changes.outputs.truenas_t6_product }}" == "minecraft-vanilla-263" ]]; then - foundry_commit="29ec6cba7df51e316b0329926aa85807805dd07b" + foundry_commit="5ee204df205edfd531304441fd1a6a24004557b1" else foundry_commit="4ba12f4a870f9af8a667056f1e2cc32f80f8e2ba" fi diff --git a/.github/workflows/qualify-vanilla263-native-admission.yml b/.github/workflows/qualify-vanilla263-native-admission.yml index c074696c8..6d4658523 100644 --- a/.github/workflows/qualify-vanilla263-native-admission.yml +++ b/.github/workflows/qualify-vanilla263-native-admission.yml @@ -30,7 +30,8 @@ jobs: req=json.loads(Path("config/truenas-vanilla263-native-admission-request.json").read_text()) old=json.loads(Path("config/truenas-rdte-run-request.json").read_text()) assert req["request_id"]!=old["request_id"] - assert req["product"]=="minecraft-vanilla-263" and old["product"]!="minecraft-vanilla-263" + assert req["product"]=="minecraft-vanilla-263" and old["product"]=="minecraft-vanilla-263" + assert old["request_id"]=="tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint" assert req["consumer_authority_issue"]==627 assert req["version"]=="25.04.2.6" PY diff --git a/.github/workflows/qualify-vanilla263-native-control-consumer.yml b/.github/workflows/qualify-vanilla263-native-control-consumer.yml index b4ac8dcca..a23e1878f 100644 --- a/.github/workflows/qualify-vanilla263-native-control-consumer.yml +++ b/.github/workflows/qualify-vanilla263-native-control-consumer.yml @@ -27,16 +27,16 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: repository: SemperSupra/truenas-app-foundry - ref: 29ec6cba7df51e316b0329926aa85807805dd07b + ref: 5ee204df205edfd531304441fd1a6a24004557b1 path: .deps/vanilla263-foundry persist-credentials: false - name: Source admission and exact producer-head negative gates run: | set -euo pipefail - test "$(git -C .deps/vanilla263-foundry rev-parse HEAD)" = 29ec6cba7df51e316b0329926aa85807805dd07b + test "$(git -C .deps/vanilla263-foundry rev-parse HEAD)" = 5ee204df205edfd531304441fd1a6a24004557b1 python3 -m unittest discover -s tests -p 'test_vanilla263_native_admission.py' -v python3 -m unittest discover -s .deps/vanilla263-foundry/tests \ - -p 'test_export_minecraft_vanilla_263_t6_control.py' -v + -p 'test_export_minecraft_vanilla_263_11531_t6_control.py' -v python3 -m unittest discover -s .deps/vanilla263-foundry/tests \ -p 'test_vanilla_263_native_source.py' -v docker compose version @@ -45,14 +45,14 @@ jobs: set -euo pipefail mkdir -p "$RUNNER_TEMP/vanilla263" python3 .deps/vanilla263-foundry/tools/validate_minecraft_world_control.py \ - --product vanilla-263 --catalog-profile 1.15.30 \ + --product vanilla-263 --catalog-profile 1.15.31 \ --target-version 25.04.2.6 \ > "$RUNNER_TEMP/vanilla263/merged-source.json" - name: Produce new signed-by-digest merged-commit control run: | set -euo pipefail - python3 .deps/vanilla263-foundry/tools/export_minecraft_vanilla_263_t6_control.py \ - --foundry-ref 29ec6cba7df51e316b0329926aa85807805dd07b \ + python3 .deps/vanilla263-foundry/tools/export_minecraft_vanilla_263_11531_t6_control.py \ + --foundry-ref 5ee204df205edfd531304441fd1a6a24004557b1 \ --target-version 25.04.2.6 \ --source-evidence "$RUNNER_TEMP/vanilla263/merged-source.json" \ --out "$RUNNER_TEMP/vanilla263/control.json" @@ -71,7 +71,7 @@ jobs: raw=dict(c) claimed=raw.pop("control_sha256") assert claimed == hashlib.sha256(json.dumps(raw,sort_keys=True,separators=(",",":")).encode()).hexdigest() - assert c["foundry_ref"] == "29ec6cba7df51e316b0329926aa85807805dd07b" + assert c["foundry_ref"] == "5ee204df205edfd531304441fd1a6a24004557b1" assert a["classification"] == "SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED" assert a["control_sha256"] == claimed assert a["foundry_ref"] == c["foundry_ref"] diff --git a/.github/workflows/vanilla263-t6-source-admission.yml b/.github/workflows/vanilla263-t6-source-admission.yml index 3b368ea5b..e87d7a2f3 100644 --- a/.github/workflows/vanilla263-t6-source-admission.yml +++ b/.github/workflows/vanilla263-t6-source-admission.yml @@ -22,27 +22,27 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: repository: SemperSupra/truenas-app-foundry - ref: 29ec6cba7df51e316b0329926aa85807805dd07b + ref: 5ee204df205edfd531304441fd1a6a24004557b1 path: .deps/vanilla-foundry persist-credentials: false - name: Validate source and test fixtures before render run: | set -euo pipefail - test "$(git -C .deps/vanilla-foundry rev-parse HEAD)" = 29ec6cba7df51e316b0329926aa85807805dd07b + test "$(git -C .deps/vanilla-foundry rev-parse HEAD)" = 5ee204df205edfd531304441fd1a6a24004557b1 python3 -m pip install --disable-pip-version-check 'PyYAML==6.0.2' python3 -m py_compile scripts/admit_vanilla263_native_t6_control.py tests/test_vanilla263_t6_source_admission.py - name: Independently source render exact native community catalog run: | set -euo pipefail python3 .deps/vanilla-foundry/tools/validate_minecraft_world_control.py \ - --product vanilla-263 --catalog-profile 1.15.30 \ + --product vanilla-263 --catalog-profile 1.15.31 \ --target-version 25.04.2.6 > "$RUNNER_TEMP/vanilla-source.json" - name: Produce signed source control at accepted merged Foundry ref run: | set -euo pipefail mkdir -p "$RUNNER_TEMP/vanilla-control" - python3 .deps/vanilla-foundry/tools/export_minecraft_vanilla_263_t6_control.py \ - --foundry-ref 29ec6cba7df51e316b0329926aa85807805dd07b \ + python3 .deps/vanilla-foundry/tools/export_minecraft_vanilla_263_11531_t6_control.py \ + --foundry-ref 5ee204df205edfd531304441fd1a6a24004557b1 \ --target-version 25.04.2.6 \ --source-evidence "$RUNNER_TEMP/vanilla-source.json" \ --out "$RUNNER_TEMP/vanilla-control/control.json" @@ -59,7 +59,7 @@ jobs: r=json.load(open(sys.argv[1])) assert r["schema"]=="semper-supra.minecraft-vanilla263-t6-consumer-admission/1" assert r["classification"]=="SOURCE_CONTROL_READY_GUEST_NOT_DISPATCHED" - assert r["control_sha256"]=="c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615" + assert r["control_sha256"]=="59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5" assert r["minecraft_version"]=="26.3" and r["minecraft_protocol"]==777 assert r["java_version"]==25 and r["truenas_version"]=="25.04.2.6" assert r["native_guest_created"] is False and r["native_guest_qualified"] is False diff --git a/scripts/admit_vanilla263_native_t6_control.py b/scripts/admit_vanilla263_native_t6_control.py index 84f396103..50d0019df 100644 --- a/scripts/admit_vanilla263_native_t6_control.py +++ b/scripts/admit_vanilla263_native_t6_control.py @@ -14,9 +14,9 @@ import sys SCHEMA = "semper-supra.minecraft-vanilla-263-truenas-t6-control/1" -PRODUCER = "29ec6cba7df51e316b0329926aa85807805dd07b" -CONTROL_SHA = "c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615" -APPS_REF = "559b9fe85212c800bc6770f152a65211df137a44" +PRODUCER = "5ee204df205edfd531304441fd1a6a24004557b1" +CONTROL_SHA = "59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5" +APPS_REF = "95b0cc78e75fcd4b128f4ef2f8428034d981f7c2" OCI = ("ghcr.io/itzg/minecraft-server:2026.10.0-java25@sha256:" "5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da") TARGET = "25.04.2.6" @@ -75,8 +75,8 @@ def verify(control: dict, *, expected_producer: str = PRODUCER, require(type(source) is dict and source.get("apps_commit") == APPS_REF and source.get("catalog_app") == "community/minecraft" and - source.get("catalog_version") == "1.15.30" and - source.get("library_version") == "2.3.4" and + source.get("catalog_version") == "1.15.31" and + source.get("library_version") == "2.3.16" and source.get("image_reference") == OCI and isinstance(source.get("source_compose_sha256"), str) and re.fullmatch("[0-9a-f]{64}", source["source_compose_sha256"]) and @@ -88,7 +88,7 @@ def verify(control: dict, *, expected_producer: str = PRODUCER, runtime.get("app_name") == APP and runtime.get("catalog_app") == "minecraft" and runtime.get("train") == "community" and - runtime.get("catalog_version_candidate") == "1.15.30" and + runtime.get("catalog_version_candidate") == "1.15.31" and runtime.get("minecraft_version") == "26.3" and runtime.get("minecraft_protocol") == 777 and runtime.get("java_version") == 25 and @@ -148,7 +148,7 @@ def verify(control: dict, *, expected_producer: str = PRODUCER, "foundry_ref": expected_producer, "control_sha256": expected_digest, "truenas_version": TARGET, - "catalog_version": "1.15.30", + "catalog_version": "1.15.31", "minecraft_version": "26.3", "minecraft_protocol": 777, "java_version": 25, diff --git a/scripts/assert_vanilla263_native_t6_receipt.py b/scripts/assert_vanilla263_native_t6_receipt.py index 78557c970..309dc88a1 100644 --- a/scripts/assert_vanilla263_native_t6_receipt.py +++ b/scripts/assert_vanilla263_native_t6_receipt.py @@ -48,7 +48,7 @@ def need(condition: bool, reason: str) -> None: need(type(shape) is dict and shape.get("name") == APP and shape.get("catalog_app") == "minecraft" and - shape.get("catalog_version_candidate") == "1.15.30" and + shape.get("catalog_version_candidate") == "1.15.31" and shape.get("minecraft_version") == "26.3" and shape.get("minecraft_protocol") == 777, "wrong native workload shape") diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index bb91bea9b..7ea22ed99 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -277,7 +277,7 @@ payload = { if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "official-catalog" else {"name": "rdte-t6-foliorelay", "images": foliorelay_images} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "foliorelay" - else {"name": "rdte-t6-minecraft-vanilla-263", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.30", "minecraft_version": "26.3", "minecraft_protocol": 777} + else {"name": "rdte-t6-minecraft-vanilla-263", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.31", "minecraft_version": "26.3", "minecraft_protocol": 777} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") == "minecraft-vanilla-263" else {"name": "rdte-t6-minecraft-paper", "catalog_app": "minecraft", "catalog_version_candidate": "1.15.29", "paper_version": "1.21.11"} if os.environ.get("R_RUNG") == "t6" and os.environ.get("R_T6_PRODUCT") in {"minecraft-paper", "collage-native-paper"} @@ -1256,7 +1256,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then NATIVE_PLAYER_ARGS+=(--native-backend-region) fi python3 "$SCRIPT_DIR/truenas_middleware_minecraft_t6_probe.py" \ - --product-profile vanilla263 --native-catalog-profile 1.15.30 \ + --product-profile vanilla263 --native-catalog-profile 1.15.31 \ --host 127.0.0.1 --port "$MIDDLEWARE_PORT" \ --status-port "$MINECRAFT_HOST_PORT" \ --status-oracle "$REPO_ROOT/.deps/minecraft-product/tools/minecraft_java_status_oracle.py" \ diff --git a/scripts/qualify_vanilla263_native_admission.py b/scripts/qualify_vanilla263_native_admission.py index 44c2f028b..f45795061 100644 --- a/scripts/qualify_vanilla263_native_admission.py +++ b/scripts/qualify_vanilla263_native_admission.py @@ -16,7 +16,7 @@ VERSION="25.04.2.6" IMAGE=("ghcr.io/itzg/minecraft-server:2026.10.0-java25@sha256:" "5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da") -APPS="559b9fe85212c800bc6770f152a65211df137a44" +APPS="95b0cc78e75fcd4b128f4ef2f8428034d981f7c2" BLOB="631467044411c96a0fe6d361688b99a794f07c80" MIDDLEWARE="244b717370fe35fb9eefc3096acfbc25f12b57b6" ISSUE=627 @@ -77,7 +77,7 @@ def check(request:dict, control:dict, registry:dict)->dict: "TRUENAS_TARGET_PROFILE_DRIFT") require(s.get("apps_commit")==APPS and s.get("catalog_app")=="community/minecraft" and - s.get("catalog_version")=="1.15.30" and + s.get("catalog_version")=="1.15.31" and s.get("image_reference")==IMAGE and type(s.get("source_compose_sha256")) is str and re.fullmatch("[a-f0-9]{64}",s["source_compose_sha256"]), @@ -86,7 +86,7 @@ def check(request:dict, control:dict, registry:dict)->dict: runtime.get("minecraft_protocol")==777 and runtime.get("java_version")==25 and runtime.get("image_selector")=="j25_image" and - runtime.get("catalog_version_candidate")=="1.15.30" and + runtime.get("catalog_version_candidate")=="1.15.31" and runtime.get("app_name")=="rdte-t6-minecraft-vanilla-263" and runtime.get("required_independent_world_dataset")== "rdtepool/minecraft-vanilla-263-t6-world", diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 4eb2da897..796db6c6e 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -125,7 +125,7 @@ def configure_exact_vanilla263() -> None: MC_IMPLEMENTATION, MC_STATUS_NAME_CONTRACT = "vanilla", "vanilla_exact" VANILLA263 = True CATALOG_PROFILES = dict(CATALOG_PROFILES) - CATALOG_PROFILES["1.15.30"] = {"apps_commit": APPS_REF, "image": OCI} + CATALOG_PROFILES["1.15.31"] = {"apps_commit": APPS_REF, "image": OCI} class MinecraftT6Error(RuntimeError): @@ -175,7 +175,7 @@ def canonical_sha256(doc: dict) -> str: def load_control(path: pathlib.Path, expected_commit: str, catalog_version: str = "1.15.29") -> dict: if VANILLA263: from admit_vanilla263_native_t6_control import verify - if catalog_version != "1.15.30": + if catalog_version != "1.15.31": raise MinecraftT6Error("vanilla263 exact native catalog version refused") try: control = json.loads((path / "control.json").read_text(encoding="utf-8")) diff --git a/tests/test_truenas_t6_vanilla263_guest.py b/tests/test_truenas_t6_vanilla263_guest.py index 704aa1ec0..0a52bd2db 100644 --- a/tests/test_truenas_t6_vanilla263_guest.py +++ b/tests/test_truenas_t6_vanilla263_guest.py @@ -33,7 +33,7 @@ def guest_fixture(): "foundry_profile_blob":native.PROFILE_BLOB}, "requested_shape":{"app":{ "name":native.APP,"catalog_app":"minecraft", - "catalog_version_candidate":"1.15.30", + "catalog_version_candidate":"1.15.31", "minecraft_version":"26.3","minecraft_protocol":777}}, "foundry_materialization":{ "schema":"truenas-minecraft-vanilla263-t6/v1", @@ -281,7 +281,7 @@ def test_explicit_vanilla_profile_is_distinct(self): self.assertEqual(mc.MC_TYPE,"VANILLA") self.assertEqual(mc.MC_VERSION,"26.3") self.assertEqual(mc.MC_PROTOCOL,777) - self.assertEqual(mc.CATALOG_PROFILES["1.15.30"]["image"],native.OCI) + self.assertEqual(mc.CATALOG_PROFILES["1.15.31"]["image"],native.OCI) def test_vanilla_status_requires_exact_version_protocol_implementation(self): mc.configure_exact_vanilla263() diff --git a/tests/test_vanilla263_native_admission.py b/tests/test_vanilla263_native_admission.py index ced494552..d349859c7 100644 --- a/tests/test_vanilla263_native_admission.py +++ b/tests/test_vanilla263_native_admission.py @@ -17,13 +17,13 @@ def tuple_(): "expected_system_version":"TrueNAS-25.04.2.6", "middleware_commit":MIDDLEWARE, "profile_git_blob_sha":BLOB}, - "source":{"apps_commit":"559b9fe85212c800bc6770f152a65211df137a44", + "source":{"apps_commit":"95b0cc78e75fcd4b128f4ef2f8428034d981f7c2", "catalog_app":"community/minecraft", - "catalog_version":"1.15.30","image_reference":IMAGE, + "catalog_version":"1.15.31","image_reference":IMAGE, "source_compose_sha256":"b"*64}, "runtime":{"minecraft_version":"26.3","minecraft_protocol":777, "java_version":25,"image_selector":"j25_image", - "catalog_version_candidate":"1.15.30", + "catalog_version_candidate":"1.15.31", "app_name":"rdte-t6-minecraft-vanilla-263", "required_independent_world_dataset":"rdtepool/minecraft-vanilla-263-t6-world", "values_template":{"minecraft":{ diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 775687ac1..f3ddeb259 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -20,13 +20,13 @@ def test_native_product_is_explicit_not_paper_alias(self): self.assertIn('|| "$T6_PRODUCT" == "minecraft-vanilla-263"',HARNESS) def test_producer_signer_and_fresh_exact_artifact_seam(self): - pin="29ec6cba7df51e316b0329926aa85807805dd07b" + pin="5ee204df205edfd531304441fd1a6a24004557b1" self.assertIn("foundry-minecraft-vanilla263-t6-control:",WORKFLOW) self.assertIn( - "export-minecraft-vanilla-263-t6-control.yml@"+pin,WORKFLOW) + "export-minecraft-vanilla-263-11531-t6-control.yml@"+pin,WORKFLOW) self.assertIn("foundry_ref: "+pin,WORKFLOW) self.assertIn( - "name: minecraft-vanilla263-java25-truenas-250426-t6-source-candidate",WORKFLOW) + "name: minecraft-vanilla263-truenas-250426-11531-signed-source-candidate",WORKFLOW) self.assertIn("scripts/admit_vanilla263_native_t6_control.py",WORKFLOW) self.assertIn('foundry_commit="'+pin+'"',WORKFLOW) self.assertIn( @@ -37,7 +37,7 @@ def test_exact_oracle_not_legacy_paper_status(self): self.assertIn("Checkout independently qualified vanilla26.3 protocol777 status oracle",WORKFLOW) self.assertIn("4df9fbbfea0ae0e824719db367bd551349a86378",WORKFLOW) self.assertIn("python3 scripts/assert_vanilla263_native_t6_receipt.py",WORKFLOW) - self.assertIn("--product-profile vanilla263 --native-catalog-profile 1.15.30",HARNESS) + self.assertIn("--product-profile vanilla263 --native-catalog-profile 1.15.31",HARNESS) self.assertIn('"minecraft_protocol": 777',HARNESS) self.assertIn('"minecraft_version": "26.3"',HARNESS) self.assertIn('"rdte-t6-minecraft-vanilla-263"',HARNESS) @@ -197,7 +197,7 @@ def test_no_fallback_guest_or_physical_promotion(self): self.assertIn("cancel-in-progress: false",WORKFLOW) # Exact accepted source consumer must refuse a wrong Foundry SHA from scripts.admit_vanilla263_native_t6_control import CONTROL_SHA - self.assertEqual(CONTROL_SHA,"c1d6d9bd6306de55b12072ea9d0d6411fa7d7f106a6a87bdbe1913c0d82c0615") + self.assertEqual(CONTROL_SHA,"59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5") if __name__=="__main__": unittest.main() diff --git a/tests/test_vanilla263_t6_source_admission.py b/tests/test_vanilla263_t6_source_admission.py index 6475043a3..4ee811e6e 100644 --- a/tests/test_vanilla263_t6_source_admission.py +++ b/tests/test_vanilla263_t6_source_admission.py @@ -14,7 +14,7 @@ import admit_vanilla263_native_t6_control as consumer FOUNDRY=ROOT/".deps"/"vanilla-foundry" -EXPORT=FOUNDRY/"tools"/"export_minecraft_vanilla_263_t6_control.py" +EXPORT=FOUNDRY/"tools"/"export_minecraft_vanilla_263_11531_t6_control.py" def producer(): @@ -36,7 +36,7 @@ def source_document(): "result":"PASS","truenas_target":"25.04.2.6", "runtime_qualification":False, "upstream":{"ref":f.SOURCE,"app":"community/minecraft"}, - "server":{"catalog_version":"1.15.30","type":"VANILLA", + "server":{"catalog_version":"1.15.31","type":"VANILLA", "minecraft_version":"26.3","image_selector":"j25_image", "image_ref":f.IMAGE,"ownership_metadata":f.IDENTITY, "world_mount":{"source":"/opt/tests/collage-vanilla-263-world"}}, From 121180e966f6e5b4c021da45e322d35a79bbd7f9 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:48:03 +0200 Subject: [PATCH 709/719] fix(G4): independently pin live official native App1.15.31 preflight, retain retired Paper hold * fix(native source): pin standalone vendor preflight to live 1.15.31; explicitly reject retired 1.15.30 * test: enforce distinct current vanilla1.15.31 vs retired Paper1.15.30 source claims --- .../official-minecraft-catalog-preflight.yml | 11 ++++++----- tests/test_collage_catalog_preflight.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/official-minecraft-catalog-preflight.yml b/.github/workflows/official-minecraft-catalog-preflight.yml index 628f46249..d7d55d447 100644 --- a/.github/workflows/official-minecraft-catalog-preflight.yml +++ b/.github/workflows/official-minecraft-catalog-preflight.yml @@ -22,21 +22,22 @@ jobs: set -euo pipefail python3 -m py_compile scripts/check_collage_catalog_preflight.py python3 -m unittest -v tests.test_collage_catalog_preflight - - name: Verify upstream live official master with exact immutable 1.15.30 admission + - name: Verify actual upstream 1.15.31 native source availability; no Paper 1.15.30 runtime claim run: | set -euo pipefail - python3 scripts/check_collage_catalog_preflight.py --expected 1.15.30 \ + python3 scripts/check_collage_catalog_preflight.py --expected 1.15.31 \ > "$RUNNER_TEMP/official-catalog-result.json" python3 - "$RUNNER_TEMP/official-catalog-result.json" <<'PY' import json,sys v=json.load(open(sys.argv[1])) assert v["classification"] == "PRE_KVM_CATALOG_VERSION_AVAILABLE" - assert v["expected_version"] == "1.15.30" - assert "1.15.30" in v["visible_versions"] + assert v["expected_version"] == "1.15.31" + assert "1.15.31" in v["visible_versions"] + assert "1.15.30" not in v["visible_versions"] assert v["guest_catalog_observed"] is False assert v["live_minecraft_qualified"] is False assert v["world_mutated"] is False PY - name: Report exact read-only external preflight boundary if: always() - run: echo 'Only official source catalog read; no KVM, native TrueNAS App, game-world mutation or physical APPLY.' >> "$GITHUB_STEP_SUMMARY" + run: echo 'Only current native vanilla 1.15.31 source catalog availability, not retired Paper 1.15.30; no KVM, native TrueNAS App, game-world mutation or physical APPLY.' >> "$GITHUB_STEP_SUMMARY" diff --git a/tests/test_collage_catalog_preflight.py b/tests/test_collage_catalog_preflight.py index f42073c7d..34017c37b 100644 --- a/tests/test_collage_catalog_preflight.py +++ b/tests/test_collage_catalog_preflight.py @@ -44,6 +44,19 @@ def test_causal_preflight_is_in_truenas_job_not_proxmox(self): self.assertLess(guest.index(guard), guest.index(" - name: Ensure bounded QEMU/KVM prerequisites")) + def test_live_native_source_contract_separate_from_retired_paper(self): + rows=[{"name":"1.15.31","type":"dir"},{"name":"item.yaml","type":"file"}] + native=evaluate(rows,"1.15.31") + paper=evaluate(rows,"1.15.30") + self.assertEqual(native["classification"],"PRE_KVM_CATALOG_VERSION_AVAILABLE") + self.assertEqual(paper["classification"],"PRE_KVM_CATALOG_DRIFT") + self.assertFalse(native["guest_catalog_observed"]) + self.assertFalse(native["live_minecraft_qualified"]) + workflow=(pathlib.Path(__file__).resolve().parents[1]/ + ".github/workflows/official-minecraft-catalog-preflight.yml").read_text() + self.assertIn("python3 scripts/check_collage_catalog_preflight.py --expected 1.15.31",workflow) + self.assertIn('assert "1.15.30" not in v["visible_versions"]',workflow) + def test_no_float_latest_pin(self): rows=[{"name":"1.15.30","type":"dir"}] for version in ("latest","1.15","1.15.29\n",None): From 96b06a115355fc10c82fbc045bc7e2202081217d Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:14:27 +0200 Subject: [PATCH 710/719] admit(G4): one fresh OP4J-017 native 1.15.31 Anvil phase qualification guest * admit(G4): distinct one-use OP4J-017 App1.15.31 backend phase and three-checkpoint disposable request * test(G4): enforce fresh 017 immutable one-use edge, consumed 016 negative and exact native G2/G3 conditions * test(rdte): update exact registered one-use request to 017 and retain independent authority constraints --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_t6_guest_routing.py | 19 ++++++++++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 86ec5b93d..cc90067f6 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4I-016 one-shot real disposable TrueNAS25.04.2.6 native community Minecraft App1.15.30 Java25 vanilla26.3/protocol777 authenticated guest DDP G4 backend-world cause test. EXACT signed immutable Foundry pinned itzg Java25 image and original 739-byte Lantern ZIP (SHA256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8), APPS_READ TLS Go six marker owner, fixed owned ZFS snapshot/release-fault rollback. Same runner-only QEMU 127.0.0.1 ephemeral port synthetic offline UUIDv3 OP Mineflayer, RCON disabled, canonical operational online authentication remains true. New three stopped-App read-only fixed r.0.0.mca region checkpoints: clean preaction fixture empty core, candidate redstone/sea lantern opened gate, rollback same action with iron bars/black concrete. Independent server-owned Anvil decoder fixed protected coordinates, source/receipt verifier exact hashes and negative/cached/foreign/replay/clean owned final cleanup. Distinct request 016 consumed once; do not replay 011-015, no other concurrent heavyweight guest. Does not authorize COLLAGE APPLY, real Mojang/stock/VIP, full Two Rivers, operations or physical TrueNAS.", + "purpose": "OP4J-017 ONE-USE distinct disposable TrueNAS25.04.2.6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 G4 backend-world causal retry AFTER consumed OP4I-016 failure. Exact SHA-bound merged Foundry 5ee204df205edfd531304441fd1a6a24004557b1 source control digest 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5, upstream truenas/apps@95b0cc78e75fcd4b128f4ef2f8428034d981f7c2, pinned Java25 OCI, real signed vendor ix-dev canonical parity; no floating catalog. Previous 016 permanently CONSUMED and never replayed. Single fresh bounded G4 real native closed guest: same immutable original 739-byte Lantern ZIP SHA256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8, read-only APPS_READ TLS COLLAGE six owner markers, G3 owned ZFS snapshot/fault rollback and 127.0.0.1 user-network forwarded protocol777 Mineflayer synthetic offline player. Operational online_mode stays true and RCON disabled. Require allowlisted last_backend_phase with NO provider exception or world bytes disclosure; preaction/candidate/rollback independent STOPPED server-owned fixed r.0.0.mca Anvil region download/decode exact protected block states, separate response/region SHAs, no stale/cached/foreign data, independently qualified receipt and owned zero-residue cleanup. Only public/free serialized GHA after fresh GitHub merge and shared heavyweight queue empty. No COLLAGE APPLY, no real authenticated player, no full Two Rivers, no physical TrueNAS APPLY or change to production IAM.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index bb3df0ec4..0abe672fb 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index f3ddeb259..981a5c7a2 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,7 +52,16 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + self.assertIn("OP4I-016",current["purpose"]) + self.assertIn("permanently CONSUMED",current["purpose"]) + self.assertIn("App1.15.31",current["purpose"]) + self.assertEqual(current["consumer_authority_issue"],627) + self.assertTrue(all(current[k] is True for k in + ("collage_readback","lantern_stage","native_world_rollback", + "native_offline_player","native_backend_region"))) + self.assertEqual(proposal["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4b-001") self.assertEqual(proposal["consumer_authority_issue"],627) self.assertIn("NOT wired",proposal["purpose"]) @@ -77,7 +86,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -112,7 +121,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -132,7 +141,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) @@ -156,7 +165,7 @@ def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("native_backend_region"), True) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") for needle in ("truenas_native_backend_region=false", "(.native_backend_region // false)", "truenas_native_backend_region=$truenas_native_backend_region", From f7733aec1811ac87012ed5b97e56f54c5217ee96 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:30:19 +0200 Subject: [PATCH 711/719] fix(G4): product-scoped 1.15.31 parser admission before guest receipt * fix(G4): admit exact native1.15.31 CLI prior to parse, deny cross-product alias before DDP * test(G4): reproduce 017 argparse pre-DDP no-receipt failure and enforce product-scoped catalog admission * ci(G4): public source gate exercises parsed 1.15.31 CLI and hostile cross-product negatives * test(G4): import true CLI module via existing runner scripts path, no network/sessions --- ...alify-native-loopback-gameplay-overlay.yml | 3 + .../truenas_middleware_minecraft_t6_probe.py | 20 +++- ...est_vanilla263_catalog_parser_preflight.py | 95 +++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 tests/test_vanilla263_catalog_parser_preflight.py diff --git a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml index 1d4d60594..251a87468 100644 --- a/.github/workflows/qualify-native-loopback-gameplay-overlay.yml +++ b/.github/workflows/qualify-native-loopback-gameplay-overlay.yml @@ -13,6 +13,7 @@ on: - 'tests/test_native_vanilla263_anvil_guest_readback.py' - 'tests/test_native_vanilla263_backend_three_checkpoint_receipt.py' - 'scripts/truenas_middleware_minecraft_t6_probe.py' + - 'tests/test_vanilla263_catalog_parser_preflight.py' - 'scripts/assert_vanilla263_native_t6_receipt.py' - 'scripts/gha_kvm_truenas_rdte.sh' - '.github/workflows/gha-kvm-system-rdte.yml' @@ -43,12 +44,14 @@ jobs: tests/test_native_vanilla263_anvil_guest_readback.py \ tests/test_native_vanilla263_backend_three_checkpoint_receipt.py \ scripts/truenas_middleware_minecraft_t6_probe.py \ + tests/test_vanilla263_catalog_parser_preflight.py \ scripts/assert_vanilla263_native_t6_receipt.py python3 -m unittest -v tests.test_native_vanilla263_loopback_player_overlay python3 -m unittest -v tests.test_native_vanilla263_mineflayer_causal python3 -m unittest -v tests.test_native_vanilla263_anvil_blocks python3 -m unittest -v tests.test_native_vanilla263_anvil_guest_readback python3 -m unittest -v tests.test_native_vanilla263_backend_three_checkpoint_receipt + python3 -m unittest -v tests.test_vanilla263_catalog_parser_preflight node --check scripts/qualify_native_vanilla263_mineflayer_causal.js bash -n scripts/gha_kvm_truenas_rdte.sh - name: Preserve operational auth and physical TrueNAS hold diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 796db6c6e..704d130d0 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -386,7 +386,14 @@ def owned_config(config: dict, wanted: dict) -> bool: return all(owner_readback_vector(config, wanted).values()) -def main() -> int: +def product_catalog_admitted(product: str, version: str) -> bool: + """Bound CLI catalog claims before any DDP/session or App mutation.""" + return (product == "legacy-paper" and version in ("1.15.29", "1.15.30")) or ( + product == "vanilla263" and version == "1.15.31") + + +def parse_guest_args(argv=None) -> argparse.Namespace: + """Exact source-only parsing; vanilla catalog isn't registered until after parse.""" p = argparse.ArgumentParser() p.add_argument("--host", default="127.0.0.1") p.add_argument("--port", required=True, type=int) @@ -407,7 +414,7 @@ def main() -> int: p.add_argument("--native-backend-region", action="store_true") p.add_argument("--native-player-forward") p.add_argument("--native-player-node-path", type=pathlib.Path) - p.add_argument("--native-catalog-profile", choices=sorted(CATALOG_PROFILES), + p.add_argument("--native-catalog-profile", choices=("1.15.29", "1.15.30", "1.15.31"), default="1.15.29") p.add_argument("--product-profile", choices=("legacy-paper", "vanilla263"), default="legacy-paper") @@ -415,7 +422,14 @@ def main() -> int: p.add_argument("--timeout", type=float, default=8) p.add_argument("--job-timeout", type=float, default=600) p.add_argument("--state-timeout", type=float, default=480) - a = p.parse_args() + a = p.parse_args(argv) + if not product_catalog_admitted(a.product_profile, a.native_catalog_profile): + p.error("PRODUCT_SCOPED_CATALOG_NOT_ADMITTED") + return a + + +def main() -> int: + a = parse_guest_args() if a.product_profile == "vanilla263": configure_exact_vanilla263() started = time.monotonic() diff --git a/tests/test_vanilla263_catalog_parser_preflight.py b/tests/test_vanilla263_catalog_parser_preflight.py new file mode 100644 index 000000000..348cbf2e7 --- /dev/null +++ b/tests/test_vanilla263_catalog_parser_preflight.py @@ -0,0 +1,95 @@ +"""Causal OP4J-017 harness failure reproduction; never opens a TrueNAS guest.""" +from __future__ import annotations +import contextlib +import io +import json +from pathlib import Path +import sys +import unittest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) +import truenas_middleware_minecraft_t6_probe as native +WORKFLOW = ROOT / ".github/workflows/gha-kvm-system-rdte.yml" +HARNESS = ROOT / "scripts/gha_kvm_truenas_rdte.sh" +REQUEST = ROOT / "config/truenas-rdte-run-request.json" + + +def cli(profile: str, catalog: str) -> list[str]: + return [ + "--host", "127.0.0.1", "--port", "10000", + "--status-port", "25580", "--status-oracle", "/never/open/oracle.py", + "--password-file", "/never/open/secret-file", + "--control-dir", "/never/open/control", + "--foundry-commit", "5ee204df205edfd531304441fd1a6a24004557b1", + "--expected-system-version", "TrueNAS-25.04.2.6", + "--out", "/never/write/probe.json", + "--product-profile", profile, + "--native-catalog-profile", catalog, + ] + + +class ProductScopedParserTests(unittest.TestCase): + def test_017_reproduced_profile_accepted_without_guest_io(self): + self.assertNotIn("1.15.31", native.CATALOG_PROFILES) + args = native.parse_guest_args(cli("vanilla263", "1.15.31")) + self.assertEqual(args.product_profile, "vanilla263") + self.assertEqual(args.native_catalog_profile, "1.15.31") + self.assertFalse(Path("/never/write/probe.json").exists()) + self.assertNotIn("1.15.31", native.CATALOG_PROFILES) + self.assertTrue(native.product_catalog_admitted("vanilla263", "1.15.31")) + + def test_old_paper_versions_remain_distinct_and_admitted(self): + for version in ("1.15.29", "1.15.30"): + with self.subTest(version=version): + args = native.parse_guest_args(cli("legacy-paper", version)) + self.assertEqual(args.native_catalog_profile, version) + self.assertTrue(native.product_catalog_admitted("legacy-paper", version)) + + def test_cross_product_and_unpinned_profiles_fail_before_guest(self): + rows = (("legacy-paper", "1.15.31"), + ("vanilla263", "1.15.29"), + ("vanilla263", "1.15.30"), + ("vanilla263", "1.15.32"), + ("legacy-paper", "1.15.32")) + for product, catalog in rows: + with self.subTest(product=product, catalog=catalog): + sink = io.StringIO() + with contextlib.redirect_stderr(sink), self.assertRaises(SystemExit) as raised: + native.parse_guest_args(cli(product, catalog)) + self.assertEqual(raised.exception.code, 2) + self.assertFalse(native.product_catalog_admitted(product, catalog)) + self.assertNotIn("secret-file", sink.getvalue()) + self.assertFalse(Path("/never/write/probe.json").exists()) + + def test_runner_uses_same_qualified_exact_catalog_without_modifying_request(self): + harness = HARNESS.read_text(encoding="utf-8") + workflow = WORKFLOW.read_text(encoding="utf-8") + request = json.loads(REQUEST.read_text(encoding="utf-8")) + self.assertIn("--product-profile vanilla263 --native-catalog-profile 1.15.31", harness) + self.assertIn('--native-catalog-profile "1.15.30"', harness) + self.assertIn("--expected 1.15.31", workflow) + self.assertEqual(request["request_id"], + "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + self.assertNotIn("018", request["request_id"]) + self.assertEqual(request["consumer_authority_issue"], 627) + self.assertEqual(request["authority_issue"], 423) + self.assertEqual(set(request), { + "schema", "request_id", "version", "rung", "product", + "authority_issue", "consumer_authority_issue", "purpose", + "collage_readback", "lantern_stage", "native_world_rollback", + "native_offline_player", "native_backend_region" + }) + + def test_runtime_parse_always_precedes_mutating_configuration(self): + source = (ROOT/"scripts/truenas_middleware_minecraft_t6_probe.py").read_text() + main = source[source.index("def main() -> int:"):] + self.assertLess(main.index("a = parse_guest_args()"), + main.index("configure_exact_vanilla263()")) + self.assertLess(main.index("configure_exact_vanilla263()"), + main.index("started = time.monotonic()")) + self.assertIn("PRODUCT_SCOPED_CATALOG_NOT_ADMITTED", source) + + +if __name__ == "__main__": + unittest.main() From 7fab60a958b472d4da9d0ca6b5e42b16f12815b8 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Fri, 16 Oct 2026 01:33:57 +0200 Subject: [PATCH 712/719] admit(G4): one distinct OP4K-018 parser-qualified native 1.15.31 Anvil guest * admit(G4): distinct OP4K-018 one-use native parser-repaired Anvil request; 016/017 consumed * test(G4): enforce distinct 018 one-use request and preserve consumed 016/017 negative oracles * test(G4): enforce distinct 018 one-use request and preserve consumed 016/017 negative oracles * test(G4): enforce distinct 018 one-use request and preserve consumed 016/017 negative oracles * test(admission): explicit immutable 016 and 017 consumed history in new 018 cause/evidence --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_catalog_parser_preflight.py | 5 +++-- tests/test_vanilla263_t6_guest_routing.py | 14 +++++++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index cc90067f6..341882c0a 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4J-017 ONE-USE distinct disposable TrueNAS25.04.2.6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 G4 backend-world causal retry AFTER consumed OP4I-016 failure. Exact SHA-bound merged Foundry 5ee204df205edfd531304441fd1a6a24004557b1 source control digest 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5, upstream truenas/apps@95b0cc78e75fcd4b128f4ef2f8428034d981f7c2, pinned Java25 OCI, real signed vendor ix-dev canonical parity; no floating catalog. Previous 016 permanently CONSUMED and never replayed. Single fresh bounded G4 real native closed guest: same immutable original 739-byte Lantern ZIP SHA256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8, read-only APPS_READ TLS COLLAGE six owner markers, G3 owned ZFS snapshot/fault rollback and 127.0.0.1 user-network forwarded protocol777 Mineflayer synthetic offline player. Operational online_mode stays true and RCON disabled. Require allowlisted last_backend_phase with NO provider exception or world bytes disclosure; preaction/candidate/rollback independent STOPPED server-owned fixed r.0.0.mca Anvil region download/decode exact protected block states, separate response/region SHAs, no stale/cached/foreign data, independently qualified receipt and owned zero-residue cleanup. Only public/free serialized GHA after fresh GitHub merge and shared heavyweight queue empty. No COLLAGE APPLY, no real authenticated player, no full Two Rivers, no physical TrueNAS APPLY or change to production IAM.", + "purpose": "OP4K-018 new one-use disposable TrueNAS25.04.2.6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 real G4 backend 3-checkpoint readback after OP4J-017 permanently consumed HARNESS_FAILURE with product CLI rejecting 1.15.31 before it could create its own receipt. Fix independently qualified/merged Agent Dispatch f7733aec1811ac87012ed5b97e56f54c5217ee96: side-effect-free source parser admits only vanilla263 App1.15.31, Paper remains limited to1.15.29/1.15.30; unknown/cross-product source rejected. Keep immutable Foundry producer5ee204df205edfd531304441fd1a6a24004557b1 SHA-bound source control 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5, itzg Java25 OCI digest and original 739-byte Lantern ZIP sha256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8. Existing isolated read-only TLS APPS_READ six marker COLLAGE, owned ZFS G3 snapshot injected-fault rollback, 127.0.0.1 runner QEMU synthetic Mineflayer offline protocol777 (RCON disabled; online-mode operational source remains true), protected fixed region r.0.0.mca STOPPED preaction/candidate/rollback independent actual server Anvil block-state/checksum decode and phase allowlist without exception text. Verify own app/world zero-residue and independent guest exact receipt. Fresh single public serialized cloud execution after approved merged-head source/queue gates; OP4I-016 and OP4J-017 permanently CONSUMED and never replayed. NO source-only game acceptance, COLLAGE APPLY, external online account/VIP, complete Two Rivers, physical TrueNAS APPLY or production credential.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 0abe672fb..dfa238545 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) diff --git a/tests/test_vanilla263_catalog_parser_preflight.py b/tests/test_vanilla263_catalog_parser_preflight.py index 348cbf2e7..35658f119 100644 --- a/tests/test_vanilla263_catalog_parser_preflight.py +++ b/tests/test_vanilla263_catalog_parser_preflight.py @@ -70,8 +70,9 @@ def test_runner_uses_same_qualified_exact_catalog_without_modifying_request(self self.assertIn('--native-catalog-profile "1.15.30"', harness) self.assertIn("--expected 1.15.31", workflow) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") - self.assertNotIn("018", request["request_id"]) + "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + self.assertNotIn("017", request["request_id"]) + self.assertIn("018", request["request_id"]) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["authority_issue"], 423) self.assertEqual(set(request), { diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index 981a5c7a2..f67f395b9 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,8 +52,12 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") + self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + self.assertIn("OP4J-017", current["purpose"]) + self.assertIn("permanently consumed", current["purpose"]) + self.assertIn("parser", current["purpose"]) self.assertIn("OP4I-016",current["purpose"]) self.assertIn("permanently CONSUMED",current["purpose"]) self.assertIn("App1.15.31",current["purpose"]) @@ -86,7 +90,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -121,7 +125,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -141,7 +145,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) @@ -165,7 +169,7 @@ def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("native_backend_region"), True) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") for needle in ("truenas_native_backend_region=false", "(.native_backend_region // false)", "truenas_native_backend_region=$truenas_native_backend_region", From f590f8ade632fe4ccee5fdcadeace66fafe985a4 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:34:39 +0200 Subject: [PATCH 713/719] fix(rdte): source-pin COLLAGE 1.15.31 and classify OP4K join failures * fix(rdte): pin COLLAGE 1.15.31 and classify child readback refusals safely * chore(rdte): pin qualified COLLAGE 1.15.31 source in scripts/gha_kvm_truenas_rdte.sh * chore(rdte): pin qualified COLLAGE 1.15.31 source in .github/workflows/gha-kvm-system-rdte.yml * chore(rdte): pin qualified COLLAGE 1.15.31 source in tests/test_vanilla263_t6_guest_routing.py * test(rdte): source-only falsification of 018 collapsed failure planes and secret leakage * fix(preflight): keep fail-closed exit3 but distinguish safe HTTP, transport and schema failures * test(preflight): distinguish 403/429/404/transport/schema without secrets or fake drift --- .github/workflows/gha-kvm-system-rdte.yml | 4 +- scripts/check_collage_catalog_preflight.py | 17 +++- scripts/gha_kvm_truenas_rdte.sh | 2 +- .../truenas_middleware_collage_readback_p2.py | 79 ++++++++++++++---- tests/test_collage_catalog_preflight.py | 31 ++++++- tests/test_collage_p2_effective_roles.py | 83 ++++++++++++++++++- tests/test_vanilla263_t6_guest_routing.py | 4 +- 7 files changed, 197 insertions(+), 23 deletions(-) diff --git a/.github/workflows/gha-kvm-system-rdte.yml b/.github/workflows/gha-kvm-system-rdte.yml index daba0c616..e5a05d968 100644 --- a/.github/workflows/gha-kvm-system-rdte.yml +++ b/.github/workflows/gha-kvm-system-rdte.yml @@ -646,7 +646,7 @@ jobs: uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: repository: SupraCraft/collage - ref: 2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8 + ref: b9256d6ec0dbac43be2f5a136103b6e3512ee2ec path: .deps/collage-source persist-credentials: false - name: Prepare pinned scoped native vanilla COLLAGE Go read-only observer @@ -659,7 +659,7 @@ jobs: if: needs.changes.outputs.truenas_rung == 't6' && needs.changes.outputs.truenas_t6_product == 'minecraft-vanilla-263' && needs.changes.outputs.truenas_collage_readback == 'true' run: | set -euo pipefail - test "$(git -C .deps/collage-source rev-parse HEAD)" = 2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8 + test "$(git -C .deps/collage-source rev-parse HEAD)" = b9256d6ec0dbac43be2f5a136103b6e3512ee2ec (cd .deps/collage-source && python3 -m unittest -v tests.test_collage_native_readback tests.test_consume_vanilla_datapack_source) (cd .deps/collage-source/tools/truenas-readonly-probe && diff --git a/scripts/check_collage_catalog_preflight.py b/scripts/check_collage_catalog_preflight.py index feb351398..5cf13de22 100644 --- a/scripts/check_collage_catalog_preflight.py +++ b/scripts/check_collage_catalog_preflight.py @@ -54,6 +54,19 @@ def evaluate(rows: object, expected: str) -> dict: } +def fixed_catalog_failure_reason(exc: Exception) -> str: + """No remote error/body/URL leakage; inconclusive is never version drift.""" + if isinstance(exc, urllib.error.HTTPError): + if exc.code in (403, 429): + return "OFFICIAL_CATALOG_HTTP_THROTTLED_OR_FORBIDDEN" + if exc.code == 404: + return "OFFICIAL_CATALOG_HTTP_NOT_FOUND" + return "OFFICIAL_CATALOG_HTTP_OTHER" + if isinstance(exc, urllib.error.URLError) or isinstance(exc, OSError): + return "OFFICIAL_CATALOG_TRANSPORT_UNAVAILABLE" + return "OFFICIAL_CATALOG_RESPONSE_INVALID" + + def main() -> int: p = argparse.ArgumentParser(description=__doc__) p.add_argument("--expected", required=True) @@ -69,11 +82,11 @@ def main() -> int: raise ValueError("CATALOG_LIST_OVERSIZED") row = evaluate(json.loads(payload), a.expected) status = 0 if row["classification"] == "PRE_KVM_CATALOG_VERSION_AVAILABLE" else 2 - except (OSError, ValueError, urllib.error.HTTPError): + except (OSError, ValueError, urllib.error.HTTPError) as exc: row = { "schema": "collage.official-catalog-preflight/1", "classification": "PRE_KVM_CATALOG_INCONCLUSIVE", - "reason": "OFFICIAL_READONLY_CATALOG_UNAVAILABLE_OR_UNEXPECTED", + "reason": fixed_catalog_failure_reason(exc), "guest_catalog_observed": False, "world_mutated": False, } status = 3 diff --git a/scripts/gha_kvm_truenas_rdte.sh b/scripts/gha_kvm_truenas_rdte.sh index 7ea22ed99..724e1103c 100644 --- a/scripts/gha_kvm_truenas_rdte.sh +++ b/scripts/gha_kvm_truenas_rdte.sh @@ -1230,7 +1230,7 @@ elif [[ "$T6_PRODUCT" == "minecraft-vanilla-263" ]]; then NATIVE_READBACK_ARGS=( --collage-source-dir "$REPO_ROOT/.deps/collage-source" --collage-https-port "$HTTPS_PORT" - --collage-source-ref "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8" + --collage-source-ref "b9256d6ec0dbac43be2f5a136103b6e3512ee2ec" ) fi NATIVE_LANTERN_ARGS=() diff --git a/scripts/truenas_middleware_collage_readback_p2.py b/scripts/truenas_middleware_collage_readback_p2.py index ce3763c2f..423bb5c46 100644 --- a/scripts/truenas_middleware_collage_readback_p2.py +++ b/scripts/truenas_middleware_collage_readback_p2.py @@ -28,7 +28,7 @@ WORLD_ID = "world-rdte-001" VANILLA263_OWNER = "rdte-t6-minecraft-vanilla-263" VANILLA263_WORLD = "lantern-rdte" -VANILLA263_SOURCE_SHA = "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8" +VANILLA263_SOURCE_SHA = "b9256d6ec0dbac43be2f5a136103b6e3512ee2ec" DEFAULT_SOURCE_SHA = "fbda94f3fea0a216ddd9c35b38a09d325bed3640" @@ -371,6 +371,62 @@ def invoke(method, params): return invoke +# Only fixed public-source refusal codes may cross from the child observer. +# Never concatenate untrusted JSON reason/error strings or stderr into receipts. +_ALLOWED_NATIVE_REFUSALS = frozenset({ + "NATIVE_READONLY_PROBE_FAILED", "PROBE_OUTPUT_NOT_JSON", + "PROBE_OUTPUT_OVERSIZED", "INVALID_PROVIDER_REPORT", + "NATIVE_APP_READBACK_INVALID", "NATIVE_APP_NOT_RUNNING", + "PROVIDER_OWNERSHIP_MISMATCH", "ARTIFACT_TARGET_DRIFT", + "SERVICE_ID_REQUIRED_FOR_PROVIDER_READBACK", + "REALM_NOT_IN_ADMITTED_SITE_BOUNDARY", + "NATIVE_VANILLA263_TARGET_PROFILE_DRIFT", + "NATIVE_VANILLA263_GUEST_PROFILE_DRIFT", + "NATIVE_VANILLA263_CATALOG_PROFILE_REFUSED", + "NATIVE_CATALOG_PROFILE_WRONG_PRODUCT", + "MISSING_BOUNDED_API_AUTHORITY", +}) + + +def validated_native_join_verdict(code: int, stdout: str, *, owner: str, + world: str) -> dict: + """Distinguish bounded failure planes while preserving read-only proof.""" + if type(stdout) is not str or len(stdout.encode("utf-8")) >= 32768: + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_OUTPUT_INVALID") + try: + v = json.loads(stdout) + except ValueError: + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_NONJSON") from None + if type(v) is not dict or v.get("schema") != "collage.native-readback/1": + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_SCHEMA_INVALID") + if code != 0: + if code == 2 and v.get("classification") == "REFUSED" and ( + v.get("mutation_authorized") is False): + why = v.get("reason") + suffix = why if type(why) is str and why in _ALLOWED_NATIVE_REFUSALS else "OTHER" + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_REFUSED_" + suffix) + if code == 3 and v.get("classification") == "INCONCLUSIVE" and ( + v.get("mutation_authorized") is False): + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_CLIENT_INCONCLUSIVE") + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_CLIENT_EXIT") + if v.get("classification") != "PROVIDER_READBACK_MATCH": + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_CLASSIFICATION_INVALID") + if v.get("mutation_authorized") is not False or ( + v.get("ownership_fields_matched") != 6 or + v.get("service_id") != SERVICE_UUID): + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_OWNER_FIELDS_INVALID") + if v.get("native_app") != owner or v.get("world_id") != world: + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_TARGET_INVALID") + digest = v.get("source_plan_sha256") + if type(digest) is not str or not re.fullmatch(r"[0-9a-f]{64}", digest): + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_SOURCE_DIGEST_INVALID") + if owner == VANILLA263_OWNER and ( + v.get("native_app_version") != "1.15.31" or + v.get("truenas_version") != "25.04.2.6"): + raise ObservationError("NATIVE_READONLY_GUEST_JOIN_EXACT_CATALOG_INVALID") + return v + + def run_real_readback(call, *, collage: pathlib.Path, https_port: int, state_dir: pathlib.Path, expected_source_sha: str = DEFAULT_SOURCE_SHA, profile: str = "paper") -> dict: @@ -403,24 +459,19 @@ def run_real_readback(call, *, collage: pathlib.Path, https_port: int, "--host", hostname, "--username", NAME, "--trusted-realm-id", REALM, "--manager-id", "manager-rdte", "--fleet-id", "rdte"] + if profile == "vanilla263": + # Caller is already bound to exact immutable 1.15.31 Foundry control. + # Explicitly select the qualified current consumer, never float catalog. + cmd += ["--native-catalog-profile", "vanilla263-11531"] try: p = subprocess.run(cmd, env=env, text=True, capture_output=True, timeout=180, check=False) except (OSError, subprocess.TimeoutExpired): raise ObservationError("NATIVE_READONLY_CLIENT_UNAVAILABLE") from None - # No raw stdout/stderr from clients or API; only extract declared schema. - try: - verdict = json.loads(p.stdout) if len(p.stdout) < 32768 else {} - except ValueError: - verdict = {} - require(p.returncode == 0 and verdict.get("schema") == "collage.native-readback/1" - and verdict.get("classification") == "PROVIDER_READBACK_MATCH" - and verdict.get("mutation_authorized") is False - and verdict.get("ownership_fields_matched") == 6 - and verdict.get("service_id") == SERVICE_UUID - and verdict.get("native_app") == expected_owner - and verdict.get("world_id") == expected_world, - "NATIVE_READONLY_GUEST_JOIN_FAILED") + # Raw stdout/stderr and arbitrary child/provider data stay private. + # The fixed-code validator does not report secrets or controller data. + verdict = validated_native_join_verdict( + p.returncode, p.stdout, owner=expected_owner, world=expected_world) return { "schema": SCHEMA, "classification": "PROVIDER_READBACK_MATCH", diff --git a/tests/test_collage_catalog_preflight.py b/tests/test_collage_catalog_preflight.py index 34017c37b..ef4901b3c 100644 --- a/tests/test_collage_catalog_preflight.py +++ b/tests/test_collage_catalog_preflight.py @@ -1,7 +1,9 @@ """Offline source tests: preflight must reject missing pins, not launch a VM.""" import unittest import pathlib -from scripts.check_collage_catalog_preflight import evaluate, version_inventory, URI +import urllib.error +from scripts.check_collage_catalog_preflight import (evaluate, version_inventory, + fixed_catalog_failure_reason, URI) class CatalogPreflightTests(unittest.TestCase): def test_public_vendor_catalog_uses_real_master_not_nonexistent_main(self): @@ -57,6 +59,33 @@ def test_live_native_source_contract_separate_from_retired_paper(self): self.assertIn("python3 scripts/check_collage_catalog_preflight.py --expected 1.15.31",workflow) self.assertIn('assert "1.15.30" not in v["visible_versions"]',workflow) + def test_catalog_http_and_transport_are_inconclusive_not_retired(self): + # The OP4K-018 standalone cheap CI returned exit 3 (not version drift). + # Classify without copying URLs, GitHub error bodies, tokens or traces. + cases = ( + (urllib.error.HTTPError("https://private.example/SECRET", 403, + "SECRET", None, None), + "OFFICIAL_CATALOG_HTTP_THROTTLED_OR_FORBIDDEN"), + (urllib.error.HTTPError("https://private.example/SECRET", 429, + "SECRET", None, None), + "OFFICIAL_CATALOG_HTTP_THROTTLED_OR_FORBIDDEN"), + (urllib.error.HTTPError("https://private.example/SECRET", 404, + "SECRET", None, None), + "OFFICIAL_CATALOG_HTTP_NOT_FOUND"), + (urllib.error.HTTPError("https://private.example/SECRET", 502, + "SECRET", None, None), + "OFFICIAL_CATALOG_HTTP_OTHER"), + (urllib.error.URLError("SECRET-transport"), + "OFFICIAL_CATALOG_TRANSPORT_UNAVAILABLE"), + (OSError("SECRET-network"), "OFFICIAL_CATALOG_TRANSPORT_UNAVAILABLE"), + (ValueError("SECRET-json"), "OFFICIAL_CATALOG_RESPONSE_INVALID"), + ) + for exc, code in cases: + with self.subTest(code=code): + category = fixed_catalog_failure_reason(exc) + self.assertEqual(category, code) + self.assertNotIn("SECRET", category) + def test_no_float_latest_pin(self): rows=[{"name":"1.15.30","type":"dir"}] for version in ("latest","1.15","1.15.29\n",None): diff --git a/tests/test_collage_p2_effective_roles.py b/tests/test_collage_p2_effective_roles.py index ae901b287..a8cb4e7ea 100644 --- a/tests/test_collage_p2_effective_roles.py +++ b/tests/test_collage_p2_effective_roles.py @@ -6,7 +6,8 @@ from scripts.truenas_middleware_collage_readback_p2 import ( ObservationError, provision_guest_readonly, bounded_rbac_call, RBAC_CALL_SEQUENCE, classify_scoped_key_ddp_failure, scoped_key_record_count_suffix, - scoped_key_expiry_ejson, + scoped_key_expiry_ejson, validated_native_join_verdict, + SERVICE_UUID, VANILLA263_OWNER, VANILLA263_WORLD, ) ROLE = "APPS_READ" @@ -328,5 +329,85 @@ def test_unauthorized_roles_never_receive_api_key(self): self.assertNotIn("api_key.create", methods) +class NativeJoin018Tests(unittest.TestCase): + """Pure sanitized child-result classification; no guest or API calls.""" + + def document(self): + return { + "schema": "collage.native-readback/1", + "classification": "PROVIDER_READBACK_MATCH", + "mutation_authorized": False, + "ownership_fields_matched": 6, + "service_id": SERVICE_UUID, + "native_app": VANILLA263_OWNER, + "world_id": VANILLA263_WORLD, + "native_app_version": "1.15.31", + "truenas_version": "25.04.2.6", + "source_plan_sha256": "a" * 64, + } + + def adjudicate(self, code, result): + return validated_native_join_verdict( + code, json.dumps(result) if not isinstance(result, str) else result, + owner=VANILLA263_OWNER, world=VANILLA263_WORLD) + + def refused(self, code, result, expected): + with self.assertRaises(ObservationError) as caught: + self.adjudicate(code, result) + self.assertEqual(str(caught.exception), expected) + self.assertNotIn("SECRET-DO-NOT-EMIT", str(caught.exception)) + + def test_independent_exact_positive_requires_readback_and_sha(self): + result = self.adjudicate(0, self.document()) + self.assertEqual(result["ownership_fields_matched"], 6) + self.assertEqual(result["native_app_version"], "1.15.31") + + def test_version_owner_service_replay_and_shape_fail_closed(self): + base = self.document() + for field, bad, reason in ( + ("native_app_version", "1.15.30", "EXACT_CATALOG_INVALID"), + ("truenas_version", "25.04.2.7", "EXACT_CATALOG_INVALID"), + ("native_app", "foreign", "TARGET_INVALID"), + ("world_id", "foreign", "TARGET_INVALID"), + ("service_id", "foreign", "OWNER_FIELDS_INVALID"), + ("ownership_fields_matched", 5, "OWNER_FIELDS_INVALID"), + ("source_plan_sha256", "wrong", "SOURCE_DIGEST_INVALID"), + ("mutation_authorized", True, "OWNER_FIELDS_INVALID"), + ("classification", "REFUSED", "CLASSIFICATION_INVALID"), + ("schema", "foreign", "SCHEMA_INVALID"), + ): + d = dict(base) + d[field] = bad + with self.subTest(field=field): + self.refused(0, d, "NATIVE_READONLY_GUEST_JOIN_" + reason) + + def test_fixed_child_codes_only_no_raw_private_reflection(self): + error = {"schema": "collage.native-readback/1", + "classification": "REFUSED", + "mutation_authorized": False, + "reason": "NATIVE_VANILLA263_GUEST_PROFILE_DRIFT", + "private": "SECRET-DO-NOT-EMIT"} + self.refused(2, error, + "NATIVE_READONLY_GUEST_JOIN_REFUSED_NATIVE_VANILLA263_GUEST_PROFILE_DRIFT") + error["reason"] = "SENSITIVE_SECRET-DO-NOT-EMIT" + self.refused(2, error, "NATIVE_READONLY_GUEST_JOIN_REFUSED_OTHER") + error["reason"] = "NATIVE_READONLY_PROBE_FAILED" + self.refused(2, error, + "NATIVE_READONLY_GUEST_JOIN_REFUSED_NATIVE_READONLY_PROBE_FAILED") + error["mutation_authorized"] = True + self.refused(2, error, "NATIVE_READONLY_GUEST_JOIN_CLIENT_EXIT") + self.refused(1, error, "NATIVE_READONLY_GUEST_JOIN_CLIENT_EXIT") + self.refused(0, "SECRET-DO-NOT-EMIT not json", + "NATIVE_READONLY_GUEST_JOIN_NONJSON") + self.refused(0, "x" * 32768, + "NATIVE_READONLY_GUEST_JOIN_OUTPUT_INVALID") + inconclusive = {"schema": "collage.native-readback/1", + "classification": "INCONCLUSIVE", + "mutation_authorized": False, + "reason": "PROBE_EXECUTION_UNAVAILABLE"} + self.refused(3, inconclusive, + "NATIVE_READONLY_GUEST_JOIN_CLIENT_INCONCLUSIVE") + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index f67f395b9..f83b09cb9 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -81,11 +81,11 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('--vanilla-collage-readback',WORKFLOW) self.assertIn('needs.changes.outputs.truenas_collage_readback',WORKFLOW) self.assertIn('--require-collage-readback',WORKFLOW) - self.assertIn('ref: 2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8',WORKFLOW) + self.assertIn('ref: b9256d6ec0dbac43be2f5a136103b6e3512ee2ec',WORKFLOW) self.assertIn('[[ "$truenas_collage_readback" == "true" ]]',WORKFLOW) self.assertIn('"$T6_PRODUCT" == "minecraft-vanilla-263"',HARNESS) self.assertIn('VANILLA_COLLAGE_READBACK=false',HARNESS) - self.assertIn('--collage-source-ref "2b6abe1141e65b8c6872eeba6c8ae39f5a9635f8"',HARNESS) + self.assertIn('--collage-source-ref "b9256d6ec0dbac43be2f5a136103b6e3512ee2ec"',HARNESS) self.assertIn('NATIVE_READBACK_ARGS=()',HARNESS) self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) From 09e30430a3eb1c2f9d25af9f35fa33332708b692 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:29:04 +0200 Subject: [PATCH 714/719] admission(op4l): one-use public disposable vanilla263 native G4 rep * admission(op4l): prepare fresh one-use 019 exact native G4 disposable request * test(op4l): accept only fresh 019 and refuse consumed 018 replay * test(op4l): accept only fresh 019 and refuse consumed 018 replay * test(op4l): accept only fresh 019 and refuse consumed 018 replay * test(op4l): assert exact qualified 1.15.31 readback instead of prior parser phase --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_catalog_parser_preflight.py | 6 +++--- tests/test_vanilla263_t6_guest_routing.py | 15 +++++++++------ 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 341882c0a..89c82b136 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4K-018 new one-use disposable TrueNAS25.04.2.6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 real G4 backend 3-checkpoint readback after OP4J-017 permanently consumed HARNESS_FAILURE with product CLI rejecting 1.15.31 before it could create its own receipt. Fix independently qualified/merged Agent Dispatch f7733aec1811ac87012ed5b97e56f54c5217ee96: side-effect-free source parser admits only vanilla263 App1.15.31, Paper remains limited to1.15.29/1.15.30; unknown/cross-product source rejected. Keep immutable Foundry producer5ee204df205edfd531304441fd1a6a24004557b1 SHA-bound source control 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5, itzg Java25 OCI digest and original 739-byte Lantern ZIP sha256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8. Existing isolated read-only TLS APPS_READ six marker COLLAGE, owned ZFS G3 snapshot injected-fault rollback, 127.0.0.1 runner QEMU synthetic Mineflayer offline protocol777 (RCON disabled; online-mode operational source remains true), protected fixed region r.0.0.mca STOPPED preaction/candidate/rollback independent actual server Anvil block-state/checksum decode and phase allowlist without exception text. Verify own app/world zero-residue and independent guest exact receipt. Fresh single public serialized cloud execution after approved merged-head source/queue gates; OP4I-016 and OP4J-017 permanently CONSUMED and never replayed. NO source-only game acceptance, COLLAGE APPLY, external online account/VIP, complete Two Rivers, physical TrueNAS APPLY or production credential.", + "purpose": "OP4L-019 distinct one-use public-free serialized disposable KVM TrueNAS25.04.2.6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 G4 backend three-checkpoint qualification after OP4K-018 permanently CONSUMED at COLLAGE read-only join and OP4I-016 and OP4J-017 permanently consumed. Independent current source fix: COLLAGE public main b9256d6ec0dbac43be2f5a136103b6e3512ee2ec exact vanilla263-11531 opt-in profile (legacy 1.15.30 untouched), Agent Dispatch merged f590f8ade632fe4ccee5fdcadeace66fafe985a4 fixed diagnostic join. Keep pinned Foundry producer5ee204df205edfd531304441fd1a6a24004557b1 canonical control SHA 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5, upstream truenas/apps@95b0cc78e75fcd4b128f4ef2f8428034d981f7c2, exact Java25 OCI sha256:5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da, immutable 739-byte Lantern zip sha256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8. Public cheap source/negative tests before one independent run; current official catalog 1.15.31 fail-closed guard and source/artifact admission required. Existing bounded ephemeral TLS APPS_READ Go native owner readback six fields zero observer mutations; G3 exact-owned ZFS injected-fault snapshot/rollback; 127.0.0.1 QEMU synthetic Mineflayer 26.3/777 client (RCON disabled and signed source online-mode policy unchanged); three independent STOPPED server-owned r.0.0.mca Anvil captures preaction/candidate/rollback and block-state/digest causality; final independently verified receipt and owned App/world zero-residue. This request creates neither COLLAGE principal-authorized APPLY nor physical/production TrueNAS APPLY, no Microsoft/VIP/stock identity, full Two Rivers MCWORLD or G5 qualification; preservation of one-use ids 016/017/018 is mandatory. No private hosted Actions minutes.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index dfa238545..c60ba2fca 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) diff --git a/tests/test_vanilla263_catalog_parser_preflight.py b/tests/test_vanilla263_catalog_parser_preflight.py index 35658f119..afaddf0e8 100644 --- a/tests/test_vanilla263_catalog_parser_preflight.py +++ b/tests/test_vanilla263_catalog_parser_preflight.py @@ -70,9 +70,9 @@ def test_runner_uses_same_qualified_exact_catalog_without_modifying_request(self self.assertIn('--native-catalog-profile "1.15.30"', harness) self.assertIn("--expected 1.15.31", workflow) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") - self.assertNotIn("017", request["request_id"]) - self.assertIn("018", request["request_id"]) + "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + self.assertNotIn("018", request["request_id"]) + self.assertIn("019", request["request_id"]) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["authority_issue"], 423) self.assertEqual(set(request), { diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index f83b09cb9..d4b9e44f9 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,12 +52,15 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") + self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + self.assertIn("OP4K-018",current["purpose"]) + self.assertIn("permanently CONSUMED",current["purpose"]) self.assertIn("OP4J-017", current["purpose"]) self.assertIn("permanently consumed", current["purpose"]) - self.assertIn("parser", current["purpose"]) + self.assertIn("vanilla263-11531", current["purpose"]) self.assertIn("OP4I-016",current["purpose"]) self.assertIn("permanently CONSUMED",current["purpose"]) self.assertIn("App1.15.31",current["purpose"]) @@ -90,7 +93,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -125,7 +128,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -145,7 +148,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) @@ -169,7 +172,7 @@ def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("native_backend_region"), True) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") for needle in ("truenas_native_backend_region=false", "(.native_backend_region // false)", "truenas_native_backend_region=$truenas_native_backend_region", From 5bd4b547e8c278629f5cf13b64b4a5de014e4217 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:39:11 +0200 Subject: [PATCH 715/719] fix(g4): bounded Anvil stat failure census without provider leakage * fix(g4): classify failed fixed Anvil stat using one bounded readonly directory census * fix(g4): allow only fixed nonsecret Anvil stat-census phases in native receipt * test(g4): read-only one-query stat failure census and secret suppression negatives * fix(g4): import shared safe Refused exception for census failure codes --- ..._native_vanilla263_anvil_guest_readback.py | 48 +++++++++++++- .../truenas_middleware_minecraft_t6_probe.py | 2 + ..._native_vanilla263_anvil_guest_readback.py | 64 ++++++++++++++++++- 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/scripts/qualify_native_vanilla263_anvil_guest_readback.py b/scripts/qualify_native_vanilla263_anvil_guest_readback.py index ddeb4ce48..fafb4086a 100644 --- a/scripts/qualify_native_vanilla263_anvil_guest_readback.py +++ b/scripts/qualify_native_vanilla263_anvil_guest_readback.py @@ -10,21 +10,26 @@ try: from .qualify_native_vanilla263_anvil_blocks import ( - MAX_REGION, SCHEMA, inspect, require) + MAX_REGION, SCHEMA, Refused, inspect, require) from .qualify_vanilla263_native_world_rollback import ( APP, WORLD, app_state) except ImportError: from qualify_native_vanilla263_anvil_blocks import ( - MAX_REGION, SCHEMA, inspect, require) + MAX_REGION, SCHEMA, Refused, inspect, require) from qualify_vanilla263_native_world_rollback import ( APP, WORLD, app_state) PATH = WORLD + "/region/r.0.0.mca" +REGION_DIRECTORY = WORLD + "/region" +REGION_BASENAME = "r.0.0.mca" +MAX_OWNED_REGION_ENTRIES = 4096 STAGES = ("preaction", "candidate", "rollback") # Only these literal, non-sensitive phases may escape as a public diagnostic. PHASES = ( "app_state_query", "app_stopped_verified", "file_stat", "file_stat_verified", + "file_stat_listing_absent", "file_stat_listing_present", + "file_stat_listing_inconclusive", "download_request", "download_route_verified", "download_job", "download_job_completed", "bytes_fetch", "bytes_verified", "anvil_decode", "anvil_verified", @@ -38,6 +43,36 @@ def record_phase(stage: str, phase: str, reporter=None) -> None: +def classify_failed_stat(*, call, stage: str, on_phase=None) -> None: + """One extra fixed-path, read-only census after a failed native stat. + + This is a diagnostic, never a fallback source of world bytes or authority. + A missing directory, provider error or malformed list is INCONCLUSIVE. + No raw paths, listing names, provider responses or errors reach the caller. + """ + try: + rows = call("filesystem.listdir", [REGION_DIRECTORY]) + except Exception: + record_phase(stage, "file_stat_listing_inconclusive", on_phase) + raise Refused("BACKEND_REGION_STAT_CENSUS_INCONCLUSIVE") from None + if (type(rows) is not list or len(rows) > MAX_OWNED_REGION_ENTRIES + or any(type(row) is not dict + or type(row.get("name")) is not str + or type(row.get("type")) is not str + for row in rows)): + record_phase(stage, "file_stat_listing_inconclusive", on_phase) + raise Refused("BACKEND_REGION_STAT_CENSUS_INVALID") from None + matched = [row for row in rows if row["name"] == REGION_BASENAME] + if not matched: + record_phase(stage, "file_stat_listing_absent", on_phase) + raise Refused("BACKEND_REGION_NOT_LISTED_AFTER_STOP") from None + if len(matched) != 1 or matched[0]["type"] != "FILE": + record_phase(stage, "file_stat_listing_inconclusive", on_phase) + raise Refused("BACKEND_REGION_CENSUS_ENTRY_UNSAFE") from None + record_phase(stage, "file_stat_listing_present", on_phase) + raise Refused("BACKEND_REGION_STAT_REFUSED_BUT_FILE_LISTED") from None + + def capture(*, call, wait_job, fetch_bytes, stage: str, on_phase=None) -> dict: """Only after existing *actual* guest runner verified owned App then stop. @@ -50,7 +85,14 @@ def capture(*, call, wait_job, fetch_bytes, stage: str, on_phase=None) -> dict: require(app_state(call) == "STOPPED", "BACKEND_QUIESCENCE_NOT_OBSERVED") record_phase(stage, "app_stopped_verified", on_phase) record_phase(stage, "file_stat", on_phase) - stat = call("filesystem.stat", [PATH]) + try: + stat = call("filesystem.stat", [PATH]) + except Exception: + # The 019 real guest died here with only RuntimeError. Distinguish a + # missing fixed region entry from a provider/stat refusal, without + # reflecting private middleware error strings or accepting a fallback. + classify_failed_stat(call=call, stage=stage, on_phase=on_phase) + raise AssertionError("unreachable") require(type(stat) is dict and stat.get("type") == "FILE" and type(stat.get("size")) is int and 8192 <= stat["size"] <= MAX_REGION, "BACKEND_REGION_FILE_NOT_EXACT") diff --git a/scripts/truenas_middleware_minecraft_t6_probe.py b/scripts/truenas_middleware_minecraft_t6_probe.py index 704d130d0..3b70803ee 100644 --- a/scripts/truenas_middleware_minecraft_t6_probe.py +++ b/scripts/truenas_middleware_minecraft_t6_probe.py @@ -857,6 +857,8 @@ def g4_phase(code): for ph in ( "app_state_query", "app_stopped_verified", "file_stat", "file_stat_verified", + "file_stat_listing_absent", "file_stat_listing_present", + "file_stat_listing_inconclusive", "download_request", "download_route_verified", "download_job", "download_job_completed", "bytes_fetch", "bytes_verified", diff --git a/tests/test_native_vanilla263_anvil_guest_readback.py b/tests/test_native_vanilla263_anvil_guest_readback.py index e36ded493..798c67819 100644 --- a/tests/test_native_vanilla263_anvil_guest_readback.py +++ b/tests/test_native_vanilla263_anvil_guest_readback.py @@ -132,13 +132,73 @@ def injected(method, args): if method == fail_method: raise RuntimeError("secret-password provider-path /mnt/private") return previous(method, args) - with self.assertRaises(RuntimeError): + with self.assertRaises(Refused if fail_method == "filesystem.stat" + else RuntimeError) as raised: capture(call=injected, wait_job=self.wait, fetch_bytes=self.fetch, stage="candidate", on_phase=phases.append) - self.assertEqual(phases[-1], last_phase) + self.assertEqual(phases[-1], + "candidate.file_stat_listing_inconclusive" + if fail_method == "filesystem.stat" else last_phase) + if fail_method == "filesystem.stat": + self.assertEqual(str(raised.exception), + "BACKEND_REGION_STAT_CENSUS_INCONCLUSIVE") self.assertNotIn("secret-password", str(phases)) self.assertNotIn("/mnt/private", str(phases)) + def test_019_stat_failure_has_one_readonly_fixed_path_census(self): + # Synthetic 019-shaped failure only, NOT evidence of real guest file. + cases = [ + ([], "BACKEND_REGION_NOT_LISTED_AFTER_STOP", "file_stat_listing_absent"), + ([{"name": "r.0.0.mca", "type": "FILE"}], + "BACKEND_REGION_STAT_REFUSED_BUT_FILE_LISTED", "file_stat_listing_present"), + ([{"name": "r.0.0.mca", "type": "DIRECTORY"}], + "BACKEND_REGION_CENSUS_ENTRY_UNSAFE", "file_stat_listing_inconclusive"), + ([{"name": "r.0.0.mca", "type": "FILE"}]*2, + "BACKEND_REGION_CENSUS_ENTRY_UNSAFE", "file_stat_listing_inconclusive"), + ({"secret": "private"}, "BACKEND_REGION_STAT_CENSUS_INVALID", + "file_stat_listing_inconclusive"), + ([{"name": "r.0.0.mca", "type": "FILE"}]*4097, + "BACKEND_REGION_STAT_CENSUS_INVALID", "file_stat_listing_inconclusive"), + ] + for entries, expected, last in cases: + with self.subTest(reason=expected): + calls, phases = [], [] + def injected(method, args): + calls.append(method) + if method == "filesystem.stat": + self.assertEqual(args, [PATH]) + raise RuntimeError("SECRET /mnt/foreign") + if method == "filesystem.listdir": + self.assertEqual(args, [PATH.rsplit("/", 1)[0]]) + return entries + return self.call(method, args) + with self.assertRaises(Refused) as raised: + capture(call=injected, wait_job=self.wait, + fetch_bytes=self.fetch, stage="candidate", + on_phase=phases.append) + self.assertEqual(str(raised.exception), expected) + self.assertEqual(calls, ["app.query", "filesystem.stat", + "filesystem.listdir"]) + self.assertEqual(phases[-1], "candidate." + last) + self.assertEqual(self.downloaded, []) + self.assertNotIn("SECRET", str(phases) + str(raised.exception)) + + def test_019_inventory_failure_is_inconclusive_never_retry_or_escalate(self): + calls, phases = [], [] + def injected(method, args): + calls.append(method) + if method in ("filesystem.stat", "filesystem.listdir"): + raise RuntimeError("SECRET provider error") + return self.call(method, args) + with self.assertRaises(Refused) as raised: + capture(call=injected, wait_job=self.wait, + fetch_bytes=self.fetch, stage="preaction", + on_phase=phases.append) + self.assertEqual(str(raised.exception), "BACKEND_REGION_STAT_CENSUS_INCONCLUSIVE") + self.assertEqual(calls, ["app.query", "filesystem.stat", "filesystem.listdir"]) + self.assertEqual(phases[-1], "preaction.file_stat_listing_inconclusive") + self.assertEqual(self.downloaded, []) + def test_download_job_and_http_failures_are_distinct(self): cases = ( (lambda job, label: (_ for _ in ()).throw( From 725d2d9f5c7a9573d28283a5844ccc359d567722 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:23:42 +0200 Subject: [PATCH 716/719] fix(g4): use Mojang Java26.3 namespaced overworld region instead of legacy root * fix(op4m): pin Java 26.3 namespaced overworld Anvil source path, never legacy fallback * test(op4m): exact Mojang 26.1+ overworld location and negative legacy path fallback --- ..._native_vanilla263_anvil_guest_readback.py | 7 ++++-- ..._native_vanilla263_anvil_guest_readback.py | 24 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/qualify_native_vanilla263_anvil_guest_readback.py b/scripts/qualify_native_vanilla263_anvil_guest_readback.py index fafb4086a..a82c3a351 100644 --- a/scripts/qualify_native_vanilla263_anvil_guest_readback.py +++ b/scripts/qualify_native_vanilla263_anvil_guest_readback.py @@ -19,8 +19,11 @@ from qualify_vanilla263_native_world_rollback import ( APP, WORLD, app_state) -PATH = WORLD + "/region/r.0.0.mca" -REGION_DIRECTORY = WORLD + "/region" +# Minecraft Java 26.1 Snapshot 6 migrated the Overworld (including region) +# into dimensions/minecraft/overworld. The root region/ is legacy 1.21.11; +# never fall back there for the exact 26.3 signed source target. +REGION_DIRECTORY = WORLD + "/dimensions/minecraft/overworld/region" +PATH = REGION_DIRECTORY + "/r.0.0.mca" REGION_BASENAME = "r.0.0.mca" MAX_OWNED_REGION_ENTRIES = 4096 STAGES = ("preaction", "candidate", "rollback") diff --git a/tests/test_native_vanilla263_anvil_guest_readback.py b/tests/test_native_vanilla263_anvil_guest_readback.py index 798c67819..8ded7871c 100644 --- a/tests/test_native_vanilla263_anvil_guest_readback.py +++ b/tests/test_native_vanilla263_anvil_guest_readback.py @@ -3,7 +3,8 @@ import unittest -from scripts.qualify_native_vanilla263_anvil_guest_readback import capture, PATH, record_phase +from scripts.qualify_native_vanilla263_anvil_guest_readback import (capture, PATH, REGION_DIRECTORY, record_phase) +from scripts.qualify_vanilla263_native_world_rollback import WORLD from scripts.qualify_native_vanilla263_anvil_blocks import Refused from scripts.qualify_vanilla263_native_world_rollback import APP from tests.test_native_vanilla263_anvil_blocks import build @@ -48,6 +49,27 @@ def reject(self, reason): self.capture_stage() self.assertEqual(str(cm.exception), reason) + def test_java_26_3_namespaced_overworld_region_is_exclusive(self): + correct = WORLD + "/dimensions/minecraft/overworld/region" + legacy = WORLD + "/region" + self.assertEqual(REGION_DIRECTORY, correct) + self.assertEqual(PATH, correct + "/r.0.0.mca") + self.assertNotEqual(PATH, legacy + "/r.0.0.mca") + # Valid-looking bytes from the old 1.21.11 location are never read. + requested = [] + def strict(name, args): + requested.append((name, args)) + if name == "filesystem.stat" and args == [legacy + "/r.0.0.mca"]: + self.fail("legacy 1.21.11 overworld fallback attempted") + return self.call(name, args) + result = capture(call=strict, wait_job=self.wait, + fetch_bytes=self.fetch, stage="candidate") + self.assertFalse(result["native_guest_backend_independent_oracle_pass"] + if "native_guest_backend_independent_oracle_pass" in result + else result["backend_independent_world_oracle_pass"]) + self.assertIn(("filesystem.stat", [PATH]), requested) + self.assertNotIn(("filesystem.stat", [legacy + "/r.0.0.mca"]), requested) + def test_fixed_native_quiesced_read_only_path_and_nonclaim(self): result = self.capture_stage() self.assertEqual(result["classification"], From f86f380d95dbd724e808d1ccd7fecf8e29a1a5a5 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 18 Oct 2026 20:28:16 +0200 Subject: [PATCH 717/719] admission(op4m): one-use native Java 26.3 namespaced Anvil G4 qualification * admission(op4m): stage unique one-use 020 for namespaced Java 26.3 Anvil proof * test(op4m): bind unique 020 request and reject 019 and legacy Overworld path * test(op4m): bind unique 020 request and reject 019 and legacy Overworld path * test(op4m): bind unique 020 request and reject 019 and legacy Overworld path --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_catalog_parser_preflight.py | 6 +++--- tests/test_vanilla263_t6_guest_routing.py | 13 ++++++++----- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 89c82b136..62299dfa4 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4L-019 distinct one-use public-free serialized disposable KVM TrueNAS25.04.2.6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 G4 backend three-checkpoint qualification after OP4K-018 permanently CONSUMED at COLLAGE read-only join and OP4I-016 and OP4J-017 permanently consumed. Independent current source fix: COLLAGE public main b9256d6ec0dbac43be2f5a136103b6e3512ee2ec exact vanilla263-11531 opt-in profile (legacy 1.15.30 untouched), Agent Dispatch merged f590f8ade632fe4ccee5fdcadeace66fafe985a4 fixed diagnostic join. Keep pinned Foundry producer5ee204df205edfd531304441fd1a6a24004557b1 canonical control SHA 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5, upstream truenas/apps@95b0cc78e75fcd4b128f4ef2f8428034d981f7c2, exact Java25 OCI sha256:5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da, immutable 739-byte Lantern zip sha256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8. Public cheap source/negative tests before one independent run; current official catalog 1.15.31 fail-closed guard and source/artifact admission required. Existing bounded ephemeral TLS APPS_READ Go native owner readback six fields zero observer mutations; G3 exact-owned ZFS injected-fault snapshot/rollback; 127.0.0.1 QEMU synthetic Mineflayer 26.3/777 client (RCON disabled and signed source online-mode policy unchanged); three independent STOPPED server-owned r.0.0.mca Anvil captures preaction/candidate/rollback and block-state/digest causality; final independently verified receipt and owned App/world zero-residue. This request creates neither COLLAGE principal-authorized APPLY nor physical/production TrueNAS APPLY, no Microsoft/VIP/stock identity, full Two Rivers MCWORLD or G5 qualification; preservation of one-use ids 016/017/018 is mandatory. No private hosted Actions minutes.", + "purpose": "OP4M-020 unique one-use $0 public serialized disposable KVM TrueNAS25.04.2.6 T6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 independent three-checkpoint server-owned Anvil backend after OP4L-019 permanently CONSUMED on obsolete pre-26.1 root-level world/region path. OP4I-016 permanently consumed; OP4J-017 permanently consumed; OP4K-018 permanently CONSUMED; OP4L-019 permanently CONSUMED; NEVER REPLAY any historical one-use request. Mojang official Java26.1 Snapshot6 changed Overworld region path to world/dimensions/minecraft/overworld/region/r.0.0.mca; exact source repaired in Agent Dispatch experiment 725d2d9f5c7a9573d28283a5844ccc359d567722, and independent real Java25/26.3 stopped OCI/OP3C/OP4E physical byte presence (not TrueNAS G4) from SupraCraft/mc-server-images bed73818a39208a31d90ffc9d4aab0d57affc65a. Pinned COLLAGE b9256d6ec0dbac43be2f5a136103b6e3512ee2ec explicit vanilla263-11531 TLS APPS_READ-only six ownership markers; no observation mutations, no authenticated principal-authorized COLLAGE APPLY. Keep immutable Foundry source5ee204df205edfd531304441fd1a6a24004557b1 canonical control sha256 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5 and truenas/apps@95b0cc78e75fcd4b128f4ef2f8428034d981f7c2, exact Java25 OCI digest sha256:5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da, immutable original 739-byte Lantern datapack sha256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8. Strict isolated world ZFS owned dataset and G3 exact owned snapshot/injected-fault rollback with stopped App, synthetic Mineflayer protocol777 127.0.0.1 QEMU loopback (no stock Mojang account/VIP, no external ingress, RCON disabled, signed operational online-mode unchanged), bounded source pin. Three STOPPED preaction/candidate/rollback fixed namespaced Overworld r.0.0.mca Anvil captures through independent read-only TrueNAS filesystem.stat/core.download, independent block-state and three distinct stage checksums, exact service/owner checks and independent receipt verifier with full App/world zero-residue and platform-health cleanup. Only one fresh run after independent public cheap red/blue CI, latest source/control/catalog and verified idle heavyweight runner queue. All prior 016-019 CONSUMED. No private GHA-hosted minutes, no physical TrueNAS APPLY, no release/world artifact promotion, no authenticated COLLAGE mutation, no G5, no complete Two Rivers MCWORLD or operational approval.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index c60ba2fca..76faad859 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) diff --git a/tests/test_vanilla263_catalog_parser_preflight.py b/tests/test_vanilla263_catalog_parser_preflight.py index afaddf0e8..948c82305 100644 --- a/tests/test_vanilla263_catalog_parser_preflight.py +++ b/tests/test_vanilla263_catalog_parser_preflight.py @@ -70,9 +70,9 @@ def test_runner_uses_same_qualified_exact_catalog_without_modifying_request(self self.assertIn('--native-catalog-profile "1.15.30"', harness) self.assertIn("--expected 1.15.31", workflow) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") - self.assertNotIn("018", request["request_id"]) - self.assertIn("019", request["request_id"]) + "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + self.assertNotIn("019", request["request_id"]) + self.assertIn("020", request["request_id"]) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["authority_issue"], 423) self.assertEqual(set(request), { diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index d4b9e44f9..d1f137be8 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,10 +52,13 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") + self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + self.assertIn("OP4L-019 permanently CONSUMED",current["purpose"]) + self.assertIn("dimensions/minecraft/overworld/region/r.0.0.mca",current["purpose"]) self.assertIn("OP4K-018",current["purpose"]) self.assertIn("permanently CONSUMED",current["purpose"]) self.assertIn("OP4J-017", current["purpose"]) @@ -93,7 +96,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -128,7 +131,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -148,7 +151,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) @@ -172,7 +175,7 @@ def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("native_backend_region"), True) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") for needle in ("truenas_native_backend_region=false", "(.native_backend_region // false)", "truenas_native_backend_region=$truenas_native_backend_region", From 2376961b4c5d2bb79843046774249cca02dc6888 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:42:39 +0200 Subject: [PATCH 718/719] fix(g4): accept exact TrueNAS single-use job-bound Anvil download URL * fix(g4): validate TrueNAS exact job-bound single-use Anvil download URL * test(g4): pin TrueNAS job+auth_token URL and reject redirects/job-mismatch/replays --- ..._native_vanilla263_anvil_guest_readback.py | 36 +++++++++--- ..._native_vanilla263_anvil_guest_readback.py | 57 ++++++++++++++++--- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/scripts/qualify_native_vanilla263_anvil_guest_readback.py b/scripts/qualify_native_vanilla263_anvil_guest_readback.py index a82c3a351..6754c0168 100644 --- a/scripts/qualify_native_vanilla263_anvil_guest_readback.py +++ b/scripts/qualify_native_vanilla263_anvil_guest_readback.py @@ -8,6 +8,8 @@ """ from __future__ import annotations +import re + try: from .qualify_native_vanilla263_anvil_blocks import ( MAX_REGION, SCHEMA, Refused, inspect, require) @@ -76,6 +78,28 @@ def classify_failed_stat(*, call, stage: str, on_phase=None) -> None: raise Refused("BACKEND_REGION_STAT_REFUSED_BUT_FILE_LISTED") from None + +def validate_single_use_download(row: object) -> tuple[int, str]: + """Accept only the exact job-bound TrueNAS single-use download URL. + + Pinned truenas/middleware@244b717 service/core_service.py issues + /_download/{job.id}?auth_token={token}. file_app.py validates both + token and job ID. Never print or copy the bearer URL into a receipt. + This adapter cannot grant a token or broaden the underlying credential. + """ + require(type(row) is list and len(row) == 2 and + type(row[0]) is int and 0 < row[0] < 10**12 and + type(row[1]) is str and len(row[1]) < 400, + "BACKEND_DOWNLOAD_ROUTE_INVALID") + job, route = row + # One fixed-origin relative path; exactly one job-bound query parameter. + # No foreign authority, fragment, traversal, percent escapes or extra args. + require(re.fullmatch( + rf"/_download/{job}\?auth_token=[A-Za-z0-9_-]{{16,256}}", route + ) is not None, "BACKEND_DOWNLOAD_ROUTE_INVALID") + return job, route + + def capture(*, call, wait_job, fetch_bytes, stage: str, on_phase=None) -> dict: """Only after existing *actual* guest runner verified owned App then stop. @@ -103,19 +127,13 @@ def capture(*, call, wait_job, fetch_bytes, stage: str, on_phase=None) -> dict: record_phase(stage, "download_request", on_phase) row = call("core.download", [ "filesystem.get", [PATH], "g4-owned-region-" + stage + ".mca", True]) - require(type(row) is list and len(row) == 2 and - type(row[0]) is int and row[0] > 0 and - type(row[1]) is str and - row[1].startswith("/_download/") and - len(row[1]) <= 300 and - ".." not in row[1] and "?" not in row[1] and "#" not in row[1], - "BACKEND_DOWNLOAD_ROUTE_INVALID") + job_id, single_use_route = validate_single_use_download(row) record_phase(stage, "download_route_verified", on_phase) record_phase(stage, "download_job", on_phase) - wait_job(row[0], "G4 read-only owned Anvil file download") + wait_job(job_id, "G4 read-only owned Anvil file download") record_phase(stage, "download_job_completed", on_phase) record_phase(stage, "bytes_fetch", on_phase) - blob = fetch_bytes(row[1]) + blob = fetch_bytes(single_use_route) require(type(blob) is bytes and len(blob) == stat["size"], "BACKEND_DOWNLOADED_FILE_DRIFT") record_phase(stage, "bytes_verified", on_phase) diff --git a/tests/test_native_vanilla263_anvil_guest_readback.py b/tests/test_native_vanilla263_anvil_guest_readback.py index 8ded7871c..98e635571 100644 --- a/tests/test_native_vanilla263_anvil_guest_readback.py +++ b/tests/test_native_vanilla263_anvil_guest_readback.py @@ -3,7 +3,7 @@ import unittest -from scripts.qualify_native_vanilla263_anvil_guest_readback import (capture, PATH, REGION_DIRECTORY, record_phase) +from scripts.qualify_native_vanilla263_anvil_guest_readback import (capture, PATH, REGION_DIRECTORY, record_phase, validate_single_use_download) from scripts.qualify_vanilla263_native_world_rollback import WORLD from scripts.qualify_native_vanilla263_anvil_blocks import Refused from scripts.qualify_vanilla263_native_world_rollback import APP @@ -15,7 +15,7 @@ def setUp(self): self.blob = build("candidate") self.state = "STOPPED" self.stat = {"type": "FILE", "size": len(self.blob)} - self.token = "/_download/synthetic-test-opaque" + self.token = "/_download/1001?auth_token=" + ("A" * 64) self.record = [] self.downloaded = [] @@ -102,12 +102,55 @@ def test_refuse_missing_or_unsafe_file_stat(self): self.stat = attr self.reject("BACKEND_REGION_FILE_NOT_EXACT") - def test_refuse_injected_redirect_or_query(self): - for path in ("https://foreign.invalid/a", "/_download/../secrets", - "/_download/a?x=secret", "/_download/a#fragment", "/outside"): - with self.subTest(path=path): - self.token = path + def test_vendor_true_nas_250426_download_shape_is_accepted(self): + # Actual pinned middleware core_service.py's return URL format. + # Token values are synthetic: no real host, credential or guest access. + route = "/_download/1001?auth_token=" + ("Qw7Nm3Kz" * 8) + job, selected = validate_single_use_download([1001, route]) + self.assertEqual((job, selected), (1001, route)) + self.token = route + self.assertEqual(self.capture_stage()["classification"], + "SOURCE_ONLY_OWNED_ANVIL_DOWNLOAD_SEAM") + self.assertEqual(self.downloaded, [(1001, "G4 read-only owned Anvil file download")]) + self.assertNotIn("auth_token", str(self.downloaded)) + + def test_fail_closed_foreign_redirect_token_job_replay_and_extra_query(self): + token = "SENSITIVE" * 8 + allowed = "/_download/1001?auth_token=" + token + candidates = ( + "https://foreign.invalid" + allowed, + "//foreign.invalid" + allowed, + "/_download/../1001?auth_token=" + token, + "/_download/1002?auth_token=" + token, + "/_download/01001?auth_token=" + token, + "/_download/1001", + "/_download/1001?x=" + token, + allowed + "&root=1", + allowed + "?extra=1", + allowed + "#fragment", + allowed.replace("auth_token=", "auth_token=%41"), + "/_download/1001?auth_token=a", + allowed + "/suffix", + ) + for candidate in candidates: + with self.subTest(route_kind=candidate.split("?")[0][:20]): + self.token = candidate self.reject("BACKEND_DOWNLOAD_ROUTE_INVALID") + self.assertEqual(self.downloaded, []) + self.assertNotIn(token, str(self.downloaded)) + for row in ([True, allowed], ["1001", allowed], + [0, allowed], [1002, allowed], [1001, allowed, "other"], + {"job": 1001, "url": allowed}, [1001, None]): + with self.subTest(type_row=str(type(row))): + with self.assertRaisesRegex(Refused, + "BACKEND_DOWNLOAD_ROUTE_INVALID") as cm: + validate_single_use_download(row) + self.assertNotIn(token, str(cm.exception)) + + def test_old_non_query_synthetic_download_is_rejected(self): + self.token = "/_download/synthetic-test-opaque" + self.reject("BACKEND_DOWNLOAD_ROUTE_INVALID") + self.assertEqual(self.downloaded, []) def test_refuse_wrong_file_bytes(self): self.blob = self.blob[:-1] From e400f3a1719a21cf91ebc86ba8d44efea05c1325 Mon Sep 17 00:00:00 2001 From: "Mark E. DeYoung" <1854350+mark-e-deyoung@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:15:21 +0200 Subject: [PATCH 719/719] admission(op4n): one-use native G4 job-bound Anvil download rep * admission(op4n): prepare fresh single-use 021 Anvil job-bound download qualification * test(op4n): assert distinct 021 request; permanently refuse consumed 020 replay * test(op4n): assert distinct 021 request; permanently refuse consumed 020 replay * test(op4n): assert distinct 021 request; permanently refuse consumed 020 replay --- config/truenas-rdte-run-request.json | 4 ++-- tests/test_truenas_rdte_target.py | 2 +- tests/test_vanilla263_catalog_parser_preflight.py | 6 +++--- tests/test_vanilla263_t6_guest_routing.py | 14 +++++++++----- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/config/truenas-rdte-run-request.json b/config/truenas-rdte-run-request.json index 62299dfa4..84693e241 100644 --- a/config/truenas-rdte-run-request.json +++ b/config/truenas-rdte-run-request.json @@ -1,12 +1,12 @@ { "schema": "gha-kvm-truenas-run-request/v1", - "request_id": "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint", + "request_id": "tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint", "version": "25.04.2.6", "rung": "t6", "product": "minecraft-vanilla-263", "authority_issue": 423, "consumer_authority_issue": 627, - "purpose": "OP4M-020 unique one-use $0 public serialized disposable KVM TrueNAS25.04.2.6 T6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 independent three-checkpoint server-owned Anvil backend after OP4L-019 permanently CONSUMED on obsolete pre-26.1 root-level world/region path. OP4I-016 permanently consumed; OP4J-017 permanently consumed; OP4K-018 permanently CONSUMED; OP4L-019 permanently CONSUMED; NEVER REPLAY any historical one-use request. Mojang official Java26.1 Snapshot6 changed Overworld region path to world/dimensions/minecraft/overworld/region/r.0.0.mca; exact source repaired in Agent Dispatch experiment 725d2d9f5c7a9573d28283a5844ccc359d567722, and independent real Java25/26.3 stopped OCI/OP3C/OP4E physical byte presence (not TrueNAS G4) from SupraCraft/mc-server-images bed73818a39208a31d90ffc9d4aab0d57affc65a. Pinned COLLAGE b9256d6ec0dbac43be2f5a136103b6e3512ee2ec explicit vanilla263-11531 TLS APPS_READ-only six ownership markers; no observation mutations, no authenticated principal-authorized COLLAGE APPLY. Keep immutable Foundry source5ee204df205edfd531304441fd1a6a24004557b1 canonical control sha256 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5 and truenas/apps@95b0cc78e75fcd4b128f4ef2f8428034d981f7c2, exact Java25 OCI digest sha256:5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da, immutable original 739-byte Lantern datapack sha256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8. Strict isolated world ZFS owned dataset and G3 exact owned snapshot/injected-fault rollback with stopped App, synthetic Mineflayer protocol777 127.0.0.1 QEMU loopback (no stock Mojang account/VIP, no external ingress, RCON disabled, signed operational online-mode unchanged), bounded source pin. Three STOPPED preaction/candidate/rollback fixed namespaced Overworld r.0.0.mca Anvil captures through independent read-only TrueNAS filesystem.stat/core.download, independent block-state and three distinct stage checksums, exact service/owner checks and independent receipt verifier with full App/world zero-residue and platform-health cleanup. Only one fresh run after independent public cheap red/blue CI, latest source/control/catalog and verified idle heavyweight runner queue. All prior 016-019 CONSUMED. No private GHA-hosted minutes, no physical TrueNAS APPLY, no release/world artifact promotion, no authenticated COLLAGE mutation, no G5, no complete Two Rivers MCWORLD or operational approval.", + "purpose": "OP4N-021 unique one-use $0 public serialized disposable KVM TrueNAS25.04.2.6 T6 native community Minecraft App1.15.31/lib2.3.16 Java25 vanilla26.3/protocol777 independently verified G4 three-checkpoint stopped server-owned Anvil backend after OP4M-020 permanently CONSUMED at preaction.download_request on provider/core.download URL contract. OP4I-016 permanently CONSUMED, OP4J-017 permanently consumed, OP4K-018 permanently CONSUMED, OP4L-019 permanently CONSUMED, OP4M-020 permanently CONSUMED; NEVER REPLAY historical one-use request ids. Source qualified read-only TrueNAS25.04.2.6 middleware@244b717370fe35fb9eefc3096acfbc25f12b57b6: core.download issues /_download/{same job}?auth_token={single-use origin-bound token}; Agent Dispatch accepted experiment 2376961b4c5d2bb79843046774249cca02dc6888 source consumer independently validates only exact same job + one bounded token, no foreign URL, replay, credential issuance or permission broadening. Exact Minecraft26.3 Overworld Anvil path world/dimensions/minecraft/overworld/region/r.0.0.mca (Mojang 26.1+ migration), positive stopped actual Java25/26.3 OCI/OP3C/OP4E source producer SupraCraft/mc-server-images@bed73818a39208a31d90ffc9d4aab0d57affc65a; no legacy root fallback. COLLAGE source b9256d6ec0dbac43be2f5a136103b6e3512ee2ec explicit vanilla263-11531 TLS APPS_READ-only six ownership markers, zero observation mutations, no authenticated COLLAGE principal APPLY. Immutable Foundry source5ee204df205edfd531304441fd1a6a24004557b1 and canonical control sha256 59516bbbcb1750af6f0cb1c624756f08113a070acc11c60affe94d2bbd8d8dd5, vendor truenas/apps@95b0cc78e75fcd4b128f4ef2f8428034d981f7c2 exact App1.15.31, Java25 OCI sha256:5a2ab74ae179c11e2dbbce67f307cdcf2c1257b3abb0f654d3a310b3d8ea57da and exact 739-byte Lantern zip sha256 da25005e8729e6f013f7108ac5b8869dd46323befc78efdae18221b26cafe5f8. One ephemeral owned ZFS world with G3 isolated baseline snapshot/injected-fault rollback; actual synthetic Mineflayer offline protocol777 player over 127.0.0.1 QEMU user-loopback only (RCON off, no real Mojang identity, no internet ingress, signed operational online-mode unchanged). Independently observe three STOPPED app/server-owned preaction/candidate/rollback namespaced r.0.0.mca Anvil captures through bounded stat then one TrueNAS origin-/job-bound single-use core.download with one HTTP GET; validate independent block-state/digest causality and exact world owner/session, G3 restore, cleanup native App/dataset zero residue and platform health, retained sanitized receipt. Only after fresh source/control/catalog and serialized public heavyweight queue admission, no private GHA-hosted minutes. Physical TrueNAS APPLY, authorized COLLAGE mutation/release, stock client HIL, G5, full Two Rivers gameplay/world and production operations remain HOLD.", "collage_readback": true, "lantern_stage": true, "native_world_rollback": true, diff --git a/tests/test_truenas_rdte_target.py b/tests/test_truenas_rdte_target.py index 76faad859..b5f0b84b0 100644 --- a/tests/test_truenas_rdte_target.py +++ b/tests/test_truenas_rdte_target.py @@ -92,7 +92,7 @@ def test_run_request_is_exact_registered_target(self): self.assertEqual(request["authority_issue"], 423) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint") self.assertIs(request.get("collage_readback"), True) self.assertIs(request.get("lantern_stage"), True) self.assertIs(request.get("native_offline_player"), True) diff --git a/tests/test_vanilla263_catalog_parser_preflight.py b/tests/test_vanilla263_catalog_parser_preflight.py index 948c82305..3407245a9 100644 --- a/tests/test_vanilla263_catalog_parser_preflight.py +++ b/tests/test_vanilla263_catalog_parser_preflight.py @@ -70,9 +70,9 @@ def test_runner_uses_same_qualified_exact_catalog_without_modifying_request(self self.assertIn('--native-catalog-profile "1.15.30"', harness) self.assertIn("--expected 1.15.31", workflow) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") - self.assertNotIn("019", request["request_id"]) - self.assertIn("020", request["request_id"]) + "tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint") + self.assertNotIn("020", request["request_id"]) + self.assertIn("021", request["request_id"]) self.assertEqual(request["consumer_authority_issue"], 627) self.assertEqual(request["authority_issue"], 423) self.assertEqual(set(request), { diff --git a/tests/test_vanilla263_t6_guest_routing.py b/tests/test_vanilla263_t6_guest_routing.py index d1f137be8..56cabd40a 100644 --- a/tests/test_vanilla263_t6_guest_routing.py +++ b/tests/test_vanilla263_t6_guest_routing.py @@ -52,12 +52,16 @@ def test_existing_request_edge_untouched_during_source_qualification(self): self.assertEqual(current["rung"],"t6") self.assertEqual(current["authority_issue"],423) self.assertEqual(current["consumer_authority_issue"],627) - self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + self.assertEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4i-016-backend-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4j-017-backend-phase-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4k-018-parsed-native-anvil-three-checkpoint") self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4l-019-pinned-readonly-join-anvil-three-checkpoint") self.assertIn("OP4L-019 permanently CONSUMED",current["purpose"]) + self.assertNotEqual(current["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + self.assertIn("OP4M-020 permanently CONSUMED",current["purpose"]) + self.assertIn("core.download",current["purpose"]) + self.assertIn("single-use",current["purpose"]) self.assertIn("dimensions/minecraft/overworld/region/r.0.0.mca",current["purpose"]) self.assertIn("OP4K-018",current["purpose"]) self.assertIn("permanently CONSUMED",current["purpose"]) @@ -96,7 +100,7 @@ def test_scoped_native_collage_readback_explicit_only_not_default_guest(self): self.assertIn('"${NATIVE_READBACK_ARGS[@]}"',HARNESS) request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("collage_readback"),True) - self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + self.assertEqual(request["request_id"],"tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint") self.assertEqual(request["product"],"minecraft-vanilla-263") self.assertEqual(request["version"],"25.04.2.6") self.assertNotIn("--vanilla-collage-readback", @@ -131,7 +135,7 @@ def test_real_lantern_native_zfs_stage_never_autoruns_or_promotes_gameplay(self) def test_native_rollback_requires_exact_unique_disposable_request_and_g2(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint") self.assertIs(request.get("native_world_rollback"),True) self.assertIs(request.get("collage_readback"),True) self.assertIs(request.get("lantern_stage"),True) @@ -151,7 +155,7 @@ def test_native_synthetic_client_is_opt_in_and_bound_to_g3_owned_qemu_forward(se request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) # 012 is consumed; source qualification of G4 cannot replay it. self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint") self.assertIs(request.get("native_offline_player"),True) self.assertIn("truenas_native_offline_player=false",WORKFLOW) self.assertIn("(.native_offline_player // false)",WORKFLOW) @@ -175,7 +179,7 @@ def test_native_backend_three_checkpoint_is_opt_in_without_consuming_015(self): request=json.loads((ROOT/"config/truenas-rdte-run-request.json").read_text()) self.assertIs(request.get("native_backend_region"), True) self.assertEqual(request["request_id"], - "tn-25-04-2-6-t6-vanilla-263-op4m-020-namespaced-overworld-anvil-three-checkpoint") + "tn-25-04-2-6-t6-vanilla-263-op4n-021-job-bound-single-use-anvil-three-checkpoint") for needle in ("truenas_native_backend_region=false", "(.native_backend_region // false)", "truenas_native_backend_region=$truenas_native_backend_region",