Only include EnvironmentFile when at least one *File option (openaiApiKeyFile, tokenFile, qdrantApiKeyFile) is set. Without this, an empty EnvironmentFile list causes systemd to fail with 'Failed to load environment files'.
162 lines
5.7 KiB
Nix
162 lines
5.7 KiB
Nix
{
|
|
description = "St-Pierre - MCP Flashcard Server";
|
|
|
|
inputs = {
|
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
|
flake-utils.url = "github:numtide/flake-utils";
|
|
};
|
|
|
|
outputs = { self, nixpkgs, flake-utils }:
|
|
(flake-utils.lib.eachDefaultSystem (system:
|
|
let
|
|
pkgs = nixpkgs.legacyPackages.${system};
|
|
st-pierre = pkgs.buildGoModule {
|
|
pname = "st-pierre";
|
|
version = "0.1.0";
|
|
src = ./.;
|
|
vendorHash = "sha256-HfaivCA4oIUL5fIVuxNeuqftmShVhniyFpMaEPkEmOA=";
|
|
};
|
|
in
|
|
{
|
|
packages.default = st-pierre;
|
|
devShells.default = pkgs.mkShell {
|
|
buildInputs = with pkgs; [ go gopls gotools ];
|
|
};
|
|
}
|
|
)) // {
|
|
nixosModules.st-pierre = { config, pkgs, lib, ... }:
|
|
let
|
|
cfg = config.services.st-pierre;
|
|
st-pierre-pkg = self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
|
gitHash = if (self ? rev) then builtins.substring 0 8 self.rev else "dirty";
|
|
lastUpdated = if (self ? lastModified) then builtins.toString self.lastModified else "0";
|
|
in
|
|
{
|
|
options.services.st-pierre = {
|
|
enable = lib.mkEnableOption "St-Pierre flashcard MCP server";
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8080;
|
|
description = "HTTP port to listen on.";
|
|
};
|
|
|
|
qdrantUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "localhost";
|
|
description = "Qdrant server hostname.";
|
|
};
|
|
|
|
qdrantPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 6334;
|
|
description = "Qdrant gRPC port.";
|
|
};
|
|
|
|
qdrantApiKeyFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = "Path to a file containing the Qdrant API key.";
|
|
};
|
|
|
|
qdrantTls = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Enable TLS for Qdrant connection.";
|
|
};
|
|
|
|
openaiApiKeyFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = "Path to a file containing the API key (OpenAI or DeepSeek).";
|
|
};
|
|
|
|
openaiApiKey = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "API key directly. Prefer openaiApiKeyFile for secrets.";
|
|
};
|
|
|
|
embeddingBaseUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "https://api.openai.com";
|
|
description = "Base URL for the embeddings API. Do not include /v1 suffix; it is appended automatically.";
|
|
};
|
|
|
|
embeddingModel = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "text-embedding-3-small";
|
|
description = "Embedding model name.";
|
|
};
|
|
|
|
tokenFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = "Path to a file containing the MCP Bearer token.";
|
|
};
|
|
|
|
token = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "MCP Bearer token directly. Prefer tokenFile for secrets.";
|
|
};
|
|
|
|
sqlitePath = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = "/var/lib/st-pierre/st-pierre.sqlite";
|
|
description = "Path to the SQLite database file.";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
users.groups.st-pierre = { };
|
|
|
|
users.users.st-pierre = {
|
|
createHome = true;
|
|
isSystemUser = true;
|
|
home = "/var/lib/st-pierre";
|
|
group = "st-pierre";
|
|
};
|
|
|
|
systemd.services.st-pierre = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" ];
|
|
|
|
serviceConfig = {
|
|
User = "st-pierre";
|
|
Group = "st-pierre";
|
|
Restart = "always";
|
|
WorkingDirectory = "/var/lib/st-pierre";
|
|
StateDirectory = "st-pierre";
|
|
ExecStart = "${st-pierre-pkg}/bin/st-pierre";
|
|
}
|
|
// lib.mkIf (cfg.openaiApiKeyFile != null || cfg.tokenFile != null || cfg.qdrantApiKeyFile != null) {
|
|
EnvironmentFile = lib.optional (cfg.openaiApiKeyFile != null) cfg.openaiApiKeyFile
|
|
++ lib.optional (cfg.tokenFile != null) cfg.tokenFile
|
|
++ lib.optional (cfg.qdrantApiKeyFile != null) cfg.qdrantApiKeyFile;
|
|
};
|
|
|
|
environment =
|
|
{
|
|
PORT = builtins.toString cfg.port;
|
|
QDRANT_URL = cfg.qdrantUrl;
|
|
QDRANT_PORT = builtins.toString cfg.qdrantPort;
|
|
QDRANT_TLS = if cfg.qdrantTls then "true" else "false";
|
|
EMBEDDING_BASE_URL = cfg.embeddingBaseUrl;
|
|
EMBEDDING_MODEL = cfg.embeddingModel;
|
|
SQLITE_PATH = builtins.toString cfg.sqlitePath;
|
|
GIT_COMMIT = gitHash;
|
|
BUILD_DATE = lastUpdated;
|
|
}
|
|
// lib.optionalAttrs (cfg.openaiApiKey != null) {
|
|
OPENAI_API_KEY = cfg.openaiApiKey;
|
|
}
|
|
// lib.optionalAttrs (cfg.token != null) {
|
|
MCP_TOKEN = cfg.token;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|