Something went wrong. Try again.
atproto, agents, terminal multiplexers
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211self: { config, lib, pkgs, ...}: let cfg = config.services.twin; inherit (lib) mkOption mkEnableOption mkIf types concatStringsSep optionalAttrs; list = concatStringsSep ",";in { options.services.twin = { enable = mkEnableOption "twin, which answers tangled mentions with an agent in a tarp daemon";
package = mkOption { type = types.package; default = self.packages.${pkgs.stdenv.hostPlatform.system}.twin; description = "The twin package."; };
user = mkOption { type = types.str; default = "twin"; description = '' The user twin runs as. The agent works in directories twin makes, so this is usually the user that runs the tarp daemon. ''; };
group = mkOption { type = types.str; default = "twin"; description = "The group twin runs as."; };
dids = mkOption { type = types.listOf types.str; example = ["did:plc:qfpnj4og54vl56wngdriaxug"]; description = "Accounts to watch. twin ignores records from all others."; };
mentionDids = mkOption { type = types.listOf types.str; default = []; example = ["did:plc:omwbu2wmkfxrsevklxbxujfx"]; description = "DIDs of the bot."; };
mentionHandles = mkOption { type = types.listOf types.str; default = []; example = ["bot.oppi.li"]; description = "Handles of the bot. twin resolves them to DIDs at start."; };
collections = mkOption { type = types.listOf types.str; default = ["sh.tangled.*"]; description = "Collections to read: NSIDs, or prefixes ending in `.*`."; };
operations = mkOption { type = types.listOf (types.enum ["create" "update"]); default = ["create"]; description = "Commit operations that can start the agent."; };
jetstreamHosts = mkOption { type = types.listOf types.str; default = ["jetstream.us-east.bsky.network" "jetstream.us-west.bsky.network"]; description = "Jetstream hosts: the first is tailed, the rest are failovers."; };
tarpAddr = mkOption { type = types.str; default = "127.0.0.1:39423"; description = "Address of the tarp daemon."; };
agentCommand = mkOption { type = types.str; default = ''exec opencode --auto --prompt "$TWIN_PROMPT"''; description = "Command run with `sh -c` in each new tarp terminal."; };
join = mkOption { type = types.bool; default = true; description = "Send a message to the terminal of its thread, when one runs."; };
promptTemplate = mkOption { type = types.nullOr types.path; default = null; description = "A Go text/template file to replace the built in prompt."; };
stateDir = mkOption { type = types.str; default = "/var/lib/twin"; description = "Holds the jetstream cursor and the prompts."; };
workDir = mkOption { type = types.str; default = "${cfg.stateDir}/work"; defaultText = lib.literalExpression ''"''${config.services.twin.stateDir}/work"''; description = "Holds one directory for each thread, where the agent starts."; };
logLevel = mkOption { type = types.enum ["debug" "info" "warn" "error"]; default = "info"; description = "Log level."; };
dryRun = mkOption { type = types.bool; default = false; description = "Log the prompt and start nothing."; };
environment = mkOption { type = types.attrsOf types.str; default = {}; description = "More environment variables for twin."; };
environmentFile = mkOption { type = types.nullOr types.path; default = null; description = "A file of more environment variables, read at start."; }; };
config = mkIf cfg.enable { assertions = [ { assertion = cfg.mentionDids != [] || cfg.mentionHandles != []; message = "services.twin: set mentionDids or mentionHandles"; } ];
users.users = mkIf (cfg.user == "twin") { twin = { isSystemUser = true; group = cfg.group; home = cfg.stateDir; }; }; users.groups = mkIf (cfg.group == "twin") {twin = {};};
systemd.tmpfiles.settings.twin = { ${cfg.stateDir}.d = { inherit (cfg) user group; mode = "0750"; }; ${cfg.workDir}.d = { inherit (cfg) user group; mode = "0750"; }; };
systemd.services.twin = { description = "twin: answers tangled mentions with an agent in tarp"; wantedBy = ["multi-user.target"]; wants = ["network-online.target"]; after = ["network-online.target"];
environment = { TWIN_DIDS = list cfg.dids; TWIN_MENTION_DIDS = list cfg.mentionDids; TWIN_MENTION_HANDLES = list cfg.mentionHandles; TWIN_COLLECTIONS = list cfg.collections; TWIN_OPERATIONS = list cfg.operations; TWIN_JETSTREAM_HOSTS = list cfg.jetstreamHosts; TWIN_TARP_ADDR = cfg.tarpAddr; TWIN_AGENT_COMMAND = cfg.agentCommand; TWIN_JOIN = lib.boolToString cfg.join; TWIN_STATE_DIR = cfg.stateDir; TWIN_WORK_DIR = cfg.workDir; TWIN_LOG_LEVEL = cfg.logLevel; TWIN_DRY_RUN = lib.boolToString cfg.dryRun; } // optionalAttrs (cfg.promptTemplate != null) { TWIN_PROMPT_TEMPLATE = "${cfg.promptTemplate}"; } // cfg.environment;
serviceConfig = { ExecStart = lib.getExe cfg.package; User = cfg.user; Group = cfg.group; EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; Restart = "always"; RestartSec = 5;
NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ReadWritePaths = [cfg.stateDir cfg.workDir]; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; LockPersonality = true; }; }; };}