Skip to content

band-ai/hermes-band-platform

Repository files navigation

Band Platform for Hermes

One paste connects a Hermes agent to Band — chat in rooms, act through tools, stay reachable by its owner.

CI Release License: MIT Python 3.11–3.13 Band

Quickstart · Install · Configuration · Reference · Contributing


A Hermes platform plugin (key: band) that links a Hermes agent to the Band platform over a persistent WebSocket and relays text between Band chat rooms and the agent. It wraps the official band-sdk BandLink — it does not reimplement the Band protocol. Beyond messaging it registers a band action toolset (create rooms, look people up, add/remove participants, send messages) and bootstraps a Hermes Hub: a private owner↔agent control room that serves as the Band main channel.

How it works

Three layers own three concerns, so each can change without the others:

add-band (catalog)     →  the copy-paste on-ramp: a thin snippet + a pointer to this repo
this repo (the plugin) →  install steps, the band adapter + toolset, the add-band setup skill
Band (the platform)    →  credentials, agent registration, access control
  • Band manages access. A message reaches the agent only if Band delivered it (someone messaged the agent or added it to a room), so the adapter trusts Band's own ACL as the gate (enforces_own_access_policy). A fresh install with just an agent id + API key is reachable out of the box; narrow it later with BAND_ALLOWED_USERS.
  • First connect is the installation. On the first successful connect the adapter resolves the owner, creates the Hermes Agent Hub room, wires it as the Band main channel, persists BAND_HUB_ROOM, and greets the owner in-band. See The Hub.

Quickstart

The fastest path is the Band web app's "Add to Hermes" flow. It hands you a curl … | bash one-liner (your Band key prefilled). Run it on the host where your Hermes gateway runs and paste the key when prompted:

# the exact line is generated by the web app — shape shown for reference
curl -fsSL https://app.band.ai/add/hermes.sh | bash

One run → one @mention → a connected agent. The snippet registers a Band agent from your key (the key never reaches the LLM — a script reads it, then drops it), installs the band plugin into the gateway's Python, and hands off to the add-band setup skill, which enables the plugin, restarts the gateway, bootstraps the hub, and sends you the agent's first message. Then verify it worked.

The snippet is just a thin fetch of band-ai/add-band's hand-authored hermes/bootstrap.sh. To run setup by hand or on a release build, see Install.

Prerequisites: Hermes already installed, with its gateway on Python 3.11–3.13 (band-sdk has no 3.14 wheels yet), shell access as the user who owns the Hermes install, and a Band account (see Credentials).

Install

Which path is mine?

You are… Use Why
An end user — "just connect my agent" Band web app (add-band) One paste, registers + installs + verifies for you. The Quickstart.
An operator on a published release PyPI release pip install + enable; dependencies come with the package.
A contributor / pre-release / dev From this repo Directory plugin, Git-ref install, the bundled skill, or Nix — install straight from source.

All paths converge on the same end state: the plugin loadable by the gateway, band enabled, credentials in the gateway .env, and a hub created on first connect. The canonical layout is a directory plugin under $HERMES_HOME (default ~/.hermes): plugin files in $HERMES_HOME/plugins/band/, its band-sdk dependency in $HERMES_HOME/band-libs/ — nothing is written to the gateway's site-packages, so it works on hosted runtimes where the gateway venv (e.g. /opt/hermes/.venv) is root-owned and read-only.

1. Band web app (add-band)

The consumer path — this is the Quickstart above. The web app serves the snippet and your key; everything else is automated by add-band/hermes/bootstrap.sh and the add-band skill it hands off to.

On a fresh box where the plugin isn't installed yet, this is the path to use — hermes /add-band doesn't exist until the plugin is installed, but the snippet installs it first. To drive setup from a different machine or a non-Hermes agent (e.g. Claude Code), hand a shell-capable agent the one-shot prompt in docs/INSTALL-PROMPT.md — it clones this repo and runs the same skill end to end.

2. PyPI release

Not yet on PyPI. Until the first release is published, use the Quickstart or From this repo. Once published:

pip install hermes-band-platform   # into the gateway's Python (must be writable)
hermes plugins enable band

Hosted runtimes with a read-only gateway venv can't take a pip install — use the installer (directory plugin, no site-packages writes) instead.

The package exposes a hermes_agent.plugins entry point, so Hermes builds with entry-point plugin management show band in hermes plugins list. If your build doesn't list entry-point plugins, add the key to ~/.hermes/config.yaml (the runtime loader honors it regardless):

