@@ -74,6 +74,52 @@ in
|
|||||||
See: https://man7.org/linux/man-pages/man7/vsock.7.html
|
See: https://man7.org/linux/man-pages/man7/vsock.7.html
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
extraMounts = mkOption {
|
||||||
|
type = types.attrsOf (types.submodule {
|
||||||
|
options = {
|
||||||
|
hostPath = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Path on the host to bind-mount into the workspace.";
|
||||||
|
};
|
||||||
|
containerPath = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Mount point inside the workspace.";
|
||||||
|
};
|
||||||
|
createHostPath = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Whether the workspace setup service should `mkdir -p` the
|
||||||
|
hostPath before the workspace starts. Set to false when the
|
||||||
|
source is managed by another service (e.g. an agenix secret
|
||||||
|
file at /run/agenix/<name>) so the setup doesn't race or
|
||||||
|
collide with the producing service.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
shift = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Pass `shift=true` to the Incus disk device so host UIDs/GIDs
|
||||||
|
are remapped into the container's userns. Set to false when
|
||||||
|
the source filesystem can't be idmapped (e.g. tmpfs under
|
||||||
|
/run on kernels without tmpfs-idmap support). With shift
|
||||||
|
disabled, host-owned files show up as nobody:nogroup inside
|
||||||
|
the container — make the file world-readable (mode 0444) if
|
||||||
|
you need processes inside to read it.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = { };
|
||||||
|
description = ''
|
||||||
|
Additional host→workspace bind mounts beyond the default workspace/,
|
||||||
|
ssh-host-keys/, and claude-config/ mounts. Useful for persisting state
|
||||||
|
(e.g. /var/lib/hermes) across container recreations on nixos-rebuild.
|
||||||
|
Only honored by the "incus" backend currently.
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
default = { };
|
default = { };
|
||||||
@@ -82,6 +128,13 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
assertions = lib.mapAttrsToList
|
||||||
|
(name: ws: {
|
||||||
|
assertion = ws.extraMounts == { } || ws.type == "incus";
|
||||||
|
message = ''sandboxed-workspace.workspaces.${name}: extraMounts is only supported for type = "incus" (got "${ws.type}").'';
|
||||||
|
})
|
||||||
|
cfg.workspaces;
|
||||||
|
|
||||||
# Automatically enable sandbox networking when workspaces are defined
|
# Automatically enable sandbox networking when workspaces are defined
|
||||||
networking.sandbox.enable = mkIf (cfg.workspaces != { }) true;
|
networking.sandbox.enable = mkIf (cfg.workspaces != { }) true;
|
||||||
|
|
||||||
@@ -145,7 +198,7 @@ in
|
|||||||
mkdir -p /home/googlebot/sandboxed/${name}/workspace
|
mkdir -p /home/googlebot/sandboxed/${name}/workspace
|
||||||
mkdir -p /home/googlebot/sandboxed/${name}/ssh-host-keys
|
mkdir -p /home/googlebot/sandboxed/${name}/ssh-host-keys
|
||||||
mkdir -p /home/googlebot/sandboxed/${name}/claude-config
|
mkdir -p /home/googlebot/sandboxed/${name}/claude-config
|
||||||
|
${lib.concatMapStrings (m: "mkdir -p ${m.hostPath}\n ") (lib.filter (m: m.createHostPath) (lib.attrValues ws.extraMounts))}
|
||||||
# Fix ownership
|
# Fix ownership
|
||||||
chown -R googlebot:users /home/googlebot/sandboxed/${name}
|
chown -R googlebot:users /home/googlebot/sandboxed/${name}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,14 @@
|
|||||||
programs.starship.enableFishIntegration = true;
|
programs.starship.enableFishIntegration = true;
|
||||||
programs.starship.settings.container.disabled = true;
|
programs.starship.settings.container.disabled = true;
|
||||||
|
|
||||||
|
# Land in ~/workspace on interactive login. Skipped if the dir doesn't exist
|
||||||
|
# (e.g. before the bind mount is wired) so we don't error on shell startup.
|
||||||
|
programs.fish.loginShellInit = ''
|
||||||
|
if test -d $HOME/workspace
|
||||||
|
cd $HOME/workspace
|
||||||
|
end
|
||||||
|
'';
|
||||||
|
|
||||||
# Basic command-line tools
|
# Basic command-line tools
|
||||||
programs.btop.enable = true;
|
programs.btop.enable = true;
|
||||||
programs.ripgrep.enable = true;
|
programs.ripgrep.enable = true;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ let
|
|||||||
let
|
let
|
||||||
nixpkgs = hostConfig.inputs.nixpkgs;
|
nixpkgs = hostConfig.inputs.nixpkgs;
|
||||||
containerSystem = nixpkgs.lib.nixosSystem {
|
containerSystem = nixpkgs.lib.nixosSystem {
|
||||||
|
specialArgs = { inherit hostConfig; };
|
||||||
modules = [
|
modules = [
|
||||||
(import ./base.nix {
|
(import ./base.nix {
|
||||||
inherit hostConfig;
|
inherit hostConfig;
|
||||||
@@ -65,6 +66,9 @@ let
|
|||||||
incus config device add ${containerName} workspace disk source=/home/googlebot/sandboxed/${name}/workspace path=/home/googlebot/workspace shift=true
|
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} 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
|
incus config device add ${containerName} claude-config disk source=/home/googlebot/sandboxed/${name}/claude-config path=/home/googlebot/claude-config shift=true
|
||||||
|
${lib.concatStrings (lib.mapAttrsToList (mountName: m: ''
|
||||||
|
incus config device add ${containerName} ${mountName} disk source=${m.hostPath} path=${m.containerPath} shift=${lib.boolToString m.shift}
|
||||||
|
'') ws.extraMounts)}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+143
@@ -153,6 +153,27 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": [
|
||||||
|
"hermes-agent",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772408722,
|
||||||
|
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": [
|
"systems": [
|
||||||
@@ -221,6 +242,31 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hermes-agent": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"npm-lockfile-fix": "npm-lockfile-fix",
|
||||||
|
"pyproject-build-systems": "pyproject-build-systems",
|
||||||
|
"pyproject-nix": "pyproject-nix",
|
||||||
|
"uv2nix": "uv2nix"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1780513327,
|
||||||
|
"narHash": "sha256-mY4zJ2N1bgqUDcx4V6YVr4bJXOfklBg19ZZTs1/aSBU=",
|
||||||
|
"owner": "NousResearch",
|
||||||
|
"repo": "hermes-agent",
|
||||||
|
"rev": "39fee4f3bc13a3b74a7ab1dfa306b3ddcbbd4e71",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NousResearch",
|
||||||
|
"repo": "hermes-agent",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"home-manager": {
|
"home-manager": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -315,6 +361,77 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"npm-lockfile-fix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"hermes-agent",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1775903712,
|
||||||
|
"narHash": "sha256-2GV79U6iVH4gKAPWYrxUReB0S41ty/Y3dBLquU8AlaA=",
|
||||||
|
"owner": "jeslie0",
|
||||||
|
"repo": "npm-lockfile-fix",
|
||||||
|
"rev": "c6093acb0c0548e0f9b8b3d82918823721930fe8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "jeslie0",
|
||||||
|
"repo": "npm-lockfile-fix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pyproject-build-systems": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"hermes-agent",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"pyproject-nix": [
|
||||||
|
"hermes-agent",
|
||||||
|
"pyproject-nix"
|
||||||
|
],
|
||||||
|
"uv2nix": [
|
||||||
|
"hermes-agent",
|
||||||
|
"uv2nix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772555609,
|
||||||
|
"narHash": "sha256-3BA3HnUvJSbHJAlJj6XSy0Jmu7RyP2gyB/0fL7XuEDo=",
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "build-system-pkgs",
|
||||||
|
"rev": "c37f66a953535c394244888598947679af231863",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "build-system-pkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pyproject-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"hermes-agent",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1772865871,
|
||||||
|
"narHash": "sha256-/ZTSg97aouL0SlPHaokA4r3iuH9QzHVuWPACD2CUCFY=",
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "pyproject.nix",
|
||||||
|
"rev": "e537db02e72d553cea470976b9733581bcf5b3ed",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "pyproject.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"agenix": "agenix",
|
"agenix": "agenix",
|
||||||
@@ -323,6 +440,7 @@
|
|||||||
"deploy-rs": "deploy-rs",
|
"deploy-rs": "deploy-rs",
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
|
"hermes-agent": "hermes-agent",
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"microvm": "microvm",
|
"microvm": "microvm",
|
||||||
"nix-index-database": "nix-index-database",
|
"nix-index-database": "nix-index-database",
|
||||||
@@ -387,6 +505,31 @@
|
|||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"uv2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"hermes-agent",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"pyproject-nix": [
|
||||||
|
"hermes-agent",
|
||||||
|
"pyproject-nix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1773039484,
|
||||||
|
"narHash": "sha256-+boo33KYkJDw9KItpeEXXv8+65f7hHv/earxpcyzQ0I=",
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "uv2nix",
|
||||||
|
"rev": "b68be7cfeacbed9a3fa38a2b5adc0cfb81d9bb1f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "uv2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -81,6 +81,19 @@
|
|||||||
flake-utils.follows = "flake-utils";
|
flake-utils.follows = "flake-utils";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Hermes agent (Nous Research)
|
||||||
|
hermes-agent = {
|
||||||
|
url = "github:NousResearch/hermes-agent";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.follows = "nixpkgs";
|
||||||
|
# Collapse duplicate copies of pyproject-nix and uv2nix that the
|
||||||
|
# hermes-agent flake otherwise pulls in at multiple revs.
|
||||||
|
pyproject-build-systems.inputs.pyproject-nix.follows = "hermes-agent/pyproject-nix";
|
||||||
|
pyproject-build-systems.inputs.uv2nix.follows = "hermes-agent/uv2nix";
|
||||||
|
uv2nix.inputs.pyproject-nix.follows = "hermes-agent/pyproject-nix";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, ... }@inputs:
|
outputs = { self, nixpkgs, ... }@inputs:
|
||||||
|
|||||||
@@ -23,6 +23,31 @@
|
|||||||
ip = "192.168.83.90";
|
ip = "192.168.83.90";
|
||||||
hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0SNSy/MdW38NqKzLr1SG8WKrs8XkrqibacaJtJPzgW";
|
hostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL0SNSy/MdW38NqKzLr1SG8WKrs8XkrqibacaJtJPzgW";
|
||||||
};
|
};
|
||||||
|
workspaces.hermes = {
|
||||||
|
type = "incus";
|
||||||
|
autoStart = true;
|
||||||
|
config = ./workspaces/hermes.nix;
|
||||||
|
ip = "192.168.83.91";
|
||||||
|
extraMounts = {
|
||||||
|
hermes-state = {
|
||||||
|
hostPath = "/home/googlebot/sandboxed/hermes/hermes-state";
|
||||||
|
containerPath = "/var/lib/hermes";
|
||||||
|
};
|
||||||
|
hermes-env = {
|
||||||
|
hostPath = "/run/agenix/hermes-env";
|
||||||
|
containerPath = "/etc/hermes-env";
|
||||||
|
createHostPath = false; # managed by agenix
|
||||||
|
shift = false; # /run is tmpfs; idmapping not supported
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# Bind-mounted into the hermes workspace with shift=false (tmpfs can't be
|
||||||
|
# idmapped). Mode 0444 lets systemd inside the container read it via the
|
||||||
|
# "other" bits — the file shows up as nobody:nogroup over an un-shifted mount.
|
||||||
|
age.secrets.hermes-env = {
|
||||||
|
file = ../../secrets/hermes-env.age;
|
||||||
|
mode = "0444";
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{ pkgs, hostConfig, ... }:
|
||||||
|
|
||||||
|
{
|
||||||
|
imports = [ hostConfig.inputs.hermes-agent.nixosModules.default ];
|
||||||
|
|
||||||
|
services.hermes-agent = {
|
||||||
|
enable = true;
|
||||||
|
addToSystemPackages = true;
|
||||||
|
container.enable = false;
|
||||||
|
|
||||||
|
# Run the daemon as the same user that owns workspace files so the agent
|
||||||
|
# can read/write the project tree without permission gymnastics.
|
||||||
|
user = "googlebot";
|
||||||
|
group = "users";
|
||||||
|
createUser = false;
|
||||||
|
|
||||||
|
extraPackages = with pkgs; [ nix git ripgrep fd jq ];
|
||||||
|
|
||||||
|
# Bind-mounted from /run/agenix/hermes-env on fry (host decrypts via agenix).
|
||||||
|
# Lives at /etc/... rather than /run/... because the workspace's systemd
|
||||||
|
# mounts a fresh tmpfs over /run at boot, which would shadow the incus mount.
|
||||||
|
# Codex OAuth is NOT here — it lives per-instance in /var/lib/hermes.
|
||||||
|
environmentFiles = [ "/etc/hermes-env" ];
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
model = {
|
||||||
|
provider = "openai-codex";
|
||||||
|
default = "gpt-5.5";
|
||||||
|
};
|
||||||
|
toolsets = [ "all" ];
|
||||||
|
terminal.backend = "local";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Daemon sets HERMES_HOME to stateDir/.hermes via the systemd unit. Setting
|
||||||
|
# it system-wide here makes interactive `hermes` (now running as googlebot)
|
||||||
|
# pick up the same auth.json that the daemon wrote.
|
||||||
|
environment.variables.HERMES_HOME = "/var/lib/hermes/.hermes";
|
||||||
|
|
||||||
|
environment.systemPackages = [ pkgs.codex ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
age-encryption.org/v1
|
||||||
|
-> ssh-ed25519 qEbiMg KYrkjemap/emxqF1CLui+DA+TDQLgosj0MlaqEIpNAE
|
||||||
|
KQfHDMnfhXD5hSq2awqXvO5jpREuI0/aX4h+K5tKHIc
|
||||||
|
-> ssh-ed25519 N7drjg wizDdRxUu6rnZU1BLtJCcSXjBlhk9Tz1+LkEHYXLEQA
|
||||||
|
d12YRFnPklP3i8+A5/LaRo+t6sWdwpMkhIsJuB9osWM
|
||||||
|
-> ssh-ed25519 jQaHAA k4nDo8oo+XG2sjwUD7KTYigk4uJ5lEycb9K8g3/5E18
|
||||||
|
0cqELOwXNGqMQrOZN/1rAR6c2Q9PT7sDevbiBrea9Uc
|
||||||
|
-> ssh-ed25519 ZDy34A UvWhI/zJ9wnm2kw2QixM7RKOXyAMa/BqKqRdTBEAng0
|
||||||
|
KFiiPZZPGLpeTFSUBitmf1+coA7Ss6GjOGETwoBeVBA
|
||||||
|
-> ssh-ed25519 w3nu8g rvc65tlm5zH1EO+vCUDHwFh00V47XF7BS56fSknjh3Q
|
||||||
|
zZKz+jqle+DMNuYDf30m2GlinMyv2iaJT5GhW/5lzpA
|
||||||
|
-> ssh-ed25519 evqvfg VPNt8w3pamyQwYgjRtgrRAM6wdDwvSvvzwBndfq8sng
|
||||||
|
BFJoWxARRTildOkh4BHyLf2jlY8hoeRn5jiEWhBKEkE
|
||||||
|
--- DkGMxA+x5XiQk5uwnbOz4Ua2PivD/2MaYScmJWajCjk
|
||||||
|
'z˜Aµœ“Þ\Oðòç·ý8:%�o¯1<^àC#+
|
||||||
@@ -55,6 +55,9 @@ with roles;
|
|||||||
# Librechat
|
# Librechat
|
||||||
"librechat-env-file.age".publicKeys = librechat;
|
"librechat-env-file.age".publicKeys = librechat;
|
||||||
|
|
||||||
|
# Hermes agent
|
||||||
|
"hermes-env.age".publicKeys = personal;
|
||||||
|
|
||||||
# For ACME DNS Challenge
|
# For ACME DNS Challenge
|
||||||
"digitalocean-dns-credentials.age".publicKeys = dns-challenge;
|
"digitalocean-dns-credentials.age".publicKeys = dns-challenge;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user