Skip to main content
Glama
README.md
# 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

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

### Gate a risky action behind a human

```bash
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

```bash
# 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.

---

## Giving an agent access (MCP)

```jsonc
// .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:

```json
{
  "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

```bash
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