The orchestrator's flake now builds the machine it shares with the cc-ci CI
server: `nixosConfigurations.cc-ci` composes cc-ci's nixosModules.cc-ci-server
(new flake input, nixpkgs + sops-nix follow ours), this repo's orchestrator
module (nix/modules/cc-ci.nix, exported as cc-ci-orchestrator, `cc-ci` kept
as an alias for notplants-nix) and the new nix/modules/orchestrator-host.nix
— the host contract those units always assumed (loops user, claude/opencode
CLIs, opencode web server + tailnet-only UI on 8443 since traefik owns
80/443, nix-ld, tool set, `ssh cc-ci` → loopback).
nix/hosts/cc-ci/{hardware,networking}.nix are PROVISIONAL copies of the old
server's layout so the flake evaluates; they get replaced by the
nixos-infect output of 195.201.88.249.
README.md is the deploy guide (Hetzner Debian → nixos-infect → this flake →
staging → data restore → cutover). archive/ holds the retired Incus/Hetzner
orchestrator host configs, the old terraform and the migration plans;
references updated. cc-ci-plan/plan-cc-ci-combined-host.md is the working
plan for the move.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FqkQq3CDmFWcQ7u1LzoyRz
131 lines
6.2 KiB
Nix
131 lines
6.2 KiB
Nix
# ⚠️ HISTORICAL / DEAD CONFIG — DO NOT DEPLOY. (superseded 2026-05-31)
|
|
# This is the OLD Incus VM (`cc-ci-orchestrator`, tailnet 100.116.55.106) config. The orchestrator
|
|
# now runs on Hetzner — the LIVE config is nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix.
|
|
# Kept only for historical comparison; it runs loops as root and hard-codes the dead cc-ci IP.
|
|
# Delete once the old Incus VM is torn down.
|
|
#
|
|
# cc-ci-orchestrator VM — NixOS config (channel-based: nixos-24.11; deployed to /etc/nixos/configuration.nix)
|
|
#
|
|
# Purpose: a reboot-resilient host for the cc-ci Builder/Adversary loops + watchdog + the orchestrator
|
|
# session, moved off the unstable 905 MiB Pi. See plan-orchestrator-migration.md.
|
|
#
|
|
# STATUS: DRAFT (Phase B). The nix-ld + claude-install bits need on-VM validation (the standalone
|
|
# Claude Code is a Bun ELF binary; NixOS needs nix-ld to run a foreign dynamic binary). The
|
|
# cc-ci-loops supervisor service is defined but NOT enabled until the workspace is staged (Phase C/D).
|
|
{ config, pkgs, lib, modulesPath, ... }:
|
|
{
|
|
imports = [
|
|
"${modulesPath}/virtualisation/incus-virtual-machine.nix"
|
|
];
|
|
|
|
# --- base (mirrors the incus-base-vm) ---
|
|
virtualisation.incus.agent.enable = true; # for `incus exec`
|
|
services.cloud-init = { enable = true; network.enable = true; };
|
|
services.openssh = { enable = true; settings.PermitRootLogin = "yes"; };
|
|
networking.useDHCP = true;
|
|
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
|
|
networking.firewall = { enable = true; trustedInterfaces = [ "tailscale0" ]; allowedTCPPorts = [ 22 ]; };
|
|
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
|
system.stateVersion = "24.11";
|
|
|
|
# --- tailscale (auto-auth from /etc/ts-auth-key, hostname from /etc/ts-hostname; written by cloud-init) ---
|
|
services.tailscale = {
|
|
enable = true;
|
|
authKeyFile = "/etc/ts-auth-key";
|
|
extraUpFlags = let h = lib.strings.removeSuffix "\n" (builtins.readFile /etc/ts-hostname);
|
|
in [ "--hostname=${h}" "--ssh" ]; # --ssh: allow tailscale-ssh as a fallback path
|
|
};
|
|
|
|
# --- swap: the Pi OOM lesson. 2 GB RAM is tight for 3 concurrent claude sessions; 4 GB disk swap
|
|
# as a real overflow tier (zram is in-RAM and doesn't add capacity). ---
|
|
swapDevices = [ { device = "/swapfile"; size = 4096; } ];
|
|
|
|
# --- nix-ld: lets the standalone Claude Code (foreign dynamic ELF / Bun) run on NixOS ---
|
|
programs.nix-ld.enable = true;
|
|
programs.nix-ld.libraries = with pkgs; [
|
|
stdenv.cc.cc.lib # libstdc++ / libgcc_s
|
|
zlib
|
|
openssl
|
|
curl
|
|
glibc
|
|
];
|
|
|
|
# --- packages the loops + launch.sh + orchestrator need ---
|
|
environment.systemPackages = with pkgs; [
|
|
git tmux python3 jq curl cacert
|
|
gnused gawk coreutils gnugrep findutils util-linux
|
|
nettools openssh # nc, ssh
|
|
docker-client # `docker` CLI is not needed (deploys run on cc-ci), but handy for probes
|
|
];
|
|
|
|
# --- loops user: non-root account for running claude (--dangerously-skip-permissions blocked for root) ---
|
|
users.users.loops = {
|
|
isNormalUser = true;
|
|
home = "/home/loops";
|
|
shell = pkgs.bash;
|
|
extraGroups = [ "wheel" ]; # sudo access
|
|
};
|
|
security.sudo.wheelNeedsPassword = false; # passwordless sudo for wheel
|
|
# Allow loops user to use tmux/claude without a password prompt
|
|
security.sudo.extraRules = [{
|
|
users = [ "loops" ];
|
|
commands = [{ command = "ALL"; options = [ "NOPASSWD" ]; }];
|
|
}];
|
|
|
|
# --- root PATH: ensure ~/.local/bin (where the standalone claude binary lives) is on root's PATH ---
|
|
environment.variables.PATH = lib.mkForce "/root/.local/bin:/run/current-system/sw/bin:/run/wrappers/bin:/usr/bin:/bin";
|
|
|
|
# --- root ssh config: reach cc-ci DIRECTLY over the VM's own tailscale (this VM is a tailnet peer,
|
|
# so NO SOCKS proxy is needed — unlike the Pi). Key staged at /root/.ssh/cc-ci-root-ed25519. ---
|
|
system.activationScripts.ccciSshConfig = ''
|
|
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
|
cat > /root/.ssh/config <<'SSHCFG'
|
|
Host cc-ci cc-nix-test 100.90.116.4
|
|
HostName 100.90.116.4
|
|
User root
|
|
IdentityFile /root/.ssh/cc-ci-root-ed25519
|
|
IdentitiesOnly yes
|
|
StrictHostKeyChecking accept-new
|
|
ServerAliveInterval 30
|
|
SSHCFG
|
|
chmod 600 /root/.ssh/config
|
|
'';
|
|
|
|
# --- claude-install: idempotent oneshot — fetch the standalone Claude Code CLI if missing.
|
|
# Runs via nix-ld. (Auth is a one-time operator step: `claude auth login` — see migration plan.) ---
|
|
systemd.services.claude-install = {
|
|
description = "Install the standalone Claude Code CLI if missing (idempotent)";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" "nix-ld.service" ];
|
|
wants = [ "network-online.target" ];
|
|
serviceConfig = { Type = "oneshot"; RemainAfterExit = true; };
|
|
path = [ pkgs.curl pkgs.bash pkgs.coreutils pkgs.gnutar pkgs.gzip pkgs.unzip ];
|
|
script = ''
|
|
if [ ! -x /root/.local/bin/claude ]; then
|
|
echo "installing standalone Claude Code CLI..."
|
|
curl -fsSL https://claude.ai/install.sh | bash || echo "claude install failed (retry next activation)"
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# --- cc-ci-loops supervisor (DEFINED, NOT YET ENABLED). Enabled in Phase D after the workspace
|
|
# (/srv/cc-ci: launch.sh, plan, prompts, the loop clones, secrets) is staged. This is the
|
|
# reboot-resilience fix: it runs launch.sh start on every boot. Mirrors the Pi's cc-ci-loops.service. ---
|
|
systemd.services.cc-ci-loops = {
|
|
description = "cc-ci Builder/Adversary loops + watchdog (launch.sh start, RESUME_PHASE)";
|
|
wantedBy = [ "multi-user.target" ]; # enabled: workspace staged (Phase C/D 2026-05-30)
|
|
after = [ "network-online.target" "tailscaled.service" "claude-install.service" ];
|
|
wants = [ "network-online.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot"; RemainAfterExit = true; User = "root";
|
|
WorkingDirectory = "/srv/cc-ci";
|
|
};
|
|
environment = { RESUME_PHASE = "1"; HOME = "/root"; };
|
|
path = [ pkgs.bash pkgs.tmux pkgs.git pkgs.python3 pkgs.openssh pkgs.nettools ];
|
|
script = ''
|
|
[ -x /srv/cc-ci/cc-ci-plan/launch.sh ] && /srv/cc-ci/cc-ci-plan/launch.sh start || \
|
|
echo "workspace not staged yet (/srv/cc-ci/cc-ci-plan/launch.sh missing) — skipping"
|
|
'';
|
|
};
|
|
}
|