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/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"; 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 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 + } +}