Commit 5fc86a6febb7
Changed files (9)
home
common
shell
modules
microvm
pkgs
my
scripts
bin
systems
aomi
home/common/shell/openssh.nix
@@ -168,31 +168,7 @@ in
identitiesOnly = true;
identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_homelab_sk";
};
- # MicroVMs on aomi (192.168.83.x subnet, reachable via jump host)
- "${globals.microvms.subnet}.*" = {
- user = "vincent";
- proxyJump = globals.microvms.host;
- identitiesOnly = true;
- identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_homelab_sk";
- extraOptions = {
- StrictHostKeyChecking = "no";
- UserKnownHostsFile = "/dev/null";
- };
- };
}
- # Generated microvm SSH aliases (with on-demand VM startup)
- // (lib.mapAttrs (name: vm: {
- hostname = vm.ip;
- user = "vincent";
- # Use ProxyCommand for on-demand VM startup instead of ProxyJump
- proxyCommand = "microvm-ssh ${name} ${vm.ip} %p";
- identitiesOnly = true;
- identityFile = lib.mkIf hasFido2Keys "~/.ssh/id_homelab_sk";
- extraOptions = {
- StrictHostKeyChecking = "no";
- UserKnownHostsFile = "/dev/null";
- };
- }) globals.microvms.vms)
) (lib.recursiveUpdate criticalInfraOverrides aomiOverrides);
extraConfig = ''
# IdentityAgent /run/user/1000/yubikey-agent/yubikey-agent.sock
modules/microvm/default.nix
@@ -1,369 +0,0 @@
-# MicroVM host module for running ephemeral Claude Code agents
-#
-# Based on: https://michael.stapelberg.ch/posts/2026-02-01-coding-agent-microvm-nix/
-#
-# Usage:
-# services.microvm-host = {
-# enable = true;
-# vms.claude-home = {
-# ip = "192.168.83.2";
-# workspace = "/home/vincent/src/home";
-# };
-# };
-{
- config,
- lib,
- pkgs,
- inputs,
- globals,
- ...
-}:
-with lib;
-let
- cfg = config.services.microvm-host;
-
- # VM configuration type
- vmType = types.submodule (
- { ... }:
- {
- options = {
- ip = mkOption {
- type = types.str;
- description = "Static IP address for the VM";
- example = "192.168.83.2";
- };
- workspace = mkOption {
- type = types.str;
- description = "Host path to mount as /workspace in the VM";
- example = "/home/vincent/src/home";
- };
- extraPackages = mkOption {
- type = types.listOf types.package;
- default = [ ];
- description = "Additional packages to install in the VM";
- };
- vcpu = mkOption {
- type = types.int;
- default = 8;
- description = "Number of virtual CPUs";
- };
- mem = mkOption {
- type = types.int;
- default = 4096;
- description = "Memory in MB";
- };
- autostart = mkOption {
- type = types.bool;
- default = false;
- description = "Whether to start the VM automatically";
- };
- };
- }
- );
-
- # Generate MAC address from VM name (deterministic)
- vmMac =
- name:
- let
- hash = builtins.hashString "sha256" name;
- in
- "02:00:00:01:${builtins.substring 0 2 hash}:${builtins.substring 2 2 hash}";
-
- # Generate short tap interface name (Linux limit is 15 chars)
- # Use "mv" prefix + first 11 chars of hash
- tapName =
- name:
- let
- hash = builtins.hashString "sha256" name;
- in
- "mv${builtins.substring 0 11 hash}";
-
- # Build VM NixOS configuration
- mkVmConfig = name: vmCfg: {
- imports = [
- inputs.microvm.nixosModules.microvm
- inputs.home-manager.nixosModules.home-manager
- ];
-
- # Basic system config
- system.stateVersion = "24.11";
- networking.hostName = name;
-
- # MicroVM configuration
- microvm = {
- hypervisor = "cloud-hypervisor";
- vcpu = vmCfg.vcpu;
- mem = vmCfg.mem;
-
- # Don't build a store image, use host's store via virtiofs
- storeOnDisk = false;
-
- # Shared directories via virtiofs
- shares = [
- {
- proto = "virtiofs";
- tag = "nix-store";
- source = "/nix/store";
- mountPoint = "/nix/store";
- }
- {
- proto = "virtiofs";
- tag = "workspace";
- source = vmCfg.workspace;
- mountPoint = "/workspace";
- }
- {
- proto = "virtiofs";
- tag = "claude-config";
- source = "/home/vincent/.claude";
- mountPoint = "/home/vincent/.claude";
- }
- {
- proto = "virtiofs";
- tag = "ssh-host-keys";
- source = "${cfg.stateDir}/${name}/ssh-host-keys";
- mountPoint = "/etc/ssh/host-keys";
- }
- ];
-
- # Network interface (name must be <=15 chars for Linux)
- interfaces = [
- {
- type = "tap";
- id = tapName name;
- mac = vmMac name;
- }
- ];
- };
-
- # Static IP networking (using systemd-networkd, microvm default)
- networking = {
- useDHCP = false;
- defaultGateway = {
- address = "${cfg.subnet}.1";
- interface = "eth0";
- };
- nameservers = [
- "1.1.1.1"
- "8.8.8.8"
- ];
- firewall.enable = false; # Trust NAT isolation
- };
-
- # Configure network via systemd-networkd
- systemd.network = {
- enable = true;
- networks."10-eth0" = {
- matchConfig.Name = "eth0";
- networkConfig = {
- Address = "${vmCfg.ip}/24";
- Gateway = "${cfg.subnet}.1";
- DNS = [
- "1.1.1.1"
- "8.8.8.8"
- ];
- };
- };
- };
-
- # SSH server with persistent host keys
- services.openssh = {
- enable = true;
- settings = {
- PasswordAuthentication = false;
- PermitRootLogin = "no";
- };
- hostKeys = [
- {
- path = "/etc/ssh/host-keys/ssh_host_ed25519_key";
- type = "ed25519";
- }
- ];
- };
-
- # User configuration
- users.users.vincent = {
- isNormalUser = true;
- home = "/home/vincent";
- shell = pkgs.zsh;
- extraGroups = [ "wheel" ];
- openssh.authorizedKeys.keys = globals.ssh.vincent;
- };
- security.sudo.wheelNeedsPassword = false;
-
- # Enable zsh system-wide
- programs.zsh.enable = true;
-
- # Home-manager for shell setup
- home-manager = {
- useGlobalPkgs = true;
- useUserPackages = true;
- users.vincent = import ./vm-home.nix {
- inherit pkgs;
- extraPackages = vmCfg.extraPackages;
- };
- };
-
- # Base packages
- environment.systemPackages = with pkgs; [
- git
- vim
- curl
- wget
- htop
- ];
- };
-in
-{
- # Import the microvm host module at module level (not in config)
- imports = [ inputs.microvm.nixosModules.host ];
-
- options.services.microvm-host = {
- enable = mkEnableOption "MicroVM host for Claude Code agents";
-
- bridge = mkOption {
- type = types.str;
- default = "microbr";
- description = "Name of the bridge interface for VM networking";
- };
-
- subnet = mkOption {
- type = types.str;
- default = "192.168.83";
- description = "First three octets of the VM subnet (e.g., 192.168.83)";
- };
-
- stateDir = mkOption {
- type = types.str;
- default = "/home/vincent/microvm";
- description = "Directory for persistent VM state (SSH keys, etc.)";
- };
-
- externalInterface = mkOption {
- type = types.str;
- default = "enp0s31f6";
- description = "External network interface for NAT";
- };
-
- vms = mkOption {
- type = types.attrsOf vmType;
- default = { };
- description = "VM definitions";
- };
- };
-
- config = mkIf cfg.enable {
- # Enable microvm host
- microvm.host.enable = true;
-
- # Keep declarative config for firewall/NAT
- networking.bridges.${cfg.bridge}.interfaces = [ ];
- networking.interfaces.${cfg.bridge} = {
- ipv4.addresses = [
- {
- address = "${cfg.subnet}.1";
- prefixLength = 24;
- }
- ];
- };
-
- # NAT for VM internet access
- networking.nat = {
- enable = true;
- internalInterfaces = [ cfg.bridge ];
- externalInterface = cfg.externalInterface;
- };
-
- # Trust the microvm bridge
- networking.firewall.trustedInterfaces = [ cfg.bridge ];
-
- # IP forwarding
- boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
-
- # Define VMs
- microvm.vms = mapAttrs (name: vmCfg: {
- inherit (vmCfg) autostart;
- restartIfChanged = true;
-
- # Pass through special args
- specialArgs = {
- inherit inputs globals;
- };
-
- config = mkVmConfig name vmCfg;
- }) cfg.vms;
-
- # Auto-generate SSH host keys for each VM
- system.activationScripts.microvm-ssh-keys = {
- text = concatStringsSep "\n" (
- mapAttrsToList (name: _vmCfg: ''
- keyDir="${cfg.stateDir}/${name}/ssh-host-keys"
- if [ ! -f "$keyDir/ssh_host_ed25519_key" ]; then
- echo "Generating SSH host keys for microvm ${name}..."
- mkdir -p "$keyDir"
- ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -N "" -f "$keyDir/ssh_host_ed25519_key"
- chown -R vincent:users "$keyDir"
- chmod 600 "$keyDir/ssh_host_ed25519_key"
- chmod 644 "$keyDir/ssh_host_ed25519_key.pub"
- fi
- '') cfg.vms
- );
- deps = [ ];
- };
-
- # Create state directories
- systemd.tmpfiles.rules = mapAttrsToList (
- name: _vmCfg: "d ${cfg.stateDir}/${name} 0755 vincent users -"
- ) cfg.vms;
-
- # Create bridge and attach tap interfaces
- systemd.services = {
- # Create bridge for VM networking
- # (networking.bridges doesn't work reliably with all network setups)
- "microvm-bridge-setup" = {
- description = "Create MicroVM bridge";
- wantedBy = [ "network.target" ];
- before = [ "network.target" ];
- after = [ "network-pre.target" ];
- serviceConfig = {
- Type = "oneshot";
- RemainAfterExit = true;
- ExecStart = pkgs.writeShellScript "create-microvm-bridge" ''
- set -e
- if ! ${pkgs.iproute2}/bin/ip link show ${cfg.bridge} &>/dev/null; then
- ${pkgs.iproute2}/bin/ip link add name ${cfg.bridge} type bridge
- fi
- ${pkgs.iproute2}/bin/ip addr replace ${cfg.subnet}.1/24 dev ${cfg.bridge}
- ${pkgs.iproute2}/bin/ip link set ${cfg.bridge} up
- '';
- ExecStop = "${pkgs.iproute2}/bin/ip link delete ${cfg.bridge}";
- };
- };
- }
- // mapAttrs' (
- name: _vmCfg:
- let
- tap = tapName name;
- in
- nameValuePair "microvm-bridge-${name}" {
- description = "Attach MicroVM '${name}' tap to bridge";
- after = [
- "microvm-tap-interfaces@${name}.service"
- "microvm-bridge-setup.service"
- ];
- requires = [
- "microvm-tap-interfaces@${name}.service"
- "microvm-bridge-setup.service"
- ];
- before = [ "microvm@${name}.service" ];
- partOf = [ "microvm@${name}.service" ];
- serviceConfig = {
- Type = "oneshot";
- RemainAfterExit = true;
- ExecStart = "${pkgs.iproute2}/bin/ip link set ${tap} master ${cfg.bridge}";
- ExecStop = "${pkgs.iproute2}/bin/ip link set ${tap} nomaster";
- };
- }
- ) cfg.vms;
- };
-}
modules/microvm/vm-home.nix
@@ -1,195 +0,0 @@
-# Home-manager configuration for microVM guests
-#
-# Provides shell setup, git config, and development tools for Claude Code agents
-{
- pkgs,
- extraPackages ? [ ],
-}:
-{ config, ... }:
-{
- home.stateVersion = "24.11";
- home.username = "vincent";
- home.homeDirectory = "/home/vincent";
-
- # Shell configuration - zsh with starship prompt
- programs.zsh = {
- enable = true;
- dotDir = "${config.xdg.configHome}/zsh"; # Use XDG config directory
- autosuggestion.enable = true;
- enableCompletion = true;
- syntaxHighlighting.enable = true;
- history = {
- size = 10000;
- save = 10000;
- share = true;
- ignoreDups = true;
- ignoreSpace = true;
- };
- shellAliases = {
- ll = "eza -la";
- la = "eza -a";
- l = "eza -l";
- ls = "eza";
- cat = "bat";
- g = "git";
- gs = "git status";
- gd = "git diff";
- gc = "git commit";
- gp = "git push";
- gl = "git log --oneline -20";
- };
- initContent = ''
- # Start in workspace
- cd /workspace 2>/dev/null || true
-
- # Claude Code convenience
- alias cc="claude --dangerously-skip-permissions"
- '';
- };
-
- programs.starship = {
- enable = true;
- enableZshIntegration = true;
- settings = {
- add_newline = true;
- format = "$hostname$directory$git_branch$git_status$nix_shell$character";
- hostname = {
- ssh_only = false;
- format = "[$hostname]($style) ";
- style = "bold cyan";
- };
- directory = {
- truncation_length = 3;
- truncate_to_repo = true;
- };
- git_branch = {
- format = "[$branch]($style) ";
- style = "bold purple";
- };
- character = {
- success_symbol = "[>](bold green)";
- error_symbol = "[>](bold red)";
- };
- nix_shell = {
- format = "[$symbol$state]($style) ";
- symbol = " ";
- };
- };
- };
-
- # Git configuration (using new option names per home-manager 26.05)
- programs.git = {
- enable = true;
- signing = {
- key = null; # No GPG in VMs
- signByDefault = false;
- };
- settings = {
- user.name = "Vincent Demeester";
- user.email = "vincent@sbr.pm";
- init.defaultBranch = "main";
- pull.rebase = true;
- push.autoSetupRemote = true;
- core.editor = "vim";
- diff.algorithm = "histogram";
- merge.conflictstyle = "zdiff3";
- rerere.enabled = true;
- # Safe directory for workspace
- safe.directory = "/workspace";
- };
- };
-
- # Delta for git diffs
- programs.delta = {
- enable = true;
- enableGitIntegration = true;
- options = {
- navigate = true;
- line-numbers = true;
- syntax-theme = "Monokai Extended";
- };
- };
-
- # Tmux for session persistence
- programs.tmux = {
- enable = true;
- clock24 = true;
- keyMode = "vi";
- terminal = "screen-256color";
- historyLimit = 50000;
- extraConfig = ''
- set -g mouse on
- set -g status-style 'bg=#333333 fg=#ffffff'
- set -g status-left '[#S] '
- set -g status-right '%H:%M'
-
- # Better splits
- bind | split-window -h -c "#{pane_current_path}"
- bind - split-window -v -c "#{pane_current_path}"
- '';
- };
-
- # FZF for fuzzy finding
- programs.fzf = {
- enable = true;
- enableZshIntegration = true;
- defaultOptions = [
- "--height 40%"
- "--layout=reverse"
- "--border"
- ];
- };
-
- # Direnv for per-project environments
- programs.direnv = {
- enable = true;
- enableZshIntegration = true;
- nix-direnv.enable = true;
- };
-
- # Development tools
- home.packages =
- with pkgs;
- [
- # Core tools
- ripgrep
- fd
- bat
- eza
- jq
- yq-go
- tree
- htop
- curl
- wget
-
- # Git tools
- delta
- gh
- lazygit
-
- # Nix tools
- nixfmt
- deadnix
- nil # Nix LSP
-
- # Claude Code (from nixpkgs-master via overlay)
- master.claude-code
-
- # Build tools
- gnumake
- ]
- ++ extraPackages;
-
- # XDG directories
- xdg.enable = true;
-
- # Environment variables
- home.sessionVariables = {
- EDITOR = "vim";
- VISUAL = "vim";
- PAGER = "less -FR";
- # Claude reads config from shared mount
- CLAUDE_CONFIG_DIR = "/home/vincent/.claude";
- };
-}
pkgs/my/scripts/bin/microvm-ssh
@@ -1,39 +0,0 @@
-#!/usr/bin/env bash
-# Start microvm on demand and connect via SSH
-# Usage: microvm-ssh <vm-name> <host> <port>
-#
-# This script is used as SSH ProxyCommand to:
-# 1. Start the microvm if not running (via aomi)
-# 2. Wait for SSH to become available
-# 3. Connect using nc/socat
-
-set -euo pipefail
-
-VM_NAME="${1:-}"
-HOST="${2:-}"
-PORT="${3:-22}"
-JUMP_HOST="${MICROVM_JUMP_HOST:-aomi.sbr.pm}"
-
-if [[ -z "$VM_NAME" ]] || [[ -z "$HOST" ]]; then
- echo "Usage: microvm-ssh <vm-name> <host> [port]" >&2
- exit 1
-fi
-
-# Check if VM is running, start if not
-# This runs on the jump host (aomi)
-ssh -q "$JUMP_HOST" "
- if ! systemctl is-active --quiet microvm@${VM_NAME}; then
- echo 'Starting microvm@${VM_NAME}...' >&2
- sudo systemctl start microvm@${VM_NAME}
- # Wait for VM to boot and SSH to be available
- for i in {1..30}; do
- if nc -z ${HOST} ${PORT} 2>/dev/null; then
- break
- fi
- sleep 1
- done
- fi
-"
-
-# Connect through the jump host
-exec ssh -q -W "${HOST}:${PORT}" "$JUMP_HOST"
systems/aomi/extra.nix
@@ -43,9 +43,6 @@
# Automated flake updates
../../modules/nix-flake-updater
-
- # MicroVMs for isolated Claude Code agents
- ./microvms.nix
];
# Firewall is enabled in openshift-port-forward.nix
systems/aomi/microvms.nix
@@ -1,94 +0,0 @@
-# MicroVM configuration for aomi
-#
-# Ephemeral VMs for running Claude Code agents in isolation.
-# VMs share host's /nix/store and mount specific workspaces.
-#
-# VM definitions come from globals.microvms, with host-specific overrides here.
-#
-# Usage:
-# sudo systemctl start microvm@claude-home
-# ssh claude-home # (uses ProxyJump via aomi.sbr.pm)
-# cd /workspace && cc # alias for claude --dangerously-skip-permissions
-#
-{
- pkgs,
- lib,
- globals,
- ...
-}:
-let
- # Base VM config from globals, with aomi-specific overrides
- vmOverrides = {
- claude-home = {
- vcpu = 8;
- mem = 4096;
- extraPackages = with pkgs; [
- # Nix development
- deadnix
- statix
- nixfmt
- nix-prefetch-scripts
- # Go (for tools in this repo)
- go
- ];
- };
- claude-tekton = {
- vcpu = 8;
- mem = 8192; # Tekton tests need more memory
- extraPackages = with pkgs; [
- # Go development
- go
- gopls
- golangci-lint
- ko
- # Kubernetes
- kubectl
- kind
- kubernetes-helm
- ];
- };
- claude-nixpkgs = {
- vcpu = 8;
- mem = 8192; # nixpkgs builds need memory
- extraPackages = with pkgs; [
- # Nix tools
- nixpkgs-review
- nix-update
- nurl
- nix-init
- nixfmt
- deadnix
- statix
- ];
- };
- };
-
- # Merge globals.microvms.vms with local overrides
- # Filter out 'description' as it's only for documentation, not a VM option
- mergedVms = lib.mapAttrs (
- name: globalVm:
- (lib.filterAttrs (k: _: k != "description") globalVm)
- // (vmOverrides.${name} or { })
- // {
- autostart = false;
- }
- ) globals.microvms.vms;
-in
-{
- imports = [ ../../modules/microvm ];
-
- services.microvm-host = {
- enable = true;
-
- # Network configuration from globals
- bridge = "microbr";
- subnet = globals.microvms.subnet;
- externalInterface = "enp0s31f6"; # ThinkPad P1 Gen3 ethernet
-
- # State directory for persistent VM data (SSH keys, etc.)
- stateDir = "/home/vincent/microvm";
-
- # VM definitions (merged from globals + local overrides)
- vms = mergedVms;
- };
-}
flake.lock
@@ -613,27 +613,6 @@
"type": "github"
}
},
- "microvm": {
- "inputs": {
- "nixpkgs": [
- "nixpkgs"
- ],
- "spectrum": "spectrum"
- },
- "locked": {
- "lastModified": 1770074118,
- "narHash": "sha256-3JFYOqJGLgn5QsEnBwOm6K+vFX3uckiiyVt3b9VT5h0=",
- "owner": "astro",
- "repo": "microvm.nix",
- "rev": "4f7e75d2be8a4c99778275ad3b3e4421029dcde0",
- "type": "github"
- },
- "original": {
- "owner": "astro",
- "repo": "microvm.nix",
- "type": "github"
- }
- },
"nix": {
"flake": false,
"locked": {
@@ -958,7 +937,6 @@
"home-manager": "home-manager",
"home-manager-25_11": "home-manager-25_11",
"lanzaboote": "lanzaboote",
- "microvm": "microvm",
"nix-github-actions": "nix-github-actions_4",
"nixos-hardware": "nixos-hardware",
"nixos-raspberrypi": "nixos-raspberrypi",
@@ -992,22 +970,6 @@
"type": "github"
}
},
- "spectrum": {
- "flake": false,
- "locked": {
- "lastModified": 1759482047,
- "narHash": "sha256-H1wiXRQHxxPyMMlP39ce3ROKCwI5/tUn36P8x6dFiiQ=",
- "ref": "refs/heads/main",
- "rev": "c5d5786d3dc938af0b279c542d1e43bce381b4b9",
- "revCount": 996,
- "type": "git",
- "url": "https://spectrum-os.org/git/spectrum"
- },
- "original": {
- "type": "git",
- "url": "https://spectrum-os.org/git/spectrum"
- }
- },
"system-manager": {
"inputs": {
"nixpkgs": [
flake.nix
@@ -300,12 +300,6 @@
inputs.nixpkgs.follows = "nixpkgs";
};
- # microvm.nix for ephemeral coding agent VMs
- microvm = {
- url = "github:astro/microvm.nix";
- inputs.nixpkgs.follows = "nixpkgs";
- };
-
# nixpkgs
nixpkgs = {
type = "github";
globals.nix
@@ -57,29 +57,6 @@ _: {
endpoint = "167.99.17.238";
};
};
- # MicroVMs for isolated Claude Code agents (hosted on aomi)
- # Used by: modules/microvm, home/common/shell/openssh.nix
- microvms = {
- host = "aomi.sbr.pm"; # SSH jump host
- subnet = "192.168.83";
- vms = {
- claude-home = {
- ip = "192.168.83.2";
- workspace = "/home/vincent/src/home";
- description = "Homelab/NixOS work";
- };
- claude-tekton = {
- ip = "192.168.83.3";
- workspace = "/home/vincent/src/tekton-pipelines";
- description = "Tekton development";
- };
- claude-nixpkgs = {
- ip = "192.168.83.4";
- workspace = "/home/vincent/src/nixpkgs";
- description = "nixpkgs contributions";
- };
- };
- };
machines = {
athena = {
net = {