plugins:
  enabled:
    - band

Then set credentials and restart the gateway.

3. From this repo

Several ways to install straight from source — the installer is the recommended one. Pick one, then set credentials and restart.

Installer (recommended) — directory plugin under $HERMES_HOME, no site-packages writes
git clone --depth 1 https://github.com/band-ai/hermes-band-platform
hermes-band-platform/install.sh

The installer stages the plugin into $HERMES_HOME/plugins/band/, resolves band-sdk>=1.0.0,<2.0.0 with the gateway's interpreter (Python 3.11–3.13; correct wheels for its platform) into the user-writable $HERMES_HOME/band-libs/, verifies import band, and runs hermes plugins enable band. The plugin prepends band-libs to sys.path at load, so the gateway venv is never written to — no sudo, works when site-packages is read-only. Re-running is safe (idempotent). Env knobs: HERMES_HOME, HERMES_PY (interpreter override), BAND_SDK_SPEC.

Migrating from a pip install? Entry-point plugins override directory plugins in the host loader, so a leftover hermes-band-platform in the gateway venv would silently keep the old code running — the installer refuses to complete until it's removed, printing the exact uv pip uninstall command (or run with BAND_UNINSTALL_PIP=1 to let it remove the pip copy itself).

Git directory installhermes plugins install
hermes plugins install band-ai/hermes-band-platform --enable

Clones the repo root into $HERMES_HOME/plugins/band and enables it. Directory plugins don't carry their own dependencies, so resolve band-sdk into band-libs (no site-packages write):

HERMES_PY="$(hermes --version 2>&1 | sed -n 's/^Project: //p')/venv/bin/python"
uv pip install --python "$HERMES_PY" --target "${HERMES_HOME:-$HOME/.hermes}/band-libs" 'band-sdk>=1.0.0,<2.0.0'

The plugin's loader shim finds band-libs on its own; if the SDK is still missing at load, the gateway log shows one error naming this exact command.

Git-ref install — pre-PyPI, into the gateway's Python

Requires a writable gateway venv (self-managed installs) — on hosted runtimes the venv is read-only; use the installer instead.

HERMES_PY="$(hermes --version 2>&1 | sed -n 's/^Project: //p')/venv/bin/python"
uv pip install --python "$HERMES_PY" "hermes-band-platform @ git+https://github.com/band-ai/hermes-band-platform.git@${BAND_HERMES_REF:-main}"
hermes plugins enable band

Pin BAND_HERMES_REF to a tag or commit for a reproducible install.

Bundled setup skillhermes /add-band

Once the plugin is installed, the bundled skill walks the full setup — identifies the gateway's Python, enables the plugin (with the plugins.enabled fallback), registers a Band agent from BAND_USER_API_KEY via scripts/register_agent.py, saves the agent-scoped credentials, restarts, and verifies the hub round-trip. The user key is read from the environment and never printed or stored. It is resumable — re-running acts only on what's still missing.

export BAND_USER_API_KEY=...
hermes /add-band
Nix — add the plugin to the gateway's environment
{
  services.hermes-agent = {
    extraPythonPackages = ps: [
      (ps.buildPythonPackage {
        pname = "hermes-band-platform";
        version = "1.0.0";
        format = "pyproject";
        src = ./.; # or fetchFromGitHub { owner = "band-ai"; repo = "hermes-band-platform"; ... }
        nativeBuildInputs = [ ps.setuptools ];
        propagatedBuildInputs = [ ps.band-sdk ]; # see note
      })
    ];
    settings.plugins.enabled = [ "band" ];
    environmentFiles = [ /run/secrets/hermes-band.env ]; # BAND_AGENT_ID, BAND_API_KEY
  };
}

Note: band-sdk is not (yet) in nixpkgs. Package it from PyPI yourself (e.g. with buildPythonPackage / poetry2nix / pip2nix) and pass it in so the plugin can import it.

Credentials

Every path needs one of two credential setups in the gateway's ~/.hermes/.env.

Auto-register (recommended) — a Band user API key that can create external agents. A helper mints an agent and saves only the agent-scoped pair; the user key never reaches the LLM:

