Merging recipe-maintainers/cc-ci-orchestrator (the VM NixOS config repo) into this repo as nix/ — the next step toward consolidating the two orchestrator repos into a single cc-ci-orchestrator. The source repo will be renamed to archived-cc-ci-orchestrator on Gitea. This repo will be renamed cc-ci-orchestrator. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
5.8 KiB
Nix
125 lines
5.8 KiB
Nix
# 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"
|
|
'';
|
|
};
|
|
}
|