From 0c6f240f74876e8540dc1e6c86a543a30a670511 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 03:10:20 -0400 Subject: [PATCH 01/11] claim: codex-b0855-1-zeta-self-register-service-20260527 - scope B-0855.1 Co-Authored-By: Codex --- ...0855-1-zeta-self-register-service-20260527.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md diff --git a/docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md b/docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md new file mode 100644 index 0000000000..60954a5f2f --- /dev/null +++ b/docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md @@ -0,0 +1,16 @@ +# Claim - codex-b0855-1-zeta-self-register-service-20260527 + +- **Session ID:** codex/20260527T0708Z +- **Harness:** codex +- **Claimed at:** 2026-05-27T07:08:30Z +- **ETA:** 2026-05-27T08:08:30Z progress signal or release +- **Scope:** B-0855.1 only: add the `zeta-self-register.service` NixOS module surface for post-install first-boot self-registration timing. +- **Durable target:** `docs/backlog/P1/B-0855-self-registration-fires-LAST-post-install-post-first-boot-idempotent-across-reboots-deduped-against-in-flight-registration-prs-aaron-2026-05-27.md` +- **Platform mirror:** none yet + +## Notes + +- Exclude B-0855.2+ implementation work unless this claim is explicitly widened later. +- Exclude removal of `zeta-install.sh` Step 6.9; that is B-0855.5. +- Keep the contested root checkout read-only. Use this dedicated worktree for any patch. +- Before implementation, inspect current installer/NixOS module layout and active heartbeats again for overlapping path claims. From 5bb7f2d8281b5a598a257319288fb370d2de038e Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 03:16:22 -0400 Subject: [PATCH 02/11] feat: codex-b0855-1-zeta-self-register-service-20260527 - add first-boot self-register service Co-Authored-By: Codex --- full-ai-cluster/flake.nix | 1 + full-ai-cluster/nixos/modules/common.nix | 5 + .../nixos/modules/zeta-self-register.nix | 92 +++++++++++++++++++ tools/ci/audit-installer-substrate.ts | 17 ++++ 4 files changed, 115 insertions(+) create mode 100644 full-ai-cluster/nixos/modules/zeta-self-register.nix diff --git a/full-ai-cluster/flake.nix b/full-ai-cluster/flake.nix index cb1cb0376a..e88805ff48 100644 --- a/full-ai-cluster/flake.nix +++ b/full-ai-cluster/flake.nix @@ -129,6 +129,7 @@ docker = ./nixos/modules/docker.nix; local-storage = ./nixos/modules/local-storage.nix; longhorn-disks = ./nixos/modules/longhorn-disks.nix; + zeta-self-register = ./nixos/modules/zeta-self-register.nix; disko-shape-2nvme = ./nixos/modules/disko-shapes/2nvme.nix; }; diff --git a/full-ai-cluster/nixos/modules/common.nix b/full-ai-cluster/nixos/modules/common.nix index e303a10271..961a963592 100644 --- a/full-ai-cluster/nixos/modules/common.nix +++ b/full-ai-cluster/nixos/modules/common.nix @@ -30,6 +30,11 @@ # Per-vendor implementation lands via B-0850 Phase 3 sub-rows # (3a-3h) that add install + login flows for each vendor's CLI. ./zeta-ai-agent.nix + # B-0855.1: post-install first-boot self-registration service. + # Disabled by default until host configs opt in after B-0855.2 + # ships the TS implementation; imported here so every node type + # has the same module surface. + ./zeta-self-register.nix ]; nix.settings = { diff --git a/full-ai-cluster/nixos/modules/zeta-self-register.nix b/full-ai-cluster/nixos/modules/zeta-self-register.nix new file mode 100644 index 0000000000..a4a9f2c774 --- /dev/null +++ b/full-ai-cluster/nixos/modules/zeta-self-register.nix @@ -0,0 +1,92 @@ +# full-ai-cluster/nixos/modules/zeta-self-register.nix +# +# B-0855.1: NixOS service surface for post-install self-registration. +# The service is intentionally disabled by default until B-0855.2 ships +# the TypeScript implementation at tools/installer/zeta-self-register.ts. +# Once enabled by a host config, it fires on first boot of the installed +# OS, after network-online and credential-restore ordering, instead of +# running inside the live-USB installer environment. + +{ config, lib, ... }: + +let + cfg = config.zeta.selfRegister; +in +{ + options.zeta.selfRegister = { + enable = lib.mkEnableOption "Zeta post-install first-boot self-registration service"; + + user = lib.mkOption { + type = lib.types.str; + default = "zeta"; + description = "User that runs zeta-self-register.service."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "users"; + description = "Primary group for the self-registration service user."; + }; + + home = lib.mkOption { + type = lib.types.str; + default = "/home/zeta"; + description = "Home directory used for credentials and local marker state."; + }; + + repoRoot = lib.mkOption { + type = lib.types.str; + default = "/home/zeta/Zeta"; + description = "Path to the checked-out Zeta repository on the installed node."; + }; + + scriptPath = lib.mkOption { + type = lib.types.str; + default = "/home/zeta/Zeta/tools/installer/zeta-self-register.ts"; + description = "Bun TypeScript entrypoint for composing self-registration intent."; + }; + + markerPath = lib.mkOption { + type = lib.types.str; + default = "/home/zeta/.config/zeta/self-registered.marker"; + description = "Fast-path local marker written after registration intent exists."; + }; + + intentDir = lib.mkOption { + type = lib.types.str; + default = "/home/zeta/.config/zeta/self-registration-intent"; + description = "Directory where the service writes registration intent for the local agent steward."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.zeta-self-register = { + description = "Zeta node self-registration intent writer (B-0855.1)"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ + "network-online.target" + "zeta-creds-restore.service" + ]; + + unitConfig = { + ConditionFirstBoot = "yes"; + }; + + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.repoRoot; + Environment = [ + "HOME=${cfg.home}" + "PATH=${cfg.home}/.bun/bin:/run/current-system/sw/bin:/usr/bin:/bin" + "ZETA_SELF_REGISTER_MARKER=${cfg.markerPath}" + "ZETA_SELF_REGISTER_INTENT_DIR=${cfg.intentDir}" + "ZETA_SELF_REGISTER_REPO=${cfg.repoRoot}" + ]; + ExecStart = "${cfg.home}/.bun/bin/bun ${cfg.scriptPath}"; + }; + }; + }; +} diff --git a/tools/ci/audit-installer-substrate.ts b/tools/ci/audit-installer-substrate.ts index 85c72b2acd..9bedf140e1 100644 --- a/tools/ci/audit-installer-substrate.ts +++ b/tools/ci/audit-installer-substrate.ts @@ -69,6 +69,8 @@ const REQUIRED_FILES: readonly FileAssertion[] = [ { path: "full-ai-cluster/nixos/modules/common.nix", minBytes: 500 }, { path: "full-ai-cluster/nixos/modules/injected-hostname.nix" }, { path: "full-ai-cluster/nixos/modules/login-banner.nix" }, + // B-0855.1 post-install first-boot self-registration service surface + { path: "full-ai-cluster/nixos/modules/zeta-self-register.nix", minBytes: 500 }, // operator-side flash tool (B-0789 + iter-5.x) { path: "full-ai-cluster/tools/zflash.ts", minBytes: 1000 }, ]; @@ -126,9 +128,24 @@ const REQUIRED_SENTINELS: readonly SentinelAssertion[] = [ "./login-banner.nix", // iter-5.2.2 pre-login banner module "services.avahi", // iter-5.1 mDNS publishing "nssmdns4", // Avahi mDNS via nss + "./zeta-self-register.nix", // B-0855.1 first-boot self-registration module ], rationale: "common.nix must import the iter-5.x modules so every host inherits them", }, + { + path: "full-ai-cluster/nixos/modules/zeta-self-register.nix", + mustContain: [ + "systemd.services.zeta-self-register", // service unit exists + "Type = \"oneshot\"", // fires once, not a loop + "ConditionFirstBoot", // installed-OS first-boot gate + "network-online.target", // waits for network before registration intent + "zeta-creds-restore.service", // ordered after restored creds when that service exists + "tools/installer/zeta-self-register.ts", // delegates implementation to B-0855.2 + "ZETA_SELF_REGISTER_MARKER", // marker path exported to implementation + "ZETA_SELF_REGISTER_INTENT_DIR", // intent handoff dir exported to implementation + ], + rationale: "B-0855.1 service must be a post-install first-boot oneshot ordered after network and credential restore surfaces", + }, { path: "full-ai-cluster/nixos/modules/injected-hostname.nix", mustContain: [ From eb6dbad52bb31d3ed54f8f7ad82f1850b731ba06 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 03:19:47 -0400 Subject: [PATCH 03/11] release: codex-b0855-1-zeta-self-register-service-20260527 - opened in PR #5416 Co-Authored-By: Codex --- ...0855-1-zeta-self-register-service-20260527.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md diff --git a/docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md b/docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md deleted file mode 100644 index 60954a5f2f..0000000000 --- a/docs/claims/codex-b0855-1-zeta-self-register-service-20260527.md +++ /dev/null @@ -1,16 +0,0 @@ -# Claim - codex-b0855-1-zeta-self-register-service-20260527 - -- **Session ID:** codex/20260527T0708Z -- **Harness:** codex -- **Claimed at:** 2026-05-27T07:08:30Z -- **ETA:** 2026-05-27T08:08:30Z progress signal or release -- **Scope:** B-0855.1 only: add the `zeta-self-register.service` NixOS module surface for post-install first-boot self-registration timing. -- **Durable target:** `docs/backlog/P1/B-0855-self-registration-fires-LAST-post-install-post-first-boot-idempotent-across-reboots-deduped-against-in-flight-registration-prs-aaron-2026-05-27.md` -- **Platform mirror:** none yet - -## Notes - -- Exclude B-0855.2+ implementation work unless this claim is explicitly widened later. -- Exclude removal of `zeta-install.sh` Step 6.9; that is B-0855.5. -- Keep the contested root checkout read-only. Use this dedicated worktree for any patch. -- Before implementation, inspect current installer/NixOS module layout and active heartbeats again for overlapping path claims. From 6626ccf02f1e8d909a49b9dd93f14ebdc799ff79 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 03:29:12 -0400 Subject: [PATCH 04/11] fix: codex-b0855-1-zeta-self-register-service-20260527 - derive self-register path defaults Co-Authored-By: Codex --- full-ai-cluster/nixos/modules/zeta-self-register.nix | 8 ++++---- tools/ci/audit-installer-substrate.ts | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/full-ai-cluster/nixos/modules/zeta-self-register.nix b/full-ai-cluster/nixos/modules/zeta-self-register.nix index a4a9f2c774..266358771c 100644 --- a/full-ai-cluster/nixos/modules/zeta-self-register.nix +++ b/full-ai-cluster/nixos/modules/zeta-self-register.nix @@ -36,25 +36,25 @@ in repoRoot = lib.mkOption { type = lib.types.str; - default = "/home/zeta/Zeta"; + default = "${cfg.home}/Zeta"; description = "Path to the checked-out Zeta repository on the installed node."; }; scriptPath = lib.mkOption { type = lib.types.str; - default = "/home/zeta/Zeta/tools/installer/zeta-self-register.ts"; + default = "${cfg.repoRoot}/tools/installer/zeta-self-register.ts"; description = "Bun TypeScript entrypoint for composing self-registration intent."; }; markerPath = lib.mkOption { type = lib.types.str; - default = "/home/zeta/.config/zeta/self-registered.marker"; + default = "${cfg.home}/.config/zeta/self-registered.marker"; description = "Fast-path local marker written after registration intent exists."; }; intentDir = lib.mkOption { type = lib.types.str; - default = "/home/zeta/.config/zeta/self-registration-intent"; + default = "${cfg.home}/.config/zeta/self-registration-intent"; description = "Directory where the service writes registration intent for the local agent steward."; }; }; diff --git a/tools/ci/audit-installer-substrate.ts b/tools/ci/audit-installer-substrate.ts index 9bedf140e1..ce7c23852b 100644 --- a/tools/ci/audit-installer-substrate.ts +++ b/tools/ci/audit-installer-substrate.ts @@ -140,6 +140,10 @@ const REQUIRED_SENTINELS: readonly SentinelAssertion[] = [ "ConditionFirstBoot", // installed-OS first-boot gate "network-online.target", // waits for network before registration intent "zeta-creds-restore.service", // ordered after restored creds when that service exists + 'default = "${cfg.home}/Zeta";', // repoRoot derives from home override + 'default = "${cfg.repoRoot}/tools/installer/zeta-self-register.ts";', // scriptPath derives from repoRoot override + 'default = "${cfg.home}/.config/zeta/self-registered.marker";', // markerPath derives from home override + 'default = "${cfg.home}/.config/zeta/self-registration-intent";', // intentDir derives from home override "tools/installer/zeta-self-register.ts", // delegates implementation to B-0855.2 "ZETA_SELF_REGISTER_MARKER", // marker path exported to implementation "ZETA_SELF_REGISTER_INTENT_DIR", // intent handoff dir exported to implementation From d87acce27ab303ee2f3a95bef97eb22e36ce167a Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Wed, 27 May 2026 04:40:15 -0400 Subject: [PATCH 05/11] fix: repair Docker NixOS install-sh harness (#5427) Co-Authored-By: Codex --- .../nixos-install-sh-test/Dockerfile | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tools/ci/dockerfiles/nixos-install-sh-test/Dockerfile b/tools/ci/dockerfiles/nixos-install-sh-test/Dockerfile index b7dfc9a855..1ead6d66a2 100644 --- a/tools/ci/dockerfiles/nixos-install-sh-test/Dockerfile +++ b/tools/ci/dockerfiles/nixos-install-sh-test/Dockerfile @@ -46,7 +46,58 @@ RUN touch /etc/NIXOS # mise/bun aren't on PATH in the fresh layer's shell). install.sh # installs mise to ~/.local/bin/mise and shims to ~/.local/share/mise/ # shims/; bun --global lands at ~/.bun/bin/. -ENV PATH=/root/.bun/bin:/root/.local/share/mise/shims:/root/.local/bin:/usr/local/bin:/usr/bin:/bin +ENV PATH=/root/.bun/bin:/root/.local/share/mise/shims:/root/.local/bin:/nix/var/nix/profiles/default/bin:/usr/local/bin:/usr/bin:/bin + +# Install the NixOS userspace commands install.sh invokes plus the runtime +# libraries expected by the dynamically linked .mise.toml toolchain downloads +# (node, dotnet, java, etc.). nix-env rebuilds the default profile, so keep +# the command set explicit instead of relying on the base image profile. +RUN set -eu; \ + nix-env -iA \ + nixpkgs.nix \ + nixpkgs.bash \ + nixpkgs.coreutils \ + nixpkgs.findutils \ + nixpkgs.gnugrep \ + nixpkgs.gnused \ + nixpkgs.gawk \ + nixpkgs.curl \ + nixpkgs.gnutar \ + nixpkgs.gzip \ + nixpkgs.unzip \ + nixpkgs.icu \ + nixpkgs.gcc \ + nixpkgs.openssl \ + nixpkgs.gcc.cc.lib \ + nixpkgs.zlib \ + nixpkgs.glibc; \ + mkdir -p /usr/local/bin; \ + cc_path="$(command -v gcc)"; \ + test -n "$cc_path"; \ + ln -sf "$cc_path" /usr/local/bin/cc; \ + mkdir -p /usr/local/nix-compat-lib; \ + for lib in \ + /nix/store/*-icu4c-*/lib/*.so* \ + /nix/store/*-openssl-*/lib/*.so* \ + /nix/store/*-xgcc-*-lib/lib/*.so* \ + /nix/store/*-zlib-*/lib/*.so*; do \ + [ -e "$lib" ] || continue; \ + ln -sf "$lib" /usr/local/nix-compat-lib/; \ + done + +# The pinned mise release is dynamically linked and asks the kernel for +# the conventional FHS loader path. nixos/nix keeps glibc in /nix/store, +# so expose the loader path this test needs without changing install.sh. +RUN set -eu; \ + glibc_loader="$(find /nix/store -maxdepth 3 -path '*-glibc-*/lib/ld-linux-*.so.*' | head -n 1)"; \ + test -n "$glibc_loader"; \ + case "$(uname -m)" in \ + x86_64) mkdir -p /lib64 && ln -sf "$glibc_loader" /lib64/ld-linux-x86-64.so.2 ;; \ + aarch64|arm64) mkdir -p /lib && ln -sf "$glibc_loader" /lib/ld-linux-aarch64.so.1 ;; \ + *) echo "unsupported Docker test architecture: $(uname -m)" >&2; exit 1 ;; \ + esac; \ + ln -sfn "$(dirname "$glibc_loader")" /usr/local/nix-glibc-lib +ENV LD_LIBRARY_PATH=/usr/local/nix-glibc-lib:/usr/local/nix-compat-lib:/nix/var/nix/profiles/default/lib:/nix/var/nix/profiles/default/lib64 # Enable nix flakes (needed for tools/setup/common/mise.sh and other # substrate that uses flake-style invocation). From e7ac91080d3d79410666f6bf4b76f542ed83470d Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 05:10:20 -0400 Subject: [PATCH 06/11] fix: align self-register Bun runtime with mise Co-Authored-By: Codex --- full-ai-cluster/nixos/modules/zeta-self-register.nix | 5 +++-- tools/ci/audit-installer-substrate.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/full-ai-cluster/nixos/modules/zeta-self-register.nix b/full-ai-cluster/nixos/modules/zeta-self-register.nix index 266358771c..20f954371f 100644 --- a/full-ai-cluster/nixos/modules/zeta-self-register.nix +++ b/full-ai-cluster/nixos/modules/zeta-self-register.nix @@ -80,12 +80,13 @@ in WorkingDirectory = cfg.repoRoot; Environment = [ "HOME=${cfg.home}" - "PATH=${cfg.home}/.bun/bin:/run/current-system/sw/bin:/usr/bin:/bin" + "PATH=${cfg.home}/.local/share/mise/shims:${cfg.home}/.bun/bin:/run/current-system/sw/bin:/usr/bin:/bin" + "BUN_INSTALL=${cfg.home}/.bun" "ZETA_SELF_REGISTER_MARKER=${cfg.markerPath}" "ZETA_SELF_REGISTER_INTENT_DIR=${cfg.intentDir}" "ZETA_SELF_REGISTER_REPO=${cfg.repoRoot}" ]; - ExecStart = "${cfg.home}/.bun/bin/bun ${cfg.scriptPath}"; + ExecStart = "${cfg.home}/.local/share/mise/shims/bun ${cfg.scriptPath}"; }; }; }; diff --git a/tools/ci/audit-installer-substrate.ts b/tools/ci/audit-installer-substrate.ts index ce7c23852b..b08c9e0adb 100644 --- a/tools/ci/audit-installer-substrate.ts +++ b/tools/ci/audit-installer-substrate.ts @@ -145,6 +145,8 @@ const REQUIRED_SENTINELS: readonly SentinelAssertion[] = [ 'default = "${cfg.home}/.config/zeta/self-registered.marker";', // markerPath derives from home override 'default = "${cfg.home}/.config/zeta/self-registration-intent";', // intentDir derives from home override "tools/installer/zeta-self-register.ts", // delegates implementation to B-0855.2 + ".local/share/mise/shims/bun", // runtime follows the repo-wide mise-managed Bun substrate + "BUN_INSTALL", // keeps Bun global CLI state anchored under cfg.home "ZETA_SELF_REGISTER_MARKER", // marker path exported to implementation "ZETA_SELF_REGISTER_INTENT_DIR", // intent handoff dir exported to implementation ], From ed6b5bd881d766858d14e517d6871810171715b6 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 05:24:49 -0400 Subject: [PATCH 07/11] fix: update ISO cosign signing to bundle output Co-Authored-By: Codex --- .github/workflows/build-ai-cluster-iso.yml | 89 ++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/.github/workflows/build-ai-cluster-iso.yml b/.github/workflows/build-ai-cluster-iso.yml index c8c0d9c011..3a0344c74c 100644 --- a/.github/workflows/build-ai-cluster-iso.yml +++ b/.github/workflows/build-ai-cluster-iso.yml @@ -71,6 +71,10 @@ on: permissions: contents: read + # id-token: write moved to jobs.build.permissions per Copilot P1 finding on + # PR #5417 — matches the repo pattern (.github/workflows/scorecard.yml + # elevates id-token at job scope only, minimising blast radius if future + # jobs added to this workflow). concurrency: group: build-ai-cluster-iso-${{ github.workflow }}-${{ github.ref }} @@ -81,6 +85,23 @@ jobs: name: build-iso runs-on: ubuntu-24.04 timeout-minutes: 60 + permissions: + contents: read + # B-0853.1 cosign keyless OIDC — sigstore/Fulcio CA mints a short-lived + # cert from the GitHub-issued OIDC token; Rekor transparency log records + # the signature. No private key material is handled in this workflow. + # + # Real safety properties (tightened per Copilot P1 review on PR #5417): + # - Short-lived cert (Fulcio mints 10-min cert tied to this run) + # - Identity bound to workflow path + ref (verifier pins via + # --certificate-identity matching this file's path + ref) + # - Steps pinned to commit SHAs (cosign-installer at vetted release) + # NOT a hard guarantee: any step in THIS job that can request the OIDC + # token could in principle transmit it off-runner + use it to mint a + # Fulcio cert + sign arbitrary blobs as THIS workflow's identity. The + # mitigation is pinned + audited steps + job-scope id-token (not + # workflow-wide; this block). + id-token: write steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -300,6 +321,63 @@ jobs: echo "to flash the stick. (ISO is ~1.5–2 GiB.)" } >> "$GITHUB_STEP_SUMMARY" + # B-0853.1: keyless sigstore signing for the ISO artifact. + # + # The GitHub OIDC token is exchanged with Fulcio for a short-lived + # signing certificate; Rekor inclusion is captured in the cosign + # bundle. No long-lived private key material is present in this + # workflow. + # + # Verification (any consumer) — pin identity to THIS specific workflow + # file + ref: + # + # cosign verify-blob \ + # --bundle .bundle \ + # --certificate-identity 'https://github.com/Lucent-Financial-Group/Zeta/.github/workflows/build-ai-cluster-iso.yml@refs/heads/main' \ + # --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + # + # + # For verifying signatures from feature branches OR tags, swap the + # `@refs/heads/main` suffix accordingly: + # --certificate-identity '.../build-ai-cluster-iso.yml@refs/heads/' + # --certificate-identity '.../build-ai-cluster-iso.yml@refs/tags/' + # + # Security: inputs to cosign are filesystem paths from steps.iso.outputs + # of THIS workflow (no github.event.* interpolation). Same discipline + # as the QEMU boot-test step above. + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + # Pin verified via gh API 2026-05-27: + # gh api repos/sigstore/cosign-installer/releases/latest + # tag: v4.1.2; published: 2026-05-07T01:27:27Z + + - name: Sign ISO with cosign (keyless OIDC + Fulcio + Rekor) + env: + # Pass ISO path via env (not direct ${{ }} interpolation in run:) + # per the workflow's existing security discipline. + ISO_PATH: ${{ steps.iso.outputs.path }} + ISO_NAME: ${{ steps.iso.outputs.name }} + run: | + set -euo pipefail + # --yes: skip interactive Fulcio confirmation (CI is non-interactive) + # Output emitted alongside the ISO: + # .bundle - cosign bundle containing signature, certificate, + # and transparency-log material. + cosign sign-blob --yes \ + --bundle "${ISO_PATH}.bundle" \ + "${ISO_PATH}" + # Sanity: the verifier artifact must be present and non-empty. + test -s "${ISO_PATH}.bundle" || { echo "::error::Empty cosign bundle"; exit 1; } + { + echo "## ISO signed via sigstore (cosign keyless OIDC)" + echo "" + echo "| Artifact | Path |" + echo "|---|---|" + echo "| Bundle | \`${ISO_NAME}.bundle\` |" + echo "" + echo "Verification: see workflow comments for the canonical \`cosign verify-blob\` command." + } >> "$GITHUB_STEP_SUMMARY" + - name: Upload ISO as workflow artifact # Available for download from the workflow run page for ~90 days. uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 @@ -309,3 +387,14 @@ jobs: if-no-files-found: error retention-days: 90 compression-level: 0 # ISO is already compressed; re-zipping wastes time + + - name: Upload cosign bundle as workflow artifact + # B-0853.1 sibling upload — bundle alongside the ISO for consumers. + # Separate artifact (not bundled with ISO) so consumers downloading + # just the ISO do not pay the small extra cost; verifiers can grab it. + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: ${{ steps.iso.outputs.name }}.cosign + path: ${{ steps.iso.outputs.path }}.bundle + if-no-files-found: error + retention-days: 90 From 354cf0475a5d25a372a05ffeedabb9beb9caac9a Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 05:37:36 -0400 Subject: [PATCH 08/11] fix: write cosign bundle to runner temp Co-Authored-By: Codex --- .github/workflows/build-ai-cluster-iso.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-ai-cluster-iso.yml b/.github/workflows/build-ai-cluster-iso.yml index 6d3980e044..8589809af4 100644 --- a/.github/workflows/build-ai-cluster-iso.yml +++ b/.github/workflows/build-ai-cluster-iso.yml @@ -358,6 +358,7 @@ jobs: # Per .claude/rules/dep-pin-search-first-authority.md - name: Sign ISO with cosign (keyless OIDC + Fulcio + Rekor) + id: sign env: # Pass ISO path via env (not direct ${{ }} interpolation in run:) # per the workflow's existing security discipline. @@ -367,13 +368,15 @@ jobs: set -euo pipefail # --yes: skip interactive Fulcio confirmation (CI is non-interactive) # Output emitted alongside the ISO: - # .bundle - cosign bundle containing signature, certificate, - # and transparency-log material. + # /.bundle - cosign bundle containing signature, + # certificate, and transparency-log material. + COSIGN_BUNDLE="${RUNNER_TEMP}/${ISO_NAME}.bundle" cosign sign-blob --yes \ - --bundle "${ISO_PATH}.bundle" \ + --bundle "${COSIGN_BUNDLE}" \ "${ISO_PATH}" # Sanity: the verifier artifact must be present and non-empty. - test -s "${ISO_PATH}.bundle" || { echo "::error::Empty cosign bundle"; exit 1; } + test -s "${COSIGN_BUNDLE}" || { echo "::error::Empty cosign bundle"; exit 1; } + echo "bundle-path=${COSIGN_BUNDLE}" >> "$GITHUB_OUTPUT" { echo "## ISO signed via sigstore (cosign keyless OIDC)" echo "" @@ -401,6 +404,6 @@ jobs: uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: ${{ steps.iso.outputs.name }}.cosign - path: ${{ steps.iso.outputs.path }}.bundle + path: ${{ steps.sign.outputs.bundle-path }} if-no-files-found: error retention-days: 90 From a0e4d3755b1c933e84d3244de17f89b04b785a1e Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 05:48:15 -0400 Subject: [PATCH 09/11] fix: retry self-register until marker exists Address the unresolved operational review finding on the B-0855.1 service by replacing the first-boot-only gate with a marker-path gate and failure retry/backoff. Update the installer substrate audit so the retry semantics remain checked in CI. Co-Authored-By: Codex --- full-ai-cluster/nixos/modules/zeta-self-register.nix | 10 ++++++---- tools/ci/audit-installer-substrate.ts | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/full-ai-cluster/nixos/modules/zeta-self-register.nix b/full-ai-cluster/nixos/modules/zeta-self-register.nix index 20f954371f..4b5e55fc07 100644 --- a/full-ai-cluster/nixos/modules/zeta-self-register.nix +++ b/full-ai-cluster/nixos/modules/zeta-self-register.nix @@ -3,9 +3,9 @@ # B-0855.1: NixOS service surface for post-install self-registration. # The service is intentionally disabled by default until B-0855.2 ships # the TypeScript implementation at tools/installer/zeta-self-register.ts. -# Once enabled by a host config, it fires on first boot of the installed -# OS, after network-online and credential-restore ordering, instead of -# running inside the live-USB installer environment. +# Once enabled by a host config, it fires after the installed OS reaches +# network-online and credential-restore ordering, then keeps retrying +# until the local marker confirms registration intent was written. { config, lib, ... }: @@ -70,7 +70,7 @@ in ]; unitConfig = { - ConditionFirstBoot = "yes"; + ConditionPathExists = "!${cfg.markerPath}"; }; serviceConfig = { @@ -87,6 +87,8 @@ in "ZETA_SELF_REGISTER_REPO=${cfg.repoRoot}" ]; ExecStart = "${cfg.home}/.local/share/mise/shims/bun ${cfg.scriptPath}"; + Restart = "on-failure"; + RestartSec = "30s"; }; }; }; diff --git a/tools/ci/audit-installer-substrate.ts b/tools/ci/audit-installer-substrate.ts index b08c9e0adb..30c7c18833 100644 --- a/tools/ci/audit-installer-substrate.ts +++ b/tools/ci/audit-installer-substrate.ts @@ -137,7 +137,9 @@ const REQUIRED_SENTINELS: readonly SentinelAssertion[] = [ mustContain: [ "systemd.services.zeta-self-register", // service unit exists "Type = \"oneshot\"", // fires once, not a loop - "ConditionFirstBoot", // installed-OS first-boot gate + "ConditionPathExists", // marker gate permits retries across failed first-boot attempts + "Restart = \"on-failure\"", // transient failures retry instead of losing first-boot opportunity + "RestartSec = \"30s\"", // bounded backoff before retrying registration intent "network-online.target", // waits for network before registration intent "zeta-creds-restore.service", // ordered after restored creds when that service exists 'default = "${cfg.home}/Zeta";', // repoRoot derives from home override @@ -150,7 +152,7 @@ const REQUIRED_SENTINELS: readonly SentinelAssertion[] = [ "ZETA_SELF_REGISTER_MARKER", // marker path exported to implementation "ZETA_SELF_REGISTER_INTENT_DIR", // intent handoff dir exported to implementation ], - rationale: "B-0855.1 service must be a post-install first-boot oneshot ordered after network and credential restore surfaces", + rationale: "B-0855.1 service must be a post-install marker-gated oneshot ordered after network and credential restore surfaces", }, { path: "full-ai-cluster/nixos/modules/injected-hostname.nix", From 6796a426edc281684fdd35b6ec47b21822485941 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 05:58:02 -0400 Subject: [PATCH 10/11] fix: align ISO signing summary text Address current Copilot workflow wording findings by matching the cosign bundle comment to the runner-temp output path and pointing verification guidance at the workflow run step summary rather than nonexistent workflow comments. Co-Authored-By: Codex --- .github/workflows/build-ai-cluster-iso.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-ai-cluster-iso.yml b/.github/workflows/build-ai-cluster-iso.yml index 8589809af4..10cbc8f0a9 100644 --- a/.github/workflows/build-ai-cluster-iso.yml +++ b/.github/workflows/build-ai-cluster-iso.yml @@ -367,7 +367,7 @@ jobs: run: | set -euo pipefail # --yes: skip interactive Fulcio confirmation (CI is non-interactive) - # Output emitted alongside the ISO: + # Output emitted to runner temp and uploaded alongside the ISO artifact: # /.bundle - cosign bundle containing signature, # certificate, and transparency-log material. COSIGN_BUNDLE="${RUNNER_TEMP}/${ISO_NAME}.bundle" @@ -384,7 +384,7 @@ jobs: echo "|---|---|" echo "| Bundle | \`${ISO_NAME}.bundle\` |" echo "" - echo "Verification: see workflow comments for the canonical \`cosign verify-blob\` command." + echo "Verification: see this workflow run's step summary for the canonical \`cosign verify-blob\` command." } >> "$GITHUB_STEP_SUMMARY" - name: Upload ISO as workflow artifact From ed3c0d22e151d96eeece0418f4a3a53f544072ec Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 06:17:40 -0400 Subject: [PATCH 11/11] fix: close self-register review gaps Co-Authored-By: Codex --- .github/workflows/build-ai-cluster-iso.yml | 10 +++++++++- full-ai-cluster/nixos/modules/zeta-self-register.nix | 9 +++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-ai-cluster-iso.yml b/.github/workflows/build-ai-cluster-iso.yml index 10cbc8f0a9..bec5db7d1d 100644 --- a/.github/workflows/build-ai-cluster-iso.yml +++ b/.github/workflows/build-ai-cluster-iso.yml @@ -384,7 +384,15 @@ jobs: echo "|---|---|" echo "| Bundle | \`${ISO_NAME}.bundle\` |" echo "" - echo "Verification: see this workflow run's step summary for the canonical \`cosign verify-blob\` command." + echo "Verification command after downloading the ISO artifact and cosign bundle artifact:" + echo "" + echo "\`\`\`bash" + echo "cosign verify-blob \\" + echo " --bundle '${ISO_NAME}.bundle' \\" + echo " --certificate-identity 'https://github.com/${GITHUB_WORKFLOW_REF}' \\" + echo " --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \\" + echo " '${ISO_NAME}'" + echo "\`\`\`" } >> "$GITHUB_STEP_SUMMARY" - name: Upload ISO as workflow artifact diff --git a/full-ai-cluster/nixos/modules/zeta-self-register.nix b/full-ai-cluster/nixos/modules/zeta-self-register.nix index 4b5e55fc07..115a704950 100644 --- a/full-ai-cluster/nixos/modules/zeta-self-register.nix +++ b/full-ai-cluster/nixos/modules/zeta-self-register.nix @@ -11,6 +11,7 @@ let cfg = config.zeta.selfRegister; + bunShimPath = "${cfg.home}/.local/share/mise/shims/bun"; in { options.zeta.selfRegister = { @@ -70,7 +71,11 @@ in ]; unitConfig = { - ConditionPathExists = "!${cfg.markerPath}"; + ConditionPathExists = [ + "!${cfg.markerPath}" + cfg.scriptPath + bunShimPath + ]; }; serviceConfig = { @@ -86,7 +91,7 @@ in "ZETA_SELF_REGISTER_INTENT_DIR=${cfg.intentDir}" "ZETA_SELF_REGISTER_REPO=${cfg.repoRoot}" ]; - ExecStart = "${cfg.home}/.local/share/mise/shims/bun ${cfg.scriptPath}"; + ExecStart = "${bunShimPath} ${cfg.scriptPath}"; Restart = "on-failure"; RestartSec = "30s"; };