This repository has been archived on 2026-09-17. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
nixos-config/hosts/pinix/localServices/finance-agent.nix
pijukka 5407a1d235 Atomare Self-Close-Sessions, Eskalationskette und Reviewer-Schwelle für Herdr-Rollen
Self-Close: Pane-Wrapper (herdr-self-close.nix), Rollenverträge und
Claude-Settings für graphics/administrator/agent-manager/orga, mit
Regressionstests in agents/tests/roles.sh. Reviewer bleibt bewusst
ausgeschlossen (read-only, keine Aktivierungsfreigabe).

Eskalationsentwurf (docs/herdr-eskalation-review-entwurf.md) freigegeben:
Reviewer-Pflicht ab Risikoschwelle statt bei jedem Diff, Agent-Manager
zusätzlich zum Administrator als Fixer für Rollenvertrags-Blocker
(agent-manager.md 2.0.0 -> 2.1.0), Rückfrage-Kanal zunächst nur als
Pilot bei Orga (orga.md 2.0.0 -> 2.1.0). Details in
docs/herdr-orchestration.md.

Begleitende Infrastruktur: agent-codex/orga-agent/finance-agent-Worker,
orga-agent-Client und hdm.0ff.space-Klon-Doku. graphify-out/, .agents/
und nvim.log neu gitignored (lokale Tool-Ausgabe/Debris, kein Repo-Inhalt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uSxdbo8zykSeH5WCMmbvp
2026-09-01 19:33:39 +02:00

158 lines
6 KiB
Nix

{ config, pkgs, ... }:
let
stateDir = "/var/lib/finance-agent";
# Lukas copies his own repo here (its own AGENTS.md/agent.md defines the
# role -- see finance-agent/README.md). Also Codex's one --add-dir/
# workspace-write target, so the role itself can maintain its data (CSV,
# downloaded invoice scans, ...) directly, unlike the read-only agent@ on
# blunix.
roleDir = "/home/drone/finance-agent";
runtimeXmppConfig = "${stateDir}/sendxmpp.conf";
listenerScript = ./finance-agent/listener.py;
workerScript = ./finance-agent/worker.py;
mailToolScript = ./finance-agent/mailtool.py;
# Strictly read-only IMAP: search INBOX for messages with attachments,
# fetch one attachment to disk. Never marks-as-read (opens the mailbox
# via IMAP EXAMINE, not SELECT), never deletes/moves/sends. Same account
# as home/tnix/localModules/aerc.nix -- reuses mailPW, doesn't duplicate
# it. Wrapped as a $PATH binary so Codex just calls `finance-mail ...`
# the same way it already calls `curl` for upload downloads, rather than
# needing to know the underlying script's store path.
mailToolBin = pkgs.writeShellApplication {
name = "finance-mail";
runtimeInputs = [ pkgs.python3 ];
text = ''
python3 ${mailToolScript} "$@"
'';
};
codexHome = "${stateDir}/codex-home";
agentHome = "${stateDir}/home";
owner = "lukas@${config.domain}";
xmppConfig = pkgs.writeText "finance-sendxmpp.conf" ''
username: finance
jserver: bot.${config.domain}
eval_password: ${pkgs.coreutils}/bin/cat ${config.age.secrets.financeXmpp.path}
'';
# Same pattern as hosts/blunix/localServices/agent-codex.nix's
# prepareCodexHome, but sourced from pinix's own drone home -- `codex
# login` has to be run once, interactively, as drone on pinix before this
# can find anything to copy.
prepareCodexHome = pkgs.writeShellScript "prepare-finance-agent-home" ''
set -eu
${pkgs.coreutils}/bin/install -d -m 700 ${codexHome} ${agentHome}
if [ ! -s ${codexHome}/auth.json ]; then
${pkgs.coreutils}/bin/install \
-m 600 \
/home/drone/.codex/auth.json \
${codexHome}/auth.json
fi
'';
commonEnvironment = {
AGENT_ALLOWED_SENDERS = owner;
AGENT_CODEX_BIN = "${pkgs.codex}/bin/codex";
AGENT_CODEX_HOME = codexHome;
# Smaller/cheaper than the account's frontier default -- this bot only
# reads invoices and answers simple financial questions, doesn't need
# top-tier reasoning, and burns through the ChatGPT usage cap fast
# otherwise (hit the weekly limit 2026-08-05).
AGENT_CODEX_MODEL = "gpt-5.6-luna";
AGENT_DEBOUNCE_SECONDS = "3";
AGENT_FAILED_DIR = "${stateDir}/failed";
AGENT_HOME = agentHome;
AGENT_MAX_INPUT_CHARS = "20000";
AGENT_MAX_RESPONSE_CHARS = "12000";
AGENT_PROCESSING_DIR = "${stateDir}/processing";
AGENT_QUEUE_DIR = "${stateDir}/inbox";
AGENT_ROLE_DIR = roleDir;
AGENT_WRITABLE_DIRS = roleDir;
AGENT_SENDER_STATE_DIR = "${stateDir}/senders";
AGENT_SESSION_DIR = "${stateDir}/sessions";
AGENT_SESSION_IDLE_SECONDS = "1800";
AGENT_SENDXMPP_BIN = "${pkgs.go-sendxmpp}/bin/go-sendxmpp";
AGENT_XMPP_CONFIG = runtimeXmppConfig;
XDG_CACHE_HOME = "${stateDir}/xdg-cache";
XDG_DATA_HOME = "${stateDir}/xdg-data";
# Same Zoho mailbox as home/tnix/localModules/aerc.nix/msmtp.nix.
# Read-only IMAP only (see mailtool.py) -- no SMTP/sending here.
FINANCE_MAIL_HOST = "imappro.zoho.eu";
FINANCE_MAIL_USER = "mail@lukasrosier.com";
FINANCE_MAIL_PASSWORD_FILE = config.age.secrets.mailPW.path;
};
in
{
# Empty until Lukas copies his repo in; owned by drone from the start so
# the copy (scp/rsync/git clone as drone) just works without a manual
# chown first.
systemd.tmpfiles.rules = [
"d ${roleDir} 0700 drone users - -"
];
systemd.services.finance-xmpp-listener = {
description = "Queue allowlisted XMPP messages for the finance agent";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
environment = commonEnvironment;
serviceConfig = {
User = "drone";
Group = "users";
StateDirectory = "finance-agent";
StateDirectoryMode = "0700";
UMask = "0077";
ExecStartPre = "${pkgs.coreutils}/bin/install -m 600 ${xmppConfig} ${runtimeXmppConfig}";
ExecStart = pkgs.writeShellScript "finance-xmpp-listener" ''
set -o pipefail
${pkgs.go-sendxmpp}/bin/go-sendxmpp \
--file ${runtimeXmppConfig} \
--listen \
| ${pkgs.python3}/bin/python3 ${listenerScript}
'';
Restart = "always";
RestartSec = "10s";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = "read-only";
};
};
# Workspace-write over roleDir (unlike blunix's read-only agent@) --
# this role's whole point is maintaining its own data (CSV, invoice
# scans) there, not just talking. No git/commit workflow: it's not a
# repo Codex needs to push anywhere, just files it owns.
systemd.services.finance-agent-worker = {
description = "Process queued XMPP messages with the finance agent";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
environment = commonEnvironment;
path = [ pkgs.curl mailToolBin ];
serviceConfig = {
Type = "oneshot";
User = "drone";
Group = "users";
WorkingDirectory = roleDir;
StateDirectory = "finance-agent";
StateDirectoryMode = "0700";
UMask = "0077";
ExecStartPre = prepareCodexHome;
ExecStart = "${pkgs.python3}/bin/python3 ${workerScript}";
TimeoutStartSec = "20min";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = "read-only";
ReadWritePaths = [ roleDir ];
};
};
systemd.paths.finance-agent-worker = {
description = "Watch the finance agent's XMPP inbox";
wantedBy = [ "multi-user.target" ];
pathConfig = {
PathExistsGlob = "${stateDir}/inbox/*.json";
Unit = "finance-agent-worker.service";
};
};
}