Files
nixos/services/winnie/default.nix
T
Hermes Agent 037c49265e fix(winnie): reinit. unique du layout Garage et ingest en Type=exec
Le premier bootstrap avait laisse un layout Garage a zero noeud (quorum de 0, inapplicable) : purge unique de meta/data avant le demarrage du service, winnie-bucket reprovisionne bucket, cle et s3.env. L'ingest passe de Type=oneshot a Type=exec pour que RuntimeMaxSec s'applique, et S3_ENV_FILE force le redemarrage du backend quand Garage est provisionne apres son premier demarrage.
2026-10-24 15:08:22 -04:00

189 lines
7.7 KiB
Nix

# Module temporaire : active Winnie depuis le flake du projet.
#
# Ecarts assumes par rapport a la source de verite, a supprimer apres :
# 1. Le code Rust du working tree ne compile pas (backend/src reference
# `winnie_backend::router` alors que la crate ne l'expose pas), donc le
# package Nix du flake est inutilisable : on epingle le binaire compile par
# cargo. Nix le copie dans le store, ce qui le protege du ramasse-miettes.
# 2. Le service backend est redeclare ici au lieu d'importer
# `nixosModules.default`, qui tirerait ce package cassable.
# 3. Garage/S3 est desormais actif : le module du projet declare un groupe
# `garage` statique que le DynamicUser du service garage de nixpkgs refuse
# ("User or group with specified name already exists"). Le module du projet
# epingle maintenant le service sur un compte statique garage (DynamicUser =
# false), donc `manageGarage = true` est utilisable tel quel.
# L'infrastructure (user/groupe winnie, base PostgreSQL + schema, secret JWT)
# vient bien du projet via l'import ci-dessous.
{ config, pkgs, lib, ... }:
let
backendBin = pkgs.runCommand "winnie-backend-pinned" { } ''
install -Dm755 ${/home/marc/git/winnie/backend/target/release/winnie-backend} $out/bin/winnie-backend
'';
ingestBin = pkgs.runCommand "winnie-ingest-pinned" { } ''
install -Dm755 ${/home/marc/git/winnie/ingest/target/release/winnie-ingest} $out/bin/winnie-ingest
'';
in {
sops.secrets."oidc_env" = {
sopsFile = ../../secrets/winnie.yml;
key = "oidc_env";
mode = "0640";
group = "winnie";
};
imports = [ /home/marc/git/winnie/services/winnie/default.nix ];
services.winnie.manageGarage = true;
# Rattrapage unique : le premier bootstrap a laisse un layout Garage a zero
# noeud (quorum de 0, impossible a appliquer). Le cluster ne contient aucune
# donnee, donc on recree son etat une seule fois avant que le service ne
# demarre ; winnie-bucket reprovisionne ensuite bucket, cle et s3.env.
systemd.services.winnie-garage-secrets.preStart = ''
if [ ! -e /var/lib/garage/.layout-reset ]; then
rm -rf /var/lib/garage/meta /var/lib/garage/data
install -d -m 0755 -o garage -g garage /var/lib/garage/meta /var/lib/garage/data
touch /var/lib/garage/.layout-reset
chown garage:garage /var/lib/garage/.layout-reset
fi
'';
# OIDC env file from sops (PKCE - no client_secret)
systemd.services.winnie-oidc-secret = {
description = "Generate winnie OIDC env file from sops";
wantedBy = [ "multi-user.target" ];
before = [ "winnie-backend.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/cp ${config.sops.secrets."oidc_env".path} /var/lib/winnie/oidc.env";
};
};
# Front Svelte 5 (web statique, Vite) : `npm run build` dans
# /home/marc/git/winnie/app -> app/dist, copie dans le store a l'evaluation,
# servi en loopback ; Pangolin l'expose sur winnie.marcpartensky.com.
# Rebuild du front = relancer `npm run build` puis un `nixos-rebuild switch`
# (builtins.path est impur : la copie dans le store date de l'evaluation).
# Le port ci-dessous DOIT etre celui de la ressource Pangolin
# `winnie-frontend` dans services/newt : un desaccord donne un 502 sur le
# domaine (le client newt met le port cible en cache, il faut le redemarrer).
systemd.services.winnie-frontend = {
description = "Winnie front (Svelte 5 statique)";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
ExecStart = lib.concatStringsSep " " [
"${pkgs.static-web-server}/bin/static-web-server"
"--host 127.0.0.1"
"--port 8888"
"--root ${builtins.path { path = /home/marc/git/winnie/app/dist; name = "winnie-ui-dist"; }}"
"--page-fallback ${builtins.path { path = /home/marc/git/winnie/app/dist/index.html; name = "winnie-ui-index"; }}"
"--health"
];
Restart = "on-failure";
RestartSec = 5;
};
};
systemd.services.winnie-backend = {
description = "Winnie backend (Rust/Axum, binaire epingle, S3/Garage)";
wantedBy = [ "multi-user.target" ];
after = [
"postgresql.service"
"winnie-schema.service"
"winnie-jwt-secret.service"
"winnie-oidc-secret.service"
"winnie-bucket.service"
"network.target"
];
# s3.env is optional at the EnvironmentFile level, but when the bucket unit
# exists the backend must read the key it writes: start after it so a fresh
# boot does not come up without S3.
wants = [ "winnie-bucket.service" ];
requires = [ "postgresql.service" "winnie-jwt-secret.service" "winnie-oidc-secret.service" ];
serviceConfig = {
User = "winnie";
Group = "winnie";
WorkingDirectory = "/var/lib/winnie";
# jwt.env est obligatoire, s3.env optionnel, oidc.env pour PKCE/Zitadel
EnvironmentFile = [ "/var/lib/winnie/jwt.env" "-/var/lib/winnie/s3.env" "/var/lib/winnie/oidc.env" ];
Environment = [
"DATABASE_URL=postgresql://winnie@127.0.0.1/winnie"
"SERVER_HOST=127.0.0.1"
"SERVER_PORT=8787"
"RUST_LOG=info"
"RESTAURANTS_JSON=${/home/marc/git/winnie/data/restaurants.json}"
"CHAT_BACKEND=openrouter"
# Marker: changing the environment forces systemd to restart the unit,
# which is required to pick up s3.env when Garage is provisioned after
# the backend's first start.
"S3_ENV_FILE=/var/lib/winnie/s3.env"
];
ExecStart = "${backendBin}/bin/winnie-backend";
Restart = "on-failure";
RestartSec = 5;
};
};
# The nightly batch reads its window, model, and zones from Postgres
# (`ingest_config`, edited in the app's Settings screen). This unit only
# makes the OpenRouter key available: the agent's .env contains a bare token
# that systemd would reject as an EnvironmentFile, so copy the one useful
# assignment into a dedicated 0640 file.
systemd.services.winnie-ingest-env = {
description = "Extract the OpenRouter key for the Winnie nightly ingest";
wantedBy = [ "multi-user.target" ];
before = [ "winnie-ingest-nightly.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -e
install -d -m 0750 -o root -g winnie /var/lib/winnie
${pkgs.gnugrep}/bin/grep '^OPENROUTER_API_KEY=' /var/lib/hermes/.hermes/.env > /var/lib/winnie/openrouter.env
chown hermes:winnie /var/lib/winnie/openrouter.env
chmod 0640 /var/lib/winnie/openrouter.env
'';
};
systemd.services.winnie-ingest-nightly = {
description = "Winnie nightly ingest (config-driven 01:00-08:00 window)";
after = [
"postgresql.service"
"winnie-schema.service"
"winnie-ingest-env.service"
"network-online.target"
];
wants = [ "network-online.target" ];
requires = [ "postgresql.service" "winnie-ingest-env.service" ];
path = [ pkgs."poppler-utils" ];
serviceConfig = {
# `exec` (not `oneshot`) so RuntimeMaxSec is enforced: the batch is a
# long-running process, not a short activation task.
Type = "exec";
User = "hermes";
WorkingDirectory = "/var/lib/winnie";
EnvironmentFile = "/var/lib/winnie/openrouter.env";
Environment = [
"DATABASE_URL=postgresql://winnie@127.0.0.1/winnie"
"RUST_LOG=winnie_ingest=info"
];
ExecStart = "${ingestBin}/bin/winnie-ingest nightly";
RuntimeMaxSec = "27000s";
TimeoutStartSec = "infinity";
};
};
systemd.timers.winnie-ingest-nightly = {
description = "Nightly Winnie ingest window";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 01:00:00";
Persistent = true;
Unit = "winnie-ingest-nightly.service";
};
};
}