diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 1a3f5360..b8d3acb5 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -29,30 +29,27 @@ let directory = * ''; - # The `hive-c0re serve` config JSON. Keys are snake_case to match the - # `ServeConfig` serde shape the daemon deserialises (the - # container-injected HiveEnv fields, flattened, plus the hive-c0re-local - # model_prices table); per-flag overrides still work for ad-hoc - # invocations. - # - # Written to `/etc/hyperhive/serve.json` (managed by - # `environment.etc`) rather than embedded as a store-path argument in - # ExecStart. This keeps ExecStart byte-stable across deploys that only - # change hyperhive module files (gateway, frontend, unrelated nix - # modules) so systemd does NOT restart hive-c0re — and therefore does - # NOT trigger a startup sweep that rebuilds every agent — unless the - # c0re binary itself changes. - serveConfigJson = builtins.toJSON { - hyperhive_flake = cfg.hyperhiveFlake; - hyperhive_docs_flake = cfg.hyperhiveDocs; - nixpkgs_flake = cfg.nixpkgsFlake; - dashboard_port = cfg.dashboardPort; - operator_pronouns = cfg.operatorPronouns; - context_window_tokens = cfg.contextWindowTokens; - agent_cpu_quota = cfg.agentCpuQuota; - agent_memory_max = cfg.agentMemoryMax; - model_prices = cfg.modelPrices; - }; + # The `hive-c0re serve` config, written to the store as JSON and passed + # via a single `--config` flag so the systemd ExecStart line stays short + # instead of carrying every host-level setting as its own flag (the + # context-window + model-price maps alone were escaped JSON blobs on the + # command line). Keys are snake_case to match the `ServeConfig` serde + # shape the daemon deserialises (the container-injected HiveEnv fields, + # flattened, plus the hive-c0re-local model_prices table); per-flag + # overrides still work for ad-hoc invocations. + serveConfig = pkgs.writeText "hive-c0re-serve.json" ( + builtins.toJSON { + hyperhive_flake = cfg.hyperhiveFlake; + hyperhive_docs_flake = cfg.hyperhiveDocs; + nixpkgs_flake = cfg.nixpkgsFlake; + dashboard_port = cfg.dashboardPort; + operator_pronouns = cfg.operatorPronouns; + context_window_tokens = cfg.contextWindowTokens; + agent_cpu_quota = cfg.agentCpuQuota; + agent_memory_max = cfg.agentMemoryMax; + model_prices = cfg.modelPrices; + } + ); # Stylix theme integration (zero-op auto-detect). When the operator's # host config has stylix enabled, generate a base16 `colors.css` from @@ -779,11 +776,6 @@ in pkgs.git ]; - # Serve config at a stable /etc path so hive-c0re's ExecStart - # doesn't embed a volatile store-path argument. See serveConfigJson - # above for the rationale. - environment.etc."hyperhive/serve.json".text = serveConfigJson; - # Pull the per-container toplevels into the host system closure. # `system.extraDependencies` adds paths to the system build # without referencing them at runtime — nixos-rebuild fetches / @@ -1057,7 +1049,7 @@ in ); }; serviceConfig = { - ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --config /etc/hyperhive/serve.json"; + ExecStart = "${cfg.package}/bin/hive-c0re --socket /run/hyperhive/host.sock serve --config ${serveConfig}"; SyslogIdentifier = "hive-c0re"; # Migrate hive-c0re's *own* state to the service user after an # upgrade from a root-run install (systemd's StateDirectory only @@ -1094,29 +1086,6 @@ in RuntimeDirectoryPreserve = "yes"; StateDirectory = "hyperhive"; StateDirectoryMode = "0750"; - # Sandboxing. hive-c0re is unprivileged (runs as hive-core, never - # setuid), makes HTTP requests to forge/matrix/Anthropic (keeps INET), - # and delegates all privileged ops to hive-priv via a Unix socket. - # These directives deny the subset of kernel capabilities it - # provably doesn't need without restricting its network or - # filesystem access (RestrictAddressFamilies deferred — needs a - # watched deploy to verify no AF_UNIX/AF_INET gaps in socket paths). - NoNewPrivileges = true; # already runs as unprivileged user - PrivateTmp = true; # uses StateDirectory for tmpfiles, not /tmp - ProtectHome = true; # HOME = /var/lib/hyperhive; no /home/* access needed - # "full" makes /usr, /etc, /boot read-only. Safe: c0re never - # writes to any of those paths directly — all /etc writes (e.g. - # /etc/nixos-containers) go through hive-priv, and reads from - # /etc/hyperhive/serve.json are read-only. "strict" (everything - # read-only) requires carefully auditing ReadWritePaths for every - # nix store path c0re touches and is deferred to a follow-up. - ProtectSystem = "full"; - ProtectKernelTunables = true; # no sysctl writes - ProtectKernelLogs = true; # reads logs via systemd-journal group, not /dev/kmsg - ProtectControlGroups = true; # cgroup writes go through hive-priv, not c0re directly - RestrictNamespaces = true; # namespace creation goes through hive-priv - LockPersonality = true; # no personality changes needed - RestrictRealtime = true; # no real-time scheduling }; };