Commit b32bd9894d84
Changed files (5)
modules/microvm/default.nix
@@ -0,0 +1,319 @@
+# 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;
+
+ # Create bridge for VM networking
+ 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;
+ };
+}
modules/microvm/vm-home.nix
@@ -0,0 +1,195 @@
+# 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";
+ };
+}
systems/aomi/extra.nix
@@ -43,6 +43,9 @@
# 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
@@ -0,0 +1,88 @@
+# MicroVM configuration for aomi
+#
+# Ephemeral VMs for running Claude Code agents in isolation.
+# VMs share host's /nix/store and mount specific workspaces.
+#
+# Usage:
+# sudo systemctl start microvm@claude-home
+# ssh vincent@192.168.83.2
+# cd /workspace && claude --dangerously-skip-permissions
+#
+{ pkgs, ... }:
+{
+ imports = [ ../../modules/microvm ];
+
+ services.microvm-host = {
+ enable = true;
+
+ # Network configuration
+ bridge = "microbr";
+ subnet = "192.168.83";
+ externalInterface = "enp0s31f6"; # ThinkPad P1 Gen3 ethernet
+
+ # State directory for persistent VM data (SSH keys, etc.)
+ stateDir = "/home/vincent/microvm";
+
+ # VM definitions
+ vms = {
+ # VM for home repository work (NixOS configs, homelab)
+ claude-home = {
+ ip = "192.168.83.2";
+ workspace = "/home/vincent/src/home";
+ vcpu = 8;
+ mem = 4096;
+ autostart = false;
+ extraPackages = with pkgs; [
+ # Nix development
+ deadnix
+ statix
+ nixfmt
+ nix-prefetch-scripts
+
+ # Go (for tools in this repo)
+ go
+ ];
+ };
+
+ # VM for Tekton pipeline development
+ claude-tekton = {
+ ip = "192.168.83.3";
+ workspace = "/home/vincent/src/tekton-pipelines";
+ vcpu = 8;
+ mem = 8192; # Tekton tests need more memory
+ autostart = false;
+ extraPackages = with pkgs; [
+ # Go development
+ go
+ gopls
+ golangci-lint
+ ko
+
+ # Kubernetes
+ kubectl
+ kind
+ kubernetes-helm
+ ];
+ };
+
+ # VM for nixpkgs contributions
+ claude-nixpkgs = {
+ ip = "192.168.83.4";
+ workspace = "/home/vincent/src/nixpkgs";
+ vcpu = 8;
+ mem = 8192; # nixpkgs builds need memory
+ autostart = false;
+ extraPackages = with pkgs; [
+ # Nix tools
+ nixpkgs-review
+ nix-update
+ nurl
+ nix-init
+ nixfmt
+ deadnix
+ statix
+ ];
+ };
+ };
+ };
+}
flake.nix
@@ -300,6 +300,12 @@
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";