export BAND_USER_API_KEY=...
HERMES_PY="$(hermes --version 2>&1 | sed -n 's/^Project: //p')/venv/bin/python"
"$HERMES_PY" hermes_band_platform/skills/add-band/scripts/register_agent.py   # saves BAND_AGENT_ID + BAND_API_KEY
unset BAND_USER_API_KEY

The bundled register_agent.py is a temporary helper that sends browser-like registration headers to avoid Cloudflare 1010. Replace it with the band-sdk CLI once band.cli.register_agent is published — keeping the same User-Agent/Accept/Accept-Language headers.

Pre-created agent (manual) — make one at app.band.ai/agents/new, then set:

BAND_AGENT_ID=<agent-uuid>
BAND_API_KEY=<band_a_key>

Then restart: hermes gateway restart.

Verify it worked

connect() succeeding only means the WebSocket opened — it does not prove the hub was created (bootstrap runs in a try/except that never blocks connect). Check the real signals:

grep -E '\[band\] Connected as agent|\[band\] Hub ready: room|✓ band connected' ~/.hermes/logs/gateway.log
grep BAND_HUB_ROOM ~/.hermes/.env   # a non-empty UUID = hub created

Then open the auto-created "Hermes Agent Hub" room in Band and @mention the agent — Band has no DMs, so an un-mentioned message is ignored by design. A reply means you're live. If you see [band] Owner unresolved — hub disabled, set BAND_OWNER_ID=<your-uuid> and restart.

Configuration

Required

Variable Description
BAND_AGENT_ID Band agent ID (UUID) for this Hermes agent.
BAND_API_KEY Band agent API key. Authenticates the WebSocket + REST link. Never logged.

Optional

