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
158 lines
6 KiB
Nix
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";
|
|
};
|
|
};
|
|
}
|