Remove credential passing to sandboxes (didn't work well enough). Move onboarding config init from host-side setup into base.nix so each workspace initializes its own Claude config on first boot. Wrap claude command in VM and Incus workspaces to always skip permission prompts.
180 lines
5.8 KiB
Nix
180 lines
5.8 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
# Incus-specific configuration for sandboxed workspaces
|
|
# Creates fully declarative Incus containers from NixOS configurations
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.sandboxed-workspace;
|
|
hostConfig = config;
|
|
|
|
incusWorkspaces = filterAttrs (n: ws: ws.type == "incus") cfg.workspaces;
|
|
|
|
# Build a NixOS LXC image for a workspace
|
|
mkContainerImage = name: ws:
|
|
let
|
|
nixpkgs = hostConfig.inputs.nixpkgs;
|
|
containerSystem = nixpkgs.lib.nixosSystem {
|
|
modules = [
|
|
(import ./base.nix {
|
|
inherit hostConfig;
|
|
workspaceName = name;
|
|
ip = ws.ip;
|
|
networkInterface = { Name = "eth0"; };
|
|
})
|
|
|
|
(import ws.config)
|
|
|
|
({ config, lib, pkgs, ... }: {
|
|
nixpkgs.hostPlatform = hostConfig.currentSystem;
|
|
boot.isContainer = true;
|
|
networking.useHostResolvConf = false;
|
|
nixpkgs.config.allowUnfree = true;
|
|
|
|
environment.systemPackages = [
|
|
(lib.hiPrio (pkgs.writeShellScriptBin "claude" ''
|
|
exec ${pkgs.claude-code}/bin/claude --dangerously-skip-permissions "$@"
|
|
''))
|
|
];
|
|
})
|
|
];
|
|
};
|
|
in
|
|
{
|
|
rootfs = containerSystem.config.system.build.images.lxc;
|
|
metadata = containerSystem.config.system.build.images.lxc-metadata;
|
|
toplevel = containerSystem.config.system.build.toplevel;
|
|
};
|
|
|
|
mkIncusService = name: ws:
|
|
let
|
|
images = mkContainerImage name ws;
|
|
hash = builtins.substring 0 12 (builtins.hashString "sha256" "${images.rootfs}");
|
|
imageName = "nixos-workspace-${name}-${hash}";
|
|
containerName = "workspace-${name}";
|
|
|
|
bridgeName = config.networking.sandbox.bridgeName;
|
|
mac = lib.mkMac "incus-${name}";
|
|
|
|
addDevices = ''
|
|
incus config device add ${containerName} eth0 nic nictype=bridged parent=${bridgeName} hwaddr=${mac}
|
|
incus config device add ${containerName} workspace disk source=/home/googlebot/sandboxed/${name}/workspace path=/home/googlebot/workspace shift=true
|
|
incus config device add ${containerName} ssh-keys disk source=/home/googlebot/sandboxed/${name}/ssh-host-keys path=/etc/ssh-host-keys shift=true
|
|
incus config device add ${containerName} claude-config disk source=/home/googlebot/sandboxed/${name}/claude-config path=/home/googlebot/claude-config shift=true
|
|
'';
|
|
in
|
|
{
|
|
description = "Incus workspace ${name}";
|
|
after = [ "incus.service" "incus-preseed.service" "workspace-${name}-setup.service" ];
|
|
requires = [ "incus.service" ];
|
|
wants = [ "workspace-${name}-setup.service" ];
|
|
wantedBy = optional ws.autoStart "multi-user.target";
|
|
|
|
path = [ config.virtualisation.incus.package pkgs.gnutar pkgs.xz pkgs.util-linux ];
|
|
|
|
restartTriggers = [ images.rootfs images.metadata ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
# Serialize incus operations - concurrent container creation causes race conditions
|
|
exec 9>/run/incus-workspace.lock
|
|
flock -x 9
|
|
|
|
# Import image if not present
|
|
if ! incus image list --format csv | grep -q "${imageName}"; then
|
|
metadata_tarball=$(echo ${images.metadata}/tarball/*.tar.xz)
|
|
rootfs_tarball=$(echo ${images.rootfs}/tarball/*.tar.xz)
|
|
incus image import "$metadata_tarball" "$rootfs_tarball" --alias ${imageName}
|
|
|
|
# Clean up old images for this workspace
|
|
incus image list --format csv | grep "nixos-workspace-${name}-" | grep -v "${imageName}" | cut -d, -f2 | while read old_image; do
|
|
incus image delete "$old_image" || true
|
|
done || true
|
|
fi
|
|
|
|
# Always recreate container for ephemeral behavior
|
|
incus stop ${containerName} --force 2>/dev/null || true
|
|
incus delete ${containerName} --force 2>/dev/null || true
|
|
|
|
incus init ${imageName} ${containerName}
|
|
${addDevices}
|
|
incus start ${containerName}
|
|
|
|
# Wait for container to start
|
|
for i in $(seq 1 30); do
|
|
if incus list --format csv | grep -q "^${containerName},RUNNING"; then
|
|
exit 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
exit 1
|
|
'';
|
|
|
|
preStop = ''
|
|
exec 9>/run/incus-workspace.lock
|
|
flock -x 9
|
|
|
|
incus stop ${containerName} --force 2>/dev/null || true
|
|
incus delete ${containerName} --force 2>/dev/null || true
|
|
|
|
# Clean up all images for this workspace
|
|
incus image list --format csv 2>/dev/null | grep "nixos-workspace-${name}-" | cut -d, -f2 | while read img; do
|
|
incus image delete "$img" 2>/dev/null || true
|
|
done
|
|
'';
|
|
};
|
|
|
|
|
|
in
|
|
{
|
|
config = mkIf (cfg.enable && incusWorkspaces != { }) {
|
|
|
|
virtualisation.incus.enable = true;
|
|
networking.nftables.enable = true;
|
|
|
|
virtualisation.incus.preseed = {
|
|
storage_pools = [{
|
|
name = "default";
|
|
driver = "dir";
|
|
config = {
|
|
source = "/var/lib/incus/storage-pools/default";
|
|
};
|
|
}];
|
|
|
|
profiles = [{
|
|
name = "default";
|
|
config = {
|
|
"security.privileged" = "false";
|
|
"security.idmap.isolated" = "true";
|
|
};
|
|
devices = {
|
|
root = {
|
|
path = "/";
|
|
pool = "default";
|
|
type = "disk";
|
|
};
|
|
};
|
|
}];
|
|
};
|
|
|
|
systemd.services = mapAttrs'
|
|
(name: ws: nameValuePair "incus-workspace-${name}" (mkIncusService name ws))
|
|
incusWorkspaces;
|
|
|
|
# Extra alias for incus shell access (ssh is also available via default.nix aliases)
|
|
environment.shellAliases = mkMerge (mapAttrsToList
|
|
(name: ws: {
|
|
"workspace_${name}_shell" = "doas incus exec workspace-${name} -- su -l googlebot";
|
|
})
|
|
incusWorkspaces);
|
|
};
|
|
}
|