Skip to main content
Glama

AgentBus

A durable coordination bus for AI agent fleets — with a human approval gate in front of anything irreversible.

Agents are increasingly allowed to deploy, send, delete, and spend. Two things break at that point, and neither is an LLM problem:

  1. Handoffs get lost. An agent finishes, hands work to the next one, and the process dies. Nothing retries, nobody notices.

  2. There is no choke point. Approval lives inside a prompt — "ask me first" — which is a suggestion, not a control, and leaves no record of who allowed what.

AgentBus is the boring infrastructure underneath: a durable queue with at-least-once delivery, a blocking human-approval gate, and an append-only audit log of every message and decision.

                    ┌──────────────────────────────┐
   agents  ────────▶│  topics · groups · leases    │────────▶  workers
   (MCP/HTTP/CLI)   │  retries · dead letters      │
                    ├──────────────────────────────┤
   agent  ─ ask ───▶│  approval gate  ── blocks ───│──▶ human (phone/dashboard)
                    ├──────────────────────────────┤
                    │  append-only audit log       │
                    └──────────────────────────────┘
                         one SQLite file

Zero runtime dependencies. No Redis, no Postgres, no broker, no cloud account. Node 24+ and a file.


Quick start

npx agentbus serve                 # http://127.0.0.1:7801 — API + dashboard

Gate a risky action behind a human

agentbus ask "Deploy api build 118 to production" \
  --action deploy.prod --risk high \
  --detail "3 commits, including a migration that drops a column." \
  --payload '{"service":"api","build":118}' \
  --wait && ./deploy.sh

ask --wait blocks and prints a link. A person opens it on their phone, approves or denies, and the command exits 0 approved · 10 denied · 11 expired — so && does exactly the right thing, and the decision is recorded against their name.

Durable work handoff

# Producer — survives a crash, deduplicated by key
agentbus pub job.render '{"file":"a.png"}' --key render-a

# Consumer — any shell command becomes a durable worker.
# Payload on stdin; exit 0 acks, non-zero retries with backoff, then dead-letters.
agentbus sub 'job.>' --group renderers --exec './render.sh'

Unacked work returns to the queue when the lease expires, so a worker that gets killed mid-job loses nothing.


Related MCP server: gotoHuman MCP Server

Giving an agent access (MCP)

// .mcp.json  — or claude_desktop_config.json
{
  "mcpServers": {
    "agentbus": {
      "command": "npx",
      "args": ["-y", "agentbus", "mcp"],
      "env": { "AGENTBUS_URL": "http://127.0.0.1:7801", "AGENTBUS_AGENT": "deploy-agent" }
    }
  }
}

The agent gets request_approval, check_approval, publish, pull, ack, nack, and stats. request_approval blocks until a human decides, and returns explicit guidance:

{
  "state": "denied",
  "approved": false,
  "decidedBy": "sami",
  "reason": "not without a backup",
  "guidance": "Not approved (denied). Do NOT proceed. Tell the user and stop."
}

A timeout returns pending, never approved — silence is never consent.


Concepts

Topics are dot-separated: deploy.prod.api. Patterns use * for one segment and > for the rest — deploy.>, *.prod.api, >.

Consumer groups each get their own copy of every matching message. Within a group, a message goes to exactly one worker at a time. A new group starts from now by default; --earliest replays the whole backlog.

Leases give a worker a visibility timeout (30s default, extendable). Miss it and the message is redelivered. After max_attempts (5) it is dead-lettered, visible on the dashboard, and replayable with one click.

Approvals carry a title, machine-readable action, free-text detail, structured payload, risk level, and optional labelled choices ("canary 10%" vs "full rollout"). They expire rather than hang forever. Every request and decision — who, when, why — lands in the audit log.

Ordering is by a SQLite sequence, not by timestamp: two messages published in the same millisecond still have a well-defined order.


HTTP API

Method

Path

POST

/v1/publish

{topic, payload, idempotencyKey?, delayMs?}

POST

/v1/pull

{group, patterns?, max?, leaseMs?, waitMs?} — long-polls

POST

/v1/ack · /v1/nack · /v1/extend

settle or extend a delivery

GET

/v1/stats · /v1/dead · /v1/audit

observability

POST

/v1/replay

requeue a dead letter

POST

/v1/approvals

create; returns a signed shareable URL

GET

/v1/approvals/:id/wait

long-poll until decided

POST

/v1/approvals/:id/decide

{decision, by, reason?, choice?}

GET

/v1/stream

SSE tail of messages and approvals

GET

/ · /a/:id

dashboard · single-approval page

Set AGENTBUS_TOKEN to require Authorization: Bearer …. Approval links carry their own HMAC token, so you can send one to a phone without handing over the API key.

Environment: AGENTBUS_URL, AGENTBUS_TOKEN, AGENTBUS_DB, AGENTBUS_SECRET, AGENTBUS_AGENT, AGENTBUS_ALLOWED_ORIGINS.

Browser-origin policy

The default is an unauthenticated server on loopback, which means a webpage you happen to have open would otherwise be able to reach it. Three rules stop that, and they apply whether or not a token is set:

  • A request carrying an Origin header is refused unless that origin is the server's own, the host in AGENTBUS_URL, or listed in AGENTBUS_ALLOWED_ORIGINS. Refused means 403 — not merely a missing CORS header, which hides the response but still performs the write.

  • POST requires content-type: application/json. Form and text/plain bodies are CORS "simple requests" that a browser sends with no preflight at all, so accepting them would reopen the hole.

  • An unexpected Host header is refused, which blocks DNS rebinding.

Non-browser clients — the CLI, the MCP server, curl, your own scripts — send no Origin and are unaffected. If you serve the dashboard on a LAN address or behind a proxy so approvals reach a phone, set AGENTBUS_URL (or --url) to that public origin; it is what approval links are built from anyway.


Development

npm test          # 66 tests, no network, no fixtures
npm run typecheck # strict, noUncheckedIndexedAccess, erasableSyntaxOnly

TypeScript runs directly on Node 24 via native type stripping — there is no build step and no compiler in the runtime path.


License

MIT

Related MCP Connectors

Related MCP Servers