From 43ffc234acfc8796a173608535566ce4f76e9231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torjus=20H=C3=A5kestad?= Date: Tue, 3 Feb 2026 18:26:29 +0100 Subject: [PATCH] feat: add NixOS module for nixos-options-mcp service Module provides: - services.nixos-options-mcp.enable - Enable the service - services.nixos-options-mcp.package - Package to use - services.nixos-options-mcp.database.type - sqlite or postgres - services.nixos-options-mcp.database.name - SQLite filename - services.nixos-options-mcp.database.connectionString - PostgreSQL URL - services.nixos-options-mcp.indexOnStart - Revisions to index on start - services.nixos-options-mcp.user/group - Service user/group - services.nixos-options-mcp.dataDir - Data directory Includes systemd hardening options. Co-Authored-By: Claude Opus 4.5 --- flake.nix | 11 +++- nix/module.nix | 145 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 nix/module.nix diff --git a/flake.nix b/flake.nix index 3c955a1..e7ad5e4 100644 --- a/flake.nix +++ b/flake.nix @@ -7,8 +7,9 @@ outputs = { self, nixpkgs }: let + lib = nixpkgs.lib; supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + forAllSystems = lib.genAttrs supportedSystems; pkgsFor = system: nixpkgs.legacyPackages.${system}; in { @@ -67,5 +68,13 @@ ''; }; }); + + nixosModules = { + nixos-options-mcp = { pkgs, ... }: { + imports = [ ./nix/module.nix ]; + services.nixos-options-mcp.package = lib.mkDefault self.packages.${pkgs.system}.nixos-options; + }; + default = self.nixosModules.nixos-options-mcp; + }; }; } diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..9d1e9f2 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,145 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.nixos-options-mcp; + + databaseUrl = if cfg.database.type == "sqlite" + then "sqlite://${cfg.dataDir}/${cfg.database.name}" + else cfg.database.connectionString; +in +{ + options.services.nixos-options-mcp = { + enable = lib.mkEnableOption "NixOS Options MCP server"; + + package = lib.mkPackageOption pkgs "nixos-options-mcp" { }; + + user = lib.mkOption { + type = lib.types.str; + default = "nixos-options-mcp"; + description = "User account under which the service runs."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "nixos-options-mcp"; + description = "Group under which the service runs."; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/nixos-options-mcp"; + description = "Directory to store data files."; + }; + + database = { + type = lib.mkOption { + type = lib.types.enum [ "sqlite" "postgres" ]; + default = "sqlite"; + description = "Database backend to use."; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "nixos-options.db"; + description = "SQLite database filename (when using sqlite backend)."; + }; + + connectionString = lib.mkOption { + type = lib.types.str; + default = ""; + description = '' + PostgreSQL connection string (when using postgres backend). + Example: "postgres://user:password@localhost/nixos_options?sslmode=disable" + ''; + }; + }; + + indexOnStart = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "nixos-unstable" "nixos-24.11" ]; + description = '' + List of nixpkgs revisions to index on service start. + Can be channel names (nixos-unstable) or git hashes. + Indexing is skipped if the revision is already indexed. + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to open the firewall for the MCP server. + Note: MCP typically runs over stdio, so this is usually not needed. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.database.type == "sqlite" || cfg.database.connectionString != ""; + message = "services.nixos-options-mcp.database.connectionString must be set when using postgres backend"; + } + ]; + + users.users.${cfg.user} = lib.mkIf (cfg.user == "nixos-options-mcp") { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + description = "NixOS Options MCP server user"; + }; + + users.groups.${cfg.group} = lib.mkIf (cfg.group == "nixos-options-mcp") { }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -" + ]; + + systemd.services.nixos-options-mcp = { + description = "NixOS Options MCP Server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ] + ++ lib.optional (cfg.database.type == "postgres") "postgresql.service"; + + environment = { + NIXOS_OPTIONS_DATABASE = databaseUrl; + }; + + preStart = lib.mkIf (cfg.indexOnStart != [ ]) '' + ${lib.concatMapStringsSep "\n" (rev: '' + echo "Indexing revision: ${rev}" + ${cfg.package}/bin/nixos-options index "${rev}" || true + '') cfg.indexOnStart} + ''; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + ExecStart = "${cfg.package}/bin/nixos-options serve"; + Restart = "on-failure"; + RestartSec = "5s"; + + # Hardening + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + MemoryDenyWriteExecute = true; + LockPersonality = true; + + ReadWritePaths = [ cfg.dataDir ]; + WorkingDirectory = cfg.dataDir; + StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/nixos-options-mcp") "nixos-options-mcp"; + }; + }; + }; +}