{ config, pkgs, ... }: let stateDir = "/var/lib/agent-codex"; roleDir = "/home/drone/nixos-config/agents"; runtimeXmppConfig = "${stateDir}/sendxmpp.conf"; runtimeBuilderXmppConfig = "${stateDir}/builder-sendxmpp.conf"; listenerScript = ./agent-codex/listener.py; workerScript = ./agent-codex/worker.py; builderScript = ./agent-codex/builder.py; codexHome = "${stateDir}/codex-home"; agentHome = "${stateDir}/home"; xmppConfig = pkgs.writeText "agent-sendxmpp.conf" '' username: agent jserver: bot.${config.domain} eval_password: ${pkgs.coreutils}/bin/cat ${config.age.secrets.agentXmpp.path} ''; builderXmppConfig = pkgs.writeText "builder-sendxmpp.conf" '' username: builder jserver: bot.${config.domain} eval_password: ${pkgs.coreutils}/bin/cat ${config.age.secrets.builderXmpp.path} ''; prepareCodexHome = pkgs.writeShellScript "prepare-agent-codex-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 = "lukas@${config.domain}"; AGENT_CODEX_BIN = "${pkgs.codex}/bin/codex"; AGENT_CODEX_HOME = codexHome; AGENT_DEBOUNCE_SECONDS = "3"; AGENT_DOMAIN = config.domain; 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_BUILDER_QUEUE_DIR = "${stateDir}/builder-inbox"; AGENT_BUILDER_PROCESSING_DIR = "${stateDir}/builder-processing"; AGENT_BUILDER_FAILED_DIR = "${stateDir}/builder-failed"; AGENT_ROLE_DIR = roleDir; AGENT_BUILDER_WRITABLE_DIR = "/home/drone/nixos-config"; AGENT_SENDER_STATE_DIR = "${stateDir}/senders"; AGENT_SESSION_DIR = "${stateDir}/sessions"; AGENT_BUILDER_SENDER_STATE_DIR = "${stateDir}/builder-senders"; AGENT_BUILDER_SESSION_DIR = "${stateDir}/builder-sessions"; AGENT_SESSION_IDLE_SECONDS = "1800"; AGENT_SENDXMPP_BIN = "${pkgs.go-sendxmpp}/bin/go-sendxmpp"; AGENT_XMPP_CONFIG = runtimeXmppConfig; AGENT_BUILDER_XMPP_CONFIG = runtimeBuilderXmppConfig; XDG_CACHE_HOME = "${stateDir}/xdg-cache"; XDG_DATA_HOME = "${stateDir}/xdg-data"; }; in { systemd.services.agent-xmpp-listener = { description = "Queue allowlisted XMPP messages for the Codex platform agent"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; environment = commonEnvironment; serviceConfig = { User = "drone"; Group = "users"; StateDirectory = "agent-codex"; StateDirectoryMode = "0700"; UMask = "0077"; ExecStartPre = "${pkgs.coreutils}/bin/install -m 600 ${xmppConfig} ${runtimeXmppConfig}"; ExecStart = pkgs.writeShellScript "agent-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"; }; }; systemd.services.agent-codex-worker = { description = "Process queued XMPP messages with the read-only Codex platform agent"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = commonEnvironment; path = [ pkgs.git pkgs.openssh ]; serviceConfig = { Type = "oneshot"; User = "drone"; Group = "users"; WorkingDirectory = roleDir; StateDirectory = "agent-codex"; StateDirectoryMode = "0700"; UMask = "0077"; ExecStartPre = prepareCodexHome; ExecStart = "${pkgs.python3}/bin/python3 ${workerScript}"; TimeoutStartSec = "20min"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = "read-only"; }; }; systemd.paths.agent-codex-worker = { description = "Watch the XMPP Codex inbox"; wantedBy = [ "multi-user.target" ]; pathConfig = { PathExistsGlob = "${stateDir}/inbox/*.json"; Unit = "agent-codex-worker.service"; }; }; # Direct messages to builder@ queue the same way agent@ does, into # builder-inbox instead of inbox. systemd.services.agent-codex-builder-listener = { description = "Queue allowlisted XMPP messages for the Codex Builder"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; environment = commonEnvironment // { AGENT_QUEUE_DIR = "${stateDir}/builder-inbox"; }; serviceConfig = { User = "drone"; Group = "users"; StateDirectory = "agent-codex"; StateDirectoryMode = "0700"; UMask = "0077"; ExecStartPre = "${pkgs.coreutils}/bin/install -m 600 ${builderXmppConfig} ${runtimeBuilderXmppConfig}"; ExecStart = pkgs.writeShellScript "agent-codex-builder-listener" '' set -o pipefail ${pkgs.go-sendxmpp}/bin/go-sendxmpp \ --file ${runtimeBuilderXmppConfig} \ --listen \ | ${pkgs.python3}/bin/python3 ${listenerScript} ''; Restart = "always"; RestartSec = "10s"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = "read-only"; }; }; # Builder converses, plans, and can now implement changes: workspace-write # sandbox over the full nixos-config checkout (AGENT_BUILDER_WRITABLE_DIR), # git/openssh on PATH to commit and push to dev. Deliberately no deploy # capability -- see builder.py and agents/builder.md. systemd.services.agent-codex-builder = { description = "Process queued XMPP messages with the write-capable Codex Builder"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; environment = commonEnvironment; path = [ pkgs.git pkgs.openssh pkgs.nix ]; serviceConfig = { Type = "oneshot"; User = "drone"; Group = "users"; WorkingDirectory = roleDir; StateDirectory = "agent-codex"; StateDirectoryMode = "0700"; UMask = "0077"; ExecStartPre = [ prepareCodexHome "${pkgs.coreutils}/bin/install -m 600 ${builderXmppConfig} ${runtimeBuilderXmppConfig}" ]; ExecStart = "${pkgs.python3}/bin/python3 ${builderScript}"; TimeoutStartSec = "20min"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = "read-only"; ReadWritePaths = [ "/home/drone/nixos-config" ]; }; }; systemd.paths.agent-codex-builder = { description = "Watch the Builder handoff inbox"; wantedBy = [ "multi-user.target" ]; pathConfig = { PathExistsGlob = "${stateDir}/builder-inbox/*.json"; Unit = "agent-codex-builder.service"; }; }; }