From 300796252bd1aa9ad2d2bca35a01f87b88a538ee Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:23:37 +0900 Subject: [PATCH 1/3] flake: add llm-agents packages Provide Hermes through the shared llm-agents input so host modules can consume a pinned, reproducible agent package. --- flake.lock | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 7 +++++ 2 files changed, 84 insertions(+) diff --git a/flake.lock b/flake.lock index 27644129..87fb4ee8 100644 --- a/flake.lock +++ b/flake.lock @@ -47,6 +47,39 @@ "type": "github" } }, + "bun2nix": { + "inputs": { + "flake-parts": [ + "llm-agents", + "flake-parts" + ], + "nixpkgs": [ + "llm-agents", + "nixpkgs" + ], + "systems": [ + "llm-agents", + "systems" + ], + "treefmt-nix": [ + "llm-agents", + "treefmt-nix" + ] + }, + "locked": { + "lastModified": 1784665499, + "narHash": "sha256-9BMxlTxCCDAeoNLtb1a/st7udtTIJep+wpUzquA29VU=", + "owner": "nix-community", + "repo": "bun2nix", + "rev": "0f2a1f0b6f42cebe3b149bf62d38754c5e0e9729", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "bun2nix", + "type": "github" + } + }, "disko": { "inputs": { "nixpkgs": [ @@ -170,6 +203,34 @@ "type": "github" } }, + "llm-agents": { + "inputs": { + "bun2nix": "bun2nix", + "flake-parts": [ + "flake-parts" + ], + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems_2", + "treefmt-nix": [ + "treefmt-nix" + ] + }, + "locked": { + "lastModified": 1785145739, + "narHash": "sha256-qX/t+D9uVsMw32Za6oXcgXWn6d2HGZqEzMlZWnVQ4GM=", + "owner": "numtide", + "repo": "llm-agents.nix", + "rev": "259abb89d0a0d3ba685987053ad344534a5d6600", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "llm-agents.nix", + "type": "github" + } + }, "multievolve-nix": { "inputs": { "nixpkgs": [ @@ -499,6 +560,7 @@ "disko": "disko", "fast-nix-gc": "fast-nix-gc", "flake-parts": "flake-parts", + "llm-agents": "llm-agents", "multievolve-nix": "multievolve-nix", "niks3": "niks3", "nix-index-database": "nix-index-database", @@ -631,6 +693,21 @@ "type": "github" } }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, "treefmt-nix": { "inputs": { "nixpkgs": [ diff --git a/flake.nix b/flake.nix index f13a8658..8c46f4ab 100644 --- a/flake.nix +++ b/flake.nix @@ -61,6 +61,13 @@ }; # Applications. + llm-agents = { + url = "github:numtide/llm-agents.nix"; + inputs.flake-parts.follows = "flake-parts"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.treefmt-nix.follows = "treefmt-nix"; + }; + multievolve-nix = { url = "github:SBEE-Lab/multievolve-nix"; inputs.nixpkgs.follows = "nixpkgs"; From 3e7796a56a3a484415745bc1b12c4e5ee3a3f582 Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:23:47 +0900 Subject: [PATCH 2/3] hermes-agent: deploy private Nero agent on tau Keep terminal-capable Slack automation isolated while preserving declarative control over credentials, tools, and skill seeding. Restrict container egress to DNS and HTTPS through tau's active interface and expose the dashboard only across the private bridge. --- .sops.nix | 1 + .sops.yaml | 5 + hosts/tau.nix | 8 ++ modules/hermes-agent/config.yml | 23 +++ modules/hermes-agent/default.nix | 229 ++++++++++++++++++++++++++++++ modules/hermes-agent/network.nix | 102 +++++++++++++ modules/hermes-agent/secrets.yaml | 26 ++++ 7 files changed, 394 insertions(+) create mode 100644 modules/hermes-agent/config.yml create mode 100644 modules/hermes-agent/default.nix create mode 100644 modules/hermes-agent/network.nix create mode 100644 modules/hermes-agent/secrets.yaml diff --git a/.sops.nix b/.sops.nix index e94aa295..f1423667 100644 --- a/.sops.nix +++ b/.sops.nix @@ -62,6 +62,7 @@ let ]; "modules/buildbot/secrets.yaml" = [ "psi" ]; "modules/monitoring/secrets.yaml" = [ "rho" ]; + "modules/hermes-agent/secrets.yaml" = [ "tau" ]; "modules/n8n/secrets.yaml" = [ "tau" ]; "modules/nextcloud/secrets.yaml" = [ "tau" ]; "modules/nfs/secrets.yaml" = [ "psi" ]; diff --git a/.sops.yaml b/.sops.yaml index 67bead49..42ad299d 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -65,6 +65,11 @@ creation_rules: - age1zdhqm6ptcnuu3tf2lzcngqmf6eud7jfah7v8falfy5mdksmnfuzq35sq54 - age1730f3cxdyh56zw8xcvlmpa7u2x7353wu4u0e58kyx24rsefgp98sxehm6s path_regex: modules/headscale/secrets.yaml + - key_groups: + - age: + - age13v0djuhkmnd06zvct0zc6sddykqpk3j9k8ev5sfgd6gtj82s0avs68psvj + - age1730f3cxdyh56zw8xcvlmpa7u2x7353wu4u0e58kyx24rsefgp98sxehm6s + path_regex: modules/hermes-agent/secrets.yaml - key_groups: - age: - age1u2ymsehnq872lzj086yf87f6k0zq52x66qz84kp3s6crtcxsts3s9fsneq diff --git a/hosts/tau.nix b/hosts/tau.nix index dbba42e6..2574e71e 100644 --- a/hosts/tau.nix +++ b/hosts/tau.nix @@ -10,6 +10,7 @@ ../modules/rustfs ../modules/backup/primary.nix ../modules/monitoring/vector/monitor-services.nix + ../modules/hermes-agent ../modules/nextcloud ../modules/n8n ../modules/vaultwarden/reverse-proxy.nix @@ -49,6 +50,13 @@ }; services.rustfs.enable = true; + + services.hermes-agent = { + enable = true; + enableDashboard = true; + externalInterface = "eth0"; + allowedSlackUsers = [ "U04GMC10NNP" ]; + }; services.sbee.backups = { primary = { psiProtected.enable = true; diff --git a/modules/hermes-agent/config.yml b/modules/hermes-agent/config.yml new file mode 100644 index 00000000..968886f0 --- /dev/null +++ b/modules/hermes-agent/config.yml @@ -0,0 +1,23 @@ +model: + default: gpt-5.5 + provider: openai-codex +platform_toolsets: + slack: + - web + - terminal + - file + - code_execution + - skills + - todo + - memory + - session_search + - clarify + - delegation + - cronjob + - no_mcp +platforms: + slack: + home_channel: + platform: slack + chat_id: C0BLJF65YG6 + name: Temporary home diff --git a/modules/hermes-agent/default.nix b/modules/hermes-agent/default.nix new file mode 100644 index 00000000..db742638 --- /dev/null +++ b/modules/hermes-agent/default.nix @@ -0,0 +1,229 @@ +{ + config, + lib, + pkgs, + self, + ... +}: +let + cfg = config.services.hermes-agent; + system = pkgs.stdenv.hostPlatform.system; + hermesPkg = self.inputs.llm-agents.packages.${system}.hermes-agent; + stateDir = "/var/lib/hermes"; + runtimePath = [ + hermesPkg + ] + ++ (with pkgs; [ + bash + coreutils + curl + fd + file + findutils + git + gnugrep + gnused + gnutar + gzip + jq + openssh + procps + ripgrep + unzip + util-linux + which + xz + ]); + runtimeEnv = { + TZ = "Asia/Seoul"; + HOME = stateDir; + HERMES_HOME = "${stateDir}/.hermes"; + HERMES_INFERENCE_PROVIDER = cfg.inferenceProvider; + HERMES_INFERENCE_MODEL = cfg.inferenceModel; + SLACK_ALLOWED_USERS = lib.concatStringsSep "," cfg.allowedSlackUsers; + }; +in +{ + imports = [ ./network.nix ]; + + options.services.hermes-agent = { + enable = lib.mkEnableOption "Hermes Agent Slack gateway"; + + inferenceProvider = lib.mkOption { + type = lib.types.str; + default = "openai-codex"; + description = "Hermes inference provider name."; + }; + + inferenceModel = lib.mkOption { + type = lib.types.str; + default = "gpt-5.5"; + description = "Hermes inference model name."; + }; + + allowedSlackUsers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Slack user IDs allowed to use Hermes."; + }; + + enableDashboard = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to run the local Hermes dashboard in the container."; + }; + + externalInterface = lib.mkOption { + type = lib.types.str; + description = "Host interface used for restricted Hermes egress."; + }; + + nameservers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "117.16.191.6" + "168.126.63.1" + ]; + description = "DNS servers available to the isolated Hermes container."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.allowedSlackUsers != [ ]; + message = "services.hermes-agent.allowedSlackUsers must list at least one Slack user ID."; + } + ]; + + sops.secrets = { + hermes-slack-bot-token.sopsFile = ./secrets.yaml; + hermes-slack-app-token.sopsFile = ./secrets.yaml; + }; + + systemd.tmpfiles.rules = [ + "d ${stateDir} 0750 - - -" + ]; + + containers.hermes = { + autoStart = true; + + bindMounts.${stateDir} = { + hostPath = stateDir; + isReadOnly = false; + }; + + extraFlags = [ + "--load-credential=slack-bot-token:${config.sops.secrets.hermes-slack-bot-token.path}" + "--load-credential=slack-app-token:${config.sops.secrets.hermes-slack-app-token.path}" + ]; + + config = _: { + system.stateVersion = "25.05"; + + users.users.hermes = { + isSystemUser = true; + group = "hermes"; + uid = 2001; + home = stateDir; + }; + users.groups.hermes.gid = 2001; + + time.timeZone = "Asia/Seoul"; + environment.systemPackages = [ hermesPkg ]; + + systemd.tmpfiles.rules = [ + "d ${stateDir} 0750 hermes hermes -" + "d ${stateDir}/.hermes 0750 hermes hermes -" + # Sticky root ownership protects future Nix-owned skill symlinks while + # still letting Hermes create and manage its own sibling skills. + "d ${stateDir}/.hermes/skills 1770 root hermes -" + "f ${stateDir}/.hermes/.no-bundled-skills 0640 hermes hermes -" + "L+ ${stateDir}/.hermes/config.yaml - - - - ${./config.yml}" + ]; + + systemd.services.hermes-skill-policy = { + description = "Apply Hermes declarative skill policy"; + wantedBy = [ "multi-user.target" ]; + before = [ "hermes.service" ] ++ lib.optional cfg.enableDashboard "hermes-dashboard.service"; + + path = runtimePath; + environment = runtimeEnv; + + serviceConfig = { + Type = "oneshot"; + User = "hermes"; + Group = "hermes"; + WorkingDirectory = stateDir; + RemainAfterExit = true; + ExecStart = pkgs.writeShellScript "hermes-skill-policy" '' + set -euo pipefail + exec ${lib.getExe hermesPkg} skills opt-out --remove --yes + ''; + }; + }; + + systemd.services.hermes = { + description = "Hermes Agent Slack gateway"; + wantedBy = [ "multi-user.target" ]; + after = [ + "hermes-skill-policy.service" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + requires = [ "hermes-skill-policy.service" ]; + + path = runtimePath; + environment = runtimeEnv; + + serviceConfig = { + User = "hermes"; + Group = "hermes"; + WorkingDirectory = stateDir; + StateDirectory = "hermes"; + ImportCredential = [ + "slack-bot-token" + "slack-app-token" + ]; + Restart = "on-failure"; + RestartSec = 30; + ExecStart = pkgs.writeShellScript "hermes-gateway" '' + set -euo pipefail + SLACK_BOT_TOKEN=$(< "$CREDENTIALS_DIRECTORY/slack-bot-token") + SLACK_APP_TOKEN=$(< "$CREDENTIALS_DIRECTORY/slack-app-token") + export SLACK_BOT_TOKEN SLACK_APP_TOKEN + exec ${lib.getExe hermesPkg} gateway run + ''; + }; + }; + + systemd.services.hermes-dashboard = lib.mkIf cfg.enableDashboard { + description = "Hermes Agent web dashboard"; + wantedBy = [ "multi-user.target" ]; + after = [ + "hermes-skill-policy.service" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + requires = [ "hermes-skill-policy.service" ]; + + path = runtimePath; + environment = runtimeEnv; + + serviceConfig = { + User = "hermes"; + Group = "hermes"; + WorkingDirectory = stateDir; + StateDirectory = "hermes"; + Restart = "on-failure"; + RestartSec = 30; + ExecStart = pkgs.writeShellScript "hermes-dashboard" '' + set -euo pipefail + exec ${lib.getExe hermesPkg} dashboard --host 127.0.0.1 --port 9119 --no-open --skip-build + ''; + }; + }; + }; + }; + }; +} diff --git a/modules/hermes-agent/network.nix b/modules/hermes-agent/network.nix new file mode 100644 index 00000000..9f511b9e --- /dev/null +++ b/modules/hermes-agent/network.nix @@ -0,0 +1,102 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.hermes-agent; + agentBridge = "br-agents"; + hostAddress = "10.233.0.1"; + localAddress = "10.233.0.10"; +in +{ + config = lib.mkIf cfg.enable { + networking = { + bridges.${agentBridge}.interfaces = [ ]; + interfaces.${agentBridge}.ipv4.addresses = [ + { + address = hostAddress; + prefixLength = 24; + } + ]; + + nat = { + enable = true; + inherit (cfg) externalInterface; + internalInterfaces = [ agentBridge ]; + }; + + # Tau still uses NixOS' iptables firewall backend. Dedicated chains keep + # agent policy isolated from unrelated host firewall rules. + firewall.extraCommands = '' + iptables -w -N hermes-input 2>/dev/null || true + iptables -w -F hermes-input + iptables -w -A hermes-input -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + iptables -w -A hermes-input -j DROP + iptables -w -D INPUT -i ${agentBridge} -j hermes-input 2>/dev/null || true + iptables -w -I INPUT 1 -i ${agentBridge} -j hermes-input + + iptables -w -N hermes-forward 2>/dev/null || true + iptables -w -F hermes-forward + iptables -w -A hermes-forward -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + iptables -w -A hermes-forward -o ${cfg.externalInterface} -p tcp --dport 443 -j ACCEPT + iptables -w -A hermes-forward -o ${cfg.externalInterface} -p udp --dport 53 -j ACCEPT + iptables -w -A hermes-forward -o ${cfg.externalInterface} -p tcp --dport 53 -j ACCEPT + iptables -w -A hermes-forward -j DROP + iptables -w -D FORWARD -i ${agentBridge} -j hermes-forward 2>/dev/null || true + iptables -w -I FORWARD 1 -i ${agentBridge} -j hermes-forward + ''; + firewall.extraStopCommands = '' + iptables -w -D INPUT -i ${agentBridge} -j hermes-input 2>/dev/null || true + iptables -w -F hermes-input 2>/dev/null || true + iptables -w -X hermes-input 2>/dev/null || true + iptables -w -D FORWARD -i ${agentBridge} -j hermes-forward 2>/dev/null || true + iptables -w -F hermes-forward 2>/dev/null || true + iptables -w -X hermes-forward 2>/dev/null || true + ''; + }; + + containers.hermes = { + privateNetwork = true; + hostBridge = agentBridge; + localAddress = "${localAddress}/24"; + + config = _: { + networking = { + useHostResolvConf = false; + nftables.enable = true; + defaultGateway = { + address = hostAddress; + interface = "eth0"; + }; + inherit (cfg) nameservers; + firewall.extraInputRules = lib.optionalString cfg.enableDashboard '' + ip saddr ${hostAddress} tcp dport 9119 accept + ''; + }; + + systemd.sockets.hermes-dashboard-proxy = lib.mkIf cfg.enableDashboard { + description = "Private bridge socket for the Hermes dashboard"; + wantedBy = [ "sockets.target" ]; + socketConfig.ListenStream = "${localAddress}:9119"; + }; + + systemd.services.hermes-dashboard-proxy = lib.mkIf cfg.enableDashboard { + description = "Proxy the private bridge to the loopback-only Hermes dashboard"; + after = [ "hermes-dashboard.service" ]; + requires = [ "hermes-dashboard.service" ]; + serviceConfig = { + User = "hermes"; + Group = "hermes"; + ExecStart = "${pkgs.systemd}/lib/systemd/systemd-socket-proxyd 127.0.0.1:9119"; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + }; + }; + }; + }; + }; +} diff --git a/modules/hermes-agent/secrets.yaml b/modules/hermes-agent/secrets.yaml new file mode 100644 index 00000000..fb99a6ec --- /dev/null +++ b/modules/hermes-agent/secrets.yaml @@ -0,0 +1,26 @@ +hermes-slack-bot-token: ENC[AES256_GCM,data:Oz0TUYxfUKIcJdxavC5aMr1p+5oUsEWA7FKgr6wPvE7G5Nkm/bMVwq89kVZQZRom3XeMtOUY03lA9Q==,iv:eimUXsJDWe1mmixqRN/FOsUQ87EoXn0Kzmu2G0W/tpA=,tag:Usr2nyQoC0XUTUDf895diA==,type:str] +hermes-slack-app-token: ENC[AES256_GCM,data:XsK5T/mg4IdRgd+Pi9WwTRn6oucP2mVVOnRaLG00JVz0StOR+uZ7+78F297wSLZ/WYPEPmYgiIL+N234PqngOcE3LB9tCBYy2gCw6r2v9xIWccPFxjClK9GPLEPpUC+bUeg=,iv:Kc0lGBa8X5OzuHo4T/B1e+lBGiOLDWs5j0kEzviXuzI=,tag:Z1J3m+ZkSfxxvXQyKwQMHw==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXYlNYOUpyOFFmQXZ2Nm9j + ZlBmZnpoTlRtUzFhbXF3TkIxOExOUGRGV0J3CkptMkEyRFJiRVVTMGRhYVFpNUhj + QTA3VnBtN2NpUjVrYkNEVEJFc0w1eTQKLS0tIGVyVGNScFlJdUxpNm1VR1pqOGdl + dGNHajdaOVRJRVIyMkJGU2JqaFRFVU0KortZVXj9NCiNC3eFCu8uk1Omm3KD4i8g + I2r3YnXsljEmX1nyziug8rHqZFEKEvrg52HaAa5cKta6YXm2ebyDnA== + -----END AGE ENCRYPTED FILE----- + recipient: age13v0djuhkmnd06zvct0zc6sddykqpk3j9k8ev5sfgd6gtj82s0avs68psvj + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZV3BReFZhaUdwdHVoVG5v + dmU1OEgxTVI5S3NDWWZtSHNOS1NmUTZrR1dZCitxcW9iaEtORGY4NUdhUDBaL255 + eSt5cFAvV21YZzRZdlhSVUU2ajk0cGMKLS0tIHdXOHF4bmtnTDFpVkI0aE56L2pG + N1dENUxwZFZvNFZ1c0FvQUxrc1ljSGsKoz+2jeCkzJmVOHUM5EzeeZ07GK4JyGiq + uwuji6tN1d9CH1FV0KrAk3VULs98ShzcnaP33155T7n+EGmQjytYQQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1730f3cxdyh56zw8xcvlmpa7u2x7353wu4u0e58kyx24rsefgp98sxehm6s + lastmodified: "2026-07-29T10:49:30Z" + mac: ENC[AES256_GCM,data:S1ZH63PT1ZdyzQKaTjckAzgFR4UB7MdVAuiW4RPK1jAf6tO8F2NZtNdBnyqQdXZhTpxkYmFnTlp3V2kdm1wx+AjYITZp0kMBQlAXlkK4rJXprUieZzNFQD+39iXgM7UxfOa0v7YZCnLsh9gN3IeJvnaA85XsxJuINoDrVb5SrA8=,iv:CrbwDAeJds/m/qWY+hTk+0dZeLeS6JbMmlzn9AfGLmg=,tag:+7qKMlJD9ZH9u53ThxYIlA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.2 From 0def3ee34655e8611d66305a44bcbadeb15469b2 Mon Sep 17 00:00:00 2001 From: mulatta <67085791+mulatta@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:23:56 +0900 Subject: [PATCH 3/3] hermes-agent: declare Nero Slack interface Keep app identity, scopes, events, and slash commands reviewable beside deployment policy so Slack-side authority cannot drift without an infrastructure change. --- modules/hermes-agent/slack-app/.envrc | 2 + .../hermes-agent/slack-app/.slack/.gitignore | 5 + .../hermes-agent/slack-app/.slack/hooks.json | 5 + modules/hermes-agent/slack-app/README.md | 65 +++ .../slack-app/slack-app-manifest.json | 389 ++++++++++++++++++ 5 files changed, 466 insertions(+) create mode 100644 modules/hermes-agent/slack-app/.envrc create mode 100644 modules/hermes-agent/slack-app/.slack/.gitignore create mode 100644 modules/hermes-agent/slack-app/.slack/hooks.json create mode 100644 modules/hermes-agent/slack-app/README.md create mode 100644 modules/hermes-agent/slack-app/slack-app-manifest.json diff --git a/modules/hermes-agent/slack-app/.envrc b/modules/hermes-agent/slack-app/.envrc new file mode 100644 index 00000000..ab50501f --- /dev/null +++ b/modules/hermes-agent/slack-app/.envrc @@ -0,0 +1,2 @@ +# shellcheck shell=bash +use flake ../../..#slack-deploy diff --git a/modules/hermes-agent/slack-app/.slack/.gitignore b/modules/hermes-agent/slack-app/.slack/.gitignore new file mode 100644 index 00000000..ee15e4c0 --- /dev/null +++ b/modules/hermes-agent/slack-app/.slack/.gitignore @@ -0,0 +1,5 @@ +apps.json +apps.dev.json +config.json +cache/ +*.log diff --git a/modules/hermes-agent/slack-app/.slack/hooks.json b/modules/hermes-agent/slack-app/.slack/hooks.json new file mode 100644 index 00000000..be0df7cb --- /dev/null +++ b/modules/hermes-agent/slack-app/.slack/hooks.json @@ -0,0 +1,5 @@ +{ + "hooks": { + "get-manifest": "sh -c 'cat slack-app-manifest.json'" + } +} diff --git a/modules/hermes-agent/slack-app/README.md b/modules/hermes-agent/slack-app/README.md new file mode 100644 index 00000000..24d6bbbf --- /dev/null +++ b/modules/hermes-agent/slack-app/README.md @@ -0,0 +1,65 @@ +# Nero Slack app + +This directory declares Slack app used by Hermes Agent on tau. Socket Mode keeps +Slack traffic outbound-only, while bot scopes and subscribed events define +Nero's messaging authority. + +## Enter tool shell + +```bash +cd modules/hermes-agent/slack-app +direnv allow +``` + +Equivalent command: + +```bash +nix develop ../../..#slack-deploy +``` + +Shell provides Slack CLI and `jq`. This directory is a minimal Slack CLI +project: `.slack/hooks.json` exposes `slack-app-manifest.json`, while local app +links remain ignored. + +## Validate manifest + +```bash +jq -e . slack-app-manifest.json >/dev/null +slack manifest info --source local --skip-update | jq -e . >/dev/null +``` + +## Link and update app + +Nero currently uses app ID `A0BL4GEHMNK` in team `T018TQRSHFY`. + +```bash +slack app link \ + --team T018TQRSHFY \ + --app A0BL4GEHMNK \ + --environment deployed +slack app install --team T018TQRSHFY --environment deployed +``` + +Review requested scope or event changes before approving installation. Slack +CLI may require browser authorization or workspace admin approval. + +## Check remote drift + +```bash +slack manifest info --source remote --app A0BL4GEHMNK --skip-update \ + | jq -S . > /tmp/nero-remote-manifest.json +jq -S . slack-app-manifest.json > /tmp/nero-local-manifest.json +diff -u /tmp/nero-local-manifest.json /tmp/nero-remote-manifest.json +``` + +Slack may normalize fields. Review normalization separately from meaningful +permission, event, command, and identity drift. + +## Secret policy + +Keep bot and app tokens only in `modules/hermes-agent/secrets.yaml` through +SOPS. Do not commit Slack CLI authentication, local app links, bot tokens, app +tokens, or workspace service tokens. + +CI should perform static validation only. Run authenticated drift checks and app +updates locally from `slack-deploy` shell. diff --git a/modules/hermes-agent/slack-app/slack-app-manifest.json b/modules/hermes-agent/slack-app/slack-app-manifest.json new file mode 100644 index 00000000..4b21c3b6 --- /dev/null +++ b/modules/hermes-agent/slack-app/slack-app-manifest.json @@ -0,0 +1,389 @@ +{ + "_metadata": { + "major_version": 1, + "minor_version": 1 + }, + "display_information": { + "name": "Nero", + "description": "SBEE-Lab Slack agent for managing schedules and workflows, finding and explaining research papers, and coordinating lab orders.", + "background_color": "#1a1a2e" + }, + "features": { + "app_home": { + "home_tab_enabled": false, + "messages_tab_enabled": true, + "messages_tab_read_only_enabled": false + }, + "bot_user": { + "display_name": "Nero", + "always_online": true + }, + "slash_commands": [ + { + "command": "/hermes", + "description": "Talk to Nero or run a subcommand", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[subcommand] [args]" + }, + { + "command": "/btw", + "description": "Alias for /background — Run a prompt in the background", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "" + }, + { + "command": "/bg", + "description": "Alias for /background — Run a prompt in the background", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "" + }, + { + "command": "/start", + "description": "Acknowledge platform start pings without a reply", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/new", + "description": "Start a new session (fresh session ID + history)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[name]" + }, + { + "command": "/retry", + "description": "Retry the last message (resend to agent)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/undo", + "description": "Back up N user turns and re-prompt (default 1)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[N]" + }, + { + "command": "/title", + "description": "Set a title for the current session", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[name]" + }, + { + "command": "/branch", + "description": "Branch the current session (explore a different path)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[name]" + }, + { + "command": "/compress", + "description": "Compress conversation context (add 'here [N]' to keep recent N turns; --preview shows what would happen)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[here [N] | focus topic | --preview|--dry-run]" + }, + { + "command": "/rollback", + "description": "List or restore filesystem checkpoints", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[number]" + }, + { + "command": "/stop", + "description": "Kill all running background processes", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/approve", + "description": "Approve a pending dangerous command", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[session|always]" + }, + { + "command": "/deny", + "description": "Deny a pending dangerous command (optionally with a reason)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[all] [reason]" + }, + { + "command": "/background", + "description": "Run a prompt in the background", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "" + }, + { + "command": "/agents", + "description": "Show active agents and running tasks", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/queue", + "description": "Queue a prompt for the next turn (doesn't interrupt)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "" + }, + { + "command": "/steer", + "description": "Inject a message after the next tool call without interrupting", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "" + }, + { + "command": "/goal", + "description": "Set a standing goal Nero works on across turns until achieved", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[text | draft | show | pause | resume | clear | status | wait | unwait]" + }, + { + "command": "/subgoal", + "description": "Add or manage extra criteria on the active goal", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[text | remove N | clear]" + }, + { + "command": "/whoami", + "description": "Show your slash command access (admin / user)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/profile", + "description": "Show active profile name and home directory", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/resume", + "description": "Resume a previously-named session", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[name]" + }, + { + "command": "/sessions", + "description": "Browse and resume previous sessions", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/codex-runtime", + "description": "Toggle codex app-server runtime for OpenAI/Codex models", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[auto|codex_app_server]" + }, + { + "command": "/personality", + "description": "Set a predefined personality", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[name]" + }, + { + "command": "/footer", + "description": "Toggle gateway runtime-metadata footer on final replies", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[on|off|status]" + }, + { + "command": "/yolo", + "description": "Toggle YOLO mode (skip all dangerous command approvals)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/reasoning", + "description": "Manage reasoning effort and display", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[level|show|hide|full|clamp] [--global]" + }, + { + "command": "/fast", + "description": "Toggle fast mode — OpenAI Priority Processing / Anthropic Fast Mode (Normal/Fast)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[normal|fast|status] [--global]" + }, + { + "command": "/voice", + "description": "Toggle voice mode", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[on|off|tts|status]" + }, + { + "command": "/memory", + "description": "Review pending memory writes / toggle the approval gate", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[pending|approve|reject|approval] [id|on|off]" + }, + { + "command": "/bundles", + "description": "List skill bundles (aliases / for multiple skills)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/learn", + "description": "Learn a reusable skill from anything you describe (dirs, URLs, this chat, notes)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "" + }, + { + "command": "/suggestions", + "description": "Review suggested automations (accept/dismiss)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[accept|dismiss N | catalog]" + }, + { + "command": "/blueprint", + "description": "Set up an automation from a blueprint template", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[name] [slot=value ...]" + }, + { + "command": "/curator", + "description": "Background skill maintenance (status, run, pin, archive, list-archived)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[subcommand]" + }, + { + "command": "/kanban", + "description": "Multi-profile collaboration board (tasks, links, comments)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[subcommand]" + }, + { + "command": "/reload-mcp", + "description": "Reload MCP servers from config", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/reload-skills", + "description": "Re-scan ~/.hermes/skills/ for newly installed or removed skills", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/commands", + "description": "Browse all commands and skills (paginated)", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[page]" + }, + { + "command": "/help", + "description": "Show available commands", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/restart", + "description": "Gracefully restart the gateway after draining active runs", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/usage", + "description": "Show token usage and rate limits; `reset` redeems a banked Codex limit reset", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[reset [--force]]" + }, + { + "command": "/insights", + "description": "Show usage insights and analytics", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": "[days]" + }, + { + "command": "/platform", + "description": "Pause, resume, or list a failing gateway platform", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands", + "usage_hint": " [name]" + }, + { + "command": "/update", + "description": "Update Hermes Agent to the latest version", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + }, + { + "command": "/version", + "description": "Show Hermes Agent version", + "should_escape": false, + "url": "https://hermes-agent.local/slack/commands" + } + ], + "agent_view": { + "agent_description": "Chat with Nero in Slack Messages." + } + }, + "oauth_config": { + "scopes": { + "bot": [ + "app_mentions:read", + "assistant:write", + "channels:history", + "channels:read", + "chat:write", + "commands", + "files:read", + "files:write", + "groups:history", + "groups:read", + "im:history", + "im:read", + "im:write", + "mpim:history", + "mpim:read", + "users:read" + ] + } + }, + "settings": { + "event_subscriptions": { + "bot_events": [ + "app_context_changed", + "app_home_opened", + "app_mention", + "message.channels", + "message.groups", + "message.im", + "message.mpim" + ] + }, + "interactivity": { + "is_enabled": true + }, + "org_deploy_enabled": false, + "socket_mode_enabled": true, + "token_rotation_enabled": false + } +}