Variable Description
BAND_BASE_URL Band host base URL (default https://app.band.ai). WS + REST URLs are derived from this host.
BAND_ALLOWED_USERS Comma-separated Band user IDs allowed to talk to the agent. Optional — Band's own ACL is trusted by default; set this only to narrow access below what Band already permits.
BAND_ALLOW_ALL Explicitly allow anyone in a room to talk to the agent. Redundant with the default Band-ACL trust; mainly useful to override a BAND_ALLOWED_USERS restriction.
BAND_TOOL_OWNERS Comma-separated platform:user_id identities allowed to drive Band actions (e.g. telegram:<tg-id>). The resolved Band owner is always authorized from Band rooms; this allowlist grants others.
BAND_GROUP_SESSIONS_PER_USER Split a group room into a separate session per participant (true) or keep one shared session for the whole room (false). Default false.
BAND_OWNER_ID Owner UUID override. Normally resolved from the agent identity on connect; anchors the hub and the owner-only gates.
BAND_HUB_ROOM Hub room UUID. Auto-created and persisted on first connect; set it to pin an existing room.
BAND_HOME_ROOM Main-channel override for cron / notification delivery (also set by /sethome from a Band room). Defaults to the hub.
BAND_HUB_FAILOVER_THRESHOLD Consecutive failed hub sends before failing over to a fresh hub room (default 3). A successful hub send resets the count. See Hub failover.
BAND_HUB_FAILOVER_MAX_PER_CONNECT Backstop cap on hub failovers per gateway connection (default 5).

Reference

Behavior

  • Inbound: subscribes to the agent's rooms and consumes message_created events. Band has no DMs — every room, including the hub, is a mention-gated group room, so a message reaches the agent only when it @mentions the agent. Band routes by mention (both /next and the live stream deliver only mention text), so the adapter mirrors that contract — no hub bypass, no active-session stickiness. The one exception is a validated owner slash command, which reaches the agent in any room without a mention.
  • Self-filter: the adapter skips its own agent messages by sender, with a sent-message-id backstop in addition to the SDK's own filtering.
  • Outbound: posts via the REST client, chunking long messages. Each reply @mentions the room's last human sender (falling back to all non-agent participants).

The Hub (main channel + command surface)

On every connect the adapter ensures a hub exists: a private room of exactly the agent and its owner (the owner_uuid resolved from the agent identity, or BAND_OWNER_ID). Bootstrap is idempotent:

  1. PinnedBAND_HUB_ROOM (persisted by a prior run, or set manually) wins. Silent: the steady state on every reconnect.
  2. Created — otherwise a new room is created, the owner is added, and a greeting is posted whose first line titles the room "Hermes Agent Hub" and introduces the agent to the owner.

Existing rooms are never adopted — a fresh install with no pinned id always gets its own dedicated room, so the hub can't collide with an unrelated owner↔agent conversation.

The resolved hub id is written back to BAND_HUB_ROOM (Hermes .env) and the hub is wired as the Band home channel — the default target for cron jobs (deliver=band) and gateway notifications. An explicit BAND_HOME_ROOM (or running /sethome in another Band room) overrides that default.

Slash-command gate. Slash commands (/help, /new, …) are accepted only from the owner — in any Band room, the hub included. A command-shaped message from anyone else is dropped before it reaches the gateway: human senders get a one-time per-room notice; other agents are dropped silently (a notice would invite bot↔bot ping-pong). The gate is fail-closed: if no owner can be resolved, Band slash commands are refused everywhere. File-path-like text (/usr/bin/ls) is not treated as a command and flows through as plain chat.

Hub failover

The hub is the agent's primary line to its owner, so the adapter watches its health. When a send to the hub room fails repeatedly — e.g. the room hit its Band message limit, or it's persistently erroring — the agent fails over to a fresh hub:

  • A successful hub send clears the counter; each failed one increments it.
  • After BAND_HUB_FAILOVER_THRESHOLD (default 3) consecutive failures the adapter creates a brand-new {agent, owner} room (never adopts — the existing hub is the broken one), greets the owner in it, then re-wires it as the main channel.
  • A failed create only re-arms after another threshold's worth of failures, and successful failovers are capped at BAND_HUB_FAILOVER_MAX_PER_CONNECT (default 5) per connection, so a platform-wide outage can't spin up rooms unbounded.

The message that triggered the failover is not auto-resent; subsequent sends go to the new hub, and the owner learns of the move from its greeting.

Tools

The plugin registers a band toolset so the agent can act on Band — create rooms, look people up, add/remove participants, and send messages — from any conversation it is in (a Band room, Telegram, or the CLI). All tools are async and call the Band REST API directly through the live adapter's authenticated link (with an env-credential fallback for out-of-process use).

The tools split into two tiers:

  • Tier A — platform-level tools take no room context; they resolve what to act on (find a contact, find or create a room).
  • Tier B — room-context tools default to the current Band room (resolved from the session) and accept an optional explicit room_id that always wins. From a non-Band session (Telegram/CLI) room_id is required — supply it from band_create_room or band_find_room.
Tool Tier Owner-gated Description
band_create_room A Yes Create a room. Composite: pass person (+ optional message, role) to resolve, create, add, and message someone in one call. Returns {room_id, added, sent}. No title arg — the server derives it.
band_find_room A No (read-only) Find existing rooms by query (matches title/id) → [{room_id, title}].
band_find_contact A No (read-only) Resolve a name/handle to a participant UUID over peers + contacts.
band_send_message B Yes Send content to a room (defaults to the current room; pass room_id to target another). Chunks long messages at 4000 chars. Mentions are mandatory — pass mention_ids or the room's participants are mentioned.
band_add_participant B Yes Add a participant (participant_id, optional role) to the room.
band_remove_participant B Yes Remove a participant from the room.
band_get_participants B No (read-only) List the room's participants → [{id, handle, name, type}].
Owner gating — who can drive the mutating tools

Creating rooms and adding/removing people is mutating and outward-facing, and the toolset can be exposed on platforms like Telegram. Hermes has no built-in cross-platform owner concept, so the Band tools enforce their own gate:

  • Owner bypass (checked first): the agent's resolved Band owner calling from any Band room is always authorized. Fail-closed when the owner is unresolved.
  • The mutating tools otherwise check the caller's platform:user_id against the BAND_TOOL_OWNERS allowlist.
  • Fail-closed: if BAND_TOOL_OWNERS is unset, or the caller is not on it (and the owner bypass doesn't apply), the mutating tools refuse with a clear error. With BAND_TOOL_OWNERS unset the gate falls back to the calling platform's own <PLATFORM>_ALLOWED_USERS allowlist.
  • Read-only tools (band_find_room, band_find_contact, band_get_participants) bypass the gate.
Enabling the tools per platform

The band toolset is enabled per platform in ~/.hermes/config.yaml. A plugin platform's default toolset is hermes-band (the messaging channel, no Band action tools), so band must be listed explicitly for every platform that should call the Band tools — including the band platform itself:

platform_toolsets:
  band:     [hermes-band, band]       # Band sessions get native + band action tools
  telegram: [hermes-telegram, band]   # drive Band from Telegram
  group_sessions_per_user: false      # (set on the band PlatformConfig) — room = one channel

Gotcha: listing only hermes-band for the band platform gives the agent the messaging channel but none of the action tools. You must add band to the band platform's toolsets as well, not just to Telegram/CLI.

The owner gate (above) is the real access boundary, not which platforms list the toolset — so exposing band on Telegram does not widen who can mutate Band.

Catching up on missed messages (Route A)

The Band platform owns a per-agent, per-message read cursor — a delivery-status state machine (delivered → processing → processed/failed) tracked server-side per message. The adapter advances it through the SDK link helpers, so Hermes never has to persist a cursor of its own: whatever the agent didn't mark processed is still owed to it, across any outage.

  • Acking each turn. When a message's turn begins the adapter marks it processing (via the gateway's on_processing_start hook); when the turn settles it marks it processed on success/cancel or failed on error (on_processing_complete). Hooks fire at true turn completion — Hermes's handle_message is fire-and-forget, so acking on handler return would fire before the reply lands. A crash mid-turn leaves the message processing, recoverable on the next connect.
  • Draining on (re)connect. On connect and on every link reconnected event, a background task drains each known room's backlog via /next (get_agent_next_message), re-picking anything stuck processing from a prior crash (get_stale_processing_messages) first. Each drained message flows through the same gate/normalize path as a live one.
  • Dedup. /next and the live WS stream both deliver only @mention text, so they cover the same set. An in-memory _seen_inbound_ids guards the narrow window where a message is both live-delivered and in the backlog at reconnect; it is intentionally not persisted.
  • Known edge — coalesced bursts. The gateway's busy-text debounce merges rapid same-sender messages into one turn, keeping only the latest id. Earlier ids in the burst aren't individually acked, so a reconnect can re-offer them via /next; they re-process and self-heal once the room is idle. Bounded at-least-once.
Sessions, close-on-leave, and rehydration
  • One channel per room. Each Band room is its own conversational channel: independent history per room, but a shared agent identity and memory across rooms. Every room — regardless of participant count — keys to one shared session anchored solely on the room id (agent:main:band:group:{room_id}). The session chat_type is a fixed constant, never derived from the participant count, so a room that gains or loses a member can never silently re-key its conversation to a fresh session. Sender attribution is preserved in the transcript.
  • Close on leave. When the agent is removed from a room (room_removed / room_deleted), its Hermes session for that room is reset so the stale local transcript will not silently resume.
  • Rehydrate when local history is gone — Band is the source of truth. A flagged room durably seeds its Hermes session transcript from the Band chat-context API (own replies as assistant turns, peers as user), so recovered history persists across turns and restarts rather than evaporating after one. The trigger and the unprocessed mention backlog (answered by the /next drain) are excluded from the seed, so nothing is answered twice, and the agent never re-answers a question already in its history. Seeding is idempotent (only into an empty transcript) and capability-guarded — an older gateway falls back to a one-shot channel_context blob. Two triggers:
    • Room re-join (room_added for a known cold room): close-on-leave reset the local transcript, so the room is flagged to reconstruct the already-seen history.
    • Agent return (every (re)connect): the catch-up drain flags any room with no local session — a fresh deploy, a lost/migrated DB, or the first run after the agent was down. A room whose local session is intact is skipped.

Limitations

  • Memory + standalone cron deferred. Memory preload/write-through and out-of-process cron delivery (standalone_sender_fn) land in later passes (extension points are marked # TODO (<pass>): in the adapter).
  • No per-message retry cap on failure. A turn that errors is marked failed, which the server may re-offer on a later /next drain. There is no attempt-count ceiling yet, so a persistently-failing message can re-deliver across reconnects.
  • Mentions are mandatory on send. The Band API rejects messages with no mentions, so every reply mentions at least one recipient. If no mentionable recipient is known for a room, the send is dropped.
  • Rooms, not threads. Band has no thread primitive; thread_id is always None and reply_to is ignored on send.
  • Message length. No confirmed Band per-message limit exists in the SDK / REST types, so a conservative MAX_MESSAGE_LENGTH = 4000 is used; revisit if Band documents a hard cap.

Contributing

Issues and PRs welcome. The release flow (release-please, the generated root plugin.yaml, and the dual install layout) is documented in RELEASING.md; tests run under pytest (pip install -e '.[dev]' && pytest).

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages