slack-agent-mesh
README.md
# Slack Agent Mesh
A portable, open-source **Slack transport and coordination layer for the agents you already run.**
Slack Agent Mesh turns a single Slack app into a mesh of virtual agent identities
(`@rook`, `@scout`, …) that humans and agents can address by name. A deterministic
broker admits the right messages, invokes your existing agents through pluggable
adapters, routes agent-to-agent delegation, and keeps everything in one durable,
human-visible Slack thread — without you adopting any particular model SDK.
> **Status:** release candidate. 146 automated tests pass (`npm test` + `npm run
> test:acceptance`). The credential-free demo runs with no network access. A live
> Slack workspace test is **NOT RUN** in this tree — see
> [Limitations](#limitations-and-non-parity).
---
## Table of contents
- [What it is / what it is not](#what-it-is--what-it-is-not)
- [Architecture](#architecture)
- [Five-minute demo (no Slack credentials)](#five-minute-demo-no-slack-credentials)
- [Real Slack setup](#real-slack-setup)
- [1. Create the app from the manifest](#1-create-the-app-from-the-manifest)
- [2. Tokens and scopes](#2-tokens-and-scopes)
- [3. Configure agents](#3-configure-agents)
- [4. Run the broker](#4-run-the-broker)
- [Agent configuration examples](#agent-configuration-examples)
- [Hermes](#hermes-command-adapter)
- [Claude Code](#claude-code-command-adapter)
- [Codex](#codex-command-adapter)
- [Generic stdin/stdout command](#generic-stdinstdout-command)
- [Webhook (remote) agent](#webhook-remote-agent)
- [MCP server and clients](#mcp-server-and-clients)
- [How admission and routing work](#how-admission-and-routing-work)
- [Deployment](#deployment)
- [Security](#security)
- [Reliability](#reliability)
- [Troubleshooting](#troubleshooting)
- [Buzz parity matrix](#buzz-parity-matrix)
- [Limitations and non-parity](#limitations-and-non-parity)
- [CLI reference](#cli-reference)
- [Contributing](#contributing)
- [Security policy](#security-policy)
- [License](#license)
- [References](#references)
---
## What it is / what it is not
**It is:**
- A **transport and coordination layer.** Slack is the durable, human-visible
channel; your existing agents remain the brains.
- **Model-agnostic.** The routing and Slack code import no model-provider SDK.
Agents are reached over a `command` (stdin/stdout) adapter, a signed `webhook`
adapter, or the built-in `mock` adapter for tests and the demo.
- **One app, many identities.** A single Slack app hosts virtual agents
(`@rook`, `@scout`) that the broker routes by name.
- **Loop-safe by construction.** Persisted event dedupe, stable trace IDs, hop
limits, per-trace message budgets, and a visited-agent set stop recursive
storms deterministically.
- **Socket Mode first.** No public ingress server is required.
- **MCP-native.** An MCP server exposes mesh messaging, directory, and thread
tools to any MCP-capable agent.
**It is not:**
- **Not an agent framework or model runtime.** It does not think, plan, or call
an LLM. It carries prompts to your agents and their replies back to Slack.
- **Not a Slack replacement or a chatbot builder.** It is the wiring between
Slack and agents you already operate.
- **Not a hosted coordination SaaS.** Beyond Slack and the agent endpoints you
configure, there is no remote service or telemetry. Secrets stay in your
environment. See [Security](#security).
- **Not a claim of exact "Buzz" parity.** See the
[parity matrix](#buzz-parity-matrix) and [limitations](#limitations-and-non-parity).
---
## Architecture
```
┌──────────────────────────── Slack workspace ───────────────────────────┐
│ #incident thread │
human ── @rook … ─────▶│ ├─ @rook: Acknowledged. … @scout please pull logs. │
│ └─ @scout: Confirmed: bad DATABASE_URL. Recommend rollback. │
└───────────────▲───────────────────────────────────┬─────────────────────┘
│ one reply object per turn │ Socket Mode events
│ (chat.postMessage, │ (app_mention, message.*)
│ chat.update for progress) ▼
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
│ slack-agent-mesh (this repo) │
│ │
│ Slack transport ──▶ Ingress normalize ──▶ Router (admission) Store (node:sqlite) │
│ (@slack/bolt, drop bot/self/edited • dedupe event_id • events • traces │
│ Socket Mode) echoes; collect • resolve targets • visited set • outputs │
│ attachments • hop / budget / • threads • delegations │
│ visited / ttl │
│ │ │
│ ▼ │
│ Broker (serial per-trace queue) │
│ • post exactly one visible reply per accepted turn │
│ • scan TRUSTED agent output for @mentions │
│ • enqueue delegates DIRECTLY (never via Slack echo) │
│ │ ▲ │
│ Adapter registry │ mesh_send / mesh_reply / … │
│ ┌──────────────┬─────────┴────────┐ │ │
│ ▼ ▼ ▼ │ │
│ command webhook mock MCP server (stdio) ◀── MCP-capable agent │
│ (stdin/stdout) (signed HTTP) (tests/demo) │
└────────────┬───────────────┬───────────────────────────────────────────────────────────────────┘
▼ ▼
Hermes / Claude remote agent
Code / Codex / (any HTTP
any CLI endpoint)
```
Key properties that fall out of this shape:
- **Directed admission.** Ordinary channel chatter never wakes an agent. The
router admits a human turn only via an explicit app mention, `/mesh`, a DM, a
configured virtual-agent mention, or a reply in a thread the mesh already owns.
- **Broker-driven delegation.** When an agent's reply mentions another agent, the
**broker** enqueues that agent directly. Correctness never depends on Slack
re-delivering the bot's own message.
- **One turn → one reply object.** Progress uses Slack update APIs, not extra
messages.
Source layout: `src/slack` (transport + ingress + gateway), `src/core`
(router, mentions, ids, redact, types), `src/broker` (coordinator),
`src/adapters` (command/webhook/mock), `src/store` (SQLite persistence),
`src/mcp` (MCP server), `src/demo` (scripted demo), `src/config` (schema + loader).
---
## Five-minute demo (no Slack credentials)
You need **Node.js ≥ 22.13** and nothing else — no Slack app, no tokens, no network.
```bash
git clone https://github.com/nickvasilescu/slack-agent-mesh.git
cd slack-agent-mesh
npm ci
npm run demo -- --script examples/demo-conversation.json
```
Expected output (deterministic):
```
=== slack-agent-mesh demo: Incident triage: human -> @rook -> @scout ===
[human U_HUMAN] @rook production API is throwing 500s, please investigate
↳ *@rook*: Acknowledged. The 500s line up with the 15:42 deploy. @scout please pull the deploy and error logs for the last hour and confirm the cause.
↳ *@scout*: Confirmed: the 15:42 deploy shipped a bad DATABASE_URL. Errors started at 15:43. Recommend immediate rollback. No further delegation needed.
--- trace receipts ---
{"trace_id":"trc_demo_1","channel":"C_DEMO","hops":1,"messages":2,"max_hops":4,"max_messages":12,"visited":["rook","scout"],"status":"active"}
```
The demo drives the **real broker, router, store, and mock adapter** — the same
code paths used in production, with Slack swapped for an in-memory gateway. The
trace receipt shows the loop-prevention accounting (hops, message budget, visited
agents) that also governs live traffic.
Want to exercise the MCP tool surface locally, still credential-free?
```bash
# Uses the mock gateway when no Slack tokens are present.
npm run mcp -- --config mesh.config.example.yaml
```
---
## Real Slack setup
### 1. Create the app from the manifest
This repo ships a ready-to-use **Socket Mode** manifest at
[`slack/manifest.json`](slack/manifest.json).
1. Go to <https://api.slack.com/apps> → **Create New App** → **From an app
manifest**.
2. Pick your workspace, choose **JSON**, and paste the contents of
`slack/manifest.json`.
3. Create the app. The manifest already declares the bot scopes, the `/mesh`
slash command, the event subscriptions, and enables Socket Mode and
interactivity.
### 2. Tokens and scopes
You need two tokens. **Never commit them.** Copy `.env.example` to `.env` and
fill in real values (the shell in [step 4](#4-run-the-broker) reads that file).
| Token | Starts with | Where to get it | Notes |
| --- | --- | --- | --- |
| **Bot token** | `xoxb-` | *OAuth & Permissions* → *Install to Workspace* | Carries the bot scopes below. |
| **App-level token** | `xapp-` | *Basic Information* → *App-Level Tokens* → add token with `connections:write` | Required for Socket Mode. |
| **Signing secret** | — | *Basic Information* → *App Credentials* | Only needed if you run an HTTP receiver instead of Socket Mode. |
Bot scopes declared in the manifest (and echoed in `.env.example`):
```
app_mentions:read channels:history groups:history im:history mpim:history
chat:write chat:write.customize commands reactions:write
```
`chat:write.customize` lets each virtual agent post under its own display name
and icon. `reactions:write` powers the `mesh_add_reaction` tool. Prompts use
Slack's stable sender ID, so the app does not request profile-directory access.
```bash
cp .env.example .env
# edit .env:
# SLACK_BOT_TOKEN=xoxb-…
# SLACK_APP_TOKEN=xapp-…
```
Invite the app to the channels it should watch (`/invite @agent-mesh`).
### 3. Configure agents
Copy the example config and point each agent at your real brain:
```bash
cp mesh.config.example.yaml mesh.config.yaml
```
The config references **environment-variable names, not secret values.** A
literal `xoxb-…`, `xapp-…`, or signing secret in the file fails validation. See
[Agent configuration examples](#agent-configuration-examples) below and the fully
annotated [`mesh.config.example.yaml`](mesh.config.example.yaml).
Validate everything before you start:
```bash
npm run doctor -- --config mesh.config.yaml
```
`doctor` reports config validity, missing env vars, each adapter's resolved
settings, and the active loop limits. Example:
```
✓ config schema: valid, no inline secrets
✓ @rook command: ./bin/hermes-agent (timeout 60000ms, cap 65536B)
! @scout url env SCOUT_WEBHOOK_URL: not set (webhook agent will fail closed)
✓ loop limits: max_hops=4, max_messages=12, ttl=3600s
```
### 4. Run the broker
```bash
npm run serve -- --config mesh.config.yaml
```
The process opens a Socket Mode connection and stays alive. Address an agent from
Slack:
- `@agent-mesh` app mention naming an agent, e.g. `@agent-mesh @rook look at the 500s`
- `/mesh @rook investigate the 500s`
- a **DM** to the app naming an agent
- a **reply** in a thread the mesh already owns (no re-mention needed)
---
## Agent configuration examples
Every agent is one entry under `agents:` with a `name`, a `description`, and an
`adapter`. Two adapter kinds reach real agents: `command` (local process) and
`webhook` (remote HTTP). The `mock` adapter is for tests and the demo.
The command adapter passes non-secret turn context to the child as environment
variables: `MESH_AGENT`, `MESH_TRACE_ID`, `MESH_HOP`, `MESH_CHANNEL`,
`MESH_THREAD_TS`, `MESH_FROM`. The **prompt arrives on stdin**; the agent's
**stdout is the reply.** The child does **not** inherit your environment — only
`env_allowlist` + `env_pass` names are forwarded — so mesh secrets never leak into
an arbitrary command.
### Hermes (command adapter)
```yaml
- name: hermes
description: General operator agent. Handles ops, research, and delegation.
presence: available
adapter:
type: command
command: ["./examples/agents/hermes-agent.sh"]
timeout_ms: 120000
max_output_bytes: 131072
# Only these names are forwarded to the child process.
env_allowlist: ["PATH", "HOME", "LANG"]
env_pass: ["HERMES_PROFILE", "OP_SERVICE_ACCOUNT_TOKEN"]
```
The shipped wrapper reads the prompt from stdin and calls the real Hermes
non-interactive interface, `hermes chat -Q --source tool -q`. Set
`HERMES_PROFILE` when you want a non-default Hermes profile. If Hermes needs
secrets, expose only their environment-variable names via `env_pass`; never put
their values in `mesh.config.yaml`.
### Claude Code (command adapter)
```yaml
- name: claude
description: Claude Code. Reads a repo, writes patches, runs tests.
adapter:
type: command
command: ["claude", "-p", "--output-format", "text"]
timeout_ms: 300000
max_output_bytes: 262144
env_allowlist: ["PATH", "HOME", "LANG", "ANTHROPIC_API_KEY"]
```
`claude -p` (print mode) reads the prompt on stdin and writes the final message
to stdout — exactly the command-adapter contract. Raise `timeout_ms` for
long-running coding turns.
### Codex (command adapter)
```yaml
- name: codex
description: OpenAI Codex CLI. Implements and reviews code changes.
adapter:
type: command
command: ["codex", "exec", "--ephemeral", "--color", "never", "-"]
timeout_ms: 300000
max_output_bytes: 262144
env_allowlist: ["PATH", "HOME", "LANG", "OPENAI_API_KEY"]
```
Any CLI that reads a prompt from stdin and prints a response works the same way;
adjust the argv to whatever your build expects for non-interactive execution.
### Generic stdin/stdout command
The lowest common denominator is a script. See
[`examples/agents/echo-agent.sh`](examples/agents/echo-agent.sh):
```bash
#!/usr/bin/env bash
set -euo pipefail
prompt="$(cat)" # prompt on stdin
# … call your model / tool here …
echo "[$MESH_AGENT] handled a ${#prompt}-char prompt on trace ${MESH_TRACE_ID}."
```
```yaml
- name: rook
description: Incident commander. Triages production issues and delegates.
adapter:
type: command
command: ["./examples/agents/echo-agent.sh"]
timeout_ms: 60000
max_output_bytes: 65536
env_allowlist: ["PATH", "HOME", "LANG"]
```
If the process times out, exits non-zero, floods stdout past
`max_output_bytes`, or fails to spawn, the turn **fails closed** with a single
`:warning:` reply — never a hang or a partial post.
### Webhook (remote) agent
For an agent that lives behind HTTP:
```yaml
- name: scout
description: Log and metrics investigator. Runs read-only diagnostics.
presence: available
adapter:
type: webhook
url_env: SCOUT_WEBHOOK_URL # env NAME holding https://scout.internal/agent
secret_env: SCOUT_WEBHOOK_SECRET # env NAME holding the shared HMAC secret
timeout_ms: 30000
max_output_bytes: 65536
```
The mesh POSTs JSON and signs it (when `secret_env` is set) so the receiver can
authenticate the caller:
```
POST <SCOUT_WEBHOOK_URL>
content-type: application/json
x-mesh-timestamp: <unix-seconds>
x-mesh-signature: v0=<hex hmac-sha256 of "v0:<timestamp>:<body>">
{ "agent": "scout", "prompt": "…", "trace": { "traceId": "…", "hop": 1, "channel": "…", "threadTs": "…", "from": "…" } }
```
Your endpoint must reply `200` with JSON matching:
```json
{ "text": "your agent's response", "trace_id": "optional-echo" }
```
Verify the signature on your side with the same construction the mesh uses
(`WebhookAdapter.verify` in `src/adapters/webhook.ts` is the reference). A non-200
status, a body over `max_output_bytes`, non-JSON, a schema mismatch, or a timeout
all fail the turn closed.
---
## MCP server and clients
The MCP server exposes the mesh to any MCP-capable agent over stdio. Start it
with:
```bash
npm run build
node dist/cli.js mcp --config mesh.config.yaml
```
Tools (every write tool accepts or derives an idempotency key; every tool returns
structured JSON):
| Tool | Purpose |
| --- | --- |
| `mesh_list_agents` | List configured agents, adapter kind, and presence. |
| `mesh_send` | Post a new message as an agent identity, starting a trace. |
| `mesh_reply` | Reply into an existing thread as an agent identity. |
| `mesh_read_thread` | Read a thread, including which agent authored each output. |
| `mesh_get_trace` | Fetch a trace receipt: hops, budget usage, visited agents. |
| `mesh_set_presence` | Set an agent identity's presence string. |
| `mesh_add_reaction` | Add an emoji reaction to a message. |
**Claude Code** (`.mcp.json` or `claude mcp add`):
```json
{
"mcpServers": {
"slack-agent-mesh": {
"command": "node",
"args": ["/abs/path/slack-agent-mesh/dist/cli.js", "mcp", "--config", "/abs/path/slack-agent-mesh/mesh.config.yaml"],
"env": { "SLACK_BOT_TOKEN": "xoxb-…", "SLACK_APP_TOKEN": "xapp-…" }
}
}
}
```
**Codex** (`~/.codex/config.toml`):
```toml
[mcp_servers.slack-agent-mesh]
command = "node"
args = ["/abs/path/slack-agent-mesh/dist/cli.js", "mcp", "--config", "/abs/path/slack-agent-mesh/mesh.config.yaml"]
```
**Generic MCP client** — any client that speaks stdio MCP launches the same
command: `node /abs/path/slack-agent-mesh/dist/cli.js mcp --config <path>`. Pass Slack tokens through
the client's `env` block (or omit them to run against the credential-free mock
gateway for local testing). More detail and copy-paste snippets live in
[`skills/slack-agent-mesh/references/mcp-clients.md`](skills/slack-agent-mesh/references/mcp-clients.md).
---
## How admission and routing work
A human turn is admitted only when it is **directed** at the mesh:
| Ingress | Admitted when |
| --- | --- |
| App mention (`@agent-mesh …`) | it names a configured agent |
| `/mesh @agent …` | it names a configured agent |
| Direct message | it names a configured agent |
| Channel message | it mentions a configured agent (plain chatter is ignored, not diagnosed) |
| Thread reply | the thread is one the mesh already owns |
Before any adapter runs, the router rejects — with **one** concise diagnostic at
most, and never for ordinary chatter or duplicates:
- a duplicate Slack `event_id` (persisted dedupe),
- a mention inside fenced code, inline code, a block quote, a Slack attachment, or
prior tool output (**untrusted content never delegates**),
- an agent mentioning itself,
- a revisit of an already-run agent in the same trace,
- a hop beyond `max_hops`,
- a trace beyond `max_messages`,
- an expired trace (older than `trace_ttl_seconds`).
Each human turn starts a **fresh trace** (new hop/visited/budget) threaded into
the same Slack thread, so a legitimate follow-up is never blocked by an earlier
turn's visited set. Agent-to-agent delegation reuses the trace, accumulating
hops, budget, and visited agents until a limit stops it.
---
## Deployment
**Docker.** A multi-stage [`Dockerfile`](Dockerfile) builds and runs the broker.
```bash
docker build -t slack-agent-mesh .
docker run --rm --env-file .env \
-v "$PWD/mesh.config.yaml:/app/mesh.config.yaml:ro" \
-v "$PWD/.mesh:/app/.mesh" \
slack-agent-mesh serve --config /app/mesh.config.yaml
```
**docker-compose.** See [`docker-compose.example.yaml`](docker-compose.example.yaml)
for a service definition with the SQLite state volume mounted for persistence
across restarts.
**Bare process.** `npm ci && npm run build && node dist/cli.js serve --config
mesh.config.yaml`, supervised by systemd/pm2/your orchestrator. Socket Mode means
**no inbound ports** and no public ingress are required.
**State.** `mesh.state_path` (default `.mesh/state.sqlite`) holds event dedupe,
traces, the visited set, output receipts (including in-flight pending claims),
threads, and delegation edges. Mount it on a durable volume so dedupe and routing
survive restarts. This keeps restarts consistent but does **not** make delivery
exactly-once across a crash: a crash mid-post can leave a pending claim that needs
manual cleanup (see [Reliability](#reliability)). Use `:memory:` only for
ephemeral/testing runs.
---
## Security
The threat model is: **untrusted humans and untrusted agent output share a Slack
channel, and secrets must never leak.**
- **Secrets stay in the environment.** Config references env-var *names*. The
loader (`assertNoInlineSecrets`) rejects any config containing a literal
`xoxb-`/`xapp-`/`xoxp-` token or a secret-bearing key holding a literal value.
`.gitignore` excludes `.env*` (except `.env.example`).
- **Least-privilege child processes.** Command adapters start with an **empty
environment**; only the names in `env_allowlist` + `env_pass` are forwarded. The
mesh's own secrets are never visible to an agent CLI unless you explicitly pass
them. Child stderr is drained but never retained or copied into errors/logs.
- **Signed webhooks.** Remote agents are called over HMAC-SHA256-signed HTTP
(`v0:<timestamp>:<body>`), verifiable in constant time.
- **Untrusted-content separation.** Mentions inside fenced code, inline code,
block quotes, Slack attachments/quoted text, or prior tool-output blocks are
**not** treated as delegations. The mesh never executes instructions merely
because quoted text contains `@agent`.
- **Prompt-injection framing.** When a message carries attachments/quoted content,
the composed prompt explicitly marks it as untrusted data, not commands.
- **Redacted logs.** Slack token classes are redacted from logs and snapshots,
and complete prompts are never logged unless `mesh.debug: true` is set — and
even then tokens stay redacted.
- **No hidden telemetry or extra coordination SaaS.** The broker talks only to
Slack and the agent endpoints/CLIs you configure; nothing else phones home.
Report vulnerabilities per [SECURITY.md](SECURITY.md).
## Reliability
- **Deterministic loop prevention.** Persisted `event_id` dedupe, stable trace
IDs, `max_hops`, `max_messages`, a visited-agent set, self-mention rejection,
and trace TTL. A defense-in-depth `reserveTurn` guard re-checks every invariant
atomically immediately before an adapter runs.
- **Idempotent output (concurrent- and retry-safe).** Each visible post is keyed
by `(trace, agent, cause)` and reserved as an atomic pending claim *before* the
Slack call. A retried turn, a duplicated adapter completion, and two concurrent
same-key injections therefore collapse to a single visible post; the losing
caller sees the existing receipt or an honest `output_pending`, never a second
message or a fabricated timestamp.
- **Bounded exactly-once, not crash-proof.** The guarantee above suppresses
concurrent and in-process retries. It is **not** exactly-once across a process
crash. A crash *after* Slack accepts a post but *before* the local claim is
finalized leaves a pending claim behind: the message was delivered, no receipt
was written, and a later same-key retry is refused as `output_pending` until
the stale row is cleared. Recovery is manual — delete the `pending` row for that
`output_key` in the SQLite `outputs` table (operator/SQLite cleanup); the mesh
does not auto-reap it.
- **One reply object per accepted turn.** Progress uses Slack update APIs.
- **Fail-closed adapters.** Timeouts, non-zero exits, oversized output, spawn
failures, malformed webhook responses, and HTTP errors produce a single
diagnostic, never a hang or partial state.
- **Serial per-trace drain.** The broker processes a trace's turns on a serial
queue, so ordering and budget accounting are stable and awaitable.
## Troubleshooting
| Symptom | Likely cause / fix |
| --- | --- |
| `doctor` says env not set | Export `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` (and any adapter env vars). `serve` needs both Slack tokens. |
| Config fails validation with "inline secret" | You pasted a real token into `mesh.config.yaml`. Use an env-var **name** and put the value in `.env`. |
| Agent never responds to a channel message | Plain channel chatter is ignored by design. Use an app mention, `/mesh`, a DM, or reply in an owned thread. Confirm the app is invited to the channel. |
| Agent named but nothing happens | Run `doctor`; the agent name must match `agents[].name` (lowercase, `-`/`_`). Check the adapter command/URL resolves. |
| `:warning: could not complete this turn (timeout)` | Raise `timeout_ms`; confirm the child actually reads stdin and writes stdout. |
| `:warning: … (output_cap)` | Your agent printed more than `max_output_bytes`. Raise the cap or trim the reply. |
| Webhook agent fails closed | Endpoint must return `200` with `{ "text": "…" }` JSON under the cap. Verify the HMAC signature and `x-mesh-timestamp`. |
| Duplicate replies after a restart | Ensure `state_path` is on a durable volume; `:memory:` loses the dedupe/output store. |
| Agent inject keeps returning `output_pending` | A crash left a stale pending claim (post likely delivered, receipt never written). Delete the `pending` row for that `output_key` in the SQLite `outputs` table. See [Reliability](#reliability). |
| Socket Mode won't connect | The app-level token needs `connections:write`; Socket Mode must be enabled (it is, in the shipped manifest). |
Turn on `mesh.debug: true` for verbose (still token-redacted) logs while
diagnosing.
---
## Buzz parity matrix
This project is inspired by [Block's Buzz](https://github.com/block/buzz), an
open-source workspace where people and agents are first-class members backed by
a Nostr relay and signed event log. Slack Agent Mesh recreates the narrow
agent-routing experience on Slack; it **does not reproduce Buzz's protocol,
identity model, forge, workflows, or complete collaboration surface.**
| Capability | Buzz | Slack Agent Mesh | Notes |
| --- | --- | --- | --- |
| Distinct first-class agent identities | ✅ | ⚠️ | One Slack app renders virtual identities via `chat:write.customize`; agents do not get independent Slack accounts or cryptographic keys. |
| Address agents by `@name` | ✅ | ✅ | App mention, `/mesh`, DM, virtual mention, owned thread. Virtual names are broker syntax, not native Slack user mentions. |
| Directed admission (no chatter) | ✅ | ✅ | Router admits only directed turns. |
| Agent-to-agent delegation | ✅ | ✅ | Broker enqueues delegates directly, not via Slack echo. |
| Deterministic loop prevention | ✅ | ✅ | Dedupe, trace IDs, hop/budget/visited/TTL. |
| Untrusted-content separation | ✅ | ✅ | Code/quote/attachment mentions do not delegate. |
| One reply object per turn | ✅ | ✅ | Progress via update APIs. |
| Thread continuity | ✅ | ✅ | Downstream turns stay in the triggering thread. |
| Idempotent output | ✅ | ⚠️ | Concurrent and in-process retries collapse by `(trace, agent, cause)`; exactly-once across a process crash is not guaranteed. |
| Model-agnostic agent transport | ✅ | ✅ | `command` / `webhook` / `mock` adapters. |
| MCP tool surface | ✅ | ✅ | This repo's seven MCP tools cover mesh messaging, directory, traces, presence, and reactions, not Buzz's full surface. |
| Socket Mode (no ingress server) | ➖ | ✅ | Default Slack transport. |
| Reactions / presence tools | ✅ | ⚠️ | `mesh_add_reaction`, `mesh_set_presence` implemented; presence is a directory string, not native Slack presence. |
| Cryptographic identity + signed event log | ✅ | ❌ | Slack authentication and audit facilities replace Nostr keys, but are not portable or protocol-equivalent. |
| Unified search and tamper-evident audit chain | ✅ | ❌ | Slack retains message history; this broker stores routing receipts only. |
| YAML workflows and approval gates | ✅ | ❌ | Not implemented. Use Slack Workflow Builder or an external orchestrator. |
| Git forge, patches, CI/review events | ✅ | ❌ | Not implemented. Existing GitHub/Slack integrations continue to work independently. |
| Canvases, huddles, media, file operations | ✅ | ❌ | Text routing only; inbound attachment text is treated as untrusted context. |
| Rich interactive components | ✅ | ❌ | No Block Kit buttons or modals; replies are Slack mrkdwn text. |
| Live-workspace end-to-end verification | ✅ | ⚠️ | **NOT RUN** in this tree (no disposable workspace credentials). See below. |
Legend: ✅ implemented · ⚠️ partial/with caveats · ➖ not applicable · ❌ not implemented.
## Limitations and non-parity
- **No live Slack test in this tree.** All 146 automated tests and the demo pass
without network access, but an end-to-end run against a real workspace is
**NOT RUN** here because no disposable workspace credentials were available.
Treat live behavior as verified-by-construction (unit + acceptance) until you
run it in your own workspace.
- **No exact Buzz parity claim.** The matrix above is a best-effort mapping, not a
certified equivalence.
- **Virtual, not sovereign identities.** The default one-app topology cannot
provide Buzz's per-agent cryptographic identity, portable history, or signed
events. Slack and workspace administrators remain the trust boundary.
- **Text replies only.** No Block Kit, buttons, modals, or interactive
components. `interactivity` is enabled in the manifest to leave room for future
work, but the broker posts Markdown text today.
- **No file re-upload.** Inbound attachment *text* is surfaced to agents as
untrusted context; binary files are not fetched or re-posted.
- **Presence is advisory.** `presence` is a directory string, not Slack's native
presence/status system.
- **Single-workspace focus.** `org_deploy_enabled` is `false` in the manifest;
org-wide/Enterprise Grid distribution is untested.
- **Socket Mode assumed by default.** The HTTP receiver path (signing secret)
exists conceptually but Socket Mode is the supported, tested transport.
---
## CLI reference
```
slack-agent-mesh serve [--config <path>] Start the Socket Mode broker (needs Slack creds)
slack-agent-mesh mcp [--config <path>] Run the MCP server over stdio
slack-agent-mesh doctor [--config <path>] Validate config and environment
slack-agent-mesh demo --script <path> Run the credential-free scripted demo
```
Options: `--config <path>` (default `mesh.config.yaml`), `--script <path>`
(default `examples/demo-conversation.json`), `-h`/`--help`.
Release-gate commands (all green on this tree):
```bash
npm ci
npm run lint
npm run typecheck
npm test # 119 unit tests
npm run build
npm run test:acceptance # 27 acceptance tests
npm run demo -- --script examples/demo-conversation.json
```
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). In short: Node ≥ 22, npm, strict
TypeScript, Biome for lint/format, Vitest for tests. Run the release-gate
commands above before opening a PR, and never commit tokens.
Agent onboarding (Hermes, Claude Code, Codex, generic MCP clients) lives in
[`skills/slack-agent-mesh/`](skills/slack-agent-mesh/) and
[AGENTS.md](AGENTS.md).
## Security policy
See [SECURITY.md](SECURITY.md) for how to report a vulnerability and the
supported-version policy.
## License
[MIT](LICENSE). No hidden telemetry, no remote SaaS dependency.
## References
Official Slack documentation:
- Slack API home — <https://api.slack.com/>
- Socket Mode — <https://api.slack.com/apis/socket-mode>
- App manifests — <https://api.slack.com/reference/manifests>
- Events API — <https://api.slack.com/apis/events-api>
- OAuth scopes — <https://api.slack.com/scopes>
- Token types — <https://api.slack.com/authentication/token-types>
- Model Context Protocol — <https://modelcontextprotocol.io/>
- Buzz source — <https://github.com/block/buzz>
- Block announcement — <https://block.xyz/inside/introducing-buzz-where-humans-and-agents-work-together>
This project does not vendor or depend on Buzz code.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues