treefmt v2.5.0traversed 580 files emitted 384 files for processing formatted 384 files (7 changed) in 859ms M modules/nixos/hermes/agent-config.nix M modules/nixos/hermes/cli.nix M modules/nixos/hermes/guest.nix M modules/nixos/hermes/host.nix M modules/nixos/hermes/options.nix M modules/nixos/hermes/simplex.nix M tests/hermes.nix diff --git a/modules/nixos/hermes/agent-config.nix b/modules/nixos/hermes/agent-config.nix index 417f808..1b03671 100644 --- a/modules/nixos/hermes/agent-config.nix +++ b/modules/nixos/hermes/agent-config.nix @@ -148,16 +148,15 @@ rec { # is the venv ROOT, not its bin dir. # Callers append config.nix.package when the user has config repos; # this file has no `config`. - path = - [ - "${stateDir}/.venv" - hermesPackage - pkgs.bash - pkgs.coreutils - pkgs.git - ] - ++ basePackages - ++ cfg.extraPackages; + path = [ + "${stateDir}/.venv" + hermesPackage + pkgs.bash + pkgs.coreutils + pkgs.git + ] + ++ basePackages + ++ cfg.extraPackages; hasConfigRepos = repos.dirs != [ ]; }; # The config repos this user's agent edits. ./options.nix, diff --git a/modules/nixos/hermes/cli.nix b/modules/nixos/hermes/cli.nix index b2b5f04..5577de6 100644 --- a/modules/nixos/hermes/cli.nix +++ b/modules/nixos/hermes/cli.nix @@ -47,7 +47,9 @@ let lib.mapAttrsToList (user: ucfg: '' ${user}) ADDRESS_FILE=${hlib.simplexAddressFile user} - AGENT_UNIT=${if ucfg.native then "hermes-simplex-address-${user}" else "microvm@${hlib.vmName user}"} + AGENT_UNIT=${ + if ucfg.native then "hermes-simplex-address-${user}" else "microvm@${hlib.vmName user}" + } ;; '') cfg.enabledUsers ); @@ -78,7 +80,9 @@ let in '' ${user}) - ${lib.concatStringsSep "\n " (lib.mapAttrsToList (n: v: "export ${n}=${lib.escapeShellArg v}") env)} + ${lib.concatStringsSep "\n " ( + lib.mapAttrsToList (n: v: "export ${n}=${lib.escapeShellArg v}") env + )} export PATH=${lib.escapeShellArg (lib.makeBinPath path)}:"$PATH" export LD_LIBRARY_PATH=${lib.escapeShellArg rt.env.LD_LIBRARY_PATH}"''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" ;; diff --git a/modules/nixos/hermes/guest.nix b/modules/nixos/hermes/guest.nix index 5378741..202a969 100644 --- a/modules/nixos/hermes/guest.nix +++ b/modules/nixos/hermes/guest.nix @@ -347,7 +347,7 @@ in stateDir = guestStateDir; workingDirectory = guestWorkspace user; addToSystemPackages = true; - settings = ac.settings; + inherit (ac) settings; environment = nixAgentEnv // ac.simplexEnv { @@ -361,7 +361,8 @@ in HERMES_SANDBOX = "microvm"; }; inherit (cfg) extraPlugins; - extraPackages = basePackages ++ cfg.extraPackages ++ lib.optional nixosConfigEnabled config.nix.package; + extraPackages = + basePackages ++ cfg.extraPackages ++ lib.optional nixosConfigEnabled config.nix.package; mcpServers = lib.optionalAttrs ucfg.spacesGateway.enable { spaces = { command = "${pkgs.socat}/bin/socat"; diff --git a/modules/nixos/hermes/host.nix b/modules/nixos/hermes/host.nix index 04b66e7..65731fd 100644 --- a/modules/nixos/hermes/host.nix +++ b/modules/nixos/hermes/host.nix @@ -310,20 +310,20 @@ in )) (forEachVmUser ( user: ucfg: { - "hermes-spaces-bridge-${user}" = lib.mkIf ucfg.spacesGateway.enable { - description = "spaces bridge socket for ${vmName user}"; - wantedBy = [ "sockets.target" ]; - # "vsock::" binds VMADDR_CID_ANY on the host. - listenStreams = [ "vsock::${toString (spacesVsockPort user)}" ]; - socketConfig.Accept = true; - # Any guest can complete a vsock connect. The helper rejects it - # post-accept. So a hostile sibling microVM could trip the - # Accept=yes trigger limit. That limit is 200 per 2s. Then it - # could fail the socket. This is a cross-VM DoS. Rejected - # instances exit within milliseconds. MaxConnections bounds the - # buildup. - socketConfig.TriggerLimitIntervalSec = 0; - }; + "hermes-spaces-bridge-${user}" = lib.mkIf ucfg.spacesGateway.enable { + description = "spaces bridge socket for ${vmName user}"; + wantedBy = [ "sockets.target" ]; + # "vsock::" binds VMADDR_CID_ANY on the host. + listenStreams = [ "vsock::${toString (spacesVsockPort user)}" ]; + socketConfig.Accept = true; + # Any guest can complete a vsock connect. The helper rejects it + # post-accept. So a hostile sibling microVM could trip the + # Accept=yes trigger limit. That limit is 200 per 2s. Then it + # could fail the socket. This is a cross-VM DoS. Rejected + # instances exit within milliseconds. MaxConnections bounds the + # buildup. + socketConfig.TriggerLimitIntervalSec = 0; + }; } )) ]; diff --git a/modules/nixos/hermes/options.nix b/modules/nixos/hermes/options.nix index 58a1db3..e5d55e1 100644 --- a/modules/nixos/hermes/options.nix +++ b/modules/nixos/hermes/options.nix @@ -592,9 +592,7 @@ in # this covers the MAC too. map (fn: { assertion = - !ucfg.enable - || ucfg.native - || lib.count (u: hlib.${fn} u == hlib.${fn} user) vsockUsers == 1; + !ucfg.enable || ucfg.native || lib.count (u: hlib.${fn} u == hlib.${fn} user) vsockUsers == 1; message = "services.hermes-microvm: ${fn} collision on ${user} — rename one of the colliding users or disable one VM (services.hermes-microvm.users..enable = false)."; }) [ "cidFor" ] ) cfg.users @@ -633,7 +631,9 @@ in ++ lib.mapAttrsToList (user: ucfg: { assertion = !(ucfg.enable && ucfg.native) - || lib.count (u: u.enable && u.native && u.simplexPort == ucfg.simplexPort) (lib.attrValues cfg.users) == 1; + || + lib.count (u: u.enable && u.native && u.simplexPort == ucfg.simplexPort) (lib.attrValues cfg.users) + == 1; message = "services.hermes-microvm: duplicate simplexPort ${toString ucfg.simplexPort} (${user}) — the hash-derived default collided in its 1000-port window; set services.hermes-microvm.users..simplexPort explicitly on one of them."; }) cfg.users; }; diff --git a/modules/nixos/hermes/simplex.nix b/modules/nixos/hermes/simplex.nix index fa8139c..7aac29d 100644 --- a/modules/nixos/hermes/simplex.nix +++ b/modules/nixos/hermes/simplex.nix @@ -78,7 +78,10 @@ in RestartSec = 5; # No prompts: a display-name or migration prompt leaves the unit dead. ExecStart = - if bindPort == null then "${startScript}" else "${daemonArgs} --chat-server-port ${toString bindPort}"; + if bindPort == null then + "${startScript}" + else + "${daemonArgs} --chat-server-port ${toString bindPort}"; }; }; diff --git a/tests/hermes.nix b/tests/hermes.nix index ed7ca7e..77daf78 100644 --- a/tests/hermes.nix +++ b/tests/hermes.nix @@ -1532,7 +1532,8 @@ in # LoadCredential resolves before the unit's own ExecStartPre, so the # dashboard unit must not mint its own token; the agent does. agentMintsToken = lib.any (lib.hasInfix "hermes-desktop-token-bob") nativeAgent.serviceConfig.ExecStartPre; - dashboardMintsNone = !lib.any (lib.hasInfix "hermes-desktop-token") nativeDashboard.serviceConfig.ExecStartPre; + dashboardMintsNone = + !lib.any (lib.hasInfix "hermes-desktop-token") nativeDashboard.serviceConfig.ExecStartPre; afterAgent = lib.elem "hermes-agent-bob.service" nativeDashboard.after; wantsAgent = lib.elem "hermes-agent-bob.service" nativeDashboard.wants; }; @@ -1552,7 +1553,12 @@ in testAgentEnv = { expr = { inherit (nativeAgent.environment) HOME HERMES_HOME HERMES_SANDBOX; - inherit (nativeAgent.serviceConfig) User Group WorkingDirectory LoadCredential; + inherit (nativeAgent.serviceConfig) + User + Group + WorkingDirectory + LoadCredential + ; gateway = lib.hasSuffix "/bin/hermes gateway" nativeAgent.serviceConfig.ExecStart; seedsModel = lib.hasInfix ".model-seeded" nativeSeedAgent.preStart; noSeedWithoutModel = lib.hasInfix ".model-seeded" nativeAgent.preStart; @@ -1590,7 +1596,8 @@ in # directly; the uid resolves at run time. testSpacesMcp = { expr = { - bob = lib.hasInfix "\"mcp_servers\":{\"spaces\":{\"args\":[],\"command\":\"/nix/store/" nativeSys.services.hermes-microvm.nativeSettingsFiles.bob.text + bob = + lib.hasInfix "\"mcp_servers\":{\"spaces\":{\"args\":[],\"command\":\"/nix/store/" nativeSys.services.hermes-microvm.nativeSettingsFiles.bob.text && lib.hasInfix "-hermes-spaces-mcp\"" nativeSys.services.hermes-microvm.nativeSettingsFiles.bob.text; noVsock = lib.hasInfix "VSOCK" nativeSys.services.hermes-microvm.nativeSettingsFiles.bob.text; aliceHasNone = nativeSys.services.hermes-microvm.nativeSettingsFiles ? alice; @@ -1643,7 +1650,7 @@ in testShim = { expr = let - text = (hermesShimOf nativeSys).text; + inherit ((hermesShimOf nativeSys)) text; in { bobNative = lib.hasInfix "bob)\n mode=native" text; @@ -1693,13 +1700,12 @@ in { daemonBindsUid = lib.hasInfix "--chat-server-port $((10000 + $(" nativeSimplex.startScript.text; bobUsesIt = lib.hasInfix "hermes-simplex-start" (svc "hermes-simplex-bob").serviceConfig.ExecStart; - aliceUsesIt = lib.hasInfix "hermes-simplex-start" (svc "hermes-simplex-alice").serviceConfig.ExecStart; - bobAgentDialsPublic = - lib.hasInfix "SIMPLEX_WS_URL=ws://127.0.0.1:${toString (hlib.simplexPortFor "bob")}" - (svc "hermes-agent-bob").preStart; - aliceAgentDialsPublic = - lib.hasInfix "SIMPLEX_WS_URL=ws://127.0.0.1:${toString (hlib.simplexPortFor "alice")}" - (svc "hermes-agent-alice").preStart; + aliceUsesIt = lib.hasInfix "hermes-simplex-start" (svc "hermes-simplex-alice") + .serviceConfig.ExecStart; + bobAgentDialsPublic = lib.hasInfix "SIMPLEX_WS_URL=ws://127.0.0.1:${toString (hlib.simplexPortFor "bob")}" (svc "hermes-agent-bob") + .preStart; + aliceAgentDialsPublic = lib.hasInfix "SIMPLEX_WS_URL=ws://127.0.0.1:${toString (hlib.simplexPortFor "alice")}" (svc "hermes-agent-alice") + .preStart; addressDialsPublic = (svc "hermes-simplex-address-bob").environment.SIMPLEX_WS_URL; distinct = hlib.simplexPortFor "alice" != hlib.simplexPortFor "bob"; quiet = failedAssertions twoNative; @@ -1756,8 +1762,11 @@ in expr = { nativeRefusesVm = lib.any (lib.hasInfix "hermes-native-guard-bob") nativeAgent.serviceConfig.ExecStartPre; dashboardToo = lib.any (lib.hasInfix "hermes-native-guard-bob") nativeDashboard.serviceConfig.ExecStartPre; - vmRefusesNative = lib.any (lib.hasInfix "hermes-vm-guard-alice") nativeSys.systemd.services."microvm-virtiofsd@hermes-alice".serviceConfig.ExecStartPre; - afterFirewall = lib.elem "hermes-firewall.service" nativeDashboard.after + vmRefusesNative = + lib.any (lib.hasInfix "hermes-vm-guard-alice") + nativeSys.systemd.services."microvm-virtiofsd@hermes-alice".serviceConfig.ExecStartPre; + afterFirewall = + lib.elem "hermes-firewall.service" nativeDashboard.after && lib.elem "hermes-firewall.service" nativeSys.systemd.services."hermes-simplex-bob".after; }; expected = {